从日志文件中提取 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

简要说明

  1. 使用sed仅在所选地址(此处为行)之间打印
  2. 再次使用 sed 用正则表达式过滤这些,将 header 行替换为组合的 unique-enough 错误字符串(包括描述),类似于您的 Java (或者至少我们可以看到它)
  3. 对这些唯一字符串进行排序和计数
  4. 按频率降序排列

我假设既然你提到了 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 上完成了这个工作。此解决方案假设:

  1. 你的 perl 的位置是 /usr/bin/perl。您可以使用 $ which perl
  2. 进行验证
  3. 上面的正则表达式 /<Error ErrorCode="([^"]*)"/ 就是您想要的计数方式。

代码正在做...

  1. my %errors 声明一个映射(哈希)。
  2. while (<>) 迭代每一行输入并将当前行分配给内置变量 $_.
  3. /<Error ErrorCode="([^"]*)"/ 隐式尝试匹配 $_.
  4. 当匹配发生时,括号捕获双引号之间的值并将捕获的字符串赋值给$1。
  5. 匹配中的正则表达式 "returns true" 只有这样计数才会增加 && $countForErrorCode{}++
  6. 对于输出,使用 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

解释:

  1. 该脚本将每个唯一错误代码映射到一个散列,该散列跟踪找到错误代码的计数、描述和唯一文件名。
  2. Perl 自动神奇地将当前输入的文件名存储到 $ARGV.
  3. 脚本计算每个唯一文件名的出现次数,但不输出这些计数。

鉴于您更新的问题:

$ 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)