ilaa-chenjeri-15's picture
Initial commit
8fa21e1
Raw
History Blame Contribute Delete
6.73 kB
import re
import json
import hashlib
from dataclasses import dataclass
@dataclass
class ParsedLog:
raw: str
cleaned: str
error_type: str
module: str
timestamp: str
log_format: str
hash: str
class LogParser:
NOISE_PATTERNS = [
r'\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[\.\,\d]*[Z]?',
r'\[\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4}\]',
r'\d{6}\s+\d{6}\s+\d+',
r'\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}',
r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?\b',
r'line \d+',
r'0x[0-9a-fA-F]+',
r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
r'\b(blk_[-\d]+)\b',
r'\bpid\s*=?\s*\d+\b',
r'\[\d+\]',
r'\brhost=\S+',
r'\buid=\d+\s+euid=\d+\b',
r'\btty=\S+',
r'\blogname=\S*',
r'\bruser=\S*',
]
TIMESTAMP_PATTERNS = [
re.compile(r'(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2})'),
re.compile(r'\[(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})\]'),
re.compile(r'(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})'),
re.compile(r'(\d{6}\s+\d{6})'),
]
FORMAT_PATTERNS = [
(re.compile(r'\[\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4}\]'), 'apache'),
(re.compile(r'^\d{6}\s+\d{6}\s+\d+'), 'hdfs'),
(re.compile(r'^\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\w+\s+\w+[\[\(]'), 'syslog'),
(re.compile(r'^\{.*\}$'), 'json'),
(re.compile(r'Traceback \(most recent call last\)'), 'python_traceback'),
(re.compile(r'\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[\.\,]\d+\s+[A-Z]+\s+\w+\s+-'), 'java'),
(re.compile(r'\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}'), 'standard'),
]
ERROR_TYPE_PATTERN = re.compile(
r'\b([A-Z][a-zA-Z]+(?:Error|Exception|Warning|Fault|Panic|Failure))\b'
)
MODULE_PATTERN = re.compile(
r'\b([\w]+\.py|[\w]+\.java|[\w]+\.js|[\w]+\.rb|[\w]+\.go)\b'
)
SYSLOG_SERVICE_PATTERN = re.compile(
r'\d{2}:\d{2}:\d{2}\s+\w+\s+([\w]+)[\(\[]'
)
SEVERITY_WORDS = re.compile(
r'\b(error|warn|warning|critical|fatal|failure|failed|exception)\b',
re.IGNORECASE
)
def detect_format(self, line: str) -> str:
for pattern, fmt in self.FORMAT_PATTERNS:
if pattern.search(line):
return fmt
return 'unknown'
def extract_timestamp(self, line: str) -> str:
for pattern in self.TIMESTAMP_PATTERNS:
match = pattern.search(line)
if match:
return match.group(1)
return ""
def extract_error_type(self, line: str) -> str:
match = self.ERROR_TYPE_PATTERN.search(line)
if match:
return match.group(1)
syslog_msg = re.search(r'\]:\s*([^;]+)', line)
if syslog_msg:
msg = syslog_msg.group(1).strip()
if msg:
return msg.title()[:50]
sev = self.SEVERITY_WORDS.search(line)
return sev.group(1).title() if sev else "Unknown"
def extract_module(self, line: str) -> str:
match = self.MODULE_PATTERN.search(line)
if match:
return match.group(1)
syslog_match = self.SYSLOG_SERVICE_PATTERN.search(line)
if syslog_match:
return syslog_match.group(1)
return ""
def extract_message(self, line: str, fmt: str) -> str:
if fmt == 'json':
try:
data = json.loads(line)
return (data.get("message") or data.get("msg")
or data.get("error") or line)
except (json.JSONDecodeError, AttributeError):
return line
if fmt == 'apache':
match = re.sub(r'\[.*?\]\s*', '', line).strip()
return match or line
if fmt == 'hdfs':
match = re.match(r'\d{6}\s+\d{6}\s+\d+\s+\w+\s+[\w\.\$]+:\s*(.*)', line)
return match.group(1) if match else line
if fmt == 'syslog':
match = re.match(r'\w{3}\s+\d+\s+[\d:]+\s+\w+\s+[\w\(\)]+\[\d+\]:\s*(.*)', line)
return match.group(1) if match else line
if fmt == 'java':
match = re.match(r'[\d\-\s:,]+\s+\w+\s+[\w\.]+\s+-\s*(.*)', line)
return match.group(1) if match else line
return line
def clean(self, line: str) -> str:
cleaned = line
for pattern in self.NOISE_PATTERNS:
cleaned = re.sub(pattern, '', cleaned)
cleaned = cleaned.lower()
cleaned = re.sub(r'[^\w\s\.\:\-]', ' ', cleaned)
cleaned = re.sub(r'\s+', ' ', cleaned).strip()
return cleaned
def is_error_line(self, line: str, fmt: str) -> bool:
if fmt in ('python_traceback', 'json'):
return True
if re.search(r'\b(ERROR|WARN|WARNING|CRITICAL|FATAL|FAILURE)\b', line):
return True
if fmt == 'syslog' and self.SEVERITY_WORDS.search(line):
return True
if fmt == 'apache' and re.search(r'\[(error|warn)\]', line, re.IGNORECASE):
return True
return False
def parse_line(self, line: str) -> 'ParsedLog | None':
line = line.strip()
if not line:
return None
fmt = self.detect_format(line)
if not self.is_error_line(line, fmt):
return None
message = self.extract_message(line, fmt)
cleaned = self.clean(message)
if len(cleaned) < 5:
return None
return ParsedLog(
raw=line,
cleaned=cleaned,
error_type=self.extract_error_type(line),
module=self.extract_module(line),
timestamp=self.extract_timestamp(line),
log_format=fmt,
hash=hashlib.md5(cleaned.encode()).hexdigest()
)
def parse_many(self, raw_text: str) -> dict:
lines = raw_text.strip().split('\n')
parsed = []
seen_hashes = set()
duplicates = 0
skipped = 0
format_counts = {}
for line in lines:
result = self.parse_line(line)
if result is None:
skipped += 1
continue
if result.hash in seen_hashes:
duplicates += 1
continue
seen_hashes.add(result.hash)
parsed.append(result)
format_counts[result.log_format] = format_counts.get(result.log_format, 0) + 1
return {
"logs": parsed,
"stats": {
"total_lines": len(lines),
"valid": len(parsed),
"duplicates": duplicates,
"skipped_info": skipped,
"formats_detected": format_counts,
}
}