Hack and / - 日志切割
如果您是一名系统管理员,日志对您而言既是祸根也是福音。在糟糕的日子里,一个行为不端的程序可能会将数 GB 的错误转储到其日志文件中,填满磁盘并像圣诞树一样点亮您的寻呼机。在美好的日子里,日志会显示您追踪数百个奇怪系统问题所需的每一个线索。现在,如果您管理任何 Web 服务器,日志会在统计方面提供更有价值的信息。今天您的主索引页有多少访问者?现在是什么蜘蛛在攻击您的网站?
现在有很多优秀的日志分析工具。有些工具提供非常漂亮的 Web 流量实时可视化,另一些工具则每晚运行并生成便于管理人员浏览的报告。所有这些程序都很棒,我建议您使用它们,但有时您需要特定的统计数据,而且您现在就需要。对于这些即时统计,我开发了一个通用的 shell 单行命令模板,它可以像保罗·班扬一样切割日志。
我发现,虽然我需要的特定信息类型可能会略有变化,但在很大程度上,算法基本保持不变。对于任何日志文件,每一行都包含我需要的一些独特信息。然后,我需要浏览日志文件,识别该信息,并保持一个运行总数,每次看到特定模式时,该总数都会递增。最后,我需要输出该信息及其最终总数,并根据总数进行排序。
有很多方法可以进行这种类型的日志解析。老派命令行爱好者可能更喜欢使用 sed 和 awk。外面的年轻人可能会选择格式良好的 Python 脚本。这些方法都没有错,但我想我属于中间派脚本编写类别——我更喜欢使用 Perl 进行这种文本处理。也许是因为 Perl 正则表达式的强大功能,或者是因为 Perl 哈希表的易用性,或者仅仅是因为我最熟悉它,但我似乎能够在 Perl 中更快地编写出这种脚本。
在给出示例脚本之前,这里有一个更具体的算法。该脚本解析输入的每一行,并使用正则表达式来匹配行中特定列或其他数据模式。然后,它使用该模式作为哈希表中的键,并递增该键的值。当它完成接受输入时,脚本会遍历哈希中的每个键,并输出该键的总数和键本身。
对于测试用例,我使用一个通用问题,只要您有 Apache Web 服务器,您就可以自己尝试。我想找出 2008 年 11 月 1 日有多少个唯一 IP 地址访问了我的网站之一,以及点击次数最多的前十个 IP。
这是一个来自日志的示例条目(IP 已更改以保护无辜者)
123.123.12.34 - - [01/Nov/2008:19:34:02 -0700] "GET ↪/talks/pxe/ui/default/iepngfix.htc HTTP/1.1" ↪404 308 "-" "Mozilla/4.0 (compatible; MSIE 7.0; ↪Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; ↪Media Center PC 5.0; .NET CLR 3.0.04506; InfoPath.2)"
而且,这是可以解析文件并提供排序输出的单行命令
perl -e 'while(<>){ if( m|(^\d+\.\d+\.\d+\.\d+).*? ↪01/Nov/2008| ){ $v{$1}++; } } foreach( keys ↪%v ){ print "$v{$_}\t$_\n"; }' ↪/var/log/apache/access.log | sort -n
当您运行此命令时,您应该看到类似于以下内容的输出,只是行数更多,并且 IP 不是假的
33 99.99.99.99 94 111.111.111.111 138 15.15.15.15
对于那些了解并喜欢 Perl 和正则表达式的人来说,这个单行命令可能不太难解析,但对于其他人来说,让我们逐步进行。有时,如果以格式化的方式查看单行命令,则更容易理解,因此这里是将单行命令的 Perl 部分翻译成仿佛它在一个常规文件中的形式
#!/usr/bin/perl while(<>){ if(m|(^\d+\.\d+\.\d+\.\d+).*?01/Nov/2008|){ $v{$1}++; } } foreach( keys %v ){ print "$v{$_}\t$_\n"; }
首先,让我们讨论 while 循环。基本上,while(<>)它遍历通过管道接收到的每一行输入,或者作为命令行上的文件参数接收到的每一行输入。在这个循环内部,我设置了一个正则表达式来匹配并提取 IP 地址。正则表达式可能值得更详细地查看
(^\d+\.\d+\.\d+\.\d+)
正则表达式的这一部分匹配行的开头 (^),然后是任意数量的数字 (\\d+),然后是一个点,另一系列数字,另一个点,另一系列数字,另一个点,最后是第四系列数字。例如,此模式将匹配行开头的 123.123.12.34。我用括号括起了正则表达式的这一部分。因为这是第一组括号,所以当 Perl 匹配它时,它会将结果匹配放入 $1 变量中,以便我稍后可以将其提取出来。
现在,那些了解正则表达式的人知道我在这里作弊了。这个正则表达式一点也不明确。首先,它会匹配完全无效的 IP 地址,例如 999.999.999.999。其次,它甚至会匹配任何四个数字之间带有点的系列,例如 12345.6.7.8910。我故意选择了一个过于通用的正则表达式来强调一个观点。有明确的正则表达式仅匹配有效的 IP 地址,但这些表达式非常长、非常复杂,并且在这种情况下完全没有必要。
因为我处理的是 Apache 日志,所以我很确信文件开头的第一个数字集是一个 IP 地址而不是其他东西,其次,Apache 记录的 IP 地址应该是相当有效的。通过采取捷径,我不仅节省了打字时间,而且即使您不是正则表达式向导,生成的正则表达式也更易于阅读和理解。
在我匹配 IP 后,我只想匹配 2008 年 11 月 1 日的日志条目
.*?01/Nov/2008
此部分匹配任意数量的字符 (.*),并且在末尾带有问号的情况下,它仅匹配所需的数量,不多也不少。然后,它匹配 2008 年 11 月 1 日的日期戳。如果我想要日志文件中每一天的总数,我可以省略正则表达式的整个部分。或者,如果我想匹配某些其他关键字(例如,当用户对特定文件执行 GET 时),我可以用该关键字替换或扩充上述部分。
一旦我在一行中匹配了 IP 地址并将其分配给 $1,我就会将其用作我在此处调用的哈希 %v 中的键并递增它($h{$1}++)。哈希的强大之处在于它强制每个键都是唯一的。这意味着每次我遇到一个新的 IP 时,它都会在哈希中获得自己的键,并且其值会递增。因此,如果这是我第一次看到 IP,它的值将为 1。第二次我看到 IP 时,它会将其递增到 2,依此类推。
一旦我完成遍历文件中的每一行,我就会转到 foreach 循环
foreach( keys %v ){ print "$v{$_}\t$_\n"; }
基本上,这所做的一切都是遍历哈希中的每个键,并输出其值(我在文件中匹配到该 IP 的次数)和 IP 本身。请注意,我没有在此处对值进行排序。我完全可以这样做——Perl 有强大的方法来对输出进行排序——但为了使代码更简单、更灵活,我选择将输出通过管道传递到命令行 sort 命令。这样,即使您不太了解 Perl,但了解命令行,您也可以调整 sort 中的参数来反转输出,甚至将其进一步通过管道传递到 tail,这样您就可以只看到前十个 IP。
如果我只想知道唯一访问者的总数,因为每一行代表一个唯一的访问者,我只需要计算总行数。为此,我只需要将输出通过管道传递到wc -l.
而且,您就得到了一个快速而简陋的单行命令,用于切割您的日志并统计结果。使用 Perl 哈希表的优点在于,您可以调整正则表达式以匹配文件中的各种值——不仅仅是 IP 地址——并统计各种有用的信息。我使用修改后的脚本版本来计算特定文件被唯一 IP 下载了多少次,甚至使用它来对 mailq 输出执行统计。
Kyle Rankin 是旧金山湾区的高级系统管理员,并且是多本书籍的作者,包括 O'Reilly Media 出版的 Knoppix Hacks 和 Ubuntu Hacks。他目前是 North Bay Linux 用户组的主席。