GoAccess 日志分析完全指南 / 08 - 自定义日志处理
08 - 自定义日志处理
8.1 概述
在实际生产环境中,日志文件的管理远比单个文件的分析复杂。本章将介绍 GoAccess 处理以下复杂场景的方法:
- 多虚拟主机日志的独立分析与聚合
- 合并多个日志文件进行统一分析
- 处理日志轮转(Log Rotation)后的历史日志
- 通过管道、FIFO、脚本等方式灵活输入日志
8.2 多虚拟主机日志分析
8.2.1 独立日志文件
最常见的配置是每个虚拟主机使用独立的日志文件:
# Nginx 配置
server {
server_name site1.example.com;
access_log /var/log/nginx/site1/access.log combined;
}
server {
server_name site2.example.com;
access_log /var/log/nginx/site2/access.log combined;
}
# 分析 site1
goaccess /var/log/nginx/site1/access.log --log-format=COMBINED \
-o site1_report.html --html-title="Site1 访问报告"
# 分析 site2
goaccess /var/log/nginx/site2/access.log --log-format=COMBINED \
-o site2_report.html --html-title="Site2 访问报告"
8.2.2 统一日志文件中的虚拟主机
当日志中包含虚拟主机字段时,需要先过滤再分析:
# Nginx 配置 — 带虚拟主机字段
log_format vhost_combined '$host $remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
日志示例:
site1.example.com 10.0.0.1 - - [10/May/2026:14:30:15 +0800] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0"
site2.example.com 10.0.0.2 - - [10/May/2026:14:30:16 +0800] "GET / HTTP/1.1" 200 512 "-" "Mozilla/5.0"
# 方法一:使用 grep 过滤
grep "^site1.example.com" /var/log/nginx/access.log | \
awk '{$1=""; print substr($0,2)}' | \
goaccess --log-format=COMBINED -
# 方法二:使用 VCOMBINED 格式(虚拟主机格式)
goaccess /var/log/nginx/access.log --log-format=VCOMBINED
# 方法三:自定义格式
goaccess /var/log/nginx/access.log \
--log-format='%^ %h - %^ [%d:%t %^] "%r" %s %b "%R" "%u"' \
--date-format=%d/%b/%Y \
--time-format=%H:%M:%S
8.2.3 批量分析所有虚拟主机
#!/bin/bash
# analyze_vhosts.sh — 批量分析所有虚拟主机
LOG_BASE="/var/log/nginx"
REPORT_DIR="/var/www/html/stats/vhosts"
mkdir -p "${REPORT_DIR}"
for vhost_dir in "${LOG_BASE}"/*/; do
vhost=$(basename "${vhost_dir}")
logfile="${vhost_dir}access.log"
if [ -f "${logfile}" ]; then
echo "分析虚拟主机: ${vhost}"
goaccess "${logfile}" --log-format=COMBINED \
-o "${REPORT_DIR}/${vhost}.html" \
--html-title="${vhost} 访问报告" \
--process-and-exit 2>/dev/null
echo " → ${REPORT_DIR}/${vhost}.html"
else
echo "跳过 ${vhost}: 日志文件不存在"
fi
done
# 生成汇总报告
echo "生成汇总报告..."
cat "${LOG_BASE}"/*/access.log | \
goaccess --log-format=COMBINED \
-o "${REPORT_DIR}/all_vhosts.html" \
--html-title="所有虚拟主机汇总" \
--process-and-exit -
echo "分析完成"
8.3 合并日志文件
8.3.1 合并多个日志文件
# 方法一:使用 cat 合并
cat /var/log/nginx/access.log.1 /var/log/nginx/access.log | \
goaccess --log-format=COMBINED -
# 方法二:使用 GoAccess 直接指定多个文件
goaccess /var/log/nginx/access.log.1 /var/log/nginx/access.log \
--log-format=COMBINED
# 方法三:使用通配符
goaccess /var/log/nginx/access.log* --log-format=COMBINED
8.3.2 合并压缩和未压缩的日志
# 合并压缩的历史日志和当前日志
zcat /var/log/nginx/access.log.*.gz | \
cat - /var/log/nginx/access.log | \
goaccess --log-format=COMBINED -
# 或使用 process substitution
goaccess <(zcat /var/log/nginx/access.log.*.gz) /var/log/nginx/access.log \
--log-format=COMBINED
8.3.3 合并多台服务器的日志
在分析分布式部署的网站时,需要先收集各节点的日志再合并分析:
#!/bin/bash
# merge_server_logs.sh — 合并多台服务器的日志
SERVERS=("web1.example.com" "web2.example.com" "web3.example.com")
REMOTE_LOG="/var/log/nginx/access.log"
LOCAL_DIR="/tmp/merged_logs"
REPORT="/var/www/html/report.html"
mkdir -p "${LOCAL_DIR}"
# 从各服务器下载日志
for server in "${SERVERS[@]}"; do
echo "下载 ${server} 日志..."
scp "${server}:${REMOTE_LOG}" "${LOCAL_DIR}/access_${server}.log"
done
# 按时间排序合并(可选,确保日志顺序正确)
sort -t '[' -k2 "${LOCAL_DIR}"/access_*.log > "${LOCAL_DIR}/merged.log"
# 分析合并后的日志
goaccess "${LOCAL_DIR}/merged.log" \
--log-format=COMBINED \
-o "${REPORT}" \
--html-title="全站访问报告"
echo "报告已生成: ${REPORT}"
# 清理
rm -rf "${LOCAL_DIR}"
注意:合并日志时,IP 地址的去重将不再准确(同一用户可能从不同节点访问)。如果需要精确的 UV 统计,应使用 Cookie 或 Session ID 作为访客标识。
8.3.4 使用 rsync + SSH 实时合并
#!/bin/bash
# realtime_merge.sh — 实时合并多台服务器的日志
SERVERS=("web1.example.com" "web2.example.com" "web3.example.com")
FIFO="/tmp/merged_access.fifo"
# 创建 FIFO
mkfifo "${FIFO}" 2>/dev/null
# 在后台通过 SSH 实时获取各服务器的日志
for server in "${SERVERS[@]}"; do
ssh "${server}" "tail -f /var/log/nginx/access.log" >> "${FIFO}" &
done
# GoAccess 读取合并后的数据
goaccess "${FIFO}" --log-format=COMBINED
# 清理
rm -f "${FIFO}"
kill %1 %2 %3 2>/dev/null
8.4 日志轮转处理
8.4.1 理解 Logrotate
Linux 系统通常使用 logrotate 管理日志文件的轮转:
access.log ← 当前日志(正在写入)
access.log.1 ← 昨天的日志
access.log.2.gz ← 前天的日志(已压缩)
access.log.3.gz ← 大前天的日志(已压缩)
...
典型的 logrotate 配置 /etc/logrotate.d/nginx:
/var/log/nginx/*.log {
daily
missingok
rotate 52
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
endscript
}
8.4.2 分析轮转后的历史日志
# 分析昨天的日志(未压缩)
goaccess /var/log/nginx/access.log.1 --log-format=COMBINED
# 分析压缩的历史日志
zcat /var/log/nginx/access.log.2.gz | goaccess --log-format=COMBINED -
# 分析最近 7 天的日志
zcat /var/log/nginx/access.log.*.gz | \
cat - /var/log/nginx/access.log.1 /var/log/nginx/access.log | \
goaccess --log-format=COMBINED -
# 按日期范围过滤(分析特定日期的历史日志)
zcat /var/log/nginx/access.log.*.gz | \
cat - /var/log/nginx/access.log | \
awk '/05\/May\/2026/' | \
goaccess --log-format=COMBINED -
8.4.3 增量日志处理
GoAccess 支持持久化解析状态,实现增量处理:
# 第一次运行:解析所有历史日志
goaccess /var/log/nginx/access.log \
--log-format=COMBINED \
-o /tmp/goaccess.db \
--persist
# 后续运行:仅解析新增的日志
goaccess /var/log/nginx/access.log \
--log-format=COMBINED \
-o /tmp/goaccess.db \
--persist
# 从持久化数据生成报告
goaccess /tmp/goaccess.db \
--restore \
-o report.html
8.4.4 配合 Logrotate 的自动化脚本
#!/bin/bash
# postrotate_goaccess.sh — 在 logrotate 后运行
LOG_FILE="/var/log/nginx/access.log"
DB_FILE="/var/lib/goaccess/state.db"
REPORT="/var/www/html/report.html"
# 增量解析并生成报告
goaccess "${LOG_FILE}" \
--log-format=COMBINED \
-o "${DB_FILE}" \
--persist
goaccess "${DB_FILE}" \
--restore \
-o "${REPORT}" \
--html-title="实时访问报告"
# 清理旧的持久化数据(如果需要重新开始)
# rm -f "${DB_FILE}"
将此脚本添加到 logrotate 的 postrotate:
/var/log/nginx/*.log {
daily
...
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
/usr/local/bin/postrotate_goaccess.sh &
endscript
}
8.5 管道输入
8.5.1 基本管道用法
# 从 cat 输入
cat /var/log/nginx/access.log | goaccess --log-format=COMBINED -
# 从 tail 实时输入
tail -f /var/log/nginx/access.log | goaccess --log-format=COMBINED -
# 从 zcat 输入压缩文件
zcat /var/log/nginx/access.log.*.gz | goaccess --log-format=COMBINED -
# 从 awk 过滤后输入
awk '/2026\/May/' /var/log/nginx/access.log | goaccess --log-format=COMBINED -
# 从 sed 处理后输入
sed 's/10.0.0.1/INTERNAL_IP/g' /var/log/nginx/access.log | goaccess --log-format=COMBINED -
8.5.2 处理非标准输入
# 从远程服务器实时分析
ssh web1.example.com "tail -f /var/log/nginx/access.log" | \
goaccess --log-format=COMBINED -
# 从多个远程服务器合并
ssh web1 "tail -f /var/log/nginx/access.log" &
ssh web2 "tail -f /var/log/nginx/access.log" &
wait | goaccess --log-format=COMBINED -
# 从 Docker 容器中读取日志
docker logs -f nginx_container | goaccess --log-format=COMBINED -
# 从 journalctl 读取
journalctl -u nginx --since "1 hour ago" -o cat | \
goaccess --log-format=COMBINED -
8.5.3 预处理管道
在将日志传给 GoAccess 之前进行预处理:
# 场景:日志中包含 ANSI 颜色码,需要先清理
cat /var/log/nginx/access.log | \
sed 's/\x1b\[[0-9;]*m//g' | \
goaccess --log-format=COMBINED -
# 场景:日志格式不标准,需要转换
cat custom_access.log | \
awk -F'|' '{print $1, "-", "-", "[" $2 ":" $3 " +0800] \"", $4, $5, "HTTP/1.1\"", $6, $7, "\"-\"", "\"-\""}' | \
goaccess --log-format=COMBINED -
# 场景:合并多种格式的日志
{
cat /var/log/nginx/access.log
awk '{gsub(/timestamp/, "time"); print}' /var/log/app/app.log
} | goaccess --log-format=COMBINED -
8.6 命名管道(FIFO)高级用法
8.6.1 使用 FIFO 实现生产者-消费者模式
#!/bin/bash
# fifo_pipeline.sh — 使用 FIFO 实现日志分析管道
INPUT_FIFO="/tmp/goaccess_input.fifo"
OUTPUT_FIFO="/tmp/goaccess_output.fifo"
# 清理旧的 FIFO
rm -f "${INPUT_FIFO}" "${OUTPUT_FIFO}"
# 创建 FIFO
mkfifo "${INPUT_FIFO}"
mkfifo "${OUTPUT_FIFO}"
# 启动 GoAccess(消费者)
goaccess "${INPUT_FIFO}" \
--log-format=COMBINED \
-o "${OUTPUT_FIFO}" \
--real-time-html &
GOACCESS_PID=$!
# 启动输出处理(消费者 2)
cat "${OUTPUT_FIFO}" > /var/www/html/report.html &
CAT_PID=$!
# 启动日志输入(生产者)
tail -f /var/log/nginx/access.log > "${INPUT_FIFO}" &
TAIL_PID=$!
# 等待信号
trap "kill ${GOACCESS_PID} ${CAT_PID} ${TAIL_PID}; rm -f ${INPUT_FIFO} ${OUTPUT_FIFO}" EXIT
echo "GoAccess 实时分析已启动 (PID: ${GOACCESS_PID})"
echo "报告: /var/www/html/report.html"
wait
8.6.2 多路复用 FIFO
# 使用 tee 将日志同时发送给多个处理器
tail -f /var/log/nginx/access.log | \
tee >(grep " 500 " > /var/log/500_errors.log) | \
tee >(grep " 404 " > /var/log/404_errors.log) | \
goaccess --log-format=COMBINED -
8.7 处理特殊日志格式
8.7.1 JSON 格式日志
# Nginx JSON 格式日志
# 需要先将 JSON 转换为 GoAccess 能识别的格式
cat access.json | \
jq -r '[.remote_addr, .time_local, .request, .status, .body_bytes_sent,
(.http_referer // "-"), (.http_user_agent // "-")]
| join(" | ")' | \
goaccess --log-format='%h | [%d:%t %^] | "%r" | %s | %b | "%R" | "%u"' \
--date-format=%d/%b/%Y \
--time-format=%H:%M:%S -
8.7.2 多行日志
某些应用日志可能跨多行,需要先合并为单行:
# 使用 awk 将多行日志合并为单行
awk '
/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ {
if (line) print line
line = $0
next
}
{ line = line " " $0 }
END { if (line) print line }
' /var/log/nginx/access.log | goaccess --log-format=COMBINED -
8.7.3 带时间戳偏差的日志
# 某些日志的时间戳可能不标准,需要预处理
cat access.log | \
sed 's/\[2026-05-10T14:30:15+08:00\]/[10\/May\/2026:14:30:15 +0800]/g' | \
goaccess --log-format=COMBINED -
8.8 日志量过大时的处理策略
8.8.1 采样分析
当日志量过大(> 10GB)时,可以使用采样策略:
# 随机采样 10% 的日志
awk 'rand() < 0.1' /var/log/nginx/access.log | \
goaccess --log-format=COMBINED -
# 每隔 N 行取一行
awk 'NR % 10 == 0' /var/log/nginx/access.log | \
goaccess --log-format=COMBINED -
注意:采样分析会丢失精度,仅适用于趋势分析而非精确统计。
8.8.2 分片分析
# 将大日志分成小块分析
split -l 1000000 /var/log/nginx/access.log /tmp/log_chunk_
for chunk in /tmp/log_chunk_*; do
goaccess "${chunk}" --log-format=COMBINED \
-o "${chunk}.json" --process-and-exit
done
# 合并 JSON 结果(需要自定义脚本)
python3 merge_json.py /tmp/log_chunk_*.json > combined_report.json
8.8.3 增量模式
# 使用持久化实现增量处理
# 第一次:完整分析
goaccess huge_access.log --log-format=COMBINED \
-o /tmp/goaccess.db --persist
# 后续:增量追加
goaccess huge_access.log --log-format=COMBINED \
-o /tmp/goaccess.db --persist
# 随时生成报告
goaccess /tmp/goaccess.db --restore -o report.html
8.9 综合实战脚本
#!/bin/bash
# comprehensive_analysis.sh — 综合日志分析脚本
set -euo pipefail
# ============ 配置 ============
NGINX_LOG_DIR="/var/log/nginx"
APP_LOG_DIR="/var/log/app"
REPORT_BASE="/var/www/html/stats"
DATE=$(date +%Y-%m-%d)
REPORT_DIR="${REPORT_BASE}/${DATE}"
GOACCESS_FORMAT="COMBINED"
# 公共 GoAccess 参数
GOACCESS_OPTS=(
--log-format="${GOACCESS_FORMAT}"
--date-format=%d/%b/%Y
--time-format=%H:%M:%S
--process-and-exit
)
# ============ 准备 ============
mkdir -p "${REPORT_DIR}"
# ============ 分析主站日志 ============
echo "[1/5] 分析主站日志..."
# 合并所有 Nginx 日志(含轮转)
{
zcat "${NGINX_LOG_DIR}"/access.log.*.gz 2>/dev/null || true
cat "${NGINX_LOG_DIR}"/access.log 2>/dev/null || true
} | goaccess "${GOACCESS_OPTS[@]}" \
--html-title="主站全量报告 - ${DATE}" \
-o "${REPORT_DIR}/main.html" -
echo " → ${REPORT_DIR}/main.html"
# ============ 分析错误日志 ============
echo "[2/5] 分析错误日志..."
{
zcat "${NGINX_LOG_DIR}"/access.log.*.gz 2>/dev/null || true
cat "${NGINX_LOG_DIR}"/access.log 2>/dev/null || true
} | grep -E '" (4[0-9]{2}|5[0-9]{2}) ' | \
goaccess "${GOACCESS_OPTS[@]}" \
--html-title="错误分析 - ${DATE}" \
-o "${REPORT_DIR}/errors.html" -
echo " → ${REPORT_DIR}/errors.html"
# ============ 分析 API 日志 ============
echo "[3/5] 分析 API 日志..."
grep '"/api/' "${NGINX_LOG_DIR}"/access.log | \
goaccess "${GOACCESS_OPTS[@]}" \
--html-title="API 分析 - ${DATE}" \
--exclude='\.(css|js|jpg|png|gif|ico|svg|woff2?)$' \
-o "${REPORT_DIR}/api.html" -
echo " → ${REPORT_DIR}/api.html"
# ============ 分析应用日志 ============
echo "[4/5] 分析应用日志..."
if [ -f "${APP_LOG_DIR}/access.log" ]; then
goaccess "${APP_LOG_DIR}/access.log" \
"${GOACCESS_OPTS[@]}" \
--log-format='%h - %^ [%d:%t %^] "%r" %s %b "%R" "%u" %T' \
--html-title="应用层分析 - ${DATE}" \
-o "${REPORT_DIR}/app.html"
echo " → ${REPORT_DIR}/app.html"
fi
# ============ 生成汇总索引 ============
echo "[5/5] 生成索引页..."
cat > "${REPORT_DIR}/index.html" << EOF
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>日志分析 - ${DATE}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; padding: 40px 20px; }
.container { max-width: 600px; margin: 0 auto; }
h1 { color: white; text-align: center; margin-bottom: 30px;
font-size: 24px; text-shadow: 0 2px 4px rgba(0,0,0,0.2); }
.card { background: white; border-radius: 12px; padding: 24px;
margin-bottom: 16px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: transform 0.2s; }
.card:hover { transform: translateY(-2px); }
.card h2 { color: #333; margin-bottom: 12px; font-size: 18px; }
.card a { color: #667eea; text-decoration: none; font-weight: 500; }
.card a:hover { text-decoration: underline; }
.footer { text-align: center; color: rgba(255,255,255,0.7);
margin-top: 30px; font-size: 14px; }
</style>
</head>
<body>
<div class="container">
<h1>📊 日志分析报告 — ${DATE}</h1>
<div class="card"><h2>🌐 主站全量报告</h2><a href="main.html">查看报告 →</a></div>
<div class="card"><h2>⚠️ 错误分析</h2><a href="errors.html">查看报告 →</a></div>
<div class="card"><h2>🔌 API 分析</h2><a href="api.html">查看报告 →</a></div>
<div class="card"><h2>📱 应用层分析</h2><a href="app.html">查看报告 →</a></div>
<div class="footer">Generated by GoAccess · $(date)</div>
</div>
</body>
</html>
EOF
echo " → ${REPORT_DIR}/index.html"
echo ""
echo "=========================================="
echo " 所有报告已生成至: ${REPORT_DIR}/"
echo " 索引页: ${REPORT_DIR}/index.html"
echo "=========================================="
8.10 小结
| 场景 | 解决方案 |
|---|---|
| 多虚拟主机 | 独立日志文件 + grep 过滤,或 VCOMBINED 格式 |
| 合并日志 | cat / 通配符 / zcat 管道 |
| 日志轮转 | zcat *.gz | cat - current.log 合并 |
| 增量处理 | --persist + --restore 持久化模式 |
| 远程日志 | ssh tail -f + 管道 |
| 大文件 | 采样 / 分片 / 增量模式 |
| JSON 日志 | jq 预处理后传入 |
下一章
下一章将深入讲解 GoAccess 各项指标的分析方法,帮助你从数据中提取有价值的业务洞察。