blob: 498a39d642e4815b78543bd0af96fac2af988e6f [file] [log] [blame]
#!/usr/bin/env python
#
# Search a given log file for patterns defined in patterns.py and produce a
# HTML snippet for the matching lines.
#
# Ideas for improvements:
# - Group together similar issues. A good rule would be by description
# so if a warning/error happens for several source files in the project
# we would only list it as a single issue. Should extend the description
# with the count (i.e. "warning: bla blup [20 times]")
# Could merge the first 2 or 3 instances to show in the details section.
# - Write highlighted html log with all matches highlighted/linked?
from cgi import escape
from collections import deque
from patterns import default_search
import os.path
import re
import sys
_JENKINS_POOL_PATTERN = r'\[.*]\s'
_JENKINS_POOL_REGEX = re.compile('^'+_JENKINS_POOL_PATTERN)
class _Matcher(object):
'''Matching engine. Keeps data structures to match a single line against
a fixed list of patterns.'''
def __init__(self, patterns):
# Create a combined master regex combining all patterns.
combined = ''
merge = ''
for pattern in patterns:
# Convert named groups into anonymous ones to avoid name clashes.
p = pattern.pattern
p = re.sub(r'\(\?P<[^>]+>', '(?:', p)
combined += merge
combined += '(?:%s)' % p
merge = '|'
self.combined_regex = re.compile(combined)
self.patterns = patterns
def match_line(self, line):
if line[0] == '[':
line = _JENKINS_POOL_REGEX.sub("", line, 1)
if not self.combined_regex.match(line):
return None
for pattern in default_search:
m = pattern.regex.search(line)
if m:
return (pattern, m.groupdict())
return None
class _Match(object):
'''Used for the results of a match.'''
def _match_with_context(matcher, istream, lines_before, lines_after):
prev_lines = deque()
needs_lines = deque()
for line in istream:
if len(needs_lines) > 0:
for match in needs_lines:
match.after.append(line)
match = needs_lines[0]
if len(match.after) == lines_after:
needs_lines.popleft()
yield match
m = matcher.match_line(line)
if m is not None:
pattern, matches = m
match = _Match()
match.line = line
match.pattern = pattern
match.matches = matches
match.before = list(prev_lines)
match.after = []
needs_lines.append(match)
prev_lines.append(line)
if len(prev_lines) > lines_before:
prev_lines.popleft()
for match in needs_lines:
yield match
def _match_summary(match):
template = match.pattern.html_template
sub = {}
sub.update(match.pattern.__dict__)
sub.update(match.matches)
if 'file_name' in sub:
sub['file_name'] = os.path.basename(sub['file_name'])
return escape(template.format(**sub))
def _sort_by_severity(matches):
return sorted(matches, key=lambda m: m.pattern.severity)
def _make_html_snippets(matches, limit):
def _prepare_lines(lines):
string = escape(''.join(lines))
if len(string) > 0 and string[-1] == '\n':
string = string[:-1]
return string
matches = list(matches)
if len(matches) == 0:
return False
sys.stdout.write('<div style="margin-bottom: 2em;">Found %d issues:</div>\n' % (len(matches), ))
limited = False
if len(matches) > limit:
# Sort by severity so we cut the less severe ones.
matches = _sort_by_severity(matches)
matches = matches[:limit]
limited = True
for match in matches:
match.summary = _match_summary(match)
match.before = _prepare_lines(match.before)
match.after = _prepare_lines(match.after)
match.line = _prepare_lines(match.line)
sys.stdout.write('''\
<details class="match">
<summary><b>{summary}</b></summary>
<pre style="margin-bottom: 1.5em;">
{before}
<span style='color: red'>{line}</span>
{after}</pre>
</details>'''.format(**match.__dict__))
if limited:
sys.stdout.write('<b>... (limited to first %d issues)</b>\n' % limit)
return True
if __name__ == '__main__':
lines_before = 5
lines_after = 2
matcher = _Matcher(default_search)
matches = _match_with_context(matcher, sys.stdin,
lines_before=lines_before,
lines_after=lines_after)
limit = 12 # Limit the amount of issues we show.
had_issues = _make_html_snippets(matches, limit)
if had_issues:
sys.exit(1)