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日志格式包含的信息较少,不包含referrer
和user-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 服务中直接设置执行脚本的 user
和 group
,无需再使用 setpriv
。这样会更加简洁。
我们甚至可以更进一步,使用systemd参数化单元(也称为模板),这样每个站点可以有一个独立的服务(而不是一个处理所有站点的服务)。这将大大简化包装脚本,并且在日志中看起来也更整洁。
然而,采用这种方法似乎无法像上文所述那样在日志轮换前精确执行。但这没关系。我们将每天执行一次,无论时间如何,并确保处理两个日志文件 access.log
和 access.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.log
和 access.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
这样应该就能正常工作了。
如您所见,参数 SITE
在 systemctl start
命令中被传递。我们只需在单元名称的 @
之后附加该参数。
现在,让我们创建另一个 GoAccess 服务文件,其唯一目的是将所有带参数的单元文件归类在一起,以便我们可以一次性启动所有单元。请注意,我们没有使用 systemd 目标,因为最终我们希望每天运行一次,而使用目标无法实现这一点。因此,我们使用一个虚拟的一次性服务。
以下是保存于 /etc/systemd/system/goaccess.service
的内容:
[Unit]
Description=更新 GoAccess 报告
Requires=
goaccess@SITE1.service
goaccess@SITE2.service
[Service]
Type=oneshot
ExecStart=true
如您所见,我们只需在Requires=
指令中列出要处理的网站。本示例中包含两个名为SITE1
和SITE2
的网站。
让我们确保一切正常:
systemctl daemon-reload
systemctl start goaccess.service
journalctl | tail
查看日志,两个站点SITE1
和SITE2
都应已被处理。
最后,让我们创建一个定时器,以便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 还拥有众多功能(详见 功能列表)。
结论
希望本教程对大家有所帮助。如有任何意见或建议,欢迎随时发送邮件。
对于这篇文章,你的反应是: