强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 各项指标的分析方法,帮助你从数据中提取有价值的业务洞察。

09 - 指标分析


扩展阅读