从日志文件中提取 Java 个错误堆栈
Pull out Java error stacks from log files
我有一个 Java 应用程序,当出现错误时,它会为每个错误写入类似于下面的错误堆栈。
<Errors>
<Error ErrorCode="Code" ErrorDescription="Description" ErrorInfo="" ErrorId="ID">
<Attribute Name="ErrorCode" Value="Code"/>
<Attribute Name="ErrorDescription" Value="Description"/>
<Attribute Name="Key" Value="Key"/>
<Attribute Name="Number" Value="Number"/>
<Attribute Name="ErrorId" Value="ID"/>
<Attribute Name="UserId" Value="User"/>
<Attribute Name="ProgId" Value="Prog"/>
<Stack>typical Java stack</Stack>
</Error>
<Error>
Similar info to the above
</Error>
</Errors>
我写了一个 Java 日志解析器来遍历日志文件并收集有关此类错误的信息,虽然它确实有效,但速度慢且效率低下,尤其是对于数百兆字节的日志文件。我基本上只是使用字符串操作来检测 start/end 标签的位置并计算它们。
有没有办法(通过 Unix grep、Python 或 Java)有效地提取错误并计算每个错误发生的次数?整个日志文件不是 XML,所以我不能使用 XML 解析器或 Xpath。我面临的另一个问题是有时错误的结尾可能会滚动到另一个文件中,因此当前文件可能没有上面的整个堆栈。
编辑 1:
这是我目前拥有的(相关部分仅用于保存space)。
//Parse files
for (File f : allFiles) {
System.out.println("Parsing: " + f.getAbsolutePath());
BufferedReader br = new BufferedReader(new FileReader(f));
String line = "";
String fullErrorStack = "";
while ((line = br.readLine()) != null) {
if (line.contains("<Errors>")) {
fullErrorStack = line;
while (!line.contains("</Errors>")) {
line = br.readLine();
try {
fullErrorStack = fullErrorStack + line.trim() + " ";
} catch (NullPointerException e) {
//End of file but end of error stack is in another file.
fullErrorStack = fullErrorStack + "</Stack></Error></Errors> ";
break;
}
}
String errorCode = fullErrorStack.substring(fullErrorStack.indexOf("ErrorCode=\"") + "ErrorCode=\"".length(), fullErrorStack.indexOf("\" ", fullErrorStack.indexOf("ErrorCode=\"")));
String errorDescription = fullErrorStack.substring(fullErrorStack.indexOf("ErrorDescription=\"") + "ErrorDescription=\"".length(), fullErrorStack.indexOf("\" ", fullErrorStack.indexOf("ErrorDescription=\"")));
String errorStack = fullErrorStack.substring(fullErrorStack.indexOf("<Stack>") + "<Stack>".length(), fullErrorStack.indexOf("</Stack>", fullErrorStack.indexOf("<Stack>")));
apiErrors.add(f.getAbsolutePath() + splitter + errorCode + ": " + errorDescription + splitter + errorStack.trim());
fullErrorStack = "";
}
}
}
Set<String> uniqueApiErrors = new HashSet<String>(apiErrors);
for (String uniqueApiError : uniqueApiErrors) {
apiErrorsUnique.add(uniqueApiError + splitter + Collections.frequency(apiErrors, uniqueApiError));
}
Collections.sort(apiErrorsUnique);
编辑 2:
抱歉忘记提及所需的输出。像下面这样的东西是理想的。
计数、错误代码、错误描述、它出现的文件列表(如果可能)
好吧,从技术上讲,这不是 grep
,但如果您愿意使用其他标准 UNIX-esque 命令,这里有一个 one-liner 可以完成这项工作,它应该是快速(实际上有兴趣在您的数据集上查看结果):
sed -r -e '/Errors/,/<\/Errors>/!d' *.log -ne 's/.*<Error\s+ErrorCode="([^"]*)"\s+ErrorDescription="([^"]*)".*$/: /p' | sort | uniq -c | sort -nr
假设它们按日期顺序排列,*.log
glob 也将解决日志滚动的问题(当然,调整以匹配您的日志命名)。
示例输出
来自我基于你的(可疑的)测试数据:
10 SomeOtherCode: This extended description
4 Code: Description
3 ReallyBadCode: Disaster Description
简要说明
- 使用
sed
仅在所选地址(此处为行)之间打印
- 再次使用
sed
用正则表达式过滤这些,将 header 行替换为组合的 unique-enough 错误字符串(包括描述),类似于您的 Java (或者至少我们可以看到它)
- 对这些唯一字符串进行排序和计数
- 按频率降序排列
我假设既然你提到了 Unix grep,你可能也有 perl。
这是一个简单的 perl 解决方案:
#!/usr/bin/perl
my %countForErrorCode;
while (<>) { /<Error ErrorCode="([^"]*)"/ && $countForErrorCode{}++ }
foreach my $e (keys %countForErrorCode) { print "$countForErrorCode{$e} $e\n" }
假设您是 运行ning *nix,保存此 perl 脚本,使其可执行并 运行 使用类似...
的命令
$ ./grepError.pl *.log
你应该得到这样的输出...
8 Code1
203 Code2
...
其中 'Code1' 等是正则表达式中双引号之间捕获的错误代码。
我使用 Cygwin 在 Windows 上完成了这个工作。此解决方案假设:
- 你的 perl 的位置是
/usr/bin/perl
。您可以使用 $ which perl
进行验证
- 上面的正则表达式
/<Error ErrorCode="([^"]*)"/
就是您想要的计数方式。
代码正在做...
my %errors
声明一个映射(哈希)。
while (<>)
迭代每一行输入并将当前行分配给内置变量 $_
.
/<Error ErrorCode="([^"]*)"/
隐式尝试匹配 $_
.
- 当匹配发生时,括号捕获双引号之间的值并将捕获的字符串赋值给$1。
- 匹配中的正则表达式 "returns true" 只有这样计数才会增加
&& $countForErrorCode{}++
。
- 对于输出,使用
foreach my $e (keys %countForErrorCode)
迭代捕获的错误代码,并在 print "$countForErrorCode{$e} $e\n"
的行上打印计数和代码。
编辑:每个更新规范的更详细输出
#!/usr/bin/perl
my %dataForError;
while (<>) {
if (/<Error ErrorCode="([^"]+)"\s*ErrorDescription="([^"]+)"/) {
if (! $dataForError{}) {
$dataForError{} = {};
$dataForError{}{'desc'} = ;
$dataForError{}{'files'} = {};
}
$dataForError{}{'count'}++;
$dataForError{}{'files'}{$ARGV}++;
}
}
my @out;
foreach my $e (keys %dataForError) {
my $files = join("\n\t", keys $dataForError{$e}{'files'});
my $out = "$dataForError{$e}{'count'}, $e, '$dataForError{$e}{'desc'}'\n\t$files\n";
push @out, $out;
}
print @out;
就像您在上面发布的一样,要递归地获取输入文件,您可以 运行 这个脚本,例如:
$ find . -name "*.log" | xargs grepError.pl
并产生如下输出:
8, Code2, 'bang'
./today.log
48, Code4, 'oops'
./2015/jan/yesterday.log
2, Code1, 'foobar'
./2014/dec/someday.log
解释:
- 该脚本将每个唯一错误代码映射到一个散列,该散列跟踪找到错误代码的计数、描述和唯一文件名。
- Perl 自动神奇地将当前输入的文件名存储到
$ARGV
.
- 脚本计算每个唯一文件名的出现次数,但不输出这些计数。
鉴于您更新的问题:
$ cat tst.awk
BEGIN{ OFS="," }
match([=10=],/\s+*<Error ErrorCode="([^"]+)" ErrorDescription="([^"]+)".*/,a) {
code = a[1]
desc[code] = a[2]
count[code]++
files[code][FILENAME]
}
END {
print "Count", "ErrorCode", "ErrorDescription", "List of files it occurs in"
for (code in desc) {
fnames = ""
for (fname in files[code]) {
fnames = (fnames ? fnames " " : "") fname
}
print count[code], code, desc[code], fnames
}
}
$
$ awk -f tst.awk file
Count,ErrorCode,ErrorDescription,List of files it occurs in
1,Code,Description,file
第三个参数仍然需要 gawk 4.* 来匹配 () 和 2D 数组,但这同样很容易在任何 awk 中解决。
这里的评论中的每个请求是一个非 gawk 版本:
$ cat tst.awk
BEGIN{ OFS="," }
/[[:space:]]+*<Error / {
split("",n2v)
while ( match([=11=],/[^[:space:]]+="[^"]+/) ) {
name = value = substr([=11=],RSTART,RLENGTH)
sub(/=.*/,"",name)
sub(/^[^=]+="/,"",value)
[=11=] = substr([=11=],RSTART+RLENGTH)
n2v[name] = value
}
code = n2v["ErrorCode"]
desc[code] = n2v["ErrorDescription"]
count[code]++
if (!seen[code,FILENAME]++) {
fnames[code] = (code in fnames ? fnames[code] " " : "") FILENAME
}
}
END {
print "Count", "ErrorCode", "ErrorDescription", "List of files it occurs in"
for (code in desc) {
print count[code], code, desc[code], fnames[code]
}
}
$
$ awk -f tst.awk file
Count,ErrorCode,ErrorDescription,List of files it occurs in
1,Code,Description,file
有多种方法可以完成上述操作,有些更简单,但是当输入包含名称=值对时,我喜欢创建一个名称2值数组(n2v[]
是我通常给它起的名字)这样我就可以访问值的名称。使代码易于理解和修改以增加字段等
这是我之前的回答,因为其中有些东西在其他情况下会很有用:
你没有说出你想要的输出是什么样子,你发布的样本输入也不足以测试和显示有用的输出,但这个 GNU awk 脚本显示了获取任何东西的计数的方法属性name/value对你喜欢:
$ cat tst.awk
match([=12=],/\s+*<Attribute Name="([^"]+)" Value="([^"]+)".*/,a) { count[a[1]][a[2]]++ }
END {
print "\nIf you just want to see the count of all error codes:"
name = "ErrorCode"
for (value in count[name]) {
print name, value, count[name][value]
}
print "\nOr if theres a few specific attributes you care about:"
split("ErrorId ErrorCode",names,/ /)
for (i=1; i in names; i++) {
name = names[i]
for (value in count[name]) {
print name, value, count[name][value]
}
}
print "\nOr if you want to see the count of all values for all attributes:"
for (name in count) {
for (value in count[name]) {
print name, value, count[name][value]
}
}
}
.
$ gawk -f tst.awk file
If you just want to see the count of all error codes:
ErrorCode Code 1
Or if theres a few specific attributes you care about:
ErrorId ID 1
ErrorCode Code 1
Or if you want to see the count of all values for all attributes:
ErrorId ID 1
ErrorDescription Description 1
ErrorCode Code 1
Number Number 1
ProgId Prog 1
UserId User 1
Key Key 1
如果你的数据分布在多个文件中,上面的内容无所谓,只需在命令行中列出它们即可:
gawk -f tst.awk file1 file2 file3 ...
它使用 GNU awk 4.* 来处理真正的多维数组,但如果需要,对于任何其他 awk 都有简单的解决方法。
运行 对目录下递归找到的文件执行 awk 命令的一种方法:
awk -f tst.awk $(find dir -type f -print)
我有一个 Java 应用程序,当出现错误时,它会为每个错误写入类似于下面的错误堆栈。
<Errors>
<Error ErrorCode="Code" ErrorDescription="Description" ErrorInfo="" ErrorId="ID">
<Attribute Name="ErrorCode" Value="Code"/>
<Attribute Name="ErrorDescription" Value="Description"/>
<Attribute Name="Key" Value="Key"/>
<Attribute Name="Number" Value="Number"/>
<Attribute Name="ErrorId" Value="ID"/>
<Attribute Name="UserId" Value="User"/>
<Attribute Name="ProgId" Value="Prog"/>
<Stack>typical Java stack</Stack>
</Error>
<Error>
Similar info to the above
</Error>
</Errors>
我写了一个 Java 日志解析器来遍历日志文件并收集有关此类错误的信息,虽然它确实有效,但速度慢且效率低下,尤其是对于数百兆字节的日志文件。我基本上只是使用字符串操作来检测 start/end 标签的位置并计算它们。
有没有办法(通过 Unix grep、Python 或 Java)有效地提取错误并计算每个错误发生的次数?整个日志文件不是 XML,所以我不能使用 XML 解析器或 Xpath。我面临的另一个问题是有时错误的结尾可能会滚动到另一个文件中,因此当前文件可能没有上面的整个堆栈。
编辑 1:
这是我目前拥有的(相关部分仅用于保存space)。
//Parse files
for (File f : allFiles) {
System.out.println("Parsing: " + f.getAbsolutePath());
BufferedReader br = new BufferedReader(new FileReader(f));
String line = "";
String fullErrorStack = "";
while ((line = br.readLine()) != null) {
if (line.contains("<Errors>")) {
fullErrorStack = line;
while (!line.contains("</Errors>")) {
line = br.readLine();
try {
fullErrorStack = fullErrorStack + line.trim() + " ";
} catch (NullPointerException e) {
//End of file but end of error stack is in another file.
fullErrorStack = fullErrorStack + "</Stack></Error></Errors> ";
break;
}
}
String errorCode = fullErrorStack.substring(fullErrorStack.indexOf("ErrorCode=\"") + "ErrorCode=\"".length(), fullErrorStack.indexOf("\" ", fullErrorStack.indexOf("ErrorCode=\"")));
String errorDescription = fullErrorStack.substring(fullErrorStack.indexOf("ErrorDescription=\"") + "ErrorDescription=\"".length(), fullErrorStack.indexOf("\" ", fullErrorStack.indexOf("ErrorDescription=\"")));
String errorStack = fullErrorStack.substring(fullErrorStack.indexOf("<Stack>") + "<Stack>".length(), fullErrorStack.indexOf("</Stack>", fullErrorStack.indexOf("<Stack>")));
apiErrors.add(f.getAbsolutePath() + splitter + errorCode + ": " + errorDescription + splitter + errorStack.trim());
fullErrorStack = "";
}
}
}
Set<String> uniqueApiErrors = new HashSet<String>(apiErrors);
for (String uniqueApiError : uniqueApiErrors) {
apiErrorsUnique.add(uniqueApiError + splitter + Collections.frequency(apiErrors, uniqueApiError));
}
Collections.sort(apiErrorsUnique);
编辑 2:
抱歉忘记提及所需的输出。像下面这样的东西是理想的。
计数、错误代码、错误描述、它出现的文件列表(如果可能)
好吧,从技术上讲,这不是 grep
,但如果您愿意使用其他标准 UNIX-esque 命令,这里有一个 one-liner 可以完成这项工作,它应该是快速(实际上有兴趣在您的数据集上查看结果):
sed -r -e '/Errors/,/<\/Errors>/!d' *.log -ne 's/.*<Error\s+ErrorCode="([^"]*)"\s+ErrorDescription="([^"]*)".*$/: /p' | sort | uniq -c | sort -nr
假设它们按日期顺序排列,*.log
glob 也将解决日志滚动的问题(当然,调整以匹配您的日志命名)。
示例输出
来自我基于你的(可疑的)测试数据:
10 SomeOtherCode: This extended description
4 Code: Description
3 ReallyBadCode: Disaster Description
简要说明
- 使用
sed
仅在所选地址(此处为行)之间打印 - 再次使用
sed
用正则表达式过滤这些,将 header 行替换为组合的 unique-enough 错误字符串(包括描述),类似于您的 Java (或者至少我们可以看到它) - 对这些唯一字符串进行排序和计数
- 按频率降序排列
我假设既然你提到了 Unix grep,你可能也有 perl。 这是一个简单的 perl 解决方案:
#!/usr/bin/perl
my %countForErrorCode;
while (<>) { /<Error ErrorCode="([^"]*)"/ && $countForErrorCode{}++ }
foreach my $e (keys %countForErrorCode) { print "$countForErrorCode{$e} $e\n" }
假设您是 运行ning *nix,保存此 perl 脚本,使其可执行并 运行 使用类似...
的命令$ ./grepError.pl *.log
你应该得到这样的输出...
8 Code1
203 Code2
...
其中 'Code1' 等是正则表达式中双引号之间捕获的错误代码。
我使用 Cygwin 在 Windows 上完成了这个工作。此解决方案假设:
- 你的 perl 的位置是
/usr/bin/perl
。您可以使用$ which perl
进行验证
- 上面的正则表达式
/<Error ErrorCode="([^"]*)"/
就是您想要的计数方式。
代码正在做...
my %errors
声明一个映射(哈希)。while (<>)
迭代每一行输入并将当前行分配给内置变量$_
./<Error ErrorCode="([^"]*)"/
隐式尝试匹配$_
.- 当匹配发生时,括号捕获双引号之间的值并将捕获的字符串赋值给$1。
- 匹配中的正则表达式 "returns true" 只有这样计数才会增加
&& $countForErrorCode{}++
。 - 对于输出,使用
foreach my $e (keys %countForErrorCode)
迭代捕获的错误代码,并在print "$countForErrorCode{$e} $e\n"
的行上打印计数和代码。
编辑:每个更新规范的更详细输出
#!/usr/bin/perl
my %dataForError;
while (<>) {
if (/<Error ErrorCode="([^"]+)"\s*ErrorDescription="([^"]+)"/) {
if (! $dataForError{}) {
$dataForError{} = {};
$dataForError{}{'desc'} = ;
$dataForError{}{'files'} = {};
}
$dataForError{}{'count'}++;
$dataForError{}{'files'}{$ARGV}++;
}
}
my @out;
foreach my $e (keys %dataForError) {
my $files = join("\n\t", keys $dataForError{$e}{'files'});
my $out = "$dataForError{$e}{'count'}, $e, '$dataForError{$e}{'desc'}'\n\t$files\n";
push @out, $out;
}
print @out;
就像您在上面发布的一样,要递归地获取输入文件,您可以 运行 这个脚本,例如:
$ find . -name "*.log" | xargs grepError.pl
并产生如下输出:
8, Code2, 'bang'
./today.log
48, Code4, 'oops'
./2015/jan/yesterday.log
2, Code1, 'foobar'
./2014/dec/someday.log
解释:
- 该脚本将每个唯一错误代码映射到一个散列,该散列跟踪找到错误代码的计数、描述和唯一文件名。
- Perl 自动神奇地将当前输入的文件名存储到
$ARGV
. - 脚本计算每个唯一文件名的出现次数,但不输出这些计数。
鉴于您更新的问题:
$ cat tst.awk
BEGIN{ OFS="," }
match([=10=],/\s+*<Error ErrorCode="([^"]+)" ErrorDescription="([^"]+)".*/,a) {
code = a[1]
desc[code] = a[2]
count[code]++
files[code][FILENAME]
}
END {
print "Count", "ErrorCode", "ErrorDescription", "List of files it occurs in"
for (code in desc) {
fnames = ""
for (fname in files[code]) {
fnames = (fnames ? fnames " " : "") fname
}
print count[code], code, desc[code], fnames
}
}
$
$ awk -f tst.awk file
Count,ErrorCode,ErrorDescription,List of files it occurs in
1,Code,Description,file
第三个参数仍然需要 gawk 4.* 来匹配 () 和 2D 数组,但这同样很容易在任何 awk 中解决。
这里的评论中的每个请求是一个非 gawk 版本:
$ cat tst.awk
BEGIN{ OFS="," }
/[[:space:]]+*<Error / {
split("",n2v)
while ( match([=11=],/[^[:space:]]+="[^"]+/) ) {
name = value = substr([=11=],RSTART,RLENGTH)
sub(/=.*/,"",name)
sub(/^[^=]+="/,"",value)
[=11=] = substr([=11=],RSTART+RLENGTH)
n2v[name] = value
}
code = n2v["ErrorCode"]
desc[code] = n2v["ErrorDescription"]
count[code]++
if (!seen[code,FILENAME]++) {
fnames[code] = (code in fnames ? fnames[code] " " : "") FILENAME
}
}
END {
print "Count", "ErrorCode", "ErrorDescription", "List of files it occurs in"
for (code in desc) {
print count[code], code, desc[code], fnames[code]
}
}
$
$ awk -f tst.awk file
Count,ErrorCode,ErrorDescription,List of files it occurs in
1,Code,Description,file
有多种方法可以完成上述操作,有些更简单,但是当输入包含名称=值对时,我喜欢创建一个名称2值数组(n2v[]
是我通常给它起的名字)这样我就可以访问值的名称。使代码易于理解和修改以增加字段等
这是我之前的回答,因为其中有些东西在其他情况下会很有用:
你没有说出你想要的输出是什么样子,你发布的样本输入也不足以测试和显示有用的输出,但这个 GNU awk 脚本显示了获取任何东西的计数的方法属性name/value对你喜欢:
$ cat tst.awk
match([=12=],/\s+*<Attribute Name="([^"]+)" Value="([^"]+)".*/,a) { count[a[1]][a[2]]++ }
END {
print "\nIf you just want to see the count of all error codes:"
name = "ErrorCode"
for (value in count[name]) {
print name, value, count[name][value]
}
print "\nOr if theres a few specific attributes you care about:"
split("ErrorId ErrorCode",names,/ /)
for (i=1; i in names; i++) {
name = names[i]
for (value in count[name]) {
print name, value, count[name][value]
}
}
print "\nOr if you want to see the count of all values for all attributes:"
for (name in count) {
for (value in count[name]) {
print name, value, count[name][value]
}
}
}
.
$ gawk -f tst.awk file
If you just want to see the count of all error codes:
ErrorCode Code 1
Or if theres a few specific attributes you care about:
ErrorId ID 1
ErrorCode Code 1
Or if you want to see the count of all values for all attributes:
ErrorId ID 1
ErrorDescription Description 1
ErrorCode Code 1
Number Number 1
ProgId Prog 1
UserId User 1
Key Key 1
如果你的数据分布在多个文件中,上面的内容无所谓,只需在命令行中列出它们即可:
gawk -f tst.awk file1 file2 file3 ...
它使用 GNU awk 4.* 来处理真正的多维数组,但如果需要,对于任何其他 awk 都有简单的解决方法。
运行 对目录下递归找到的文件执行 awk 命令的一种方法:
awk -f tst.awk $(find dir -type f -print)