使用Perl正则表达式通过关键字查找Java堆栈跟踪信息

4

我需要通过关键字从日志文件中提取完整的堆栈跟踪信息。

这段代码可以正常工作,但在大文件上速度较慢(超过一个文件会更慢)。 我认为改进正则表达式以查找关键字是最好的方法,但我无法完成它。


#!/usr/bin/perl

use strict;
use warnings;

my $regexp;
my $stacktrace;
undef $/;

$regexp = shift;
$regexp = quotemeta($regexp);

while (<>) {
  while ( $_ =~ /(?<LEVEL>^[E|W|D|I])\s
                 (?<TIMESTAMP>\d{6}\s\d{6}\.\d{3})\s
                 (?<THREAD>.*?)\/
                 (?<CLASS>.*?)\s-\s
                 (?<MESSAGE>.*?[\r|\n](?=^[[E|W|D|I]\s\d{6}\s\d{6}\.\d{3}]?))/gsmx ) {
    $stacktrace = $&;
    if ( $+{MESSAGE} =~ /$regexp/ ) {
      print "$stacktrace";
    }
  }
}

用法:./grep_log4j.pl <模式> <文件>

示例:./grep_log4j.pl Exception sample.log

我认为问题在于$stacktrace = $&;,因为如果删除这行并简单地打印所有匹配的行,脚本就可以快速工作了。 打印所有匹配项的脚本版本:

#!/usr/bin/perl

use strict;
use warnings;

undef $/;

while (<>) {
  while ( $_ =~ /(?<LEVEL>^[E|W|D|I])\s
                 (?<TIMESTAMP>\d{6}\s\d{6}\.\d{3})\s
                 (?<THREAD>.*?)\/
                 (?<CLASS>.*?)\s-\s
                 (?<MESSAGE>.*?[\r|\n](?=^[[E|W|D|I]\s\d{6}\s\d{6}\.\d{3}]?))/gsmx ) {
    print_result();
  }
}

sub print_result {
    print "LEVEL: $+{LEVEL}\n";
    print "TIMESTAMP: $+{TIMESTAMP}\n";
    print "THREAD: $+{THREAD}\n";
    print "CLASS: $+{CLASS}\n";
    print "MESSAGE: $+{MESSAGE}\n";
}

使用方法:./grep_log4j.pl <文件>

示例:./grep_log4j.pl sample.log

Log4j模式:%-1p %d %t/%c{1} - %m%n

日志文件示例:

I 111012 141506.000 thread/class - Received message: something
E 111012 141606.000 thread/class - Failed handling mobile request
java.lang.NullPointerException
  at javax.servlet.http.HttpServlet.service(HttpServlet.java:710)
  at java.lang.Thread.run(Thread.java:619)
W 111012 141706.000 thread/class - Received message: something
E 111012 141806.000 thread/class - Failed with Exception
java.lang.NullPointerException
  at javax.servlet.http.HttpServlet.service(HttpServlet.java:710)
  at java.lang.Thread.run(Thread.java:619)
D 111012 141906.000 thread/class - Received message: something
S 111012 142006.000 thread/class - Received message: something
I 111012 142106.000 thread/class - Received message: something
I 111013 142206.000 thread/class - Metrics:0/1

你可以在http://gskinner.com/RegExr/找到我的正则表达式,它与log4j关键字相关。


1
一般情况下,你应该避免使用$& - 参见 perldoc perlre 中的 WARNING。在你的程序中使用它会导致速度上的惩罚。 - ErikR
2个回答

1

您正在使用:

$/ = undef;

这使 Perl 将整个文件读入内存。

我会像这样逐行处理该文件(假设堆栈跟踪与上面的消息相关联):

my $matched;
while (<>) {
  if (m/^(?<LEVEL>\S+) \s+ (?<TIMESTAMP>(\d+) \s+ ([\d.])+) \s+ (?<THREADCLASS>\S+) \s+ - \s+ (?<REST>.*)/x) {
    my %captures = %+;
    $matched = ($+{REST} =~ $regexp);
    if ($matched) {
      print "LEVEL: $captures{LEVEL}\n";
      ...
    }
  } elsif ($matched) {
    print;
  }
}

这里有一个解析多行块的通用技巧。 以下循环逐行读取STDIN并将日志文件的完整块提供给子例程process

my $first;
my $stack = "";
while (<STDIN>) {
  if (m/^\S /) {
    process($first, $stack) if $first;
    $first = $_;
    $stack = "";
  } else {
    $stack .= $_;
  }
}
process($first, $stack) if $first;

sub process {
  my ($first, $stack) = @_;
  # ... do whatever you want here ...
}

谢谢你的回答。我知道如果我使用$/ = undef;,我会将文件读入内存,这不会让我感到害怕 :) 否则,如果逐行读取文件,我该如何在第二行及更多行中查找关键字匹配? - Gofrolist
例如:我需要通过关键字“java.lang.Thread.run”在日志文件中查找所有的堆栈跟踪。 另外一点需要注意的是,在你的代码示例中,第一个if语句匹配字符串并使用命名组,但是下一行如果为真,则使用了另一个正则表达式$matched = ($+{REST} =~ $regexp);,这个字符串重写了之前的所有命名组。因此,在我的第一个代码示例中,我使用了$stacktrace = $&; - Gofrolist
关于覆盖%+的问题,你说得对 - 我会修复这个例子。 - ErikR
增加了另一种通用方法来解析多行块的示例。 - ErikR
感谢user5402。这两个示例都可以正常工作,第二个示例逐行比第一个示例快了5倍! - Gofrolist

0
问题在于错误使用正则表达式中的[][...] 用于定义字符类(...) 用于分组。
您需要做的就是将[E|W|D|I] 在所有地方更改为[EWDI],并且不要在MESSAGE中使用[]进行分组。
这是可以运行的最终代码:
#!/usr/bin/perl

use strict;
use warnings;

undef $/;

while (<>) {
    while (
        $_ =~ /(?<LEVEL>^[EWDIS])\s
                 (?<TIMESTAMP>\d{6}\s\d{6}\.\d{3})\s
                 (?<THREAD>.*?)\/
                 (?<CLASS>.*?)\s-\s
                 (?<MESSAGE>.*?[\r\n](?=[EWDIS]\s\d{6}\s\d{6}\.\d{3}|$))/gmxs
      )
    {
        print_result();
    }
}

sub print_result {
    print "LEVEL: $+{LEVEL}\n";
    print "TIMESTAMP: $+{TIMESTAMP}\n";
    print "THREAD: $+{THREAD}\n";
    print "CLASS: $+{CLASS}\n";
    print "MESSAGE: $+{MESSAGE}\n";
}

请注意,在标志列表中,您错过了“S”字母。
这个例子可能也包含错误,但总体上它是有效的。

谢谢您纠正我的正则表达式。 但是我的问题不在这里。这段代码的示例打印了所有日志消息,但接下来我需要找到包含我的关键字的每个日志消息。例如:从样本日志文件中打印出包含java.lang.Thread.run的两个日志消息。 - Gofrolist
你可以通过两种方式实现这个目标:
  1. 在 while() 循环中检查 $+{MESSAGE} 是否包含所需的字符串。由于你正在使用 /g,这不会花费太多时间。
  2. <MESSAGE>.* 更改为 <MESSAGE>[^\n]*?java\.lang.Thread.run。这也应该有效。
此外,我建议将那个过于复杂的正则表达式分成两部分:在 <MESSAGE> 之前和 <MESSAGE> 本身,并依次使用它们。
- yko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接