日志合并

Published: by Creative Commons Licence

  • Tags:

场景

最近工作时遇到这样一个问题,公司的项目是多模块的,不同的模块的日志会输出到不同的文件中去。所以这就带来了一个问题,一次请求无法在同一个日志文件找到。

我们的日志通过MDC加了请求号,每个请求号都是uuid,因此可以通过请求号定位同一次请求的所有日志。因此查找请求相关的日志的手段大概为:

$ cat ./logs/* | grep <请求号>

但是这会带来一个问题,就是日志输出是按文件排序的,而非按日志。因此打印在控制台的请求是乱序的。

解决方案

为了解决这个问题,我考虑实现一个基于时间合并所有日志文件的工具。但是将日志合并并保存到一个文件中价值不大,因为我们之所以按模块输出日志,就是希望日志能细分,现在合并并没有什么意义。但是日志合并在通过请求号查找关联日志时非常有用,所以我会直接输出到标准输出流中,以配合其它工具。现在日志的查看应该是:

$ logmerge ./logs/* --region=0,20 | grep <请求号>

region表示按日志的第0到第20个字符组成的子串进行升序排序。

首先我们考虑到不同日志的分隔符不同。一般来说日志的分隔符都是换行符,但是在输出异常的时候一般换行符会在异常信息中间被输出,因此不能选择换行符作为分隔符。因此我们考虑参考HTTP多文件上传时使用一段较长的随机字符串作为分隔符,"——–d—-gre——yt———b–m———"。为了让工具能更加泛用,因此允许手动指定分隔符。

$ logmerge ./logs/* --region=0,20 --delimiter=--------d----gre------yt---------b--m--------- | grep <请求号>

但是由于输入文件是分隔符的,所以也希望输出文件也是带分隔符的,因此现在请求内容大致为。

$ logmerge ./logs/* --region=0,20 --delimiter=--------d----gre------yt---------b--m--------- --output-delimtier=--------d----gre------yt---------b--m--------- | grep <请求号>

由于文件可能不是使用系统默认字符集,因此允许用户自行选择使用的输入输出字符集。

$ logmerge ./logs/* --region=0,20 --delimiter=--------d----gre------yt---------b--m--------- --output-delimtier=--------d----gre------yt---------b--m--------- --charset=utf8 | grep <请求号>

由于日志本身问题,可能日志头部和尾部会有不必要的空白字符。头部的空白字符会让region的定位变得困难,因此需要去除。

$ logmerge ./logs/* --region=0,20 --delimiter=--------d----gre------yt---------b--m--------- --output-delimtier=--------d----gre------yt---------b--m--------- --charset=utf8 --trim-head --trim-tail | grep <请求号>

工具设计

我们在上面设计了一个logmerge工具,并支持了一系列的功能。这个工具我已经在log-merger上提供了,大家可以直接使用,我这边提一些关键的设计思路。

首先如何对多文件日志进行排序,由于日志文件可能很大,把所有文件都读入到内存中再排序是不现实的。因此我们可以利用归并排序的合并部分算法,仅保存每个日志文件扫描到的第一个日志记录,并将其按序输出。这里可以使用最小堆,来避免在文件数量多的情况下每次输出一个日志都需要扫描所有的文件。这部分性能大概为O(M+Nlog2(F)),M表示日志文件的总大小,N表示总共的日志记录数目,F表示文件数目。一般Nlog2(F)会小于M(一条日志一般都会有数十个字符组成),所以可以视作O(M)。

接下来我们考虑如何按分隔符分割日志文件。这里可以使用序列匹配工具,比如正则表达式,但是正则表达式不支持流式匹配(在线匹配),因此还是需要我们把整个日志文件读到内存中才行。而使用String#indexOf暴力搜索会导致时间复杂度很高,因此选择使用KMP算法,进行流式匹配。总的时间复杂度为O(M+K),K是分隔符长度,一般来说K远小于M,因此可以视作O(M)。

总的时间复杂度为O(M),准确来说是O(M+Nlog2(F)+K)。

而由于我们仅需要在内存中保存必要的日志文件,因此内存的复杂度为O(L+K),L为每个日志文件中最长日志记录的长度之和。