[博客翻译]如何仅使用Nginx和纯bash跟踪网站分析


原文地址:https://sanixdk.xyz/blogs/how-to-add-website-analytics-using-only-nginx


如何仅使用NGINX和纯BASH追踪网站分析数据

今天,我要和大家分享一个小技巧,仅使用bashnginx来实现类似Google Analytics的网站追踪服务。
你可能会问...“为什么要这么做?”;你看,很久以前,我决定只用C语言来制作我的个人网站。
没错,就是你现在正在浏览和阅读的这个网站;我使用了很多现有的“markdown -> html”C代码,并加入了自己的调味料(虽然有点乱,但我很喜欢)。
图片
查看仓库
这真的是一个非常非常非常糟糕的主意....我说“非常”够了吗?
为什么?嗯...基本上,我的大部分博客都是markdown格式的,为了符合“现代浏览互联网的方式”,我编写了一个lib.c来控制网站的构建(这篇文章不会深入讨论这个)。
但是,嘿,犯错让你成长...对吧?....对吧?
无论如何,构建一个只有html/markdown的东西的问题是,你没有涉及到JS...说实话,这是第一个原因(不是因为我讨厌现代JS 向我的JS朋友们眨眼);
因此,对我来说,Google Analytics是“不可能的”...
值得吗?绝对不值得,我还会再这么做吗?哦,当然会!
无论如何...让我们开始吧,好吗?

正式介绍

由于我只是在网站/博客构建后返回基本的html,没有“严肃的JS(除了博客底部的GitHub评论)”,我无法追踪谁、有多少人访问了这个页面或那个页面。
但是,我(仍然)使用nginx来处理请求部分,我问自己,如果这个服务基本上记录了访问日志,为什么不直接使用它来收集指标呢?(很合理,对吧?)。
因此,基于Nginx添加一个“分析系统”通常涉及配置Nginx以基本上只记录请求(带有足够的详细信息),处理这些日志以计算浏览量、IP、时间,并可能可视化数据。

如何“实际”做到这一点

在NGINX中启用日志记录

你知道,nginx默认记录请求,但你可能需要自定义日志格式以跟踪相关细节,例如URL或IP地址。
编辑你的Nginx配置文件,通常位于/etc/nginx/nginx.conf/etc/nginx/sites-available/<your-site>

$ vim /etc/nginx/nginx.conf
# 或者使用'nano'作为编辑器,如果你像我一样是个新手。

然后在http部分,

# 简单的浏览量示例
http {
  log_format views '$remote_addr - $remote_user [$time_local] "$request" '
           '$status $body_bytes_sent "$http_referer" '
           '"$http_user_agent" "$host" "$request_uri"';
  access_log /var/log/nginx/access.log views;
}
  • $remote_addr: 追踪客户端IP。
  • $host$request_uri: 捕获域名和URL。

重新加载Nginx以应用更改:

$ sudo systemctl reload nginx

可选地,你还可以检查你的nginx配置是否仍然正常:

$ nginx -t

下面是一个捕获更多跟踪信息的示例:

# 最详细的示例,比以前捕获更多信息
http {
  log_format analytics '$remote_addr - $remote_user [$time_local] '
            '"$request" $status $body_bytes_sent '
            '"$http_referer" "$http_user_agent" '
            '$request_time $upstream_response_time '
            '$scheme $server_name $request_method '
            '$ssl_protocol $ssl_cipher';
  access_log /var/log/nginx/access.log analytics;
}
  1. IP地址 : $remote_addr
  2. 用户名(如果使用基本认证): $remote_user
  3. 时间戳 : $time_local
  4. 原始请求 : $request(包括URL、查询、HTTP版本)
  5. HTTP状态 : $status
  6. 响应大小 : $body_bytes_sent
  7. 引用来源 : $http_referer
  8. 用户代理 : $http_user_agent
  9. 请求时间 : $request_time
  10. 上游时间 : $upstream_response_time
  11. 方案 : $scheme (http/https)
  12. 服务器名称 : $server_name
  13. 请求方法 : $request_method (GET/POST, 等)
  14. SSL协议 : $ssl_protocol (例如,TLSv1.3)
  15. SSL加密 : $ssl_cipher (例如,AES128-GCM-SHA256)

“瞧”,最重要的部分完成了,你现在所有的请求日志都会记录到配置中指定的路径,在我们的例子中是_/var/log/nginx/access.log_,它看起来像这样:

$ cat /var/log/nginx/access.log
18.220.122.122 - - [12/Jan/2025:17:15:19 +0000] "GET / HTTP/1.1" 400 666 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/126.0.0.0 Safari/537.36" "147.182.205.116" "/"$
54.36.148.9 - - [12/Jan/2025:17:16:15 +0000] "GET /robots.txt HTTP/1.1" 200 719 "-" "Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)" "sanixdk.xyz" "/robots.txt"$
51.222.253.18 - - [12/Jan/2025:17:16:16 +0000] "GET /blogs/how-to-make-a-password-generator-using-brainfuck-part-1-3 HTTP/1.1" 200 7569 "-" "Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)" "sanixdk.xyz" "/blogs/how-to-make-a-password-generator-using-brainfuck-part-1-3"$
124.44.90.81 - - [12/Jan/2025:17:22:41 +0000] "GET / HTTP/1.0" 404 162 "-" "curl/7.88.1" "147.182.205.116" "/"$
95.214.55.226 - - [12/Jan/2025:17:26:19 +0000] "GET / HTTP/1.1" 404 162 "-" "-" "147.182.205.116" "/"$

现在,我们有了这些日志,让我们来读取、解析、理解并利用它们。

现在,让我们做一些编程

你知道我喜欢bash...我是说,我热爱bash,为什么?
嗯,现在应该很明显了,但无论你使用什么Linux发行版...或者你刚刚安装的,它已经在那里了,不需要安装gcc,或python或任何东西...
因为它是shell,所以我们将使用它。
让我们分解每个步骤,从源代码到收集我们所需所有指标的服务。

源代码:

每条指令都尽可能详细地记录,如有问题请在评论区提问。

#!/usr/bin/env bash
#
# nginx-analytics.sh - 收集Nginx日志指标并输出JSON分析数据。
#
# 作者:dk (https://github.com/sanix-darker)
LOG_FILE="/var/log/nginx/access.log"
OUTPUT_FILE="/var/www/html/nginx-analytics.json"
# 安全检查:确保LOG_FILE存在
if [[ ! -f "$LOG_FILE" ]]; then
 echo "错误:在$LOG_FILE找不到Nginx日志文件"
 exit 1
fi
# 1) 按URL的访问量(前5)
#  - 使用'awk'从"$request"字段(假设它在$5)中提取URL。
#  - 然后将请求字符串“METHOD /some/url HTTP/1.x”拆分为令牌,
#   并保留第二个令牌(实际路径/some/url)。
visits_by_url=$(
 awk '{
  # $5可能是这样的:"GET /index.html HTTP/1.1"
  # 所以我们按空格拆分。
  split($5, req_parts, " ")
  url=req_parts[2]
  if (url != "") {
   urls[url]++
  }
 } END {
  # 按频率排序并选择前5
  for (u in urls) {
   print urls[u] " " u
  }
 }' "$LOG_FILE" \
 | sort -nr \
 | head -n 5
)
# 2) 前5个IP地址
top_ips=$(
 awk '{
  ips[$1]++
 } END {
  for (ip in ips) {
   print ips[ip] " " ip
  }
 }' "$LOG_FILE" \
 | sort -nr \
 | head -n 5
)
# 3) 平均请求时间($request_time),假设它是日志格式中的第10个字段
avg_request_time=$(
 awk '{
  sum+=$10; count++
 } END {
  if (count > 0) {
   printf("%.5f", sum/count)
  } else {
   print "0"
  }
 }' "$LOG_FILE"
)
# 4) 平均上游响应时间($upstream_response_time),假设它是日志格式中的第11个字段
avg_upstream_time=$(
 awk '{
  sum+=$11; count++
 } END {
  if (count > 0) {
   printf("%.5f", sum/count)
  } else {
   print "0"
  }
 }' "$LOG_FILE"
)
# 5) 前5个HTTP状态码,假设它是日志格式中的第6个字段
top_status_codes=$(
 awk '{
  status[$6]++
 } END {
  for (s in status) {
   print status[s] " " s
  }
 }' "$LOG_FILE" \
 | sort -nr \
 | head -n 5
)
# ------------------------------------------------------------
# 现在,不那么痛苦的部分,
# 将上述变量转换为可理解的JSON。
# 我们将进行最少的解析以生成有效的JSON数组/对象。
# ------------------------------------------------------------
# 'lines_to_json_array'将“计数 值”行转换为JSON数组条目
# 例如,“123 /home” -> { "value": "/home", "count": 123 }
function lines_to_json_array() {
 local input="$1"
 local result="["
 local first=1
 while IFS= read -r line; do
  # 这里我们只是将行拆分为“计数”和“值”
  count=$(echo "$line" | awk '{print $1}')
  value=$(echo "$line" | awk '{print $2}')
  # 重要提示:
  # 如果“值”有空格,请小心处理。(在简单情况下,不会有。)
  # 如果你有更复杂的解析,你会进行更健壮的拆分。
  if [ "$first" -eq 1 ]; then
   first=0
  else
   result="$result,"
  fi
  result="$result {\"value\":\"$value\",\"count\":$count}"
 done <<< "$input"
 result="$result]"
 echo "$result"
}
# visits_by_url -> JSON-array
visits_by_url_json=$(lines_to_json_array "$visits_by_url")
# top_ips -> JSON-array
top_ips_json=$(lines_to_json_array "$top_ips")
# top_status_codes -> JSON-array
top_status_codes_json=$(lines_to_json_array "$top_status_codes")
# 瞧,我们的json准备好了
json_output=$(
 cat <<EOF
{
 "visits_by_url": $visits_by_url_json,
 "top_ips": $top_ips_json,
 "avg_request_time": "$avg_request_time",
 "avg_upstream_time": "$avg_upstream_time",
 "top_status_codes": $top_status_codes_json
}
EOF
)
# 将JSON写入OUTPUT_FILE(例如,用于Web仪表板或进一步处理)
echo "$json_output" > "$OUTPUT_FILE"
# 或者简单地输出到stdout,如果你喜欢:
# echo "$json_output"
exit 0 # 你可以选择退出其他内容,你的代码,你选择 :wink:

将以下内容保存到/usr/local/bin/nginx-analytics.sh(根据需要调整路径)
**注意:**别忘了让它可执行

$ sudo chmod +x /usr/local/bin/nginx-analytics.sh

创建一个SYSTEMD单元

现在我们有了脚本,我们需要让它作为一个“服务”运行,这样如果服务器启动,我们仍然可以在后台启动它,所以下面是一个示例systemd服务文件,用于将此脚本作为一次性服务运行。创建一个文件/etc/systemd/system/nginx-analytics.service

[Unit]
Description=收集Nginx日志指标并输出JSON
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/nginx-analytics.sh
[Install]
WantedBy=multi-user.target

现在,_重新加载systemd_以注册新服务:

sudo systemctl daemon-reload

然后,手动运行服务(按需):

sudo systemctl start nginx-analytics.service

最后_启用启动时运行_(如果需要):

sudo systemctl enable nginx-analytics.service

(可选...但我强烈推荐)设置一个SYSTEMD计时器

如果你希望此脚本定期运行(例如,每5分钟或每小时,或者每秒钟为那些疯狂的读者),创建一个匹配的.timer单元。例如,/etc/systemd/system/nginx-analytics.timer

[Unit]
Description=定期运行nginx-analytics服务
[Timer]
OnBootSec=5min
OnUnitActiveSec=1h
[Install]
WantedBy=timers.target

然后:

sudo systemctl daemon-reload
sudo systemctl enable --now nginx-analytics.timer

这将每小时运行一次nginx-analytics.service(从而运行nginx-analytics.sh),并在启动后5分钟运行。

想看结果吗???

每次运行后,你