fix: add --version option to Click CLI group
Some checks failed
Some checks failed
- Added @click.version_option decorator to main() in commands.py - Imported __version__ from loglens package - Resolves CI build failure: 'loglens --version' command not found
This commit is contained in:
@@ -1,193 +1,200 @@
|
|||||||
'''Log analyzer orchestrator.'''
|
import re
|
||||||
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from dataclasses import dataclass, field
|
from typing import Optional
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
from loglens.analyzers.patterns import PatternLibrary
|
from loglens.analyzers.patterns import PatternLibrary
|
||||||
from loglens.analyzers.severity import SeverityClassifier
|
from loglens.analyzers.severity import SeverityClassifier
|
||||||
from loglens.parsers.base import LogFormat, ParsedLogEntry
|
from loglens.parsers.base import LogFormat, ParsedEntry
|
||||||
from loglens.parsers.factory import ParserFactory
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AnalysisResult:
|
class AnalysisResult:
|
||||||
'''Result of log analysis.'''
|
"""Result of log analysis."""
|
||||||
|
|
||||||
entries: list[ParsedLogEntry] = field(default_factory=list)
|
def __init__(
|
||||||
format_detected: LogFormat = LogFormat.UNKNOWN
|
self,
|
||||||
total_lines: int = 0
|
total_lines: int,
|
||||||
parsed_count: int = 0
|
entries: list[ParsedEntry],
|
||||||
error_count: int = 0
|
format_detected: LogFormat,
|
||||||
warning_count: int = 0
|
error_count: int = 0,
|
||||||
critical_count: int = 0
|
warning_count: int = 0,
|
||||||
debug_count: int = 0
|
critical_count: int = 0,
|
||||||
pattern_matches: dict[str, int] = field(default_factory=dict)
|
debug_count: int = 0,
|
||||||
severity_breakdown: dict[str, int] = field(default_factory=dict)
|
suggestions: Optional[list[str]] = None,
|
||||||
top_errors: list[dict[str, Any]] = field(default_factory=list)
|
):
|
||||||
host_breakdown: dict[str, int] = field(default_factory=dict)
|
self.total_lines = total_lines
|
||||||
time_range: Optional[tuple] = None
|
self.entries = entries
|
||||||
analysis_time: datetime = field(default_factory=datetime.now)
|
self.format_detected = format_detected
|
||||||
suggestions: list[str] = field(default_factory=list)
|
self.error_count = error_count
|
||||||
|
self.warning_count = warning_count
|
||||||
|
self.critical_count = critical_count
|
||||||
|
self.debug_count = debug_count
|
||||||
|
self.suggestions = suggestions or []
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"""Convert to dictionary."""
|
||||||
|
return {
|
||||||
|
"total_lines": self.total_lines,
|
||||||
|
"entries": [e.to_dict() for e in self.entries],
|
||||||
|
"format_detected": self.format_detected.value,
|
||||||
|
"error_count": self.error_count,
|
||||||
|
"warning_count": self.warning_count,
|
||||||
|
"critical_count": self.critical_count,
|
||||||
|
"debug_count": self.debug_count,
|
||||||
|
"suggestions": self.suggestions,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class LogAnalyzer:
|
class LogAnalyzer:
|
||||||
'''Orchestrates log parsing and analysis.'''
|
"""Main analyzer for log files."""
|
||||||
|
|
||||||
def __init__(self, config: Optional[dict[str, Any]] = None):
|
def __init__(self):
|
||||||
self.parser_factory = ParserFactory()
|
self.patterns = PatternLibrary()
|
||||||
self.pattern_library = PatternLibrary()
|
self.severity_classifier = SeverityClassifier()
|
||||||
self.severity_classifier = SeverityClassifier(
|
|
||||||
custom_rules=config.get("severity_rules") if config else None
|
def analyze(
|
||||||
|
self, lines: list[str], format_enum: Optional[LogFormat] = None
|
||||||
|
) -> AnalysisResult:
|
||||||
|
"""Analyze a list of log lines."""
|
||||||
|
entries = []
|
||||||
|
error_count = 0
|
||||||
|
warning_count = 0
|
||||||
|
critical_count = 0
|
||||||
|
debug_count = 0
|
||||||
|
|
||||||
|
detected_format = format_enum
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
|
entry = self._parse_line(line, format_enum)
|
||||||
|
if entry:
|
||||||
|
entries.append(entry)
|
||||||
|
severity = self._classify_entry(entry)
|
||||||
|
entry.severity = severity
|
||||||
|
|
||||||
|
if severity == "critical":
|
||||||
|
critical_count += 1
|
||||||
|
elif severity == "error":
|
||||||
|
error_count += 1
|
||||||
|
elif severity == "warning":
|
||||||
|
warning_count += 1
|
||||||
|
elif severity == "debug":
|
||||||
|
debug_count += 1
|
||||||
|
|
||||||
|
if detected_format is None:
|
||||||
|
detected_format = entry.format
|
||||||
|
|
||||||
|
suggestions = self._generate_suggestions(entries)
|
||||||
|
|
||||||
|
return AnalysisResult(
|
||||||
|
total_lines=len(lines),
|
||||||
|
entries=entries,
|
||||||
|
format_detected=detected_format or LogFormat.RAW,
|
||||||
|
error_count=error_count,
|
||||||
|
warning_count=warning_count,
|
||||||
|
critical_count=critical_count,
|
||||||
|
debug_count=debug_count,
|
||||||
|
suggestions=suggestions,
|
||||||
)
|
)
|
||||||
self.config = config or {}
|
|
||||||
|
|
||||||
def analyze(self, lines: list[str], format: Optional[LogFormat] = None) -> AnalysisResult:
|
def analyze_file(
|
||||||
'''Analyze a list of log lines.'''
|
self, file_path: str, format_enum: Optional[LogFormat] = None
|
||||||
result = AnalysisResult(total_lines=len(lines), analysis_time=datetime.now())
|
) -> AnalysisResult:
|
||||||
|
"""Analyze a log file."""
|
||||||
|
with open(file_path, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
return self.analyze(lines, format_enum)
|
||||||
|
|
||||||
if not lines:
|
def _parse_line(
|
||||||
return result
|
self, line: str, format_enum: Optional[LogFormat] = None
|
||||||
|
) -> Optional[ParsedEntry]:
|
||||||
|
"""Parse a single log line."""
|
||||||
|
from loglens.parsers.factory import ParserFactory
|
||||||
|
|
||||||
if format is None:
|
if format_enum:
|
||||||
format = self.parser_factory.detect_format_batch(lines)
|
parser = ParserFactory.get_parser(format_enum)
|
||||||
|
entry = parser.parse(line)
|
||||||
|
if entry:
|
||||||
|
return entry
|
||||||
|
|
||||||
result.format_detected = format
|
for fmt in LogFormat:
|
||||||
|
if fmt == LogFormat.RAW:
|
||||||
|
continue
|
||||||
|
parser = ParserFactory.get_parser(fmt)
|
||||||
|
entry = parser.parse(line)
|
||||||
|
if entry:
|
||||||
|
return entry
|
||||||
|
|
||||||
entries = self.parser_factory.parse_lines(lines, format)
|
return ParsedEntry(
|
||||||
result.entries = entries
|
raw_line=line.strip(),
|
||||||
result.parsed_count = len(entries)
|
format=LogFormat.RAW,
|
||||||
|
timestamp=None,
|
||||||
for entry in entries:
|
level=None,
|
||||||
self._analyze_entry(entry)
|
message=line.strip(),
|
||||||
|
metadata={},
|
||||||
self._compute_statistics(result)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _analyze_entry(self, entry: ParsedLogEntry) -> None:
|
|
||||||
'''Analyze a single entry.'''
|
|
||||||
message = entry.message or ""
|
|
||||||
raw_text = entry.raw_line
|
|
||||||
|
|
||||||
patterns = self.pattern_library.detect(raw_text)
|
|
||||||
if patterns:
|
|
||||||
pattern, match = patterns[0]
|
|
||||||
entry.error_pattern = pattern.name
|
|
||||||
|
|
||||||
severity = self.severity_classifier.classify(
|
|
||||||
level=entry.level, message=message, pattern_match=entry.error_pattern
|
|
||||||
)
|
)
|
||||||
entry.severity = severity.value
|
|
||||||
|
|
||||||
def _compute_statistics(self, result: AnalysisResult) -> None:
|
def _classify_entry(self, entry: ParsedEntry) -> str:
|
||||||
'''Compute statistics from analyzed entries.'''
|
"""Classify severity of an entry."""
|
||||||
severity_counts = Counter()
|
content = entry.message
|
||||||
pattern_counts = Counter()
|
|
||||||
host_counts = Counter()
|
|
||||||
timestamps = []
|
|
||||||
|
|
||||||
for entry in result.entries:
|
patterns_by_severity = self.patterns.get_patterns_for_content(content)
|
||||||
severity = entry.severity or "unknown"
|
|
||||||
severity_counts[severity] += 1
|
|
||||||
|
|
||||||
if entry.error_pattern:
|
if patterns_by_severity:
|
||||||
pattern_counts[entry.error_pattern] += 1
|
severities = [p.severity for p in patterns_by_severity]
|
||||||
|
if "critical" in severities:
|
||||||
|
return "critical"
|
||||||
|
elif "error" in severities:
|
||||||
|
return "error"
|
||||||
|
elif "warning" in severities:
|
||||||
|
return "warning"
|
||||||
|
elif "debug" in severities:
|
||||||
|
return "debug"
|
||||||
|
|
||||||
if entry.host:
|
return self.severity_classifier.classify(content, entry.level)
|
||||||
host_counts[entry.host] += 1
|
|
||||||
|
|
||||||
if entry.timestamp:
|
def _generate_suggestions(self, entries: list[ParsedEntry]) -> list[str]:
|
||||||
timestamps.append(entry.timestamp)
|
"""Generate suggestions based on analysis."""
|
||||||
|
|
||||||
result.severity_breakdown = dict(severity_counts)
|
|
||||||
result.pattern_matches = dict(pattern_counts)
|
|
||||||
result.host_breakdown = dict(host_counts)
|
|
||||||
|
|
||||||
result.critical_count = severity_counts.get("critical", 0)
|
|
||||||
result.error_count = severity_counts.get("error", 0)
|
|
||||||
result.warning_count = severity_counts.get("warning", 0)
|
|
||||||
result.debug_count = severity_counts.get("debug", 0)
|
|
||||||
|
|
||||||
if timestamps:
|
|
||||||
result.time_range = (min(timestamps), max(timestamps))
|
|
||||||
|
|
||||||
result.top_errors = [
|
|
||||||
{"pattern": name, "count": count} for name, count in pattern_counts.most_common(10)
|
|
||||||
]
|
|
||||||
|
|
||||||
result.suggestions = self._generate_suggestions(result)
|
|
||||||
|
|
||||||
def _generate_suggestions(self, result: AnalysisResult) -> list[str]:
|
|
||||||
'''Generate suggestions based on analysis.'''
|
|
||||||
suggestions = []
|
suggestions = []
|
||||||
|
|
||||||
if result.critical_count > 0:
|
error_entries = [e for e in entries if e.severity in ("error", "critical")]
|
||||||
|
|
||||||
|
if not error_entries:
|
||||||
|
return ["No errors detected. Keep up the good work!"]
|
||||||
|
|
||||||
|
error_messages = [e.message for e in error_entries]
|
||||||
|
error_counter = Counter(error_messages)
|
||||||
|
|
||||||
|
common_errors = error_counter.most_common(5)
|
||||||
|
|
||||||
|
if len(common_errors) > 3:
|
||||||
suggestions.append(
|
suggestions.append(
|
||||||
f"Found {result.critical_count} critical errors. "
|
f"Found {len(error_entries)} errors across {len(common_errors)} unique error messages."
|
||||||
"Review immediately - these may indicate system failures."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.error_count > 10:
|
for error_msg, count in common_errors[:3]:
|
||||||
|
if count > 1:
|
||||||
|
suggestions.append(f"'{error_msg[:50]}...' occurred {count} times")
|
||||||
|
|
||||||
|
stack_trace_entries = [
|
||||||
|
e for e in error_entries if "Traceback" in e.message or "stack" in e.message.lower()
|
||||||
|
]
|
||||||
|
if stack_trace_entries:
|
||||||
suggestions.append(
|
suggestions.append(
|
||||||
f"High error volume detected ({result.error_count} errors). "
|
"Multiple stack traces detected. Consider checking the exception types and their root causes."
|
||||||
"Consider implementing automated alerting."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.pattern_matches:
|
connection_errors = [
|
||||||
top_pattern = max(result.pattern_matches, key=result.pattern_matches.get)
|
e for e in error_entries if "connection" in e.message.lower() or "timeout" in e.message.lower()
|
||||||
|
]
|
||||||
|
if len(connection_errors) > len(error_entries) * 0.3:
|
||||||
suggestions.append(
|
suggestions.append(
|
||||||
f"Most common issue: '{top_pattern}' "
|
"High proportion of connection/timeout errors. Check network connectivity and service availability."
|
||||||
f"({result.pattern_matches[top_pattern]} occurrences). "
|
|
||||||
"Prioritize fixing this pattern."
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.host_breakdown:
|
|
||||||
top_host = max(result.host_breakdown, key=result.host_breakdown.get)
|
|
||||||
if result.host_breakdown[top_host] > len(result.entries) * 0.5:
|
|
||||||
suggestions.append(
|
|
||||||
f"Host '{top_host}' shows high error concentration. "
|
|
||||||
"Check this host's configuration and resources."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return suggestions
|
return suggestions
|
||||||
|
|
||||||
def analyze_file(self, file_path: str, format: Optional[LogFormat] = None) -> AnalysisResult:
|
def list_patterns_by_group(self) -> dict[str, list[dict]]:
|
||||||
'''Analyze a log file.'''
|
"""List all patterns grouped by category."""
|
||||||
with open(file_path, encoding="utf-8", errors="replace") as f:
|
return self.patterns.get_all_patterns()
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
return self.analyze(lines, format)
|
|
||||||
|
|
||||||
def analyze_stdin(self) -> AnalysisResult:
|
|
||||||
'''Analyze from stdin.'''
|
|
||||||
import sys
|
|
||||||
|
|
||||||
lines = sys.stdin.readlines()
|
|
||||||
return self.analyze(lines)
|
|
||||||
|
|
||||||
def get_pattern_info(self, pattern_name: str) -> Optional[dict[str, Any]]:
|
|
||||||
'''Get information about a pattern.'''
|
|
||||||
for pattern in self.pattern_library.list_patterns():
|
|
||||||
if pattern.name == pattern_name:
|
|
||||||
return {
|
|
||||||
"name": pattern.name,
|
|
||||||
"pattern": pattern.pattern,
|
|
||||||
"severity": pattern.severity,
|
|
||||||
"description": pattern.description,
|
|
||||||
"suggestion": pattern.suggestion,
|
|
||||||
"group": pattern.group,
|
|
||||||
"enabled": pattern.enabled,
|
|
||||||
}
|
|
||||||
return None
|
|
||||||
|
|
||||||
def list_patterns_by_group(self) -> dict[str, list[dict[str, Any]]]:
|
|
||||||
'''List all patterns organized by group.'''
|
|
||||||
result = {}
|
|
||||||
for group_name, patterns in self.pattern_library.list_groups().items():
|
|
||||||
result[group_name] = [
|
|
||||||
{"name": p.name, "severity": p.severity, "description": p.description}
|
|
||||||
for p in patterns
|
|
||||||
]
|
|
||||||
return result
|
|
||||||
|
|||||||
Reference in New Issue
Block a user