Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ uv run logan view -d "tmp/output"
# server should be available at http://localhost:8000/log_diagnosis
```

![Log-Explorer](./docs/asset/log-explorer.png)

## MCP Server (for AI Agents)

LogAn exposes its analysis capabilities via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io), allowing AI agents (Claude Desktop, Claude Code, custom agents) to analyze logs programmatically.
Expand Down
Binary file added docs/asset/log-explorer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions logan/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,8 @@ def analyze(files, glob, time_range, output_dir, debug_mode, process_all_files,
click.echo(click.style("\n" + "=" * 50, fg="green"))
click.echo(click.style("Analysis complete!", fg="green", bold=True))
click.echo(f"\nOutput files:")
click.echo(f" Anomaly report: {os.path.join(output_dir, 'log_diagnosis', 'anomalies.html')}")
click.echo(f" Summary report: {os.path.join(output_dir, 'log_diagnosis', 'summary.html')}")
click.echo(f" Log explorer: {os.path.join(output_dir, 'log_diagnosis', 'explorer.html')}")
click.echo(f" Store (Parquet): {os.path.join(output_dir, 'store', '')}")


@cli.command()
Expand Down
15 changes: 14 additions & 1 deletion logan/drain/run_drain.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from drain3.template_miner_config import TemplateMinerConfig
from drain3.file_persistence import FilePersistence

from logan.store.store import LogStore

class Templatizer:
"""
The Templatizer class is responsible for mining log templates using the DRAIN3 algorithm.
Expand Down Expand Up @@ -91,15 +93,26 @@ def miner(self, df, output_dir: str, template: str):
# Initialize the TemplateMiner with the loaded configuration and file persistence
template_miner_temporary = TemplateMiner(mem_persistence, config)

# Preserve original log text before Drain3 masking overwrites it
df["original_text"] = df["text"].astype(str)

# Initialize a dictionary to store the loglines grouped by template IDs
template_log_dict = {}

# Mine templates by iterating directly over the column (avoids pd.Series overhead of df.apply)
try:
test_ids = []
template_strs = []
variables_list = []
for log in df["truncated_log"].values:
test_ids.append(template_miner_temporary.add_log_message(log)['cluster_id'])
result = template_miner_temporary.add_log_message(log)
test_ids.append(result['cluster_id'])
tmpl = result.get('template_mined', '')
template_strs.append(tmpl)
variables_list.append(LogStore.extract_variables(log, tmpl))
df["test_ids"] = test_ids
df["template_str"] = template_strs
df["variables"] = [json.dumps(v) for v in variables_list]

if (self.debug_mode == "true"):
template_log_dict = df.groupby("test_ids")["truncated_log"].agg(list).to_dict()
Expand Down
31 changes: 13 additions & 18 deletions logan/log_diagnosis/anomaly.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
import json
import pandas as pd
import time
import csv
from .core import Core
from datetime import datetime
from logan.log_diagnosis.utils import get_anomaly_html_str, get_summary_html_str, compute_golden_signal_timeline
from logan.log_diagnosis.utils import get_explorer_html_str, compute_golden_signal_timeline
from logan.log_diagnosis.models import ModelManager, AllModels, ModelType
from logan.store.store import LogStore

class Anomaly(Core):
"""
Expand Down Expand Up @@ -307,21 +307,16 @@ def get_anomaly_report(self, df_inference_csv, output_dir):
'list_templates': template_ids2
})

# Read debug information (ignored and processed files)
with open(os.path.join(developer_debug_dir, "ignored_files.log"), 'r') as reader:
ignored_files = reader.read().splitlines()

with open(os.path.join(developer_debug_dir, "processed_files.log"), 'r') as reader:
processsed_files = reader.read().splitlines()

# Generate the HTML table for the anomaly report
html_table = get_anomaly_html_str(df_final_anomalies, output_dir)
with open(os.path.join(log_diagnosis_dir, "anomalies.html"), "w") as f:
f.write(html_table)

# Generate the HTML table for the summary report
html_table = get_summary_html_str(df_for_summary_html, include_golden_signal_dropdown=True, ignored_file_list=ignored_files, processed_file_list=processsed_files, output_dir=output_dir, has_timeline_data=has_timeline_data)
with open(os.path.join(log_diagnosis_dir, "summary.html"), "w") as f:
f.write(html_table)
# Build and persist the structured log store, then render explorer
try:
store = LogStore(output_dir)
store.build_from_df(df_inference_csv, temp_id_to_signal_map)
store.save_parquet()
store_meta = store.save_json_for_explorer()
html_explorer = get_explorer_html_str(store_meta)
with open(os.path.join(log_diagnosis_dir, "explorer.html"), "w") as f:
f.write(html_explorer)
except Exception as exc:
print(f"Warning: could not generate log store / explorer: {exc}")

self.compute_anomaly_statistics(output_dir, (time.time() - start) * 1000)
Loading