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

GoAccess 日志分析完全指南 / 06 - HTML 报告

06 - HTML 报告

6.1 概述

GoAccess 生成的 HTML 报告是一个完全自包含的单文件,包含所有 CSS、JavaScript 和数据,无需外部依赖即可在任何现代浏览器中打开。这使得报告的分享和部署极为简便。

HTML 报告的核心特性:

  • 📦 自包含:所有资源内嵌在一个 HTML 文件中
  • 📊 交互式:支持排序、分页、搜索、面板切换
  • 📱 响应式:适配桌面和移动设备
  • 🎨 可定制:支持主题、配色、布局调整
  • 📤 可分享:通过文件、链接或嵌入方式分发

6.2 生成基本报告

# 最基本的报告生成
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  -o report.html

# 带标题的报告
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  -o report.html \
  --html-title="网站访问分析报告"

# 指定报告标题
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  -o report.html \
  --html-title="2026年5月报告" \
  --html-report-title="详细统计"

6.3 自定义样式与主题

6.3.1 使用内置主题

GoAccess 提供多种内置主题,通过 --html-prefs 参数设置:

# Bright 主题(亮色)
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"bright"}'

# Dark 主题(暗色)
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"darkGreen"}'

# Mono Green
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"monoGreen"}'

# Mono Blue
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"monoBlue"}'

# Mono Pink
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"monoPink"}'

# Monokai(暗色经典)
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"monokai"}'

# Dracula
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"dracula"}'

# Classic(经典)
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"classic"}'

6.3.2 主题效果对比

主题 背景色 适用场景
bright 白色 打印、正式报告
darkGreen 深绿 长时间阅读、暗色环境
monoGreen 黑底绿字 极客风格、终端爱好者
monoBlue 黑底蓝字 专业风格
monoPink 黑底粉字 个性化
monokai 深灰 编辑器用户熟悉
dracula 深紫 暗色主题爱好者
classic 浅灰 传统风格

6.3.3 完整 HTML 偏好设置

{
  "theme": "bright",
  "perPage": 20,
  "layout": "horizontal",
  "showIndianMap": false,
  "visitors": {
    "sortBy": "hits",
    "sortOrder": "desc"
  },
  "requests": {
    "sortBy": "hits",
    "sortOrder": "desc"
  },
  "status_codes": {
    "sortBy": "hits",
    "sortOrder": "desc"
  }
}
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"darkGreen","perPage":50,"layout":"horizontal"}'

6.3.4 自定义 CSS 样式

由于 HTML 报告是自包含的,你可以在生成后注入自定义 CSS:

#!/bin/bash
# generate_report.sh — 生成带自定义样式的报告

# 生成报告
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  -o /tmp/goaccess_raw.html

# 注入自定义 CSS
cat > /tmp/custom_style.css << 'CSS'
/* 自定义样式覆盖 */
body {
  font-family: 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
}

#dashboard .grid .item {
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

table th {
  background-color: #2c3e50 !important;
  color: white;
}
CSS

# 合并 CSS 到 HTML
sed -i '/<\/style>/i\
/* Custom CSS */' /tmp/goaccess_raw.html
sed -i "/<\/style>/r /tmp/custom_style.css" /tmp/goaccess_raw.html

# 添加中文字体支持
sed -i 's|<head>|<head><link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700\&display=swap" rel="stylesheet">|' /tmp/goaccess_raw.html

mv /tmp/goaccess_raw.html /var/www/html/report.html
echo "报告已生成: /var/www/html/report.html"

6.4 图表配置

GoAccess 的 HTML 报告包含多种交互式图表:

6.4.1 内置图表类型

图表类型 展示内容 面板
面积图 (Area Chart) 访问量随时间变化趋势 通用仪表盘
折线图 (Line Chart) 访客数、带宽趋势 通用仪表盘
水平柱状图 (Bar Chart) 文件排行、IP 排行 请求文件、主机面板
饼图 (Pie Chart) 状态码分布、浏览器分布 状态码、浏览器面板
数据表格 (Table) 详细数据列表 所有面板

6.4.2 图表交互

HTML 报告中的图表支持以下交互操作:

  • 鼠标悬停:显示详细数据
  • 点击图例:切换显示/隐藏数据系列
  • 分页浏览:大数据集分页显示
  • 排序:点击表头排序
  • 搜索:在表格中搜索关键词

6.5 报告自动化生成

6.5.1 使用 Cron 定时生成

# 编辑 crontab
crontab -e
# 每天凌晨 1 点生成前一天的日报
0 1 * * * /usr/local/bin/goaccess /var/log/nginx/access.log --log-format=COMBINED -o /var/www/html/daily/$(date +\%Y-\%m-\%d).html --process-and-exit 2>&1

# 每周一凌晨 2 点生成周报(合并上周日志)
0 2 * * 1 /usr/local/bin/cat /var/log/nginx/access.log.1 | /usr/local/bin/goaccess --log-format=COMBINED -o /var/www/html/weekly/$(date +\%Y-\%W).html --process-and-exit 2>&1

# 每月 1 日凌晨 3 点生成月报
0 3 1 * * /usr/local/bin/zcat /var/log/nginx/access.log.*.gz | /usr/local/bin/goaccess --log-format=COMBINED -o /var/www/html/monthly/$(date +\%Y-\%m).html --process-and-exit 2>&1

6.5.2 完整的自动化脚本

#!/bin/bash
# auto_report.sh — GoAccess 自动化报告生成脚本

set -euo pipefail

# ============ 配置 ============
LOG_DIR="/var/log/nginx"
REPORT_DIR="/var/www/html/stats"
GOACCESS="/usr/local/bin/goaccess"
LOG_FORMAT="COMBINED"
DATE=$(date +%Y-%m-%d)
YESTERDAY=$(date -d "yesterday" +%Y-%m-%d)

# ============ 创建目录 ============
mkdir -p "${REPORT_DIR}/daily"
mkdir -p "${REPORT_DIR}/weekly"
mkdir -p "${REPORT_DIR}/monthly"

# ============ 生成日报 ============
echo "[${DATE}] 生成日报..."

# 过滤昨天的日志
awk -v date="$(date -d 'yesterday' +'%d/%b/%Y')" '$0 ~ date' \
  "${LOG_DIR}/access.log" | \
  ${GOACCESS} --log-format="${LOG_FORMAT}" \
  -o "${REPORT_DIR}/daily/${YESTERDAY}.html" \
  --html-title="日报 - ${YESTERDAY}" \
  --process-and-exit - 2>/dev/null

echo "[${DATE}] 日报已生成: ${REPORT_DIR}/daily/${YESTERDAY}.html"

# ============ 生成周报(每周一) ============
DOW=$(date +%u)  # 1=Monday, 7=Sunday
if [ "${DOW}" = "1" ]; then
    WEEK=$(date -d "last week" +%Y-W%V)
    echo "[${DATE}] 生成周报 ${WEEK}..."

    cat "${LOG_DIR}/access.log".1 "${LOG_DIR}/access.log" | \
      ${GOACCESS} --log-format="${LOG_FORMAT}" \
      -o "${REPORT_DIR}/weekly/${WEEK}.html" \
      --html-title="周报 - ${WEEK}" \
      --process-and-exit - 2>/dev/null

    echo "[${DATE}] 周报已生成: ${REPORT_DIR}/weekly/${WEEK}.html"
fi

# ============ 生成月报(每月 1 日) ============
DOM=$(date +%d)
if [ "${DOM}" = "01" ]; then
    MONTH=$(date -d "last month" +%Y-%m)
    echo "[${DATE}] 生成月报 ${MONTH}..."

    zcat "${LOG_DIR}/access.log"*.gz | \
      ${GOACCESS} --log-format="${LOG_FORMAT}" \
      -o "${REPORT_DIR}/monthly/${MONTH}.html" \
      --html-title="月报 - ${MONTH}" \
      --process-and-exit - 2>/dev/null

    echo "[${DATE}] 月报已生成: ${REPORT_DIR}/monthly/${MONTH}.html"
fi

# ============ 清理旧报告 ============
# 保留最近 90 天的日报
find "${REPORT_DIR}/daily" -name "*.html" -mtime +90 -delete 2>/dev/null

echo "[${DATE}] 报告生成完成"

6.5.3 使用 systemd Timer 定时执行

# /etc/systemd/system/goaccess-report.service
[Unit]
Description=GoAccess Daily Report Generator
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/auto_report.sh
User=www-data
Group=www-data
# /etc/systemd/system/goaccess-report.timer
[Unit]
Description=Run GoAccess report daily

[Timer]
OnCalendar=*-*-* 01:00:00
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now goaccess-report.timer

# 查看定时器状态
sudo systemctl list-timers goaccess-report.timer

6.6 报告分享策略

6.6.1 通过 Web 服务器分享

# Nginx 配置 — 统计报告站点
server {
    listen 443 ssl;
    server_name stats.example.com;

    ssl_certificate /etc/ssl/certs/stats.pem;
    ssl_certificate_key /etc/ssl/private/stats.key;

    root /var/www/html/stats;
    index index.html;

    # Basic Auth 保护
    auth_basic "Statistics";
    auth_basic_user_file /etc/nginx/.htpasswd;

    # 目录浏览
    autoindex on;
    autoindex_exact_size off;
    autoindex_localtime on;
}
# 创建 htpasswd 文件
sudo htpasswd -c /etc/nginx/.htpasswd admin

6.6.2 通过邮件发送报告

#!/bin/bash
# email_report.sh — 通过邮件发送 GoAccess 报告

REPORT_PATH="/var/www/html/stats/daily/$(date -d yesterday +%Y-%m-%d).html"
RECIPIENT="[email protected]"
SUBJECT="GoAccess 日报 - $(date -d yesterday +%Y-%m-%d)"

if [ -f "${REPORT_PATH}" ]; then
    mail -s "${SUBJECT}" \
         -a "Content-Type: text/html; charset=UTF-8" \
         "${RECIPIENT}" < "${REPORT_PATH}"
    echo "报告已发送至 ${RECIPIENT}"
else
    echo "报告文件不存在: ${REPORT_PATH}"
    exit 1
fi

6.6.3 嵌入到现有页面

<!-- 将 GoAccess 报告嵌入 iframe -->
<iframe
  src="/stats/daily/2026-05-09.html"
  width="100%"
  height="800px"
  frameborder="0"
  style="border: 1px solid #ddd; border-radius: 4px;">
</iframe>

6.6.4 批量导出历史报告

#!/bin/bash
# batch_export.sh — 批量导出历史日志为 HTML 报告

LOG_DIR="/var/log/nginx"
OUTPUT_DIR="/var/www/html/stats/archive"
mkdir -p "${OUTPUT_DIR}"

# 处理所有压缩的日志文件
for logfile in "${LOG_DIR}"/access.log.*.gz; do
    filename=$(basename "${logfile}" .gz)
    date_part=$(echo "${filename}" | grep -oP '\d{8}' || echo "unknown")
    output="${OUTPUT_DIR}/${date_part}.html"

    if [ ! -f "${output}" ]; then
        echo "处理: ${logfile}${output}"
        zcat "${logfile}" | \
          goaccess --log-format=COMBINED \
          -o "${output}" \
          --html-title="历史报告 - ${date_part}" \
          --process-and-exit - 2>/dev/null
    else
        echo "跳过: ${output} 已存在"
    fi
done

echo "批量导出完成"

6.7 生成 JSON/CSV 报告

6.7.1 JSON 报告

# 生成 JSON 格式报告
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  -o report.json

# 输出到标准输出(便于管道处理)
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  -o - \
  --no-global-config | jq '.general'

6.7.2 CSV 报告

# 生成 CSV 报告
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  -o report.csv

# CSV 报告包含所有面板的数据,用注释分隔各面板

6.7.3 多格式同时输出

# 同时生成 HTML 和 JSON(使用两次调用)
goaccess access.log --log-format=COMBINED -o report.html
goaccess access.log --log-format=COMBINED -o report.json

# 使用持久化避免重复解析
goaccess access.log --log-format=COMBINED -o /tmp/db --persist
goaccess /tmp/db --restore -o report.html
goaccess /tmp/db --restore -o report.json
goaccess /tmp/db --restore -o report.csv

6.8 报告索引页

当有多个报告文件时,可以生成一个索引页方便浏览:

#!/bin/bash
# generate_index.sh — 生成报告索引页

REPORT_DIR="/var/www/html/stats"
INDEX="${REPORT_DIR}/index.html"

cat > "${INDEX}" << 'HTML'
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>网站访问统计报告</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Noto Sans SC', sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 800px; margin: 0 auto; }
        h1 { color: #2c3e50; margin-bottom: 30px; text-align: center; }
        .section { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .section h2 { color: #34495e; margin-bottom: 15px; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
        .report-list { list-style: none; }
        .report-list li { padding: 8px 0; border-bottom: 1px solid #eee; }
        .report-list li:last-child { border-bottom: none; }
        .report-list a { color: #3498db; text-decoration: none; font-size: 15px; }
        .report-list a:hover { text-decoration: underline; }
        .report-list .date { color: #7f8c8d; font-size: 13px; margin-left: 10px; }
    </style>
</head>
<body>
    <div class="container">
        <h1>📊 网站访问统计报告</h1>

        <div class="section">
            <h2>📋 月报</h2>
            <ul class="report-list" id="monthly">
                <!-- 由脚本动态生成 -->
            </ul>
        </div>

        <div class="section">
            <h2>📅 周报</h2>
            <ul class="report-list" id="weekly">
                <!-- 由脚本动态生成 -->
            </ul>
        </div>

        <div class="section">
            <h2>📆 日报</h2>
            <ul class="report-list" id="daily">
                <!-- 由脚本动态生成 -->
            </ul>
        </div>
    </div>
</body>
</html>
HTML

# 生成日报链接
echo '<script>' >> "${INDEX}"
echo 'const daily = [' >> "${INDEX}"
ls -r "${REPORT_DIR}/daily/"*.html 2>/dev/null | while read f; do
    name=$(basename "${f}" .html)
    echo "  { name: '${name}', path: 'daily/${name}.html' }," >> "${INDEX}"
done
echo '];' >> "${INDEX}"

echo 'const weekly = [' >> "${INDEX}"
ls -r "${REPORT_DIR}/weekly/"*.html 2>/dev/null | while read f; do
    name=$(basename "${f}" .html)
    echo "  { name: '${name}', path: 'weekly/${name}.html' }," >> "${INDEX}"
done
echo '];' >> "${INDEX}"

echo 'const monthly = [' >> "${INDEX}"
ls -r "${REPORT_DIR}/monthly/"*.html 2>/dev/null | while read f; do
    name=$(basename "${f}" .html)
    echo "  { name: '${name}', path: 'monthly/${name}.html' }," >> "${INDEX}"
done
echo '];' >> "${INDEX}"

cat >> "${INDEX}" << 'JS'
function render(list, containerId) {
    const ul = document.getElementById(containerId);
    if (!list.length) {
        ul.innerHTML = '<li style="color:#999">暂无数据</li>';
        return;
    }
    ul.innerHTML = list.map(item =>
        `<li><a href="${item.path}">${item.name}</a></li>`
    ).join('');
}
render(daily, 'daily');
render(weekly, 'weekly');
render(monthly, 'monthly');
</script>
</html>
JS

echo "索引页已生成: ${INDEX}"

6.9 报告数据导出与二次分析

6.9.1 导出为 JSON 后用 Python 分析

# 导出 JSON
goaccess /var/log/nginx/access.log --log-format=COMBINED -o report.json

# 用 Python 分析
python3 << 'PYEOF'
import json

with open('report.json', 'r') as f:
    data = json.load(f)

# 打印概览
g = data['general']
print(f"总请求数: {g['total_requests']}")
print(f"独立访客: {g['unique_visitors']}")
print(f"带宽消耗: {g['bandwidth'] / 1024 / 1024:.1f} MB")

# Top 10 请求文件
print("\nTop 10 请求文件:")
for item in data['requests']['data'][:10]:
    print(f"  {item['data']}: {item['hits']['count']} 次")

# 状态码分布
print("\n状态码分布:")
for item in data['status_codes']['data']:
    print(f"  {item['data']}: {item['hits']['count']} ({item['hits']['percent']}%)")
PYEOF

6.9.2 导入到数据库

# 将 JSON 数据导入 SQLite
goaccess /var/log/nginx/access.log --log-format=COMBINED -o report.json

python3 << 'PYEOF'
import json
import sqlite3

with open('report.json', 'r') as f:
    data = json.load(f)

conn = sqlite3.connect('goaccess.db')
c = conn.cursor()

# 创建表
c.execute('''CREATE TABLE IF NOT EXISTS visitors (
    ip TEXT, hits INTEGER, visitors INTEGER,
    hit_pct REAL, visitor_pct REAL
)''')

c.execute('''CREATE TABLE IF NOT EXISTS requests (
    file TEXT, hits INTEGER, visitors INTEGER,
    hit_pct REAL, visitor_pct REAL, bandwidth TEXT
)''')

c.execute('''CREATE TABLE IF NOT EXISTS status_codes (
    code TEXT, hits INTEGER, percent REAL
)''')

# 插入访客数据
for item in data['visitors']['data']:
    c.execute('INSERT INTO visitors VALUES (?,?,?,?,?)',
              (item['data'], item['hits']['count'],
               item['visitors']['count'],
               item['hits']['percent'],
               item['visitors']['percent']))

# 插入请求数据
for item in data['requests']['data']:
    c.execute('INSERT INTO requests VALUES (?,?,?,?,?,?)',
              (item['data'], item['hits']['count'],
               item['visitors']['count'],
               item['hits']['percent'],
               item['visitors']['percent'],
               item.get('bandwidth', {}).get('count', '0')))

# 插入状态码
for item in data['status_codes']['data']:
    c.execute('INSERT INTO status_codes VALUES (?,?,?)',
              (item['data'], item['hits']['count'],
               item['hits']['percent']))

conn.commit()
conn.close()
print("数据已导入 SQLite: goaccess.db")
PYEOF

6.10 小结

功能 命令/方法
基本报告 -o report.html
自定义主题 --html-prefs='{"theme":"bright"}'
自定义标题 --html-title="标题"
定时生成 Cron / systemd timer
安全分享 Nginx + Basic Auth
批量导出 遍历日志文件脚本
二次分析 JSON 导出 + Python/jq

下一章

下一章将详细介绍 GoAccess 的过滤与排除功能,包括日期范围过滤、状态码过滤、IP 排除和请求模式排除。

07 - 过滤与排除


扩展阅读