【问题标题】:Pull out Java error stacks from log files从日志文件中提取 Java 错误堆栈
【发布时间】:2015-02-11 14:27:19
【问题描述】:

我有一个 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 日志解析器来检查日志文件并收集有关此类错误的信息,虽然它确实有效,但它速度慢且效率低,尤其是对于数百兆字节的日志文件。我基本上只是使用字符串操作来检测开始/结束标签的位置并计算它们。

有没有办法(通过 Unix grep、Python 或 Java)有效地提取错误并计算每个错误发生的次数?整个日志文件不是 XML,所以我不能使用 XML 解析器或 Xpath。我面临的另一个问题是,有时错误的结尾可能会滚动到另一个文件中,因此当前文件可能没有上面的整个堆栈。

编辑1:

这是我目前拥有的(相关部分只是为了节省空间)。

//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:

很抱歉忘记提及所需的输出。像下面这样的东西是理想的。

Count、ErrorCode、ErrorDescription、它出现的文件列表(如果可能的话)

【问题讨论】:

  • 你能发布你目前拥有的Java代码吗?
  • 请查看编辑 - 我不确定它是否太大而无法容纳,但我只包括了重要的部分。
  • 尝试使用 StringBuilder 而不是 String 来收集堆栈跟踪。

标签: java python xml unix grep


【解决方案1】:

好吧,从技术上讲,它不是 grep,但如果您愿意使用其他标准的 UNIX 式命令,这里有一个可以完成这项工作的单行程序,它应该很快(有兴趣看到结果实际上,在您的数据集上):

sed -r -e '/Errors/,/<\/Errors>/!d' *.log -ne 's/.*<Error\s+ErrorCode="([^"]*)"\s+ErrorDescription="([^"]*)".*$/\1: \2/p' | sort | uniq -c | sort -nr

假设它们按日期顺序排列,*.log glob 也将解决日志滚动问题(当然,调整以匹配您的日志命名)。

样本输出

来自我基于你的(可疑的)测试数据:

 10 SomeOtherCode: This extended description
  4 Code: Description
  3 ReallyBadCode: Disaster Description

简要说明

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

【讨论】:

  • 感谢您的回复。我有几个问题/cmets:1.)为了使其递归搜索子文件夹,我将其添加到前面“find . -type f -print0 | xargs -0” - 这是最好的方法吗? 2.) 是否可以对此进行扩展以添加发生错误的每个文件的列表? 3.) 较低的优先级,但由于 -r 标志,上述内容不会在 AIX 上运行。我可以将日志文件复制到 RedHat Linux,但直接从 AIX 运行会更容易一些。
  • @Matt: 1) 是的,这应该可以很好地工作,并允许空格 2) 不是这样,无论如何都不会使其 lot 更复杂(可能比用 Python / Perl / Ruby / Java / Groovy 等)。 3) 没问题,只需删除-r 和反斜杠+ 和括号:sed -e '/Errors/,/&lt;\/Errors&gt;/!d' *.log -ne 's/.*&lt;Error\s+ErrorCode="\([^"]*\)"\s+ErrorDescription="\([^"]*\)".*$/\1: \2/p'
【解决方案2】:

鉴于您更新的问题:

$ cat tst.awk
BEGIN{ OFS="," }
match($0,/\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.* 才能 match() 和 2D 数组,但同样可以在任何 awk 中轻松解决。

根据 cmets 中的请求,这里有一个非 gawk 版本:

$ cat tst.awk
BEGIN{ OFS="," }
/[[:space:]]+*<Error / {
    split("",n2v)
    while ( match($0,/[^[:space:]]+="[^"]+/) ) {
        name = value = substr($0,RSTART,RLENGTH)
        sub(/=.*/,"",name)
        sub(/^[^=]+="/,"",value)
        $0 = substr($0,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

上面有多种方法可以完成,更简单一些,但是当输入包含 name=value 对时,我喜欢创建一个 name2value 数组(n2v[] 是我通常给它的名称),所以我可以通过它们访问值名字。使代码易于理解和修改以添加字段等。


这是我之前的回答,因为其中有些东西在其他情况下会很有用:

您没有说您希望输出看起来像什么,并且您发布的示例输入并不足以测试和显示有用的输出,但是这个 GNU awk 脚本显示了获取任何属性名称计数的方法/你喜欢的值对:

$ cat tst.awk         
match($0,/\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)

【讨论】:

  • 感谢您的回复 - 是否可以使用此脚本并将日志文件的文件夹传递给它并递归计算所有错误?
  • Do out 字面意思是递归的,即降级目录查找文件?如果文件都在一个级别,那么只需使用 dir/* 而不是一个文件名来调用它。
  • 是的——我的意思是递归的。其他文件夹的子文件夹中会有日志文件,我想全部解析一下。
  • 知道了。 awk 是用于操作文本的 UNIX 命令。 [递归] 查找文件的 UNIX 命令是 find。 UNIX shell 是一个环境,从该环境中可以使用一种语言调用工具来对这些调用进行排序。所以 - 你需要编写一个简短的 shell 脚本,调用 find 来查找文件并将它们传递给 awk 以操作这些文件中的文本。我更新了我的答案以显示一种方法 - 只要您没有大量文件并且它们的名称中不包含空格,它就会起作用,但如果其中任何一个条件为真,您就会需要更复杂的 shell 脚本。
  • 再次感谢您。这里的所有答案都非常好,给了我很多学习的机会,但这个答案回答了所有问题并按预期工作,因此将其标记为解决方案。
【解决方案3】:

我假设既然你提到了 Unix grep,你可能也有 perl。 这是一个简单的 perl 解决方案:

#!/usr/bin/perl

my %countForErrorCode;
while (<>) { /<Error ErrorCode="([^"]*)"/ && $countForErrorCode{$1}++ }
foreach my $e (keys %countForErrorCode) { print "$countForErrorCode{$e} $e\n" }

假设您正在运行 *nix,保存这个 perl 脚本,使其可执行并使用类似的命令运行...

$ ./grepError.pl *.log

你应该得到类似...的输出

8 Code1
203 Code2
...

其中 'Code1' 等是正则表达式中双引号之间捕获的错误代码。

我使用 Cygwin 在 Windows 上完成了这项工作。该解决方案假定:

  1. perl 的位置是/usr/bin/perl。您可以通过$ which perl 进行验证
  2. 上面的正则表达式 /&lt;Error ErrorCode="([^"]*)"/ 是您想要的计数方式。

代码正在做...

  1. my %errors 声明一个映射(哈希)。
  2. while (&lt;&gt;) 迭代输入的每一行并将当前行分配给内置变量$_
  3. /&lt;Error ErrorCode="([^"]*)"/ 隐式尝试匹配 $_
  4. 发生匹配时,括号捕获双引号之间的值并将捕获的字符串分配给 $1。
  5. 只有在匹配时,正则表达式“返回真”才会增加计数&amp;&amp; $countForErrorCode{$1}++
  6. 对于输出,使用foreach my $e (keys %countForErrorCode) 迭代捕获的错误代码,并使用print "$countForErrorCode{$e} $e\n" 在一行上打印计数和代码。

编辑:根据更新后的规范提供更详细的输出

#!/usr/bin/perl

my %dataForError;

while (<>) {
  if (/<Error ErrorCode="([^"]+)"\s*ErrorDescription="([^"]+)"/) {
    if (! $dataForError{$1}) {
      $dataForError{$1} = {}; 
      $dataForError{$1}{'desc'} = $2;
      $dataForError{$1}{'files'} = {};
    }
    $dataForError{$1}{'count'}++;
    $dataForError{$1}{'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. 脚本计算每个唯一文件名出现的次数,但不输出这些计数。

【讨论】:

  • 感谢您的回复 - 但我在运行您的脚本时遇到问题。它在 test.pl 第 20 行出现语法错误,在“push”附近,第 20 行是“push @out, $out;”。有什么想法吗?
  • 尝试删除 $dataForError{$e}{'desc'} 周围的单引号。它们是可选的,仅用于输出格式。我在 cygwin 上运行 perl 5.14.4。
  • 刚刚注意到 push() 之前的行没有以分号结尾。我编辑了答案并添加了分号。
  • 再次感谢您的回复。但是,我在运行时仍然遇到错误。这是 Red Hat Linux 6.5 和 Perl 5.10:“arg 1 到键的类型必须是 /home/orionoms/bin/test.pl 第 18 行,靠近“})”的哈希(不是哈希元素),第 18 行是“我的 $files = join("\n\t", 键 $dataForError{$e}{'files'});".我将其称为 ">find . -name "*.log" | xargs ~/bin/test.pl" 以递归方式获取文件夹中的所有日志文件,并且并非所有文件夹都可能具有匹配的日志 - 不确定这是否是问题。
  • 似乎在 perl 5.10 中,我需要在第 18 行使用带有%{} 的环绕$dataForError{$e}{'files'} 并且脚本有效。我从stackoverflow.com/questions/20824920/…推断出这个变化。
猜你喜欢
  • 2011-08-31
  • 1970-01-01
  • 1970-01-01
  • 2011-01-05
  • 1970-01-01
  • 1970-01-01
  • 2011-03-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多