日志分析工具 GoAccess 配置详细教程

GoAccess v1.9.4 几周前刚刚发布!让我们借此机会撰写一篇详尽的教程。我们将逐一讲解安装和使用 GoAccess 的每个步骤。这篇教程专为那些并非每天从事系统管理工作的用户设计,因此内容较为冗长。我已尽力提供全面的解释,确保这不仅仅是一篇“复制粘贴”式的教程。而对于那些每天都做系统管理员的人:请尽量不要在阅读时睡着,如果发现这里有任何不准确的地方,请随时给我发邮件。谢谢!

简介

元素周期表那么 GoAccess是什么呢?GoAccess是一个网络日志分析工具,它允许你可视化网站的流量,并更深入地了解访客信息:访客数量、页面访问量、访客来源(地理位置、操作系统、浏览器等)。它通过解析网页服务器的访问日志实现这些功能,无论是Apache、NGINX还是其他服务器均适用。

GoAccess 提供了多种统计数据展示方式,本教程将重点介绍生成 HTML 报告的功能。这意味着您可以直接在网页浏览器中查看网站统计数据,以单个 HTML 页面形式呈现。

例如,您可以查看我的博客统计数据:https://goaccess.arnaudr.io

GoAccess 采用 C 语言编写,依赖项极少,已存在约 10 年,并以 MIT 许可证分发。

假设

本教程涉及安装和配置,因此假设所有命令均以 root 用户身份执行,无需在每个命令前添加 sudo

我使用的是 Apache 网页服务器,运行在 Debian 系统上。不过我认为这对本教程影响不大。如果你使用的是 NGINX,也没问题,可以继续阅读。

此外,我将使用 SITE 作为要使用 GoAccess 分析的网站名称。请将此处替换为你的网站真实名称。

我假设你的文件位于以下位置:

  • 网站位于 /var/www/SITE
  • 日志位于 /var/log/apache2/SITE(是的,有一个子目录)
  • GoAccess 数据库将保存于 /var/lib/goaccess-db/SITE

若您的文件位于 /srv/SITE/{log,www},无需担心,只需相应调整路径即可,相信您能轻松完成。

安装

GoAccess 的最新版本是 v1.9.4,目前尚未在 Debian 软件仓库中提供。因此,您可以按照官方 GoAccess 下载页面 中的说明进行操作。安装步骤已详细说明,因此我无需多言 😊

完成安装后,让我们开始基础设置。

我们这里讨论的是最新版本 v1.9.4,让我们确认一下:

$ goaccess --version
GoAccess - 1.9....

现在让我们尝试创建一个 HTML 报告。假设您已经有一个运行中的网站。

GoAccess需要解析访问日志。这些日志是可选的,可能由你的Web服务器生成,也可能不生成,这取决于其配置。通常,这些日志文件名为access.log,这并不令人意外。

你可以通过运行以下命令检查系统中是否存在这些日志:

find /var/log -name access.log

另一个需要了解的重要点是,这些日志可能采用不同格式。本教程假设我们使用的是组合日志格式,因为它似乎是最常见的默认格式。

要查看您的网页服务器生成何种访问日志,您需要查看网站的配置文件。

对于 Apache 网页服务器,您应在文件 /etc/apache2/sites-enabled/SITE.conf 中找到类似以下内容的行:

CustomLog ${APACHE_LOG_DIR}/SITE/access.log combined

对于NGINX,配置类似。配置文件通常为/etc/nginx/sites-enabled/SITE,启用访问日志的配置行类似于:

access_log /var/log/nginx/SITE/access.log

需要注意的是,NGINX默认以组合格式写入访问日志,因此在上面的行中你不会看到combined这个词:它是隐含的。

好,从现在起,我们假设您确实拥有访问日志文件,且这些文件采用组合日志格式。如果情况确实如此,那么您可以直接运行 GoAccess 并生成报告,例如针对日志文件 /var/log/apache2/access.log

goaccess 
    --log-format COMBINED 
    --output /tmp/report.html 
    /var/log/apache2/access.log

GoAccess 支持处理多个日志文件,因此如果你有例如 access.log.1 这样的文件,也可以使用它:

goaccess 
    --log-format COMBINED 
    --output /tmp/report.html 
    /var/log/apache2/access.log 
    /var/log/apache2/access.log.1

如果 GoAccess 成功运行(应该会成功),那么你已经走在了正确的轨道上!

要完成此测试,剩下的就是查看生成的 HTML 报告。这是一个单一的 HTML 页面,因此你可以轻松地将其 scp 到你的机器上,或者直接将其移动到网站的文档根目录,然后在网页浏览器中打开它。

看起来不错?那么让我们继续探讨更有趣的内容。

Web 服务器配置

这一部分非常简短,因为在Web服务器配置方面,几乎无需进行任何操作。如上所述,你从Web服务器唯一需要的是生成访问日志文件。然后你需要确保GoAccess和你的Web服务器在这些文件的格式上达成一致。

在上文我们使用了组合日志格式,但 GoAccess 默认支持多种常见日志格式,甚至允许解析自定义日志格式。如需更多细节,请参阅 GoAccess 手册页中的 --log-format 选项(https://goaccess.io/man#options)。

另一种常见日志格式名为common。它甚至拥有自己的维基百科页面。但与combined相比,common日志格式包含的信息较少,不包含referreruser-agent值,这意味着这些信息不会出现在GoAccess报告中。

因此,您应该明白,毫不奇怪,GoAccess 只能告诉您访问日志中包含的内容,不多也不少。

这就是关于 Web 服务器配置的全部内容。

以非特权用户身份运行 GoAccess 的配置

现在我们将为 GoAccess 创建一个用户和组,这样就无需以 root 身份运行它。原因很简单,对于服务器上运行的所有无人值守任务,运行在 root 权限下的代码越少越好。这是良好的实践和常识。

在这种情况下,GoAccess 只是一个日志分析工具。它只需读取网页服务器的日志文件,而这并不需要以 root 身份运行,一个无特权用户同样可以完成任务,前提是该用户对 /var/log/apache2/var/log/nginx 具有读取权限。

Web 服务器的日志文件通常属于 adm 组(尽管这可能因您的发行版而异,我不确定)。您可以通过以下命令轻松检查这一点:

ls -l /var/log | grep -e apache2 -e nginx

结果应类似于以下内容:

drwxr-x--- 2 root adm 20480 Jul 22 00:00 /var/log/apache2/

如您所见,目录 apache2 属于 adm 组。这意味着您无需以 root 身份阅读日志,任何属于 adm 组的普通用户均可执行此操作。

现在,让我们创建 goaccess 用户,并将其添加到 adm 组:

adduser --system --group --no-create-home goaccess
addgroup goaccess adm

接下来,让我们以非特权用户身份运行 GoAccess,并验证其是否仍能读取日志文件:

setpriv 
    --reuid=goaccess --regid=goaccess 
    --init-groups --inh-caps=-all 
    -- 
    goaccess 
    --log-format COMBINED 
    --output /tmp/report2.html 
    /var/log/apache2/access.log

setpriv 是用于降低权限的命令。其语法较为冗长,不太适合教程使用,但无需担心,可查阅手册页了解其功能。

无论如何,该命令应能正常工作,此时意味着已准备好 goaccess 用户,我们将使用该用户以无特权模式运行 GoAccess。

集成方案 A – 每天通过 logrotate 钩子运行 GoAccess

在此部分,我们将各项设置整合,使 GoAccess 每天处理日志文件,将新日志添加到其内部数据库,并从汇总数据生成报告。最终结果将是一个单一的 HTML 页面。

介绍 logrotate

为了实现这一目标,我们将使用 logrotate 钩子。logrotate 是一个小工具,应该已经安装在您的服务器上,它每天运行一次,负责轮换日志文件。“轮换日志”意味着将 access.log 移动到 access.log.1,依此类推。使用 logrotate,每天都会创建一个新的日志文件,而过期的日志文件会被删除。这就是防止日志填满磁盘的基本原理 😊

您可以通过以下命令检查 logrotate 是否已正确安装并启用(假设您的初始化系统为 systemd):

systemctl status logrotate.timer

对我们来说,有趣的是logrotate允许你在日志轮换前后运行脚本,因此它是运行GoAccess的理想位置。简而言之,我们希望在日志被轮换之前运行GoAccess,即在prerotate钩子中运行。

但让我们按部就班地进行。首先,我们需要编写一个小型的包装脚本,该脚本负责以正确的参数运行 GoAccess,并处理所有网站。

包装脚本

此包装脚本设计用于处理多个网站,但如果您只有一个网站,它同样适用。

我先把脚本贴出来,之后再详细解释。以下是我的包装脚本:

#!/bin/bash

# Process log files /var/www/apache2/SITE/access.log,
# only if /var/lib/goaccess-db/SITE exists.
# Create HTML reports in $1, a directory that must exist.

set -eu

OUTDIR=
LOGDIR=/var/log/apache2
DBDIR=/var/lib/goaccess-db

fail() { echo >&2 "$@"; exit 1; }

[ $# -eq 1 ] || fail "Usage: $(basename $0) OUTPUT_DIRECTORY"

OUTDIR=$1

[ -d "$OUTDIR" ] || fail "'$OUTDIR' is not a directory"
[ -d "$LOGDIR" ] || fail "'$LOGDIR' is not a directory"
[ -d "$DBDIR"  ] || fail "'$DBDIR' is not a directory"

for d in $(find "$LOGDIR" -mindepth 1 -maxdepth 1 -type d); do
    site=$(basename "$d")
    dbdir=$DBDIR/$site
    logfile=$d/access.log
    outfile=$OUTDIR/$site.html

    if [ ! -d "$dbdir" ] || [ ! -e "$logfile" ]; then
        echo "‣ Skipping site '$site'"
        continue
    else
        echo "‣ Processing site '$site'"
    fi

    setpriv 
        --reuid=goaccess --regid=goaccess 
        --init-groups --inh-caps=-all 
        -- 
    goaccess 
        --agent-list 
        --anonymize-ip 
        --persist 
        --restore 
        --config-file /etc/goaccess/goaccess.conf 
        --db-path "$dbdir" 
        --log-format "COMBINED" 
        --output "$outfile" 
        "$logfile"
done

因此,您可以将此脚本安装到 /usr/local/bin/goaccess-wrapper 例如,并使其可执行:

chmod +x /usr/local/bin/goaccess-wrapper

需要注意的几点:

  • 我们使用 --persist 参数运行 GoAccess,这意味着我们将解析的日志保存到内部数据库中,并使用 --restore 参数,这意味着我们在报告中包含数据库中的所有内容。换句话说,我们在每次运行时聚合数据,报告会随着每次运行而变得更大。
  • 参数 --config-file /etc/goaccess/goaccess.conf 是对 #1849 的临时解决方案。对于 GoAccess 版本 > 1.4,该参数将不再需要。

目前,脚本假设您的网站日志存储在子目录 /var/log/apache2/SITE/ 中。如果实际情况不同,请相应调整包装器中的设置。

该子目录的名称将用于定位 GoAccess 数据库目录 /var/lib/goaccess-db/SITE/。该目录必须存在,即如果您未自行创建该目录,包装器将不会处理该特定网站。这是控制哪些网站由 GoAccess 包装器处理、哪些网站不处理的简单方法。

因此,如果您希望 goaccess-wrapper 处理网站 SITE,只需在 /var/lib/goaccess-db 下创建一个与该网站同名的目录:

mkdir -p /var/lib/goaccess-db/SITE
chown goaccess:goaccess /var/lib/goaccess-db/SITE

现在让我们创建一个输出目录:

mkdir /tmp/goaccess-reports
chown goaccess:goaccess /tmp/goaccess-reports

然后让我们尝试运行包装器脚本:

goaccess-wrapper /tmp/goaccess-reports
ls /tmp/goaccess-reports

这应该会显示:

SITE.html

同时,你可以检查GoAccess是否已将数据库填充了一系列文件:

ls /var/lib/goaccess-db/SITE

配置 logrotate 的 prerotate 钩子

目前,包装脚本已就位。现在,让我们添加一个 prerotate 钩子,以便 goaccess-wrapper 每天在日志旋转前运行一次。

Apache2 的 logrotate 配置文件位于 /etc/logrotate.d/apache2,而 NGINX 的配置文件位于 /etc/logrotate.d/nginx。在该文件中,以下内容对我们尤为重要:

  • daily 表示日志每天轮换一次
  • sharedscripts 表示预旋转和后旋转脚本在每次日志轮换时仅执行一次,而非每个日志文件执行一次。

配置文件中还包含以下片段:

prerotate
    if [ -d /etc/logrotate.d/httpd-prerotate ]; then 
        run-parts /etc/logrotate.d/httpd-prerotate; 
    fi; 
endscript

这表明目录 /etc/logrotate.d/httpd-prerotate/ 中的脚本将在轮换前执行。如需更多详情,请参阅手册页 run-parts(8)

将所有内容综合起来,这意味着Web服务器的日志每天旋转一次,如果我们希望在旋转前运行脚本,只需将它们放在httpd-prerotate目录中即可。简单吧?

首先,如果该目录不存在,请创建它:

mkdir -p /etc/logrotate.d/httpd-prerotate/

然后在 /etc/logrotate.d/httpd-prerotate/goaccess 创建一个小型脚本:

#!/bin/sh
exec goaccess-wrapper /tmp/goaccess-reports

别忘了赋予其可执行权限:

chmod +x /etc/logrotate.d/httpd-prerotate/goaccess

如你所见,这个脚本唯一的作用就是调用包装器并传入正确的参数,即生成HTML报告的输出目录。

就这样。现在你可以明天回来,检查日志,确保钩子已执行并成功。例如,以下命令可以快速告诉你是否成功:

journalctl | grep logrotate

集成,选项 B – 每天从 systemd 服务运行 GoAccess

好,我们刚刚看到了如何使用 logrotate 钩子。这种方法的一个缺点是,我们在包装脚本中必须降低权限,因为 logrotate 以 root 身份运行,而我们不希望 GoAccess 以 root 身份运行。因此,我们需要使用 setpriv 命令来实现权限切换。

与其将此类逻辑嵌入包装脚本,我们可改为从 systemd 服务调用包装脚本,并在 systemd 服务文件中直接定义运行包装脚本的用户。

介绍 systemd 的便利功能

我们可以创建一个 systemd 服务,并配合一个 systemd 定时器 每天触发。我们可以在 systemd 服务中直接设置执行脚本的 usergroup,无需再使用 setpriv。这样会更加简洁。

我们甚至可以更进一步,使用systemd参数化单元(也称为模板),这样每个站点可以有一个独立的服务(而不是一个处理所有站点的服务)。这将大大简化包装脚本,并且在日志中看起来也更整洁。

然而,采用这种方法似乎无法像上文所述那样在日志轮换前精确执行。但这没关系。我们将每天执行一次,无论时间如何,并确保处理两个日志文件 access.logaccess.log.1(即当前日志和昨天的日志)。这样,我们就能确保不会遗漏日志中的任何一行。

需要注意的是,GoAccess 足够智能,只会考虑日志文件中的新条目,并丢弃已经存在于数据库中的条目。换句话说,多次解析同一日志文件是安全的,GoAccess 会正确处理。有关更多详细信息,请参阅 man goaccess 中的“增量日志处理”。

实现

以下是整个流程的示例。

首先,一个用于 GoAccess 的包装脚本:

#!/bin/bash

# Usage: $0 SITE DBDIR LOGDIR OUTDIR

set -eu

SITE=$1
DBDIR=$2
LOGDIR=$3
OUTDIR=$4

LOGFILES=()
for ext in log log.1; do
    logfile="$LOGDIR/access.$ext"
    [ -e "$logfile" ] && LOGFILES+=("$logfile")
done

if [ ${#LOGFILES[@]} -eq 0 ]; then
    echo "No log files in '$LOGDIR'"
    exit 0
fi

goaccess 
    --agent-list 
    --anonymize-ip 
    --persist 
    --restore 
    --config-file /etc/goaccess/goaccess.conf 
    --db-path "$DBDIR" 
    --log-format "COMBINED" 
    --output "$OUTDIR/$SITE.html" 
    "${LOGFILES[@]}"

这个包装脚本几乎不做任何事情。实际上,它唯一的作用是检查两个日志文件 access.logaccess.log.1 的存在,以确保我们不会让 GoAccess 处理一个不存在的文件(GoAccess 不会喜欢这样)。

将此文件保存到 /usr/local/bin/goaccess-wrapper,别忘了赋予可执行权限:

chmod +x /usr/local/bin/goaccess-wrapper

然后,创建一个 systemd 参数化单元文件,以便将此包装器作为 systemd 服务运行。将其保存到 /etc/systemd/system/goaccess@.service

[Unit]
Description=更新 GoAccess 报告 - %i
ConditionPathIsDirectory=/var/lib/goaccess-db/%i
ConditionPathIsDirectory=/var/log/apache2/%i
ConditionPathIsDirectory=/tmp/goaccess-reports
PartOf=goaccess.service

[Service]
Type=oneshot
User=goaccess
Group=goaccess
Nice=19
ExecStart=/usr/local/bin/goaccess-wrapper 
 %i 
 /var/lib/goaccess-db/%i 
 /var/log/apache2/%i 
 /tmp/goaccess-reports

那么,什么是 systemd 参数化单元?它是一个在启用时可以传递参数的服务。单元定义中的%i将被此参数替换。在我们的案例中,该参数将是我们要处理的网站名称。

如您所见,我们广泛使用了ConditionPathIsDirectory=指令,因此如果所需目录之一不存在,该单元将被跳过(并在日志中标记为跳过)。这是一种优雅的失败方式。

我们以用户和组 goaccess 的身份运行该包装程序,这得益于 User=Group= 设置。我们还使用 Nice= 设置为该进程分配较低的优先级。

此时已可进行测试。请确保已创建 GoAccess 数据库目录:

mkdir -p /var/lib/goaccess-db/SITE
chown goaccess:goaccess /var/lib/goaccess-db/SITE

同时确保输出目录存在:

mkdir /tmp/goaccess-reports
chown goaccess:goaccess /tmp/goaccess-reports

然后重新加载 systemd 并启动单元以验证是否正常工作:

systemctl daemon-reload
systemctl start goaccess@SITE.service
journalctl | tail

这样应该就能正常工作了。

如您所见,参数 SITEsystemctl start 命令中被传递。我们只需在单元名称的 @ 之后附加该参数。

现在,让我们创建另一个 GoAccess 服务文件,其唯一目的是将所有带参数的单元文件归类在一起,以便我们可以一次性启动所有单元。请注意,我们没有使用 systemd 目标,因为最终我们希望每天运行一次,而使用目标无法实现这一点。因此,我们使用一个虚拟的一次性服务。

以下是保存于 /etc/systemd/system/goaccess.service 的内容:

[Unit]
Description=更新 GoAccess 报告
Requires= 
 goaccess@SITE1.service 
 goaccess@SITE2.service

[Service]
Type=oneshot
ExecStart=true

如您所见,我们只需在Requires=指令中列出要处理的网站。本示例中包含两个名为SITE1SITE2的网站。

让我们确保一切正常:

systemctl daemon-reload
systemctl start goaccess.service
journalctl | tail

查看日志,两个站点SITE1SITE2都应已被处理。

最后,让我们创建一个定时器,以便systemd每天运行一次goaccess.service。将其保存到/etc/systemd/system/goaccess.timer

[Unit]
Description=GoAccess报告的每日更新

[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true

[Install]
WantedBy=timers.target

最后,启用定时器:

systemctl daemon-reload
systemctl enable --now goaccess.timer

此时,一切应已就绪。明天回来后,可使用以下命令检查日志:

journalctl | grep goaccess

最后提醒:如果你只有一个网站需要处理,当然可以简化流程,例如在文件goaccess.service中直接硬编码所有路径,而非使用参数化单元。具体操作由你决定。

日常操作

在此部分,我们假设你已完成GoAccess的全部配置并正常运行(例如每天运行一次)。让我们回顾几个值得注意的要点。

生成报告

到目前为止,本教程中我们在 /tmp/goaccess-reports 目录中创建了报告,但这只是为了示例。你可能希望将报告保存到由你的网页服务器提供的目录中,这样你就可以在网页浏览器中查看它,这才是重点,对吧?

如何实现这一点超出了本教程的范围,而且我猜如果你想监控你的网站,你已经有一个网站了,所以你应该没有问题托管 GoAccess HTML 报告。

然而,有一个重要的细节需要注意:GoAccess 在报告中显示了所有访问者的 IP 地址。只要报告是私有的,这没问题,但如果你将GoAccess报告公开,那么你一定要使用--anonymize-ip选项来调用GoAccess。

关注日志

在本教程中,我们创建的报告以及GoAccess数据库会每天增长,永远如此。这也意味着GoAccess的处理时间会每天增加一点。

因此,首先要做的是监控日志,查看GoAccess每天完成任务所需的时间。此外,您可能还想监控GoAccess数据库的大小,使用以下命令:

du -sh /var/lib/goaccess-db/SITE

如果您的网站访问量较少,我怀疑这不会成为问题。

您还可以采取一些主动措施来预防未来出现此类问题,例如将报告拆分为月度报告。这意味着每月在新的目录中创建新的数据库,并启动新的HTML报告。这样您就能获得月度报告,同时通过将数据库大小限制在一个月内来控制GoAccess的处理时间。

这可以通过在数据库目录和HTML报告中包含类似YEAR-MONTH的字符串来轻松实现。你可以在包装脚本中自动处理这一点,例如:

sfx=$(date +'%Y-%m')

mkdir -p $DBDIR/$sfx

goaccess 
    --db-path $DBDIR/$sfx 
    --output '$OUTDIR/$SITE-$sfx.html' 
    ...

你明白我的意思。

进一步说明

v1.4 之前的旧版本迁移

使用 --persist 选项时,GoAccess 会将日志中的所有信息保存在数据库中,以便后续重复使用。在之前的版本中,GoAccess 使用 Tokyo Cabinet 键值存储来实现这一功能。然而从 v1.4 开始,GoAccess 取消了对该库的依赖,转而使用自己的数据库格式。

因此,之前的数据库无法再使用,您需要将其删除并从头开始。目前尚无法将旧数据库中的数据转换为新格式。如果您感兴趣,上游已在#1783中讨论了此问题。

v1.4 开始新版本中另一个变化是部分命令行选项的名称调整。例如,--load-from-disk已被替换为--restore,而--keep-db-files改为--persist。因此您需要查阅文档并相应更新脚本。

GoAccess 的其他使用方式

您还可以采用完全不同的方式。您可以让 GoAccess 像守护进程一样持续运行,使用 --real-time-html 选项,使其持续处理日志,而非定期调用。

借助 libncurses,您还可以直接在终端中查看 GoAccess 报告,而非生成 HTML 报告。

此外,GoAccess 还拥有众多功能(详见 功能列表)。

结论

希望本教程对大家有所帮助。如有任何意见或建议,欢迎随时发送邮件。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注