跳过正文
  1. 博客文章/

使用 Grafana Loki 构建网站访问统计分析系统

·1307 字·7 分钟·
监控 SRE Loki Grafana 日志分析 网站监控
Zayn
作者
Zayn
专注 Kubernetes、CI/CD、可观测性等云原生技术栈,记录生产环境中的实战经验与踩坑复盘。
目录

在网站运营过程中,访问数据分析是了解用户行为和优化网站性能的重要手段。本文将分享如何使用 Grafana Loki 构建一个轻量级且功能强大的网站访问统计分析系统。

项目背景
#

现有方案的局限性
#

在寻找网站统计解决方案的过程中,传统的统计工具存在一些局限性:

  • Google Search Console:主要关注搜索引擎相关数据,对整体访问统计支持有限
  • CDN 厂商统计:功能相对简单,无法提供基于 IP 的精确 PV(页面浏览量)和 UV(独立访客)统计
  • 第三方统计服务:可能存在数据隐私和依赖性问题

解决方案选择
#

经过调研,我选择了基于 Grafana Loki 的解决方案,主要原因包括:

  1. 轻量化部署:可以使用 Grafana Cloud 免费版本,无需自建基础设施
  2. 强大的日志分析能力:Loki 专为日志数据设计,查询性能优秀
  3. 丰富的可视化:基于现成的 Nginx Dashboard 快速构建
  4. 成本控制:免费额度足够个人网站使用

最终效果展示
#

经过配置和优化,最终实现的监控面板效果如下:

Loki 网站统计面板

图:基于 Loki 的网站访问统计面板,包含 PV/UV、地理分布、访问趋势等关键指标

网站技术架构
#

当前技术栈
#

在开始配置监控之前,先介绍网站的技术架构,这有助于理解后续的配置过程:

静态网站生成
#

  • 框架Hugo - 高性能的静态网站生成器
  • 优势:构建速度快,生成的静态文件便于部署和缓存

容器化部署
#

  • 容器化:使用 Docker + Nginx 将静态文件打包成镜像
  • 部署方式:在云主机上运行 Docker 容器
  • 自动更新:使用 Watchtower 实现镜像自动更新

反向代理层
#

  • 代理服务OpenResty - 基于 Nginx 的高性能 Web 平台
  • 功能特性
    • 端口复用:通过反向代理实现 443 端口的多服务共享
    • 安全防护:集成 WAF 规则 提供 Web 应用防火墙功能
    • 性能优化:提供缓存、压缩等性能优化功能

CI/CD 流程
#

  • 构建:基于 Pipeline 自动构建和推送镜像到 Docker Hub
  • 部署:Watchtower 监控镜像更新并自动重新部署

这种架构的优势在于:

  • 简单可靠:避免了 Kubernetes 等复杂编排工具的维护成本
  • 成本效益:适合中小型网站的流量规模
  • 易于维护:自动化程度高,减少人工干预

Loki 技术介绍
#

什么是 Loki
#

Loki 是由 Grafana Labs 开发的现代化日志聚合系统,专为云原生环境设计。它采用了与传统日志系统不同的设计理念,重点关注成本效益和查询性能。

核心特性
#

1. 高效的索引策略
#

  • 标签索引:仅对日志流的标签创建索引,而不是日志内容本身
  • 成本优势:大幅降低存储成本和索引维护开销
  • 查询效率:通过标签快速定位相关日志流

2. 与 Grafana 生态集成
#

  • 原生支持:在 Grafana 中直接查询和可视化 Loki 数据
  • 统一界面:将日志、指标和链路追踪数据整合在同一个界面中
  • 告警集成:支持基于日志数据的告警规则

3. 多租户架构
#

  • 数据隔离:每个租户拥有独立的日志数据空间
  • 权限控制:细粒度的访问控制和数据安全
  • 资源共享:多租户共享基础设施,提高资源利用率

4. 水平扩展能力
#

  • 微服务架构:各组件可独立扩展
  • 云原生设计:支持 Kubernetes 等容器编排平台
  • 弹性伸缩:根据负载自动调整资源

5. PromQL 兼容的查询语言
#

  • LogQL:与 Prometheus 的 PromQL 语法相似
  • 学习成本低:熟悉 Prometheus 的用户可以快速上手
  • 强大功能:支持复杂的日志查询、过滤和聚合操作

Loki 架构组件
#

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

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

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

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

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

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

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

  7. Compactor:负责压缩和优化在长期存储中的数据。

  8. Ruler:负责执行预定义的规则和警报。

配置日志格式
#

由于我使用的是 OneinStack 一键部署的 OpenResty,按照该 Dashboard 中的描述,需要对日志配置 GeoIP,需要对 OpenResty 重新编译开启。查看了 GeoIP 的选项,我选择 GeoIP2 Databases 作为数据库,只需要编译一下 module,在使用时进行 load 即可。

相关资源:

  • 模块仓库地址:https://ghproxy.com/https://github.com/leev/ngx_http_geoip2_module.git
  • 参考文档:https://www.electrosoftcloud.com/en/compile-geoip2-in-openresty-and-how-to-use-it/

OpenResty 编译 GeoIP2 模块
#

安装依赖
#

按照上面文档的描述,首先需要安装 maxminddb 依赖。在 CentOS 7 系统中使用以下命令安装:

yum install -y libmaxminddb-devel libmaxminddb

克隆 GeoIP2 模块代码
#

克隆 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

配置编译环境
#

进入 OpenResty 源码目录进行编译。使用 OneinStack 安装的话,包会统一存放在 ./src 目录下。

注意:这里 OneinStack ROOT_PATH 为 /data/scripts/oneinstack,请替换为你的实际路径

# 进入 OpenResty 源码目录
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 添加为动态模块
eval ./configure $COMPILEOPTIONS --add-dynamic-module=/tmp/compile/openresty-1.19.3.1/modules/ngx_http_geoip2_module/

image-20230729192055972

编译模块
#

上一步成功后,开始执行编译:

# 仅编译模块
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 使用,我们使用上面那个即可

配置 OpenResty 加载 GeoIP2 动态库
#

# 创建模块目录
mkdir -p /usr/local/openresty/nginx/modules

# 复制动态库文件,方便后续引用
cp -a objs/*.so /usr/local/openresty/nginx/modules/

# 编辑 Nginx 主配置文件,加入模块加载指令
vim /etc/nginx/nginx.conf

# 在配置文件开头添加以下行
load_module modules/ngx_http_geoip2_module.so;

image-20230729192420002

OpenResty 配置对接 GeoIP 数据库
#

模块加载后,实际使用还需要一个 GeoIP 数据库。到官网下载需要注册账号,有账号的可以通过官网下载。如果注册遇到问题,可以使用 GitHub 上的镜像仓库:

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

下载并配置 GeoIP2 数据库
#

下载 GeoIP2 数据库到 /etc/nginx/geoip2 并配置加载:

# 在 Nginx 主配置文件的 http 段中加入以下内容
vim /etc/nginx/nginx.conf

# GeoIP 配置
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

配置日志格式
#

按照 Dashboard 文档配置日志格式 json_analytics

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

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

处理真实 IP 获取问题
#

⚠️ 注意:这里日志所返回的 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/

# 创建 docker-compose.yaml 文件
cat > docker-compose.yaml << EOF
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
EOF

创建 Promtail 配置文件
#

创建 config/config.yml 配置文件:

注意:更改 USER_IDTOKEN 为你在 Grafana Cloud 页面生成的实际值,登录 Cloud 后找到 Loki 相关配置

mkdir -p config

cat > config/config.yml << EOF
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
EOF

image-20230729202401293

启动 Promtail 日志收集
#

docker-compose up -d

image-20230729202633653

配置 Dashboard
#

回到 Grafana Cloud 主页,点击进入 Grafana。

image-20230729203117680

加载 Dashboard
#

按照以下步骤导入 Dashboard:

image-20230729203251382

image-20230729203335944

image-20230729203418924

image-20230729203441700

调整变量和 Dashboard
#

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

image-20230729203635081

image-20230729203653434

image-20230729203714291

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

image-20230729203842784

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

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 并使用微服务模式进行部署,但始终无法达到 Cloud 上的使用体验。

总体来说,使用 Loki 统计博客日志的整体体验很不错,通过本文的配置,我们成功实现了:

项目成果
#

  1. 完整的日志收集链路:从 OpenResty 日志格式配置到 Promtail 收集,再到 Loki 存储
  2. 丰富的可视化面板:包含 PV/UV 统计、地理分布、访问趋势等关键指标
  3. 成本控制:基于 Grafana Cloud 免费版本,无需自建基础设施
  4. 实时监控:支持实时的访问数据分析和告警

遗留问题
#

经过这次实践,还有几个问题有待解决:

1. UV 区间增长统计问题
#

统计 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

2. GeoIP 城市信息获取问题
#

加载 GeoLite2-City.mmdb 库时,无法获取正确的城市名称(目前暂时用不到,后续可能会需要)。

3. Promtail 时区问题
#

Promtail 打印日志时存在时区问题,目前只能通过修改源码并重新编译来解决。

后续优化方向
#

  1. 完善 LogQL 查询:优化 UV 统计的查询语句,解决重叠问题
  2. 扩展地理信息:研究城市级别的地理位置统计
  3. 告警配置:基于访问异常情况配置告警规则
  4. 性能优化:优化日志格式和查询性能

这套基于 Loki 的网站访问统计系统为个人网站提供了企业级的监控能力,同时保持了轻量化和成本效益的优势。

相关文章

Gitea Actions ActRunner 基于 Systemd 部署安装
·142 字·1 分钟
SRE Devops Linux
Sonatype Nexus Repository(Nexus3) 私服文件下载至本地 - (使用进阶篇 一)
·198 字·1 分钟
SRE Nexus3 Devops
Linux LVM 分区扩容完整指南
·827 字·4 分钟
SRE Linux Lvm Storage