How to grep errors from log files but filter off false alerts?

Viewed 250

The goal is to get output as below, containing file name and error count for each file.

/var/log/file2.log:5
/var/log/file3.log:7
/var/log/file5.log:3
/var/log/file7.log:2

Assuming there are many log files named "*.log" and all are in same format as below.

2021-03-16 01:01:01.001 ERROR 1234 --- [main] com.example.ClassAAAA      : real error
2021-03-16 01:01:01.001  INFO 1234 --- [main] com.example.ClassAAAA      : message
2021-03-16 01:01:01.001 ERROR 1234 --- [main] com.example.LibBB          : real error
2021-03-16 01:01:01.001  INFO 1234 --- [main] com.example.LibBB          : message
2021-03-16 01:01:01.001 ERROR 1234 --- [main] com.example.ClassCCCCCC    : real error
2021-03-16 01:01:01.001 ERROR 1234 --- [main] com.example.ClassCCCCCC    : real error
2021-03-16 01:01:01.001  INFO 1234 --- [main] com.example.ClassCCCCCC    : message
2021-03-16 01:01:01.001 ERROR 1234 --- [main] com.example.ClassD         : false alert
2021-03-16 01:01:01.001  INFO 1234 --- [main] com.example.ClassD         : message
2021-03-16 01:01:01.001 ERROR 1234 --- [main] com.example.LibEEE         : false alert
2021-03-16 01:01:01.001  INFO 1234 --- [main] com.example.LibEEE         : message
2021-03-16 01:01:01.001 ERROR 1234 --- [main] com.example.LibFFFFF       : false alert
2021-03-16 01:01:01.001 ERROR 1234 --- [main] com.example.LibFFFFF       : false alert
2021-03-16 01:01:01.001  INFO 1234 --- [main] com.example.LibFFFFF       : message

The major problem is that there are some known false alerts that shouldn't be counted.

For example, we hope to count errors logged by any class other than ClassD, LibEEE and LibFFFFF by below commmand.

grep -Pc '^.* ERROR .*((?<!(ClassD|LibEEE|LibFFFFF)).)*$' *.log | grep -v :0

But it doesn't work and below error is printed.

grep: lookbehind assertion is not fixed length

If it worked, the count would be 4 for above example log piece, logged by ClassAAAA, LibBB and ClassCCCCCC respectively.

UPDATE

Sincerely thank all of you for taking the time to share your idea. The reason I went into the think set of lookbehind is that I'd like to qualify the class name by "\s*:" initially, but the space and colon was missed eventually.

Though the goal can be achieved for now, just curious - is it possible to use lookbehind in this case?

4 Answers

This should be a task for awk, could you please try following. Written and tested with shown samples in GNU awk. You could pass multiple files to this command and it will print file by file(with filename and errors count like shown samples).

awk '
BEGIN{ OFS=":" }
FNR==1{
  if(count){
    print FILENAME,count
  }
  count=0
}
/ERROR/ && !/ClassD|LibEEE|LibFFFFF/{
  count++
}
END{
  if(count){
    print FILENAME,count
  }
}
'  *.log

Explanation: Adding detailed explanation for above.

awk '                        ##Starting awk program from here.
BEGIN{ OFS=":" }             ##Setting OGFS to colon in BEGIN section of this program.
FNR==1{                      ##Checking condition if this is first line then do following.
  if(count){                 ##Checking if count is not null then.
    print FILENAME,count     ##Printing file name with OFS and count value here.
  }
  count=0                    ##Setting count to 0 here.
}
/ERROR/ && !/ClassD|LibEEE|LibFFFFF/{  ##checking condition if line does not contain ClassD, LibEEE and LibFFFFF then do following.
  count++                    ##Increase count with 1 here.
}
END{                         ##Starting END block of this program from here.
  if(count){                 ##Checking if count is not null then.
    print FILENAME,count     ##Printing file name with OFS and count value here.
  }
}
'  *.log                     ##Passing all .log files to program here.

To print the correct filenames:

awk '
FNR==1 {
  if (c)
    print f,c
  c=0
  f=FILENAME
}
$3=="ERROR" && $7!~/ClassD|LibEEE|LibFFFFF/ {
  ++c
}
END {if (c) print f,c}
' *.log

or less typing but more memory usage:

awk '
$3=="ERROR" && $7!~/ClassD|LibEEE|LibFFFFF/ {
  ++c[FILENAME]
}
END {for (f in c) print f,c[f]}
' *.log

Inside a PCRE regex lookbehind, there can only be patterns of different length on the top level: (?<!ClassD|LibEEE|LibFFFFF)is fine, but (?<!(ClassD|LibEEE|LibFFFFF)). is not as the alternations are inside a capturing group.

Actually, here, you can use a lookahead where the length of the alternatives does not matter:

grep -Pc '\sERROR(?!.*(ClassD|LibEEE|LibFFFFF))\s' | grep -v :0

See the regex demo.

This pattern matches

  • \s - any whitespace
  • ERROR - a substring
  • (?!.*(ClassD|LibEEE|LibFFFFF)) - a negative lookahead that matches any zero or more chars other than line break chars as many as possible followed with one of the words in the group
  • \s - any whitespace

With GNU awk for ENDFILE:

$ awk '
    ($3=="ERROR") && $7 !~ /\.(ClassD|Lib(EEE|FFFFF))$/ { c++ }
    ENDFILE { printf "%s:%d\n", FILENAME, c; c=0 }
' file*
file:4
Related