如何合并日志文件,即按时间排序但也有多行的文件,其中只有第一行有时间,其余行没有。
日志1
01:02:03.6497,2224,0022 foo
foo1
2foo
foo3
01:04:03.6497,2224,0022 bar
1bar
bar2
3bar
日志2
01:03:03.6497,2224,0022 FOO
FOO1
2FOO
FOO3
预期结果
01:02:03.6497,2224,0022 foo
foo1
2foo
foo3
01:03:03.6497,2224,0022 FOO
FOO1
2FOO
FOO3
01:04:03.6497,2224,0022 bar
1bar
bar2
3bar
如果不是以数字开头的非时间戳行,那么简单的sort -nm log1 log2
会做。
在 unix/linux cmd 行上有没有简单的方法来完成这项工作?
编辑由于这些日志文件通常以 GB 为单位,因此合并时不应重新排序(已排序的)日志文件,也不应将文件完全加载到内存中。
答案1
有点棘手。虽然可以使用date
bash 数组,但这确实是一种可以从真正的编程语言中受益的东西。例如在 Perl 中:
$ perl -ne '$d=$1 if /(.+?),/; $k{$d}.=$_; END{print $k{$_} for sort keys(%k);}' log*
01:02:03.6497,2224,0022 foo
foo1
2foo
foo3
01:03:03.6497,2224,0022 FOO
FOO1
2FOO
FOO3
01:04:03.6497,2224,0022 bar
1bar
bar2
3bar
以下是未浓缩为注释脚本的相同内容:
#!/usr/bin/env perl
## Read each input line, saving it
## as $_. This while loop is equivalent
## to perl -ne
while (<>) {
## If this line has a comma
if (/(.+?),/) {
## Save everything up to the 1st
## comma as $date
$date=$1;
}
## Add the current line to the %k hash.
## The hash's keys are the dates and the
## contents are the lines.
$k{$date}.=$_;
}
## Get the sorted list of hash keys
@dates=sort(keys(%k));
## Now that we have them sorted,
## print each set of lines.
foreach $date (@dates) {
print "$k{$date}";
}
请注意,这假设所有日期变更线和仅有的日期行包含逗号。如果不是这种情况,你可以改用以下格式:
perl -ne '$d=$1 if /^(\d+:\d+:\d+\.\d+),/; $k{$d}.=$_; END{print $k{$_} for sort keys(%k);}' log*
上述方法需要将文件的全部内容保存在内存中。如果这是个问题,这里有一个不需要的方法:
$ perl -pe 's/\n/\0/; s/^/\n/ if /^\d+:\d+:\d+\.\d+/' log* |
sort -n | perl -lne 's/\0/\n/g; printf'
01:02:03.6497,2224,0022 foo
foo1
2foo
foo3
01:03:03.6497,2224,0022 FOO
FOO1
2FOO
FOO3
01:04:03.6497,2224,0022 bar
1bar
bar2
3bar
这个命令只是将连续时间戳之间的所有行放到一行上,方法是用 替换换行符\0
(如果这可以在您的日志文件中,请使用任何您知道永远不会出现的字符序列)。这传递给sort
,然后tr
取回行。
正如 OP 非常正确地指出的那样,上述所有解决方案都需要重新考虑,并且没有考虑到文件可以合并。这里有一个可以合并的文件,但与其他文件不同,它只能在两个文件上工作:
$ sort -m <(perl -pe 's/\n/\0/; s/^/\n/ if /^\d+:\d+:\d+\.\d+/' log1) \
<(perl -pe 's/\n/\0/; s/^/\n/ if /^\d+:\d+:\d+\.\d+/' log2) |
perl -lne 's/[\0\r]/\n/g; printf'
如果将 perl 命令保存为别名,则可以得到:
$ alias a="perl -pe 's/\n/\0/; s/^/\n/ if /^\d+:\d+:\d+\.\d+/'"
$ sort -m <(a log1) <(a log2) | perl -lne 's/[\0\r]/\n/g; printf'
答案2
有一种方法可以做到(感谢@terdon 提出的换行替换想法):
- 在每个输入文件中,将所有多行替换为单行,例如用 NUL 替换
sort -m
对替换的文件执行- 将 NUL 替换回换行符
例子
由于多行连接使用多次,因此我们将alias
其除去:
alias a="awk '{ if (match(\$0, /^[0-9]{2}:[0-9]{2}:[0-9]{2}\\./, _))\
{ if (NR == 1) printf \"%s\", \$0; else printf \"\\n%s\", \$0 }\
else printf \"\\0%s\", \$0 } END { print \"\" }'"
这是使用上述别名的合并命令:
sort -m <(a log1) <(a log2) | tr '\0' '\n'
作为 Shell 脚本
为了像这样使用它
merge-logs log1 log2
我将其放入一个 shell 脚本中:
x=""
for f in "$@";
do
x="$x <(awk '{ if (match(\$0, /^[0-9]{2}:[0-9]{2}:[0-9]{2}\\./, _)) { if (NR == 1) printf \"%s\", \$0; else printf \"\\n%s\", \$0 } else printf \"\\0%s\", \$0 } END { print \"\" }' $f)"
done
eval "sort -m $x | tr '\0' '\n'"
不确定我是否可以提供可变数量的日志文件,而无需诉诸邪恶eval
。
答案3
如果您可以选择使用 Java,请尝试日志合并:
java -jar log-merger-0.0.3-jar-with-dependencies.jar -f 1 -tf "HH:MM:ss.SSS" -d "," -i log1,log2
01:02:03.6497,2224,0022 foo
foo1
2foo
foo3
01:03:03.6497,2224,0022 FOO
FOO1
2FOO
FOO3
01:04:03.6497,2224,0022 bar
1bar
bar2
3bar
答案4
(假设你有安装有锈迹)
cargo install super_speedy_syslog_searcher
然后
s4 log1 log2
然而,超快速系统日志搜索器希望找到日期时间戳。如果您可以将日志记录格式更改为该格式,则s4
可以对其进行排序。