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, } }