Apache 日志

日志文件相关的模块主要有 mod_log_configmod_log_forensicmod_logiomod_cgi

Apache 提供了多种不同的机制来记录日志,从初始的请求,经过 URL 映射的过程,到连接最终的解析,包括期间所有可能发生的错误。

此外,第三方的模块也可以提供日志功能,或者向现有日志插入条目。

各种应用程序如 CGI 程序、PHP 脚本或其它处理程序可以向服务器错误日志发送消息。

安全警示

如果用户有权写入日志文件所在目录,他几乎一定会获取到 root 权限。

绝对不要随便给用户授予对日志所在目录的写入权限。

另外,日志文件会包含直接由客户端提供的信息,没有经过转义。因此,怀有恶意的客户端有可能把控制字符插入日志文件中,因此在处理原始日志时一定要特别小心。

错误日志

相关模块 :core

相关指令 :ErrorLogErrorLogFormatLogLevel

服务器的错误日志,其文件名和位置由 ErrorLog 指令指定,它是最重要的日志文件。httpd 会把诊断信息记录在其中,还会把处理请求的过程中遇到的错误记录下来。在服务端启动出错错误时,或服务端运行出问题时,首先就应该查看错误日志,因为它不仅包含了错误的细节,还会提示如何修复这些错误。

错误日志通常会写入一个文件中,Linux 中通常为 error_log,根据需要,服务端还可以把错误发送给 syslog 或用管道发送给某个程序。

错误日志的格式由 ErrorLogFormat 指令指定,可以自定义要记录哪些内容。典型的日志消息格式:

[Fri Sep 09 10:42:29.902022 2011] [core:error] [pid 35708:tid 4328636416] [client 72.15.99.187] File does not exist: /usr/local/apache2/htdocs/favicon.ico

[Fri Sep 09 10:42:29.902022 2011] :消息产生的日期和时间

[core:error] :产生消息的模块及消息的严重等级

[pid 35708:tid 4328636416] :进程 ID 与 线程 ID

[client 72.15.99.187] :客户端地址

File does not exist: /usr/local/apache2/htdocs/favicon.ico :消息具体内容

错误日志中可以有多种不同类型的消息,大多格式都如上例所示。错误日志也可以包含 CGI 脚本的调试输出,CGI 脚本写到标准错误的所有信息都会直接复制到错误日志中。

在错误日志与访问日志中同时放入一个 %L 字符串会生成一个日志条目 ID,通过它可以把两个日志中相关的条目关联起来。如果 mod_unique_id 已经加载,其唯一的请求 ID 也会用作日志条目 ID。

进行测试时,持续监控错误日志往往非常有用:

tail -f error_log

每模块日志

通过 LogLevel 指令可以基于特定模块指定一个日志的严重级别。通过这种办法,如果你正在为某个特定的模块排错,可以单独将其敏感级别调低,以输出更详细的日志。对其它模块的日志没有任何影响。尤其适合于 mod_proxymod_rewrite 这样的模块。

LogLevel info rewrite:trace5

该命令把日志的主要级别调整为 info,但把模块 mod_rewrite 的级别调到了 trace5

访问日志

相关模块 :mod_log_configmod_setenvif

相关指令 :CustomLogLogFormatSetEnvIf

服务端的访问日志用于记录服务端处理过的所有请求。日志的位置和内容由 CustomLog 指令控制,LogFormat 指令可用于日志的内容。

当然,把信息保存在访问日志中只是日志管理的开始而已,下一步是分析这些信息,以生成有用的统计数据。

不同版本的 Apache httpd 曾经使用了其它的模块和指令来控制访问日志,包括 mod_log_refere、mod_log_agent 以及 TransferLog 指令。现在,CustomLog 指令将所有旧版本的功能全部归纳。

访问日志的格式是高可定制的,其格式是由一个看上去类似 C 风格 printf(1) 的格式化字符串指定的。

通用日志格式

Common Log Format,CLF 通用日志格式。

访问日志典型的配置为:

LogFormat "%h %l %u %t \"%r\" %>s %b" common
CustomLog logs/access_log common

该命令定义了昵称 common,将其与特定的格式化字符串关联到一起。格式化字符串由百分号指令组成,每个指令都告诉服务端记录一段特定的信息。真实的字符也可以放置于格式化字符串中,将会直接复制到日志输出中。双引号 " 必须被转义,还可以包含换行符 \n 和制表符 \t

第二行的 CustomLog 指令用定义好的昵称来设置一个新日志文件。访问日志的文件名是相对于 ServerRoot 的,除非以斜线开头。

这两行配置将会以通用的日志格式来写入日志条目。这种标准的格式可以由许多不同的网页服务端程序生成,并可以被许多日志分析程序读取,CLF 中生成的日志文件条目其格式大致如下:

127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326
127.0.0.1 (%h)

客户端的 IP 地址,对服务端的请求是从该主机发起的。如果 HostnameLookups 设置为 On,服务端会尝试解析其主机名,然后替换掉 IP 地址。然而,不推荐这样的配置,因为会严重拖慢服务端的运行。取而代之,最好使用后处理程序来处理主机名的解析,如 logresovle。此处报告的 IP 地址不一定是用户所处的机器的地址,如果在用户与服务端之间存在代理服务器,该地址将是代理的地址,而不是原始主机的地址。

- (%l)

这个破折号表明被请求的信息不可用。这种情况下,不可用的信息是指由 RFC 1413 定义的客户端身份,由客户端主机中的 identd 决定。该信息是高度不可靠的,几乎永远不要使用,除非是在内部完全可控的网络中。Apache httpd 甚至不会去判断该信息,除非 IdentityCheck 设置为 On

frank (%u)

这是由 HTTP 验证判断的发起文档请求的用户的 ID。相同的值通常会用变量 REMOTE_USER 赋给 CGI 脚本。如果该请求的状态码为 401,则该值不能信任,因为用户还没通过验证。如果文档没有密码保护,该值应该也是 -

[10/Oct/2000:13:55:36 -0700] (%t)

接收到请求的时间,格式为:

[day/month/year:hour:minute:second zone]

day = 2 位数字

month = 3 位字母

year = 4 位数字

hour = 2 位数字

minute = 2 位数字

second = 2 位数字

zone = (+' | -‘) 4 位数字

如果需要,可以自定义时间显示格式,通过在日志格式字符串中设定 %{format}t 来实现。其中 format 可以是 C 标准库中的 strftime(3),也可以是支持的特殊字符。

"GET /apache_pb.gif HTTP/1.0" ("%r")

来自客户端的请求是用双引号括起来的,这行请求包含了大量的有用信息。

客户端使用的方法是 GET

客户端请求的资源为 /apache_pb.gif

客户端使用的协议为 HTTP/1.0

如果需要,也可以决定请求行中的信息,哪些需要保存到日志。当前使用的字符是 %r,其实如果换成 %m %U%q %H 同样会把方法、路径、查询字符串、协议保存到日志中,和当前效果一样。

200 (%>s)

这是服务端返回给客户端的状态码。这个信息非常有价值,因为它反映了请求得到了一个成功的回复(代码是以 2 开头的)。

以 3 开头的表示重定向,以 4 开头表示客户端发生了错误,以 5 开头表示服务端发生了错误,完整的状态码列表

2326 (%b)

最后这部分代表返回给客户端的对象的大小,不包括回复的标头。

如果没有任何内容返回给客户端,该值也应该是 -

如果希望没有内容返回时用 0 来记录,此处用 %B 来替换。

合并日志格式

Combined Log Format

另一个常用的格式字符串称为合并日志格式:

LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" combined
CustomLog log/access_log combined

该格式其实与通用格式一样,只不过是加了几个字段而已。每个字段也都使用百分号指令 %{header}i,其中 header 可以是任何 HTTP 的请求标头。

这种格式的访问日志长这个样子:

127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"

新加的字段:

"http://www.example.com/start.html" ("%{Referer}i")

这是 HTTP 请求标头的引用者,给出了客户端报告引用的站点,即链接到或包含 /apache_pb.gif 的那个页面。

"Mozilla/4.08 [en] (Win98; I ;Nav)" ("%{User-agent}i")

用户代理 HTTP 请求标头。这是客户端浏览器报告自身的标识信息。

多个访问日志

通过在配置文件中指定多个 CustomLog,可以生成多个访问日志。

LogFormat "%h %l %u %t \"%r\" %>s %b" common
CustomLog logs/access_log common
CustomLog logs/referer_log "%{Referer}i -> %U"
CustomLog logs/agent_log "%{User-agent}i"

该配置将会生成三个访问日志。第一个包含基本的 CLF 信息,第二个和第三个包含引用者和浏览器信息。

基于条件的日志

有时会基于客户端请求的具体细节,需要从访问日志中排除特定的条目。

通常借助环境变量的帮助来完成:必须设置环境变量,令其能代表满足特定条件的请求。通常使用 SetEnvIf 指令来 设定条件。然后在 CustomLog 指令中使用 env= 子句,来包含或排除环境变量所代表的请求。

范例一
# 忽略来自环回接口的请求
SetEnvIf Remote_Addr "127\.0\.0\.1" dontlog
# 忽略对 robots.txt 的请求
SetEnvIf Request_URI "^/robots\.txt$" dontlog
# 其余的保存到日志
CustomLog logs/access_log common env=!dontlog
范例二

把讲英语的用户发来的请求保存到一个日志,非英语用户保存到另一个日志:

SetEnvIf Accept-Language "en" english
CustomLog logs/english_log common env=english
CustomLog logs/non_english_log common env=!english
范例三

把缓存的效率保存到日志:

SetEnv CACHE_MISS 1
LogFormat "%h %l %u %t "%r " %>s %b %{CACHE_MISS}e" common-cache
CustomLog logs/access_log common-cache

mod_cache 会在 mod_env 之前运行,如果运行成功,将在没有它的情况下提供内容。在这种情况下,缓存命中将记录 -,而缓存错过将记录 1。

范例四

除了 env= 子句,LogFormat 还支持根据 HTTP 回复代码 来保存日志:

LogFormat "%400,501{User-agent}i" browserlog
LogFormat "%!200,304,302{Referer}i" refererlog

第一行,如果 HTTP 状态码为 400 或 501,则保存 User-agent,其它情况将保存 -

第二行,如果 HTTP 状态码非 200、204、302,则保存 Referer

虽然条件日志非常强大、灵活,但它不是唯一控制日志内容的方法。实际上,日志所包含的内容越完整才越有用,在需要时对日志进行后期处理才是最实际的做法。

日志的滚动

即使在一个中等繁忙的服务端上,其日志文件所保存信息的数量也很大。访问日志文件一般每一万个请求就会增长 1 MB。因此有必要阶段性地滚动日志文件,移动或删除现有的日志。服务端运行时是无法进行的,因为只要 Apache httpd 还打开着日志文件,它就会持续写入。日志文件被移动或删除以后,需要重启服务端,以便让它打开新的日志文件。

通过优雅的重启,服务端在授命打开新日志文件时,不会丢失任何现有及未决的客户端连接。然而,为了实现这个目的,在服务端完成之前的请求时,它必须继续向原日志文件写入。因此服务端重启后,对日志文件进行任何处理之前,有必要等待一段时间。

典型的单纯滚动日志并压缩旧日志的方法:

mv access_log access_log.old
mv error_log error_log.old
apachectl graceful
sleep 600
gzip access_log.old error_log.old

管道日志

Apache httpd 可以通过管道把错误日志和访问日志写入另一个进程,而不是直接写入文件中。这个能力显著地提升了日志的灵活性。要想把日志写入管道,只需用管道操作符 | 替换文件名即可,后面跟接收日志的程序。服务端启动时将会启动管道日志,服务端运行期间,如果管道日志崩溃也会被重启,因此管道是可靠的日志。

管道日志进程是由 Aapche httpd 进程创建的,并继承其 UID,这意味着管道日志程序通常以 root 身份运行。因此必须保证程序的简单、安全。

管道日志一个重要的用处是不重启服务端的情况下进行日志的滚动。Apache HTTP Server 包含一个名为 rotatelogs 的简单的程序,就是做这个用的。例如,想要每 24 小时滚动日志:

CustomLog "|/usr/local/apache/bin/rotatelogs /var/log/access_log 86400" common

注意:要作为管道末端的命令整体被双括号引用。

对于条件日志,管道日志也是非常强大的工具,但是如果有更简单的办法就不要使用,比如可以离线事后处理的时候。

创建管道日志时默认是不会调用 shell 的,要想使用 shell,不要用 |,而该用 |$

# 用 shell 调用 "rotatelogs"
CustomLog "|$/usr/local/apache/bin/rotatelogs   /var/log/access_log 86400" common

这是 Apache 2.2 的默认行为。根据所指定的 shell 的具体情况,可能会生成一个 shell 进程,在日志管道程序的生存期一直存在,但在重启时会产生信号处理问题。为了与 Apache 2.2 兼容,|| 这种标记法也可以使用,与 | 等效。

虚拟主机的日志

运行一个含有许多虚拟主机的服务端时,有几个选项可用来处理日志文件:

统一管理

可以像在单一主机服务端一样使用日志,只需把日志指令从 <VirtualHost> 拿出来,放到主服务器区块中。可以把所有请求都保存到同一个访问日志和一个错误日志中。此时要想单独把每个虚拟主机的统计提出来是不可能的。

分别管理

如果 CustomLogErrorLog 指令放进了 <VirtualHost> 区块中,所有对该虚拟主机发起的请求或错误都会被保存到单独的文件。

如果某虚拟主机没有其自已的日志指令,对它发起的请求仍然会被保存到主服务端日志中。

这种技术对于数量较少的虚拟机比较有用,但如果数量特别大,管理起来会特别复杂。而且经常会因为文件描述符不足而产生问题。

可行办法

对于访问日志来说,有一个好的解决办法:把虚拟主机的信息加入日志格式化字符串中,这样,可以把所有主机都保存到同一个日志中,事后再分隔成单独的文件。

LogFormat "%v %l %u %t \"%r\" %>s %b" comonvhost
CustomLog logs/access_log comonvhost

其中 %v 用于保存响应请求的虚拟主机的名称。然后,使用 split-logfile 这样的程序来处理访问日志,将其按虚拟主机分割成独立的文件。

其它日志文件

相关模块 :mod_logiomod_log_configmod_log_forensicmod_cgi

相关指令 :LogFormatBufferedLogsForensicLogPidFileScriptLogScriptLogBufferScriptLogLength

实际传输的日志字节数

mod_logio 额外增加了两个 LogFormat 字段,%I%O,用于保存在网络上实际接收和发送的字节数。

日志

mod_log_forensic 用于在日志中鉴证客户端请求。在请求处理之前和之后分别保存一次日志,因此针对每个请求,鉴证日志包含两个日志行。鉴证日志的格式是非常严格的,不允许自定义。它是一个宝贵的调试和安全工具。

PID 文件

Apache httpd 启动时,会把 httpd 父进程的 ID 保存到文件 logs/httpd.pid 中。该文件名可以用 PidFile 指令来修改。该 PID 是提供给管理员用于重启或终止守护进程用的,通过向父进程发送信号来实现。

脚本日志

为了协助调试,ScriptLog 指令可以帮助记录给予 CGI 脚本的输入,以及从 CGI 脚本得到的输出。该指令应该只用于测试,不能用于实时服务端。