今日天气
  1. 博客/

站点开始使用 Grafana loki 统计分析

·4828 字·10 分钟· ·
Loki SRE DevOps Logging
Johny
作者
Johny
熟练的 云原生搬砖师
Table of Contents

说明
#

建站已接近快三年,一直没有怎么维护和管理,查看站点访问数据目前还使用的是Google Search Console& 正在对接用的CDN厂商 (又拍云) 所提供的后台统计,又拍云所提供的后台统计有一个痛点,就是无法根据访问 IP 来统计 PV(Page Views) & UV(Unique Visitors)的真实情况,有点鸡肋。所以开始在网上查找合适轻量化解决方案,对眼上了 Grafana 的这款 Dashboard,查看其简介后,此 Dashboard 基于 Loki 进行实现,能够接入 Grafana Cloud 解决轻量化问题,恰好之前也有对 Loki 有一点了解,部署起来应该问题不大,开始着手探索。

来看一下我最终折腾后的效果,总体还不错的吧

image-20230729174820026


站点情况
#

开始部署前,先说明一下我站点的技术栈选型,目前我站点基于 Hugo 实现静态文件的生成,将渲染过后的静态文件通过 Docker + Nginx 打包成镜像,方便后续接入 K8S 种平台,但后面 K8S 这里我最终放弃,因为站点实际流量不大,没有必要,不适合懒人维护还不环保。而站点实际运行的是在一台云主机中, 使用Docker运行博客容器,再通过 OpenResty 反向代理一下,这里还我套一个 OpenResty 是因为 443,这类端口珍贵,通过反代可以实现复用功能,同时还可以加一些 WAF (Web Application Firewall) 的操作使得站点更加的安全、耐操。博客镜像的更新我选择使用的 Watchtower,只要博客的镜像基于 Pipeline 生成最新镜像并推送指 Dockerhub 中,Watchtower 它会 自动 帮我完成博客的重载和更新。上述描述即是我目前博客的运行情况。


技术说明
#

  • LoKi 是什么?

    Loki 是 Grafana Labs 开发的一个开源、多租户的日志聚合系统,它的设计目标是能够在公有云、私有云和混合云环境中高效地提供即时日志搜索和探索功能。Loki 的设计理念是将日志数据和监控数据(例如,指标和追踪数据)紧密地结合在一起,从而提供一种统一的、高效的方式来观察和诊断系统行为。

    Loki 的主要特点包括:

    1. 索引简化:Loki 不会为每个日志行创建索引,而是为每个日志流创建索引。这种方法大大降低了存储成本,并提高了查询效率。

    2. 紧密集成 Grafana:Loki 与 Grafana 紧密集成,可以在 Grafana 的界面中直接查询和查看 Loki 的日志数据。

    3. 多租户支持:Loki 支持多租户,每个租户都有自己的隔离的日志数据。

    4. 高度可扩展:Loki 的架构支持水平扩展,可以通过添加更多的节点来处理更大的日志数据。

    5. 兼容 Prometheus:Loki 的查询语言(LogQL)设计上兼容 Prometheus 的查询语言(PromQL),使得在 Loki 中查询日志数据和在 Prometheus 中查询指标数据的体验非常相似。

    6. 支持多种数据源:Loki 支持多种日志数据源,包括但不限于 systemd journal、docker logs、fluentd 等。

  • Loki 的主要组件有哪些?

    Loki 的架构由几个主要组件构成,这些组件可以在单个二进制文件中一起运行,也可以作为单独的进程运行。以下是 Loki 的主要组件:

    1. Promtail:Promtail 是 Loki 的代理,它负责收集日志并将它们发送到 Loki。Promtail 通常在产生日志的机器上运行,可以直接读取日志文件,也可以接收由其他进程(如 Fluentd 或 Fluent Bit)转发的日志。

    2. Loki:Loki 是主要的日志聚合和查询组件,它接收并存储日志,同时提供了一个查询接口。Loki 通过索引日志流(而不是每一行日志)来提供高效的存储和查询。

    3. Distributor:Distributor 是 Loki 的组件,它负责接收来自 Promtail 的日志数据,然后将这些数据分发到多个 Ingester。

    4. Ingester:Ingester 是 Loki 的组件,它负责接收日志数据,将数据压缩后存储在内存中,然后定期将这些数据刷新到长期存储(如 Amazon S3 或 Google Cloud Storage)。

    5. Querier:Querier 是 Loki 的组件,它负责处理来自用户的查询请求。Querier 会从 Ingester 和长期存储中获取数据,然后返回查询结果。

    6. Query Frontend:Query Frontend 是 Loki 的组件,它负责优化和加速查询。Query Frontend 会将大查询分解为多个小查询,然后并行执行这些小查询。

    7. Compactor:Compactor 是 Loki 的组件,它负责压缩和优化在长期存储中的数据。

    8. Ruler:Ruler 是 Loki 的组件,它负责执行预定义的规则和警报。


配置日志格式
#

因我使用的是 OneinStack 一键部署的 OpenRestry,按照该 Dashboard 中的描述,需要对日志配置 GeoIP,需要对 OpenRestry 重新编译开启,查看了一下 GeoIP 的选项,我选择 GeoIP2 Databases作为我的库,只需要编译一下 module ,在使用时进行 load 即可,对于的 module 地址 & 参考文档


OpenRestry 编译 GeoIP2 模块
#

  • 按照上面文档的描述,首先需要安装 maxminddb 依赖,我这里使用的是 CentOS 7 系统,使用下述命令安装

    yum install -y libmaxminddb-devel libmaxminddb
    
  • Clone GeoIP2 模块仓库代码至目录 (编译时要用)

    mkdir -p /tmp/compile/openresty-$(nginx -v 2>&1|cut -d "/" -f2)/modules
    cd /tmp/compile/openresty-$(nginx -v 2>&1|cut -d "/" -f2)/modules
    git clone https://ghproxy.com/https://github.com/leev/ngx_http_geoip2_module.git
    
  • 进入 ${NGINX_SOURCE_CODE_PATH} (OpenRestry源码目录) 进行编译,用 OneinStack 安装的话,包会统一存放在 ./src 目录下

    我这里 OneinStack ROOT_PATH 为 /data/scripts/oneinstack,替换为你实际的路径

    # cd ${NGINX_SOURCE_CODE_PATH}/bundle/nginx-$(nginx -v 2>&1|cut -d "/" -f2|grep -oP '^d+.d+.d+')
    
    cd /data/scripts/oneinstack/src/openresty-1.19.3.1/bundle/nginx-1.19.3
    
    # 配置编译时所需的环境变量(否则将失败)
    export LUAJIT_LIB="/usr/local/openresty/luajit/lib/"
    export LUAJIT_INC="../LuaJIT-*/src/"
    
    # 获取已安装的 OpenResty 编译选项,以避免 "二进制不兼容" 错误
    
    COMPILEOPTIONS=$(nginx -V 2>&1|grep -i "arguments"|cut -d ":" -f2-)
    
    # 使用这些选项配置编译
    # 将 GeoIP2 添加为动态模块,指向你 Clone 的路径
    eval ./configure $COMPILEOPTIONS --add-dynamic-module=/tmp/compile/openresty-1.19.3.1/modules/ngx_http_geoip2_module/
    

    image-20230729192055972

  • 上一步成功后,开始执行编译动作

    # Compile just the module
    make modules
    

    image-20230729192151965

    这一步成功后,会在当前 objs 下生成 所需的 动态库文件

    ls -lh objs/*.so
    -rwxr-xr-x 1 root root 86K Jul 27 11:22 objs/ngx_http_geoip2_module.so
    -rwxr-xr-x 1 root root 62K Jul 27 11:22 objs/ngx_stream_geoip2_module.so  #  这个动态库文件为 L4 时使用,我们且用上面那个即可
    
  • OpenRestry 配置加载 GeoIP2 动态库

    mkdir -p /usr/local/openresty/nginx/modules
    
    cp -a objs/*.so /usr/local/openresty/nginx/modules/ # COPY 动态库文件,方便后续引用
    
    vim /etc/nginx/nginx.conf # 编辑 NGINX 主配置文件加入这行
    
    load_module modules/ngx_http_geoip2_module.so;
    

    image-20230729192420002


OpenRestry 配置对接 Geo_IP Databases
#

模块加载后,实际使用还需要一个 GEO 的数据库,到官网下载到话,需要注册一个账号,有账号的小伙伴可以通过 官网下载,我尝试注册一个提示我使用 VPN 过不了风控,直接给我干劝退。不过还好有其他方案,就是有人以将数据库文件上传至 Github 中,可用仓库地址如下

我下载的 GeoLite2-Country.mmdb ,目前已够用

  • 下载 geoip2 数据库至 /etc/nginx/geoip2 并加载

    # Nginx 主配置文件加入如下内容,放置到 http 段中
    
    vim /etc/nginx/nginx.conf
    
     # Geo_IP
       geoip2 /etc/nginx/geoip2/GeoLite2-Country.mmdb {
            auto_reload 5m;
            $geoip2_metadata_country_build metadata build_epoch;
            $geoip2_data_country_code default=US country iso_code;
            $geoip2_data_country_name country names en;
       }
    

    image-20230729193104645

  • 按照 Dashbaord 文档配置日志格式 json_analytics

    注意 geoip_country_code 这里因为我们使用的是 Geo_IP2 ,需要替换为我们上面所定义的变量 geoip2_data_country_name

    vim /etc/nginx/nginx.conf
    
    # 添加 Dashboard 所需的日志格式
    log_format json_analytics escape=json '{'
                                '"msec": "$msec", ' # request unixtime in seconds with a milliseconds resolution
                                '"connection": "$connection", ' # connection serial number
                                '"connection_requests": "$connection_requests", ' # number of requests made in connection
                        '"pid": "$pid", ' # process pid
                        '"request_id": "$request_id", ' # the unique request id
                        '"request_length": "$request_length", ' # request length (including headers and body)
                        '"remote_addr": "$remote_addr", ' # client IP
                        '"remote_user": "$remote_user", ' # client HTTP username
                        '"remote_port": "$remote_port", ' # client port
                        '"time_local": "$time_local", '
                        '"time_iso8601": "$time_iso8601", ' # local time in the ISO 8601 standard format
                        '"request": "$request", ' # full path no arguments if the request
                        '"request_uri": "$request_uri", ' # full path and arguments if the request
                        '"args": "$args", ' # args
                        '"status": "$status", ' # response status code
                        '"body_bytes_sent": "$body_bytes_sent", ' # the number of body bytes exclude headers sent to a client
                        '"bytes_sent": "$bytes_sent", ' # the number of bytes sent to a client
                        '"http_referer": "$http_referer", ' # HTTP referer
                        '"http_user_agent": "$http_user_agent", ' # user agent
                        '"http_x_forwarded_for": "$http_x_forwarded_for", ' # http_x_forwarded_for
                        '"http_host": "$http_host", ' # the request Host: header
                        '"server_name": "$server_name", ' # the name of the vhost serving the request
                        '"request_time": "$request_time", ' # request processing time in seconds with msec resolution
                        '"upstream": "$upstream_addr", ' # upstream backend server for proxied requests
                        '"upstream_connect_time": "$upstream_connect_time", ' # upstream handshake time incl. TLS
                        '"upstream_header_time": "$upstream_header_time", ' # time spent receiving upstream headers
                        '"upstream_response_time": "$upstream_response_time", ' # time spend receiving upstream body
                        '"upstream_response_length": "$upstream_response_length", ' # upstream response length
                        '"upstream_cache_status": "$upstream_cache_status", ' # cache HIT/MISS where applicable
                        '"ssl_protocol": "$ssl_protocol", ' # TLS protocol
                        '"ssl_cipher": "$ssl_cipher", ' # TLS cipher
                        '"scheme": "$scheme", ' # http or https
                        '"request_method": "$request_method", ' # request method
                        '"server_protocol": "$server_protocol", ' # request protocol, like HTTP/1.1 or HTTP/2.0
                        '"pipe": "$pipe", ' # "p" if request was pipelined, "." otherwise
                        '"gzip_ratio": "$gzip_ratio", '
                        '"http_cf_ray": "$http_cf_ray",'
                        '"geoip_country_code": "$geoip2_data_country_code"'
                        '}';
    
  • 对应虚拟主机日志格式更改

    # 更改对应虚拟主机中的日志格式,为上面定义的 json_analytics
    
    vim vhost/www.conf
    server {
      listen 443 http2;
      server_name www.treesir.pub;
      access_log /data/wwwlogs/nps_www_access_nginx.log json_analytics;
      ...
    }
    

    查看效果,以变成我们所期望的格式

    image-20230729193836030

    ⚠️ 这里日志所返回的 geoip_country_code 也有可能不是你所期待的效果,比如你的 站点位于 CDN 或者 代理服务器的后端的时候,会导致 remote_addr 地址不是真正客户端的真实IP,影响到最终结果,这里概举这两种方式解决

    • 使用 Nginx 自带的 set_real_ip_from
    • 模块提供的 geoip2_proxy 相关参数

    推荐 set_real_ip_from 这种,同时可以让日志中 remote_addr 也获取到真实的IP,使日志便于后续统计和分析,配置方法如下

    vim /etc/nginx/nginx.conf # 更改主配置文件,http 段加入如下内容
    
      # 获取 CDN 后真实 IP
      set_real_ip_from 0.0.0.0/0;
      real_ip_header X-Forwarded-For;
    

配置 Loki
#

由于我比较懒,不太想在自己的 HomeLab 中部署 Loki (主要部署单实例的 Loki 使用体验不好,部署微服务架构又太重了,白嫖难道不香嘛?),我们这里基于 Grafana Cloud 实现 Loki 和 Dashboard 的展示。这里省略创建账号这类操作,登陆入口地址如下,有 Google 账号的直接第三方登陆即可

什么是 Grafana Cloud ?
#

Grafana Cloud 是 Grafana Labs 提供的一种托管服务,它提供了 Grafana、Prometheus 和 Loki 的托管版本。这意味着你可以使用这些强大的开源监控和可视化工具,而无需自己管理和维护底层的基础设施。

以下是 Grafana Cloud 的一些主要特性:

  1. 托管的 Grafana:你可以使用最新版本的 Grafana,而无需自己进行安装和升级。

  2. 托管的 Prometheus 和 Alertmanager:你可以使用 Prometheus 和 Alertmanager 来收集和管理你的指标数据,而无需自己进行安装和配置。

  3. 托管的 Loki:你可以使用 Loki 来收集和查询你的日志数据,而无需自己进行安装和配置。

  4. 托管的 Grafana Tempo:Grafana Tempo 是一个高度可扩展的、易于操作的分布式追踪后端。你可以使用它来存储和查询你的追踪数据。

  5. 集成的警报和通知:你可以使用 Grafana Cloud 的警报和通知功能,来及时了解你的系统状态。

  6. 安全和可靠:Grafana Cloud 提供了数据加密、备份和高可用性等安全和可靠性特性。


部署 Promtail 日志代理
#

这里的 Promtail 为 Loki 的日志代理,通过将主机中的日志收集起来,Post 到 loki 中,实现日志的统一存储。下面的文档中我们会使用 Docker Compose 来部署 Promtail

  • 安装 Docker Compose

    以安装请省略

    curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose \
    && chmod +x /usr/local/bin/docker-compose \
    && ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose \
    && docker-compose --version
    
  • 初始化 promtail 部署

    /data/wwwlogs 为你所要收集日志的目录,按你实际情况更改

    mkdir -p /data/docker-compose/loki-promtail/
    
    cd /data/docker-compose/loki-promtail/
    
    
    cat docker-compose.yaml # 内容如下
    version: "3"
    
    networks:
      loki:
    
    services:
      promtail:
        image: grafana/promtail:2.7.4
        volumes:
          - /data/wwwlogs:/data/wwwlogs:ro
          - ./config/config.yml:/etc/promtail/config.yml:ro
          - /etc/localtime:/etc/localtime
        command: -config.file=/etc/promtail/config.yml
    

    config/config.yml 配置文件如下

    更改 USER_ID & TOKEN 为你实际页面生成的,登陆 Cloud 后找 Loki

    server:
      http_listen_port: 0
      grpc_listen_port: 0
    
    positions:
      filename: /tmp/positions.yaml
    
    clients:
      - url: https://${USER_ID}:${TOKEN}@logs-prod-021.grafana.net/loki/api/v1/push
    
    scrape_configs:
        - job_name: system
          pipeline_stages:
          - replace:
              expression: '(?:[0-9]{1,3}\.){3}([0-9]{1,3})'
              replace: '***'
          static_configs:
          - targets:
             - www.treesir.pub
            labels:
             job: nginx_access_log
             host: ali-vps
             agent: promtail
             __path__: /data/wwwlogs/nps_www_access_nginx.log
    

    image-20230729202401293

  • 启动 promtail 日志收集

    docker-compose up -d
    

    image-20230729202633653


配置 Dashboard
#

回到 Cloud 主页 ,点击进入 Grafana

image-20230729203117680

加载 Dashboard
#

image-20230729203251382

image-20230729203335944

image-20230729203418924

image-20230729203441700


修剪变量 & Dashboard
#

导入会有一些报错,不用担心,我们调整一下变量即可,是缺少变量导致的

image-20230729203635081

image-20230729203653434

image-20230729203714291

datasource 这里添加过滤 .*-logs,后点击 Apply,防止刷新页面失效,记得点击 Save Dashboard

image-20230729203842784

选择 Dashbaord 参数,就可以看到对应的数值了

image-20230729204126893

可以看到 Top Countries 这里有点乱码,点击编辑,右边选项栏往下滑,找到 Mapping 把问号删除即可,或者改成你想要的映射内容。

image-20230729204313061

image-20230729204356085

更改后不要忘记 Save 一下

image-20230729204505730


添加 PV & UV 指标
#

该图表,默认没有提供 PV & UV 指标,Loki 提供了一套与 Prometheus 类似的查询语法,叫 LogQL , 我们可以通过此查询语法,通过自定义 Visualization ,得到我们想要的内容。

  • 获取 24H PV 指标, logQL 示例

    由于我使用了 Blackbox Exporter 监控探针,避免影响数据的真实性,我这里把这部分请求添加了过滤

    sum(count_over_time(
      {job="$job"}
      | json 
      |  __error__="" and remote_addr != "" and http_user_agent !~ "^Blackbox.*" 
      [24h]
    ))
    

    新建图形

    image-20230729205538890

    image-20230729205618899

    输入 logQL 测试运行,可以看到已经有结果了,由于我们这里且需要基于 现在时间 往前推 24h 的 PV 统计,但可以看到下图,它自动查询到了 1473 份数据,并按照这个数据绘制了 区间水平线,这其实有点没有必要,我们进行优化一下。

    image-20230729205746086

    优化查询参数,更改最大查询数据为 1 , 时间区间选择 15s, 同时隐藏 时间信息。这样就得到我们预期的结果了。

    image-20230729210106616

  • 获取 24H UV 指标, logQL 示例

    配置方法与 上面的 PV 配置一致,如果你想要好看的样式,可以基于现有 Dashbaord 的图表进行参考配置,这部分就由自己的自由发挥,此篇文档不做这里介绍。

    sum(sum by (remote_addr) (
      sum by (remote_addr, geoip_country_code) (
        count_over_time(
          {job="$job"}
          | json
          | __error__="" and remote_addr != "" and http_user_agent !~ "^Blackbox.*"
          [24h]
        )
      ) ^ 0
    ))
    
  • 统计区间 PV 增长情况, logQL 示例如下

    这里的区间我们选择使用 $__interval 内置变量,可以在使用时很好的和主页上的 区间选择器,进行联动查询。

    image-20230729210708408

    sum by (remote_addr) ((count_over_time(
      {job="$job"}
      | json 
      |  __error__="" and remote_addr != "" and http_user_agent !~ "^Blackbox.*" 
      [$__interval]
    )))
    

    这次是用到的区间查询,配置方法与上面的两个不一样了,再次点击 New Visualization,选择 Time series 类型

    image-20230729211154019

    优化显示,现在看这个图,显的有的臃肿,我们优化一下。Legend 这里我们输入 {{remote_addr}}

    image-20230729211346275

    左边找到 Legend,我们把它的 可见效关掉。现在就看起来舒服多了

    image-20230729211715321


    最终效果如下。已经与我最初所展示的效果接近。美化的工作交给你自己,如果实在不行,那你参考我这个导出的 Json 文件吧。

    image-20230729212141818


总结
#

Grafana 可玩性还是挺高,白嫖的 Grafana Cloud 真香。Grafana Cloud 的查询性能是真不错,我尝试自建 Loki 同时使用 Loki 的微服务模式进行部署,却始终无法达到 Cloud 上的使用体验。后面再做深入研究吧,总体来说使用 Loki 统计博客日志的整体体验还是很不错的,不过经过这次折腾也还有几个问题没有得到有效解决

  • 统计 UV 的区间增长指时,这里会得到唯一的 1 ,如果与 PV 同时只有一个时,此处会得到重叠,语法和图片如下

    求大佬给个解疑的思路吧

    
    sum(sum by (remote_addr) (
      sum by (remote_addr, geoip_country_code) (
        count_over_time(
          {job="$job"}
          | json
          | __error__="" and remote_addr != "" and http_user_agent !~ "^Blackbox.*"
          [24h]
        )
      ) ^ 0
    ))
    

    image-20230729213825942

  • Geo_IP 加载 GeoLite2-City.mmdb 库时,无法获取的正确的 City 名称(不过现在也用不着,后面说不定用的找呢)

  • Promtail 打印日志时,时区存在问题,目前且能通过更改源码,通过编译再使用解决

相关文章

Gitea Actions ActRunner 基于 Systemd 部署安装
·897 字·2 分钟·
SRE DevOps linux
Sonatype Nexus Repository(Nexus3) 私服文件下载至本地 - (使用进阶篇 一)
·729 字·2 分钟·
SRE nexus3 DevOps
Linux 使用 LVM 来扩充分区
·143 字·1 分钟·
SRE linux lvm
K8S 使用 CronJob 备份 MySQL 数据至 MInIO
·527 字·2 分钟·
SRE backup mysql
修复 SSH 免密无法连接
·296 字·1 分钟·
SRE linux sshd
Argocd Cli Usage Tips
·269 字·1 分钟·
devops argocd