说明

建站已接近快三年,一直没有怎么维护和管理,查看站点访问数据目前还使用的是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 系统,使用下述命令安装

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

    1
    2
    3
    
    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,替换为你实际的路径

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    # 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

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

    1
    2
    
    # Compile just the module
    make modules
    

    image-20230729192151965

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

    1
    2
    3
    
    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 动态库

    1
    2
    3
    4
    5
    6
    7
    
    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 并加载

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    # 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

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    
    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"'
                        '}';
    
    
  • 对应虚拟主机日志格式更改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    # 更改对应虚拟主机中的日志格式,为上面定义的 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,使日志便于后续统计和分析,配置方法如下

    1
    2
    3
    4
    5
    
    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

    以安装请省略

    1
    2
    3
    4
    
    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 为你所要收集日志的目录,按你实际情况更改

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    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

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    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 日志收集

    1
    
    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 监控探针,避免影响数据的真实性,我这里把这部分请求添加了过滤

    1
    2
    3
    4
    5
    6
    
    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 的图表进行参考配置,这部分就由自己的自由发挥,此篇文档不做这里介绍。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    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

    1
    2
    3
    4
    5
    6
    
    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 同时只有一个时,此处会得到重叠,语法和图片如下

    求大佬给个解疑的思路吧

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    
    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 打印日志时,时区存在问题,目前且能通过更改源码,通过编译再使用解决