logler 1.0.7__cp311-cp311-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
logler/terminal.py ADDED
@@ -0,0 +1,252 @@
1
+ """
2
+ Beautiful terminal output using Rich.
3
+ """
4
+
5
+ import asyncio
6
+ from pathlib import Path
7
+ from typing import Optional
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+ from rich.panel import Panel
11
+ from rich.text import Text
12
+
13
+ from .parser import LogParser, LogEntry
14
+
15
+
16
+ class TerminalViewer:
17
+ """Beautiful terminal log viewer with Rich."""
18
+
19
+ LEVEL_COLORS = {
20
+ "TRACE": "bright_black",
21
+ "DEBUG": "cyan",
22
+ "INFO": "green",
23
+ "WARN": "yellow",
24
+ "WARNING": "yellow",
25
+ "ERROR": "red",
26
+ "CRITICAL": "bold red",
27
+ "FATAL": "bold red on white",
28
+ "UNKNOWN": "white",
29
+ }
30
+
31
+ def __init__(self, use_colors: bool = True):
32
+ self.console = Console(
33
+ force_terminal=use_colors, color_system="auto" if use_colors else None
34
+ )
35
+ self.parser = LogParser()
36
+
37
+ async def view_file(
38
+ self,
39
+ file_path: str,
40
+ lines: Optional[int] = None,
41
+ follow: bool = False,
42
+ level_filter: Optional[str] = None,
43
+ pattern: Optional[str] = None,
44
+ thread_filter: Optional[str] = None,
45
+ ):
46
+ """View a log file with beautiful formatting."""
47
+ path = Path(file_path)
48
+
49
+ if not path.exists():
50
+ self.console.print(f"[red]❌ File not found: {file_path}[/red]")
51
+ return
52
+
53
+ self.console.print(f"\n[bold cyan]📄 {path.name}[/bold cyan]")
54
+ self.console.print(f"[dim]{path.absolute()}[/dim]\n")
55
+
56
+ # Read file
57
+ with open(file_path, "r") as f:
58
+ all_lines = f.readlines()
59
+
60
+ # Parse entries
61
+ entries = [self.parser.parse_line(i + 1, line.rstrip()) for i, line in enumerate(all_lines)]
62
+
63
+ # Apply filters
64
+ entries = self._apply_filters(entries, level_filter, pattern, thread_filter)
65
+
66
+ # Apply line limit
67
+ if lines and not follow:
68
+ entries = entries[-lines:]
69
+
70
+ # Display entries
71
+ if follow:
72
+ await self._follow_mode(
73
+ path, entries[-lines:] if lines else entries, level_filter, pattern, thread_filter
74
+ )
75
+ else:
76
+ self._display_entries(entries)
77
+
78
+ def _apply_filters(
79
+ self,
80
+ entries: list,
81
+ level_filter: Optional[str],
82
+ pattern: Optional[str],
83
+ thread_filter: Optional[str],
84
+ ) -> list:
85
+ """Apply filters to log entries."""
86
+ filtered = entries
87
+
88
+ if level_filter:
89
+ level_upper = level_filter.upper()
90
+ filtered = [e for e in filtered if e.level == level_upper]
91
+
92
+ if pattern:
93
+ filtered = [e for e in filtered if pattern.lower() in e.message.lower()]
94
+
95
+ if thread_filter:
96
+ filtered = [e for e in filtered if e.thread_id == thread_filter]
97
+
98
+ return filtered
99
+
100
+ def _display_entries(self, entries: list):
101
+ """Display log entries with beautiful formatting."""
102
+ if not entries:
103
+ self.console.print("[yellow]No matching log entries found.[/yellow]")
104
+ return
105
+
106
+ for entry in entries:
107
+ self._print_entry(entry)
108
+
109
+ def _print_entry(self, entry: LogEntry):
110
+ """Print a single log entry with rich formatting."""
111
+ # Build the line components
112
+ parts = []
113
+
114
+ # Line number
115
+ parts.append(Text(f"{entry.line_number:6}", style="dim"))
116
+
117
+ # Timestamp
118
+ if entry.timestamp:
119
+ ts_str = entry.timestamp.strftime("%Y-%m-%d %H:%M:%S")
120
+ parts.append(Text(ts_str, style="cyan"))
121
+
122
+ # Level with color
123
+ level_color = self.LEVEL_COLORS.get(entry.level, "white")
124
+ parts.append(Text(f"{entry.level:8}", style=level_color))
125
+
126
+ # Thread ID if present
127
+ if entry.thread_id:
128
+ parts.append(Text(f"[{entry.thread_id}]", style="magenta"))
129
+
130
+ # Correlation ID if present
131
+ if entry.correlation_id:
132
+ parts.append(Text(f"[{entry.correlation_id}]", style="blue"))
133
+
134
+ # Message
135
+ parts.append(Text(entry.message, style="white"))
136
+
137
+ # Combine and print
138
+ line = Text(" ").join(parts)
139
+ self.console.print(line)
140
+
141
+ # Print trace info if present
142
+ if entry.trace_id or entry.span_id:
143
+ trace_info = []
144
+ if entry.trace_id:
145
+ trace_info.append(f"trace:{entry.trace_id}")
146
+ if entry.span_id:
147
+ trace_info.append(f"span:{entry.span_id}")
148
+ self.console.print(f" [dim]└─ {' '.join(trace_info)}[/dim]")
149
+
150
+ # Print additional fields if present
151
+ if entry.fields:
152
+ for key, value in entry.fields.items():
153
+ self.console.print(f" [dim] {key}: {value}[/dim]")
154
+
155
+ async def _follow_mode(
156
+ self,
157
+ path: Path,
158
+ initial_entries: list,
159
+ level_filter: Optional[str],
160
+ pattern: Optional[str],
161
+ thread_filter: Optional[str],
162
+ ):
163
+ """Follow log file in real-time."""
164
+ # Display initial entries
165
+ self._display_entries(initial_entries)
166
+
167
+ self.console.print("\n[bold green]📡 Following log file... (Ctrl+C to stop)[/bold green]\n")
168
+
169
+ # Track file position
170
+ with open(path, "r") as f:
171
+ f.seek(0, 2) # Go to end
172
+ line_number = len(initial_entries)
173
+
174
+ while True:
175
+ line = f.readline()
176
+ if line:
177
+ line_number += 1
178
+ entry = self.parser.parse_line(line_number, line.rstrip())
179
+
180
+ # Apply filters
181
+ filtered = self._apply_filters([entry], level_filter, pattern, thread_filter)
182
+ if filtered:
183
+ self._print_entry(entry)
184
+ else:
185
+ await asyncio.sleep(0.1)
186
+
187
+ def display_thread_view(self, entries: list):
188
+ """Display beautiful thread-correlated view."""
189
+ from .tracker import ThreadTracker
190
+
191
+ tracker = ThreadTracker()
192
+ for entry in entries:
193
+ tracker.track(entry)
194
+
195
+ self.console.print("\n[bold]🧵 Thread View[/bold]\n")
196
+
197
+ threads = tracker.get_all_threads()
198
+
199
+ if not threads:
200
+ self.console.print("[yellow]No thread information found.[/yellow]")
201
+ return
202
+
203
+ table = Table(title="Threads", show_header=True, header_style="bold magenta")
204
+ table.add_column("Thread ID", style="cyan", no_wrap=True)
205
+ table.add_column("Logs", justify="right", style="green")
206
+ table.add_column("Errors", justify="right", style="red")
207
+ table.add_column("Duration", style="yellow")
208
+
209
+ for thread in threads:
210
+ duration = ""
211
+ if thread["first_seen"] and thread["last_seen"]:
212
+ delta = thread["last_seen"] - thread["first_seen"]
213
+ duration = f"{delta.total_seconds():.2f}s"
214
+
215
+ table.add_row(
216
+ thread["thread_id"],
217
+ str(thread["log_count"]),
218
+ str(thread["error_count"]),
219
+ duration,
220
+ )
221
+
222
+ self.console.print(table)
223
+
224
+ # Show thread timeline
225
+ for thread in threads[:5]: # Show top 5
226
+ self._display_thread_timeline(thread, entries)
227
+
228
+ def _display_thread_timeline(self, thread: dict, all_entries: list):
229
+ """Display timeline for a specific thread."""
230
+ thread_entries = [e for e in all_entries if e.thread_id == thread["thread_id"]]
231
+
232
+ if not thread_entries:
233
+ return
234
+
235
+ panel_title = f"🧵 Thread: {thread['thread_id']}"
236
+ panel_content = []
237
+
238
+ for entry in thread_entries[:10]: # Show first 10
239
+ level_color = self.LEVEL_COLORS.get(entry.level, "white")
240
+ panel_content.append(f"[{level_color}]●[/{level_color}] {entry.message[:80]}")
241
+
242
+ if len(thread_entries) > 10:
243
+ panel_content.append(f"[dim]... and {len(thread_entries) - 10} more[/dim]")
244
+
245
+ self.console.print(
246
+ Panel(
247
+ "\n".join(panel_content),
248
+ title=panel_title,
249
+ border_style="cyan",
250
+ )
251
+ )
252
+ self.console.print()
logler/tracker.py ADDED
@@ -0,0 +1,138 @@
1
+ """
2
+ Thread and trace tracking for log correlation.
3
+ """
4
+
5
+ from typing import Dict, List, Optional
6
+ from collections import defaultdict
7
+
8
+ from .parser import LogEntry
9
+
10
+
11
+ class ThreadTracker:
12
+ """Track threads and correlate log entries."""
13
+
14
+ def __init__(self):
15
+ self.threads: Dict[str, dict] = {}
16
+ self.traces: Dict[str, dict] = {}
17
+ self.correlations: Dict[str, List[LogEntry]] = defaultdict(list)
18
+
19
+ def track(self, entry: LogEntry):
20
+ """Track a log entry."""
21
+ # Track by thread ID
22
+ if entry.thread_id:
23
+ self._track_thread(entry)
24
+
25
+ # Track by correlation ID
26
+ if entry.correlation_id:
27
+ self.correlations[entry.correlation_id].append(entry)
28
+
29
+ # Track by trace ID
30
+ if entry.trace_id:
31
+ self._track_trace(entry)
32
+
33
+ def _track_thread(self, entry: LogEntry):
34
+ """Track thread information."""
35
+ thread_id = entry.thread_id
36
+
37
+ if thread_id not in self.threads:
38
+ self.threads[thread_id] = {
39
+ "thread_id": thread_id,
40
+ "first_seen": entry.timestamp,
41
+ "last_seen": entry.timestamp,
42
+ "log_count": 0,
43
+ "error_count": 0,
44
+ "correlation_ids": set(),
45
+ }
46
+
47
+ thread = self.threads[thread_id]
48
+ thread["log_count"] += 1
49
+
50
+ if entry.level in ["ERROR", "FATAL", "CRITICAL"]:
51
+ thread["error_count"] += 1
52
+
53
+ if entry.timestamp:
54
+ if not thread["first_seen"] or entry.timestamp < thread["first_seen"]:
55
+ thread["first_seen"] = entry.timestamp
56
+ if not thread["last_seen"] or entry.timestamp > thread["last_seen"]:
57
+ thread["last_seen"] = entry.timestamp
58
+
59
+ if entry.correlation_id:
60
+ thread["correlation_ids"].add(entry.correlation_id)
61
+
62
+ def _track_trace(self, entry: LogEntry):
63
+ """Track trace information."""
64
+ trace_id = entry.trace_id
65
+
66
+ if trace_id not in self.traces:
67
+ self.traces[trace_id] = {
68
+ "trace_id": trace_id,
69
+ "spans": [],
70
+ "services": set(),
71
+ "start_time": entry.timestamp,
72
+ "end_time": entry.timestamp,
73
+ }
74
+
75
+ trace = self.traces[trace_id]
76
+
77
+ if entry.span_id:
78
+ trace["spans"].append(
79
+ {
80
+ "span_id": entry.span_id,
81
+ "timestamp": entry.timestamp,
82
+ "message": entry.message,
83
+ }
84
+ )
85
+
86
+ if entry.service_name:
87
+ trace["services"].add(entry.service_name)
88
+
89
+ if entry.timestamp:
90
+ if not trace["start_time"] or entry.timestamp < trace["start_time"]:
91
+ trace["start_time"] = entry.timestamp
92
+ if not trace["end_time"] or entry.timestamp > trace["end_time"]:
93
+ trace["end_time"] = entry.timestamp
94
+
95
+ def get_thread(self, thread_id: str) -> Optional[dict]:
96
+ """Get thread information."""
97
+ thread = self.threads.get(thread_id)
98
+ if thread:
99
+ thread["correlation_ids"] = list(thread["correlation_ids"])
100
+ return thread
101
+
102
+ def get_all_threads(self) -> List[dict]:
103
+ """Get all tracked threads."""
104
+ result = []
105
+ for thread in self.threads.values():
106
+ thread_copy = thread.copy()
107
+ thread_copy["correlation_ids"] = list(thread["correlation_ids"])
108
+ result.append(thread_copy)
109
+ return sorted(result, key=lambda x: x["log_count"], reverse=True)
110
+
111
+ def get_trace(self, trace_id: str) -> Optional[dict]:
112
+ """Get trace information."""
113
+ trace = self.traces.get(trace_id)
114
+ if trace:
115
+ trace_copy = trace.copy()
116
+ trace_copy["services"] = list(trace["services"])
117
+ if trace_copy["start_time"] and trace_copy["end_time"]:
118
+ delta = trace_copy["end_time"] - trace_copy["start_time"]
119
+ trace_copy["duration_ms"] = delta.total_seconds() * 1000
120
+ return trace_copy
121
+ return None
122
+
123
+ def get_all_traces(self) -> List[dict]:
124
+ """Get all tracked traces."""
125
+ result = []
126
+ for trace_id in self.traces:
127
+ trace = self.get_trace(trace_id)
128
+ if trace:
129
+ result.append(trace)
130
+ return result
131
+
132
+ def get_by_correlation(self, correlation_id: str) -> List[LogEntry]:
133
+ """Get logs by correlation ID."""
134
+ return self.correlations.get(correlation_id, [])
135
+
136
+ def get_all_correlations(self) -> List[str]:
137
+ """Get all correlation IDs."""
138
+ return list(self.correlations.keys())