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/__init__.py +22 -0
- logler/bootstrap.py +57 -0
- logler/cache.py +75 -0
- logler/cli.py +589 -0
- logler/helpers.py +282 -0
- logler/investigate.py +3962 -0
- logler/llm_cli.py +1426 -0
- logler/log_reader.py +267 -0
- logler/parser.py +207 -0
- logler/safe_regex.py +124 -0
- logler/terminal.py +252 -0
- logler/tracker.py +138 -0
- logler/tree_formatter.py +807 -0
- logler/watcher.py +55 -0
- logler/web/__init__.py +3 -0
- logler/web/app.py +810 -0
- logler/web/static/css/tailwind.css +1 -0
- logler/web/static/css/tailwind.input.css +3 -0
- logler/web/static/logler-logo.png +0 -0
- logler/web/tailwind.config.cjs +9 -0
- logler/web/templates/index.html +1454 -0
- logler-1.0.7.dist-info/METADATA +584 -0
- logler-1.0.7.dist-info/RECORD +28 -0
- logler-1.0.7.dist-info/WHEEL +4 -0
- logler-1.0.7.dist-info/entry_points.txt +2 -0
- logler-1.0.7.dist-info/licenses/LICENSE +21 -0
- logler_rs/__init__.py +5 -0
- logler_rs/logler_rs.cp311-win_amd64.pyd +0 -0
logler/cli.py
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for Logler.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import asyncio
|
|
10
|
+
import socket
|
|
11
|
+
from contextlib import closing
|
|
12
|
+
|
|
13
|
+
from .terminal import TerminalViewer
|
|
14
|
+
from .web.app import run_server
|
|
15
|
+
from .llm_cli import llm as llm_group
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group(invoke_without_command=True)
|
|
19
|
+
@click.pass_context
|
|
20
|
+
@click.version_option()
|
|
21
|
+
def main(ctx):
|
|
22
|
+
"""
|
|
23
|
+
š Logler - Beautiful local log viewer
|
|
24
|
+
|
|
25
|
+
A modern log viewer with thread tracking, real-time updates, and beautiful output.
|
|
26
|
+
"""
|
|
27
|
+
if ctx.invoked_subcommand is None:
|
|
28
|
+
click.echo(ctx.get_help())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _find_open_port(host: str, start_port: int, max_tries: int = 20) -> int:
|
|
32
|
+
"""Find the next available port starting from start_port."""
|
|
33
|
+
for candidate in range(start_port, start_port + max_tries):
|
|
34
|
+
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
|
|
35
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
36
|
+
try:
|
|
37
|
+
sock.bind((host, candidate))
|
|
38
|
+
return candidate
|
|
39
|
+
except OSError:
|
|
40
|
+
continue
|
|
41
|
+
raise RuntimeError(f"No open port found in range {start_port}-{start_port + max_tries - 1}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@main.command()
|
|
45
|
+
@click.option("--host", default="0.0.0.0", help="Host to bind to")
|
|
46
|
+
@click.option("--port", default=7607, help="Port to bind to (default 7607 ~ 'LOGL')")
|
|
47
|
+
@click.option(
|
|
48
|
+
"--auto-port/--no-auto-port",
|
|
49
|
+
default=True,
|
|
50
|
+
help="Pick the next free port if the chosen one is busy",
|
|
51
|
+
)
|
|
52
|
+
@click.option("--open", "-o", is_flag=True, help="Open browser automatically")
|
|
53
|
+
@click.argument("files", nargs=-1, type=click.Path(exists=True))
|
|
54
|
+
def serve(host: str, port: int, auto_port: bool, open: bool, files: tuple):
|
|
55
|
+
"""
|
|
56
|
+
Start the web server interface.
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
logler serve # Start with file picker
|
|
60
|
+
logler serve app.log # Start with specific file
|
|
61
|
+
logler serve *.log # Start with multiple files
|
|
62
|
+
"""
|
|
63
|
+
if auto_port:
|
|
64
|
+
chosen_port = _find_open_port(host, port)
|
|
65
|
+
if chosen_port != port:
|
|
66
|
+
click.echo(f"ā ļø Port {port} busy, using {chosen_port} instead")
|
|
67
|
+
port = chosen_port
|
|
68
|
+
|
|
69
|
+
click.echo(f"š Starting Logler web server on http://{host}:{port}")
|
|
70
|
+
|
|
71
|
+
file_paths = [str(Path(f).absolute()) for f in files] if files else []
|
|
72
|
+
|
|
73
|
+
if open:
|
|
74
|
+
import webbrowser
|
|
75
|
+
|
|
76
|
+
webbrowser.open(f"http://localhost:{port}")
|
|
77
|
+
|
|
78
|
+
asyncio.run(run_server(host, port, file_paths))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@main.command()
|
|
82
|
+
@click.argument("files", nargs=-1, required=True, type=click.Path(exists=True))
|
|
83
|
+
@click.option("-n", "--lines", type=int, help="Number of lines to show")
|
|
84
|
+
@click.option("-f", "--follow", is_flag=True, help="Follow log file in real-time")
|
|
85
|
+
@click.option("--level", type=str, help="Filter by log level (DEBUG, INFO, WARN, ERROR)")
|
|
86
|
+
@click.option("--grep", type=str, help="Search for pattern")
|
|
87
|
+
@click.option("--thread", type=str, help="Filter by thread ID")
|
|
88
|
+
@click.option("--no-color", is_flag=True, help="Disable colored output")
|
|
89
|
+
def view(
|
|
90
|
+
files: tuple,
|
|
91
|
+
lines: Optional[int],
|
|
92
|
+
follow: bool,
|
|
93
|
+
level: Optional[str],
|
|
94
|
+
grep: Optional[str],
|
|
95
|
+
thread: Optional[str],
|
|
96
|
+
no_color: bool,
|
|
97
|
+
):
|
|
98
|
+
"""
|
|
99
|
+
View log files in the terminal with beautiful output.
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
logler view app.log # View entire file
|
|
103
|
+
logler view app.log -n 100 # Last 100 lines
|
|
104
|
+
logler view app.log -f # Follow in real-time
|
|
105
|
+
logler view app.log --level ERROR # Show only errors
|
|
106
|
+
logler view app.log --grep "timeout" # Search for pattern
|
|
107
|
+
logler view app.log --thread worker-1 # Filter by thread
|
|
108
|
+
"""
|
|
109
|
+
viewer = TerminalViewer(use_colors=not no_color)
|
|
110
|
+
|
|
111
|
+
for file_path in files:
|
|
112
|
+
try:
|
|
113
|
+
asyncio.run(
|
|
114
|
+
viewer.view_file(
|
|
115
|
+
file_path=file_path,
|
|
116
|
+
lines=lines,
|
|
117
|
+
follow=follow,
|
|
118
|
+
level_filter=level,
|
|
119
|
+
pattern=grep,
|
|
120
|
+
thread_filter=thread,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
except KeyboardInterrupt:
|
|
124
|
+
click.echo("\nš Goodbye!")
|
|
125
|
+
sys.exit(0)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
click.echo(f"ā Error: {e}", err=True)
|
|
128
|
+
sys.exit(1)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@main.command()
|
|
132
|
+
@click.argument("files", nargs=-1, required=True, type=click.Path(exists=True))
|
|
133
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
134
|
+
def stats(files: tuple, output_json: bool):
|
|
135
|
+
"""
|
|
136
|
+
Show statistics for log files.
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
logler stats app.log # Show statistics
|
|
140
|
+
logler stats app.log --json # Output as JSON
|
|
141
|
+
"""
|
|
142
|
+
from .parser import LogParser
|
|
143
|
+
from rich.console import Console
|
|
144
|
+
from rich.table import Table
|
|
145
|
+
|
|
146
|
+
console = Console()
|
|
147
|
+
parser = LogParser()
|
|
148
|
+
|
|
149
|
+
for file_path in files:
|
|
150
|
+
with open(file_path, "r") as f:
|
|
151
|
+
entries = [parser.parse_line(i + 1, line.rstrip()) for i, line in enumerate(f)]
|
|
152
|
+
|
|
153
|
+
stats_data = {
|
|
154
|
+
"total": len(entries),
|
|
155
|
+
"by_level": {},
|
|
156
|
+
"by_thread": {},
|
|
157
|
+
"errors": 0,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for entry in entries:
|
|
161
|
+
level = str(entry.level)
|
|
162
|
+
stats_data["by_level"][level] = stats_data["by_level"].get(level, 0) + 1
|
|
163
|
+
|
|
164
|
+
if entry.level in ["ERROR", "FATAL", "CRITICAL"]:
|
|
165
|
+
stats_data["errors"] += 1
|
|
166
|
+
|
|
167
|
+
if entry.thread_id:
|
|
168
|
+
stats_data["by_thread"][entry.thread_id] = (
|
|
169
|
+
stats_data["by_thread"].get(entry.thread_id, 0) + 1
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if output_json:
|
|
173
|
+
console.print_json(data=stats_data)
|
|
174
|
+
else:
|
|
175
|
+
console.print(f"\n[bold]š Statistics for {file_path}[/bold]\n")
|
|
176
|
+
|
|
177
|
+
table = Table(title="Log Levels")
|
|
178
|
+
table.add_column("Level", style="cyan")
|
|
179
|
+
table.add_column("Count", justify="right", style="green")
|
|
180
|
+
|
|
181
|
+
for level, count in sorted(stats_data["by_level"].items()):
|
|
182
|
+
table.add_row(level, str(count))
|
|
183
|
+
|
|
184
|
+
console.print(table)
|
|
185
|
+
|
|
186
|
+
console.print(f"\n[bold red]Errors:[/bold red] {stats_data['errors']}")
|
|
187
|
+
console.print(f"[bold]Total:[/bold] {stats_data['total']} entries\n")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@main.command()
|
|
191
|
+
@click.argument("files", nargs=-1, required=True, type=click.Path(exists=True))
|
|
192
|
+
@click.option("--auto-insights", is_flag=True, help="Run automatic insights analysis")
|
|
193
|
+
@click.option("--errors", is_flag=True, help="Show only errors with analysis")
|
|
194
|
+
@click.option("--patterns", is_flag=True, help="Find repeated patterns")
|
|
195
|
+
@click.option("--thread", type=str, help="Follow specific thread ID")
|
|
196
|
+
@click.option("--correlation", type=str, help="Follow specific correlation ID")
|
|
197
|
+
@click.option(
|
|
198
|
+
"--hierarchy", is_flag=True, help="Show thread hierarchy tree (with --thread or --correlation)"
|
|
199
|
+
)
|
|
200
|
+
@click.option("--waterfall", is_flag=True, help="Show waterfall timeline (with --hierarchy)")
|
|
201
|
+
@click.option("--flamegraph", is_flag=True, help="Show flamegraph visualization (with --hierarchy)")
|
|
202
|
+
@click.option(
|
|
203
|
+
"--show-error-flow",
|
|
204
|
+
is_flag=True,
|
|
205
|
+
help="Analyze error propagation through hierarchy (with --hierarchy)",
|
|
206
|
+
)
|
|
207
|
+
@click.option("--max-depth", type=int, help="Maximum hierarchy depth to display")
|
|
208
|
+
@click.option(
|
|
209
|
+
"--min-confidence",
|
|
210
|
+
type=float,
|
|
211
|
+
default=0.0,
|
|
212
|
+
help="Minimum confidence for hierarchy detection (0.0-1.0)",
|
|
213
|
+
)
|
|
214
|
+
@click.option("--context", type=int, default=3, help="Number of context lines (default: 3)")
|
|
215
|
+
@click.option(
|
|
216
|
+
"--output",
|
|
217
|
+
type=click.Choice(["full", "summary", "count", "compact"]),
|
|
218
|
+
default="summary",
|
|
219
|
+
help="Output format (default: summary)",
|
|
220
|
+
)
|
|
221
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
222
|
+
@click.option(
|
|
223
|
+
"--min-occurrences", type=int, default=3, help="Minimum pattern occurrences (default: 3)"
|
|
224
|
+
)
|
|
225
|
+
def investigate(
|
|
226
|
+
files: tuple,
|
|
227
|
+
auto_insights: bool,
|
|
228
|
+
errors: bool,
|
|
229
|
+
patterns: bool,
|
|
230
|
+
thread: Optional[str],
|
|
231
|
+
correlation: Optional[str],
|
|
232
|
+
hierarchy: bool,
|
|
233
|
+
waterfall: bool,
|
|
234
|
+
flamegraph: bool,
|
|
235
|
+
show_error_flow: bool,
|
|
236
|
+
max_depth: Optional[int],
|
|
237
|
+
min_confidence: float,
|
|
238
|
+
context: int,
|
|
239
|
+
output: str,
|
|
240
|
+
output_json: bool,
|
|
241
|
+
min_occurrences: int,
|
|
242
|
+
):
|
|
243
|
+
"""
|
|
244
|
+
Investigate log files with smart analysis and insights.
|
|
245
|
+
|
|
246
|
+
Examples:
|
|
247
|
+
logler investigate app.log --auto-insights # Auto-detect issues
|
|
248
|
+
logler investigate app.log --errors # Analyze errors
|
|
249
|
+
logler investigate app.log --patterns # Find repeated patterns
|
|
250
|
+
logler investigate app.log --thread worker-1 # Follow specific thread
|
|
251
|
+
logler investigate app.log --correlation req-123 # Follow request
|
|
252
|
+
logler investigate app.log --thread req-123 --hierarchy # Show hierarchy tree
|
|
253
|
+
logler investigate app.log --thread req-123 --hierarchy --waterfall # Show waterfall timeline
|
|
254
|
+
logler investigate app.log --thread req-123 --hierarchy --flamegraph # Show flamegraph
|
|
255
|
+
logler investigate app.log --hierarchy --show-error-flow # Analyze error propagation
|
|
256
|
+
logler investigate app.log --output summary # Token-efficient output
|
|
257
|
+
"""
|
|
258
|
+
from .investigate import (
|
|
259
|
+
analyze_with_insights,
|
|
260
|
+
search,
|
|
261
|
+
find_patterns,
|
|
262
|
+
follow_thread,
|
|
263
|
+
follow_thread_hierarchy,
|
|
264
|
+
get_hierarchy_summary,
|
|
265
|
+
analyze_error_flow,
|
|
266
|
+
format_error_flow,
|
|
267
|
+
)
|
|
268
|
+
from rich.console import Console
|
|
269
|
+
from rich.table import Table
|
|
270
|
+
from rich.panel import Panel
|
|
271
|
+
|
|
272
|
+
console = Console()
|
|
273
|
+
file_list = list(files)
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
# Auto-insights mode (most powerful)
|
|
277
|
+
if auto_insights:
|
|
278
|
+
console.print("[bold cyan]šÆ Running automatic insights analysis...[/bold cyan]\n")
|
|
279
|
+
result = analyze_with_insights(files=file_list, auto_investigate=True)
|
|
280
|
+
|
|
281
|
+
if output_json:
|
|
282
|
+
console.print_json(data=result)
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
# Display overview
|
|
286
|
+
overview = result["overview"]
|
|
287
|
+
console.print(
|
|
288
|
+
Panel(
|
|
289
|
+
f"[bold]Total Logs:[/bold] {overview['total_logs']}\n"
|
|
290
|
+
f"[bold]Error Count:[/bold] {overview['error_count']}\n"
|
|
291
|
+
f"[bold]Error Rate:[/bold] {overview['error_rate']:.1%}\n"
|
|
292
|
+
f"[bold]Log Levels:[/bold] {overview['log_levels']}",
|
|
293
|
+
title="š Overview",
|
|
294
|
+
border_style="cyan",
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Display insights
|
|
299
|
+
if result["insights"]:
|
|
300
|
+
console.print("\n[bold cyan]š” Automatic Insights[/bold cyan]\n")
|
|
301
|
+
for i, insight in enumerate(result["insights"], 1):
|
|
302
|
+
severity_color = {"high": "red", "medium": "yellow", "low": "green"}.get(
|
|
303
|
+
insight["severity"], "white"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
severity_icon = {"high": "š“", "medium": "š”", "low": "š¢"}.get(
|
|
307
|
+
insight["severity"], "āŖ"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
console.print(
|
|
311
|
+
f"{severity_icon} [bold {severity_color}]Insight #{i}:[/bold {severity_color}] {insight['type']}"
|
|
312
|
+
)
|
|
313
|
+
console.print(
|
|
314
|
+
f" [dim]Severity:[/dim] [{severity_color}]{insight['severity'].upper()}[/{severity_color}]"
|
|
315
|
+
)
|
|
316
|
+
console.print(f" [dim]Description:[/dim] {insight['description']}")
|
|
317
|
+
console.print(f" [dim]Suggestion:[/dim] {insight['suggestion']}\n")
|
|
318
|
+
|
|
319
|
+
# Display suggestions
|
|
320
|
+
if result["suggestions"]:
|
|
321
|
+
console.print("[bold cyan]š Suggestions[/bold cyan]\n")
|
|
322
|
+
for i, suggestion in enumerate(result["suggestions"], 1):
|
|
323
|
+
console.print(f" {i}. {suggestion}")
|
|
324
|
+
|
|
325
|
+
# Display next steps
|
|
326
|
+
if result["next_steps"]:
|
|
327
|
+
console.print("\n[bold cyan]š Next Steps[/bold cyan]\n")
|
|
328
|
+
for i, step in enumerate(result["next_steps"], 1):
|
|
329
|
+
console.print(f" {i}. {step}")
|
|
330
|
+
|
|
331
|
+
# Pattern detection mode
|
|
332
|
+
elif patterns:
|
|
333
|
+
console.print(
|
|
334
|
+
f"[bold cyan]š Finding repeated patterns (min {min_occurrences} occurrences)...[/bold cyan]\n"
|
|
335
|
+
)
|
|
336
|
+
result = find_patterns(files=file_list, min_occurrences=min_occurrences)
|
|
337
|
+
|
|
338
|
+
if output_json:
|
|
339
|
+
console.print_json(data=result)
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
pattern_list = result.get("patterns", [])
|
|
343
|
+
if pattern_list:
|
|
344
|
+
table = Table(title=f"Found {len(pattern_list)} Patterns")
|
|
345
|
+
table.add_column("Pattern", style="cyan", no_wrap=False)
|
|
346
|
+
table.add_column("Count", justify="right", style="green")
|
|
347
|
+
table.add_column("First Seen", style="yellow")
|
|
348
|
+
table.add_column("Last Seen", style="yellow")
|
|
349
|
+
|
|
350
|
+
for pattern in pattern_list[:20]: # Show top 20
|
|
351
|
+
pattern_text = pattern.get("pattern", "")[:80]
|
|
352
|
+
count = pattern.get("occurrences", 0)
|
|
353
|
+
first = pattern.get("first_seen", "N/A")
|
|
354
|
+
last = pattern.get("last_seen", "N/A")
|
|
355
|
+
table.add_row(pattern_text, str(count), first, last)
|
|
356
|
+
|
|
357
|
+
console.print(table)
|
|
358
|
+
else:
|
|
359
|
+
console.print("[yellow]No repeated patterns found.[/yellow]")
|
|
360
|
+
|
|
361
|
+
# Thread/correlation following mode
|
|
362
|
+
elif thread or correlation:
|
|
363
|
+
identifier = thread or correlation
|
|
364
|
+
id_type = "thread" if thread else "correlation"
|
|
365
|
+
|
|
366
|
+
# Hierarchy mode
|
|
367
|
+
if hierarchy:
|
|
368
|
+
console.print(
|
|
369
|
+
f"[bold cyan]š³ Building hierarchy for {id_type}: {identifier}...[/bold cyan]\n"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
hier_result = follow_thread_hierarchy(
|
|
374
|
+
files=file_list,
|
|
375
|
+
root_identifier=identifier,
|
|
376
|
+
max_depth=max_depth,
|
|
377
|
+
min_confidence=min_confidence,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
if output_json:
|
|
381
|
+
console.print_json(data=hier_result)
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
# Import tree formatter
|
|
385
|
+
from .tree_formatter import format_tree, format_waterfall, format_flamegraph
|
|
386
|
+
|
|
387
|
+
# Show summary first
|
|
388
|
+
summary = get_hierarchy_summary(hier_result)
|
|
389
|
+
console.print(summary)
|
|
390
|
+
console.print()
|
|
391
|
+
|
|
392
|
+
# Show tree visualization
|
|
393
|
+
if waterfall:
|
|
394
|
+
console.print("[bold cyan]š Waterfall Timeline[/bold cyan]\n")
|
|
395
|
+
waterfall_str = format_waterfall(hier_result, width=100)
|
|
396
|
+
console.print(waterfall_str)
|
|
397
|
+
elif flamegraph:
|
|
398
|
+
console.print("[bold cyan]š„ Flamegraph Visualization[/bold cyan]\n")
|
|
399
|
+
flamegraph_str = format_flamegraph(hier_result, width=100)
|
|
400
|
+
console.print(flamegraph_str)
|
|
401
|
+
else:
|
|
402
|
+
console.print("[bold cyan]š² Hierarchy Tree[/bold cyan]\n")
|
|
403
|
+
tree_str = format_tree(
|
|
404
|
+
hier_result,
|
|
405
|
+
mode="detailed",
|
|
406
|
+
show_duration=True,
|
|
407
|
+
show_errors=True,
|
|
408
|
+
max_depth=max_depth,
|
|
409
|
+
use_colors=True,
|
|
410
|
+
)
|
|
411
|
+
console.print(tree_str)
|
|
412
|
+
|
|
413
|
+
# Show error flow analysis if requested
|
|
414
|
+
if show_error_flow:
|
|
415
|
+
console.print()
|
|
416
|
+
console.print("[bold cyan]š Error Flow Analysis[/bold cyan]\n")
|
|
417
|
+
error_analysis = analyze_error_flow(hier_result)
|
|
418
|
+
if output_json:
|
|
419
|
+
console.print_json(data=error_analysis)
|
|
420
|
+
else:
|
|
421
|
+
error_flow_str = format_error_flow(error_analysis)
|
|
422
|
+
console.print(error_flow_str)
|
|
423
|
+
|
|
424
|
+
except Exception as e:
|
|
425
|
+
console.print(f"[red]ā Error building hierarchy: {e}[/red]")
|
|
426
|
+
console.print("[yellow]Falling back to regular thread following...[/yellow]\n")
|
|
427
|
+
hierarchy = False # Fall through to regular mode
|
|
428
|
+
|
|
429
|
+
# Regular thread following mode
|
|
430
|
+
if not hierarchy:
|
|
431
|
+
console.print(f"[bold cyan]š§µ Following {id_type}: {identifier}...[/bold cyan]\n")
|
|
432
|
+
|
|
433
|
+
result = follow_thread(
|
|
434
|
+
files=file_list, thread_id=thread, correlation_id=correlation
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
if output_json:
|
|
438
|
+
console.print_json(data=result)
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
entries = result.get("entries", [])
|
|
442
|
+
total = result.get("total_entries", len(entries))
|
|
443
|
+
duration = result.get("duration_ms", 0)
|
|
444
|
+
|
|
445
|
+
console.print(f"[bold]Found {total} entries[/bold]")
|
|
446
|
+
if duration:
|
|
447
|
+
console.print(f"[bold]Duration:[/bold] {duration}ms\n")
|
|
448
|
+
|
|
449
|
+
# Display entries
|
|
450
|
+
for entry in entries[:50]: # Limit display to 50
|
|
451
|
+
timestamp = entry.get("timestamp", "N/A")
|
|
452
|
+
level = entry.get("level", "INFO")
|
|
453
|
+
message = entry.get("message", "")[:100]
|
|
454
|
+
|
|
455
|
+
level_color = {
|
|
456
|
+
"ERROR": "red",
|
|
457
|
+
"FATAL": "red",
|
|
458
|
+
"WARN": "yellow",
|
|
459
|
+
"WARNING": "yellow",
|
|
460
|
+
"INFO": "cyan",
|
|
461
|
+
"DEBUG": "dim",
|
|
462
|
+
"TRACE": "dim",
|
|
463
|
+
}.get(level, "white")
|
|
464
|
+
|
|
465
|
+
console.print(
|
|
466
|
+
f"[dim]{timestamp}[/dim] [{level_color}]{level:8s}[/{level_color}] {message}"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
if len(entries) > 50:
|
|
470
|
+
console.print(f"\n[dim]... and {len(entries) - 50} more entries[/dim]")
|
|
471
|
+
|
|
472
|
+
# Error analysis mode
|
|
473
|
+
elif errors:
|
|
474
|
+
console.print("[bold cyan]ā Analyzing errors...[/bold cyan]\n")
|
|
475
|
+
result = search(
|
|
476
|
+
files=file_list, level="ERROR", context_lines=context, output_format=output
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
if output_json:
|
|
480
|
+
console.print_json(data=result)
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
if output == "summary":
|
|
484
|
+
total = result.get("total_matches", 0)
|
|
485
|
+
unique = result.get("unique_messages", 0)
|
|
486
|
+
console.print(f"[bold]Total Errors:[/bold] {total}")
|
|
487
|
+
console.print(f"[bold]Unique Messages:[/bold] {unique}\n")
|
|
488
|
+
|
|
489
|
+
top_messages = result.get("top_messages", [])
|
|
490
|
+
if top_messages:
|
|
491
|
+
table = Table(title="Top Error Messages")
|
|
492
|
+
table.add_column("Message", style="red", no_wrap=False)
|
|
493
|
+
table.add_column("Count", justify="right", style="green")
|
|
494
|
+
table.add_column("First Seen", style="yellow")
|
|
495
|
+
|
|
496
|
+
for msg in top_messages[:10]:
|
|
497
|
+
message = msg.get("message", "")[:80]
|
|
498
|
+
count = msg.get("count", 0)
|
|
499
|
+
first = msg.get("first_seen", "N/A")
|
|
500
|
+
table.add_row(message, str(count), first)
|
|
501
|
+
|
|
502
|
+
console.print(table)
|
|
503
|
+
|
|
504
|
+
elif output == "count":
|
|
505
|
+
console.print_json(data=result)
|
|
506
|
+
|
|
507
|
+
elif output == "compact":
|
|
508
|
+
matches = result.get("matches", [])
|
|
509
|
+
for match in matches[:50]:
|
|
510
|
+
time = match.get("time", "N/A")
|
|
511
|
+
msg = match.get("msg", "")
|
|
512
|
+
console.print(f"[dim]{time}[/dim] [red]ERROR[/red] {msg}")
|
|
513
|
+
|
|
514
|
+
else: # full
|
|
515
|
+
results = result.get("results", [])
|
|
516
|
+
for item in results[:50]:
|
|
517
|
+
entry = item.get("entry", {})
|
|
518
|
+
timestamp = entry.get("timestamp", "N/A")
|
|
519
|
+
message = entry.get("message", "")
|
|
520
|
+
console.print(f"[dim]{timestamp}[/dim] [red]ERROR[/red] {message}")
|
|
521
|
+
|
|
522
|
+
# Default search mode
|
|
523
|
+
else:
|
|
524
|
+
console.print("[bold cyan]š Searching logs...[/bold cyan]\n")
|
|
525
|
+
result = search(files=file_list, context_lines=context, output_format=output)
|
|
526
|
+
|
|
527
|
+
if output_json:
|
|
528
|
+
console.print_json(data=result)
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
total = result.get("total_matches", 0)
|
|
532
|
+
console.print(f"[bold]Total matches:[/bold] {total}\n")
|
|
533
|
+
|
|
534
|
+
if output == "summary":
|
|
535
|
+
console.print_json(data=result)
|
|
536
|
+
elif output == "count":
|
|
537
|
+
console.print_json(data=result)
|
|
538
|
+
else:
|
|
539
|
+
results = result.get("results", [])
|
|
540
|
+
for item in results[:50]:
|
|
541
|
+
entry = item.get("entry", {})
|
|
542
|
+
timestamp = entry.get("timestamp", "N/A")
|
|
543
|
+
level = entry.get("level", "INFO")
|
|
544
|
+
message = entry.get("message", "")[:100]
|
|
545
|
+
console.print(f"[dim]{timestamp}[/dim] {level:8s} {message}")
|
|
546
|
+
|
|
547
|
+
except Exception as e:
|
|
548
|
+
console.print(f"[red]ā Error:[/red] {e}", err=True)
|
|
549
|
+
if "--debug" in sys.argv:
|
|
550
|
+
import traceback
|
|
551
|
+
|
|
552
|
+
traceback.print_exc()
|
|
553
|
+
sys.exit(1)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
@main.command()
|
|
557
|
+
@click.argument("pattern", required=True)
|
|
558
|
+
@click.option("--directory", "-d", default=".", help="Directory to watch")
|
|
559
|
+
@click.option("--recursive", "-r", is_flag=True, help="Watch recursively")
|
|
560
|
+
def watch(pattern: str, directory: str, recursive: bool):
|
|
561
|
+
"""
|
|
562
|
+
Watch for new log files matching a pattern.
|
|
563
|
+
|
|
564
|
+
Examples:
|
|
565
|
+
logler watch "*.log" # Watch current directory
|
|
566
|
+
logler watch "app-*.log" -d /var/log # Watch specific directory
|
|
567
|
+
logler watch "*.log" -r # Watch recursively
|
|
568
|
+
"""
|
|
569
|
+
from .watcher import FileWatcher
|
|
570
|
+
from rich.console import Console
|
|
571
|
+
|
|
572
|
+
console = Console()
|
|
573
|
+
console.print(f"š Watching for files matching: [cyan]{pattern}[/cyan]")
|
|
574
|
+
console.print(f"š Directory: [yellow]{directory}[/yellow]")
|
|
575
|
+
|
|
576
|
+
watcher = FileWatcher(pattern, directory, recursive)
|
|
577
|
+
|
|
578
|
+
try:
|
|
579
|
+
asyncio.run(watcher.watch())
|
|
580
|
+
except KeyboardInterrupt:
|
|
581
|
+
console.print("\nš Stopped watching")
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
# Register the LLM command group
|
|
585
|
+
main.add_command(llm_group)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
if __name__ == "__main__":
|
|
589
|
+
main()
|