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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ ANTHROPIC_MODEL=claude-haiku-4-5-20251001
# Unknown Patterns panel and counted as new_template_count in /api/analyze.
#AI_LOG_ANALYZER_TEMPLATE_STORE=~/.cache/netlog-ai/templates.json

# ── Incident memory (optional) ────────────────────────────────────────────────
# JSONL journal of past analysis runs. When set, action items carry a
# "seen NΓ— before" recurrence block and /api/incidents/similar?q= searches
# the history. Local-only, bounded, never leaves the host.
#AI_LOG_ANALYZER_INCIDENT_STORE=~/.cache/netlog-ai/incidents.jsonl

# ── Custom classification rules (optional) ────────────────────────────────────
# JSON file with a list of {"pattern", "severity", "category", "description"}
# objects. Loaded at startup; rules take precedence over the built-in KB.
Expand Down
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,50 @@ All notable changes to **netlog-ai** are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/); this project uses
loose semantic versioning.

## [0.5.0] - 2026-07-13

Research-driven wave (competitive study of 2026 NOC tooling β€” SSE-first live
viewers, incident-history UX, Cisco/Nokia syslog references).

### Added

- **πŸ”΄ Live tail** β€” `GET /api/tail/<source_id>` streams classified events
from any syslog listener source via Server-Sent Events (zero new deps, no
polling, server-side `?min_severity=` filter, keepalive frames, query-token
auth for EventSource). New Live Tail panel in the Logs tab with start/stop,
source picker, and a rolling 200-event view. Backed by a new cursor-based
`fetch_new()` on the syslog source (monotonic ingest counter β€” no
duplicates, wraparound-safe).
- **↻ Incident memory** (`memory.py`) β€” with `AI_LOG_ANALYZER_INCIDENT_STORE`
set, every run journals its action items locally (bounded JSONL); future
runs annotate items with `recurrence` ("seen 3Γ— before, last Jul 5, on
spine-01") shown as a badge in the UI, and `/api/incidents/similar?q=`
answers free-text "have we seen this before?" via token-overlap search.
- **🎬 Demo mode** β€” `ai-log-analyzer demo` runs a deterministic synthetic
incident storyline (6 devices, 4 vendor dialects: flapping interface, BGP
collapse, NX-OS service crash, PSU failure, SSH brute force, SR Linux
churn, one never-seen shape) through the full pipeline and prints the
result; `--serve` starts the UI with a UDP feeder so Live Tail streams the
story in real time. Zero setup, zero LLM key needed.
- **Cisco IOS-XE / NX-OS / Nokia SR Linux classifier patterns** β€”
`%LINK-3-UPDOWN`, `%LINEPROTO-5-UPDOWN`, `%ETHPORT-5-IF_DOWN`,
`%SYSMGR-2-SERVICE_CRASHED`, `%VPC-2-*`, `%MODULE-2/%PLATFORM-2`,
`%DUAL-5-NBRCHANGE` (EIGRP), `%HSRP-5-STATECHANGE`, `%ENVMON`,
`%SEC_LOGIN-4/5`, `%SYS-5-CONFIG_I`, and SR Linux `bgp_mgr` session /
oper-state transitions β€” mapped onto the existing canonical descriptions so
stability flap-pairing, recovery filtering, and the KB all work unchanged.
- **Per-event confidence score** β€” every classified event now carries
`confidence`: 1.0 custom rule, 0.9 KB pattern, 0.6 raw-severity promotion,
0.3 unmatched snippet.

### Performance

- Literal-gate extraction now recovers guaranteed literals from alternations
whose common prefix sre hoists into a short run (`%a|%b` β†’ `%` + branch) β€”
unguarded always-scanned patterns drop from 2 to **0**.

Suite 338 β†’ 369 tests.

## [0.4.0] - 2026-07-13

### Added
Expand Down
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

> **Network logs in. Ranked actions out.** A local, dark-themed dashboard that classifies syslog events from any vendor (Junos, Arista EOS, FRR), builds a prioritized action list, and lets an LLM write the root-cause analysis with copy-pastable CLI fixes.

[![CI](https://github.com/gesh75/netlog-ai/actions/workflows/ci.yml/badge.svg)](https://github.com/gesh75/netlog-ai/actions/workflows/ci.yml) ![Tests](https://img.shields.io/badge/tests-338%20passing-brightgreen) ![License](https://img.shields.io/badge/license-MIT-blue) ![Python](https://img.shields.io/badge/python-3.10%2B-blue) ![Stack](https://img.shields.io/badge/stack-Flask%20%2B%20vanilla%20JS-1f6feb)
[![CI](https://github.com/gesh75/netlog-ai/actions/workflows/ci.yml/badge.svg)](https://github.com/gesh75/netlog-ai/actions/workflows/ci.yml) ![Tests](https://img.shields.io/badge/tests-369%20passing-brightgreen) ![License](https://img.shields.io/badge/license-MIT-blue) ![Python](https://img.shields.io/badge/python-3.10%2B-blue) ![Stack](https://img.shields.io/badge/stack-Flask%20%2B%20vanilla%20JS-1f6feb)

> πŸ““ Recent changes β€” cross-source correlation + per-device triage (MCP tools **and** Device-tab UI) β€” are in [`CHANGELOG.md`](CHANGELOG.md).

Expand Down Expand Up @@ -66,7 +66,10 @@ Tools exposed: `list_sources`, `add_source`, `fetch_logs`, `search_logs`,
| πŸ€– **MCP server mode** | Claude Code / Cursor / Continue can call the analyzer directly as agent tools |
| πŸ”— **Cross-source correlation** | **Device tab** β†’ *Correlate Sources*: scans every registered source and tags each host `confirmed` (flagged by β‰₯ 2 sources) or `suspected` (1) in a sortable, severity-coded table |
| πŸ”¬ **Per-device triage** | **Device tab** β†’ *Triage Device*: one host's verdict + 0–100 health score, severity histogram, top processes, and deduped error patterns in a single panel |
| πŸ”Ž **Classify** | 50+ regex patterns across Junos, EOS, FRR, IOS, RFC-3164/5424 β€” plus your own rules via `AI_LOG_ANALYZER_CUSTOM_RULES` / `POST /api/rules` |
| πŸ”Ž **Classify** | 70+ regex patterns across Junos, EOS, FRR, **Cisco IOS-XE, NX-OS, Nokia SR Linux**, RFC-3164/5424 β€” each event carries a confidence score; add your own rules via `AI_LOG_ANALYZER_CUSTOM_RULES` / `POST /api/rules` |
| πŸ”΄ **Live tail** | Real-time SSE stream of classified events from any syslog listener source β€” zero polling, zero build step, server-side severity filter, its own UI panel |
| ↻ **Incident memory** | `AI_LOG_ANALYZER_INCIDENT_STORE` journals every run locally; action items get "seen 3Γ— before (last Jul 5)" recurrence badges and `/api/incidents/similar?q=` answers "have we seen this before?" |
| 🎬 **Demo mode** | `ai-log-analyzer demo` β€” zero-setup synthetic incident storyline through the full pipeline; `--serve` adds a live feeder so Live Tail streams it in real time |
| πŸ”­ **Unknown patterns** | Lines no rule matched are template-mined (Drain-style, zero deps): variables masked, shapes clustered, error-smelling templates ranked first β€” novel failure modes surface instead of vanishing as `info` noise. Set `AI_LOG_ANALYZER_TEMPLATE_STORE` for cross-run memory: shapes never seen in any prior run get flagged πŸ†• |
| 🧭 **Prioritize** | Deduped action items, ranked by severity Γ— count, recovery events excluded |
| πŸ“Ά **Fabric stability** | Per-device flap detection (interface/BGP/OSPF/LAG/VPN down↔up oscillation), event-rate bursts vs the device's own baseline, trend + heuristic 24h risk band β€” in `/api/analyze` and its own UI panel |
Expand Down Expand Up @@ -113,6 +116,10 @@ ai-log-analyzer eval # heuristic, offline
ai-log-analyzer eval --use-llm # blend a real LLM judge
ai-log-analyzer eval --file result.json --min-score 7 # CI gate

# Zero-setup demo (synthetic incident storyline; --serve adds live streaming)
ai-log-analyzer demo
ai-log-analyzer demo --serve

# Run the full test suite
pytest --cov=src --cov-report=term-missing
```
Expand Down Expand Up @@ -328,6 +335,8 @@ src/ai_log_analyzer/
patterns.py Drain-lite template miner for lines no rule matched
stability.py Flap/burst/trend scoring + 24h risk per device
judge.py LLM-as-Judge playbook quality scoring (eval CLI)
memory.py Incident journal β€” recurrence badges + similarity search
demo.py Synthetic incident storyline + live feeder (demo CLI)
kb.py Rule-based deep-analysis KB (fallback when LLM is off)
llm.py Docker Model Runner (TCP + UDS) + Anthropic Claude
analyzer.py End-to-end pipeline: classify β†’ actions β†’ score β†’ summary
Expand Down Expand Up @@ -360,6 +369,8 @@ src/ai_log_analyzer/
| `GET` | `/api/sites` | List bundled site bundles |
| `POST` | `/api/analyze` | Full pipeline β€” see request shapes below (response includes `unknown_patterns`: mined templates of unmatched lines) |
| `GET` | `/api/rules` | Built-in rule count + registered custom classification rules |
| `GET` | `/api/tail/<source_id>` | SSE live stream of classified events (`?min_severity=`, `?token=` on tokened deployments) |
| `GET` | `/api/incidents/similar` | Free-text search over the local incident journal (`?q=`, needs `AI_LOG_ANALYZER_INCIDENT_STORE`) |
| `POST` | `/api/rules` | Add custom rules at runtime β€” `{"rules": [{"pattern", "severity", "category", "description"}]}` |
| `POST` | `/api/optimize` | Device-level config audit + patches |
| `POST` | `/api/optimize/site` | Cross-device site analysis |
Expand Down Expand Up @@ -407,7 +418,7 @@ src/ai_log_analyzer/

## Tested & accessible

- **338 unit + integration tests** (pytest)
- **369 unit + integration tests** (pytest)
- Frontend audited across 8 review rounds:
- WCAG-AA: `:focus-visible` rings, `aria-live` regions, `role=tablist/tab/tabpanel`, skip-to-main link, `prefers-reduced-motion` fallback
- Responsive ≀ 1100px, PWA-ready (`theme-color`, `mobile-web-app-capable`, SVG favicon)
Expand All @@ -418,9 +429,11 @@ src/ai_log_analyzer/
## Roadmap

- [ ] Multi-site comparison view (delta between two sites)
- [ ] Real-time tail mode (websocket stream of new events)
- [x] Real-time tail mode β€” SSE stream (`/api/tail/<source>`) + Live Tail UI panel
- [x] Slack / generic-JSON alert webhooks per severity threshold (`AI_LOG_ANALYZER_WEBHOOK_URL` β€” see `.env.example`)
- [ ] More vendor adapters (Nokia SR Linux, Cisco IOS-XE, Cumulus NCLU)
- [x] More vendor coverage β€” Cisco IOS-XE + NX-OS + Nokia SR Linux classifier patterns (Cumulus NCLU still open)
- [x] Incident memory β€” local journal with recurrence badges + `/api/incidents/similar`
- [x] One-command demo mode (`ai-log-analyzer demo [--serve]`)
- [ ] Snapshot / replay analysis runs for regression testing
- [x] Custom classification rules β€” JSON file (`AI_LOG_ANALYZER_CUSTOM_RULES`) + runtime `POST /api/rules`; UI editor still open
- [x] Unknown-pattern template mining β€” Drain-style clustering of unmatched lines, surfaced in UI/API/MCP
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "netlog-ai"
version = "0.4.0"
version = "0.5.0"
description = "AI-powered network syslog analyzer with pluggable LLM providers (local Docker Model Runner / Anthropic Claude) and lab adapters."
readme = "README.md"
license = { text = "MIT" }
Expand Down
18 changes: 16 additions & 2 deletions src/ai_log_analyzer/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ class ActionItem:
devices: list[str]
sample_messages: list[str]
deep_analysis: dict[str, Any] = field(default_factory=dict)
recurrence: dict[str, Any] | None = None # history from the incident store

def to_dict(self) -> dict:
return {
d = {
"severity": self.severity,
"category": self.category,
"description": self.description,
Expand All @@ -76,6 +77,9 @@ def to_dict(self) -> dict:
"sample_messages": self.sample_messages,
"deep_analysis": self.deep_analysis,
}
if self.recurrence:
d["recurrence"] = self.recurrence
return d


@dataclass
Expand Down Expand Up @@ -590,6 +594,16 @@ def analyze(events: Iterable[LogEvent], use_llm: bool = True, llm_top_n: int = 3

any_llm = summary_llm or any(a.deep_analysis.get("llm_powered") for a in action_items)
top_devices = _finalize_top_devices(by_host)
generated_at = datetime.now(timezone.utc).isoformat(timespec="seconds")

# Opt-in incident memory: stamp each action item with its recurrence
# history from prior runs, then journal this run (env-gated, local-only).
from ai_log_analyzer.memory import get_store
store = get_store()
if store is not None:
for a in action_items:
a.recurrence = store.recurrence(a.description)
store.record([a.to_dict() for a in action_items], generated_at)

return AnalysisResult(
score=score, grade=grade, grade_label=grade_label,
Expand All @@ -598,7 +612,7 @@ def analyze(events: Iterable[LogEvent], use_llm: bool = True, llm_top_n: int = 3
classified_events=top_events,
executive_summary=summary,
llm_powered=any_llm,
generated_at=datetime.now(timezone.utc).isoformat(timespec="seconds"),
generated_at=generated_at,
unknown_patterns=miner.to_dict(top_n=10),
stability=tracker.report(),
)
Expand Down
34 changes: 33 additions & 1 deletion src/ai_log_analyzer/classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@
(r"%bgp-3-notification|%bgp-5-adjchange.*down", "high", "routing", "BGP notification / adjacency down (FRR/IOS)"),
(r"ospf.*(?:neighbor.*down|adj.*change|dead.*timer)", "high", "routing", "OSPF neighbor state change"),
(r"%ospf-5-adjchg.*down|ospf.*from\s+full\s+to", "high", "routing", "OSPF adjacency loss (FRR/IOS)"),
# Cisco IOS-XE / NX-OS (%FACILITY-SEV-MNEMONIC) + Nokia SR Linux
(r"%dual-5-nbrchange.*down", "high", "routing", "EIGRP neighbor down (IOS)"),
(r"bgp_mgr.*(?:to\s+(?:idle|active|connect)\b|session.*(?:down|fail))", "high", "routing", "BGP peer down / connect failure"),
(r"%(?:link-3-updown|lineproto-5-updown).*to\s+down", "high", "interface", "Interface link down"),
(r"%ethport-5-if_down|%eth_port_channel.*down", "high", "interface", "Interface link down"),
(r"\w+_mgr.*oper.*state.*down|srlinux.*port.*down", "high", "interface", "Interface link down"),
(r"%sec_login-4-login_failed|%authpriv.*fail", "high", "security", "Authentication failure"),
(r"%sysmgr-2-service_crashed|%sysmgr.*(?:crash|terminated)", "critical", "system", "NX-OS service crash"),
(r"%vpc-2-|vpc.*peer.*(?:down|fail)", "high", "lag", "vPC peer failure (NX-OS)"),
(r"%hsrp-5-statechange|%fhrp", "high", "redundancy", "VRRP/gateway failover"),
(r"%envmon.*(?:fan|temperature|supply)|%platform.*(?:thermal|power)", "critical", "hardware", "Power supply or fan failure"),
(r"%module-2-|%platform-2-", "critical", "hardware", "Chassis alarm triggered"),
(r"bfd.*(?:down|removed|session.*fail)", "high", "routing", "BFD session down"),
(r"isis.*(?:adj.*down|adj.*change|lsp.*purge)", "high", "routing", "IS-IS adjacency change"),
(r"license.*(?:expir|invalid|warn)|expir.*license", "high", "compliance", "License expiration warning"),
Expand All @@ -43,6 +55,7 @@
(r"l3_entry|l2_entry|memory\s+block|blk:", "high", "hardware", "ASIC forwarding table error"),
# ── MEDIUM ───────────────────────────────────────────────────────────
(r"snmp_trap_link_up|link\s*up|carrier.*up|if_up", "medium", "interface", "Interface link up"),
(r"%(?:link-3-updown|lineproto-5-updown).*to\s+up", "medium", "interface", "Interface link up"),
(r"rpd_bgp.*establ|bgp.*established|%bgp-5-adjchange.*up", "medium", "routing", "BGP peer established"),
(r"ospf.*(?:neighbor.*full|adj.*full)|%ospf-5-adjchg.*full", "medium", "routing", "OSPF neighbor established"),
(r"ntp.*(?:unreachable|stratum.*16|no.*server|sync\s+lost)", "medium", "ntp", "NTP sync lost/unreachable"),
Expand All @@ -61,6 +74,8 @@
(r"accepted\s+(?:publickey|password|keyboard)|sshd.*accept", "low", "auth", "SSH login accepted"),
(r"session\s+(?:opened|closed)|pam_unix.*session", "low", "auth", "User session opened/closed"),
(r"commit.*confirmed|config.*change|commit\b", "low", "config", "Configuration change committed"),
(r"%sys-5-config_i|%vshd-5-vshd_syslog_config_i", "low", "config", "Configuration change committed"),
(r"%sec_login-5-login_success", "low", "auth", "SSH login accepted"),
(r"lldp.*(?:neighbor|add|delete|update)", "low", "discovery", "LLDP neighbor change"),
(r"snmpd|agentx|snmp.*trap", "low", "monitoring", "SNMP daemon/trap activity"),
(r"sshd.*(?:disconnect|close|exit)", "low", "auth", "SSH session disconnected"),
Expand Down Expand Up @@ -136,9 +151,17 @@ def _prefer(cur: set[str] | None, new: set[str] | None) -> set[str] | None:
# IN / ANY / AT / NOT_LITERAL etc. prove nothing β€” skip
_flush()

solid = [r for r in runs if len(r) >= 3] or runs
# Prefer a solid (β‰₯3 char) literal run; else fall back to branch-derived
# literals (sre hoists common alternation prefixes like '%' into a short
# run, which would otherwise mask the long per-alternative literals);
# else settle for the short run.
solid = [r for r in runs if len(r) >= 3]
if solid:
return {max(solid, key=len)}
if best and all(len(b) >= 3 for b in best):
return best
if runs:
return {max(runs, key=len)}
return best


Expand Down Expand Up @@ -272,6 +295,9 @@ class ClassifiedEvent:
action: str
message: str
sample_message: str = field(default="") # short version for UI
# How the verdict was reached: 1.0 custom rule, 0.9 KB pattern,
# 0.6 raw-severity promotion, 0.3 unmatched (description = raw snippet).
confidence: float = field(default=0.9)

def to_dict(self) -> dict:
return {
Expand All @@ -285,6 +311,7 @@ def to_dict(self) -> dict:
"action": self.action,
"message": self.message,
"sample_message": self.sample_message,
"confidence": self.confidence,
}


Expand Down Expand Up @@ -324,11 +351,13 @@ def iter_classify(events: Iterable[LogEvent]) -> Iterator[ClassifiedEvent]:
clean_message = strip_ansi(ev.message)
combined = f"{ev.appname} {clean_message}".lower()
sev, cat, desc = "info", "other", ""
confidence = 0.9

# Custom user rules take precedence over the built-in KB.
for pattern, p_sev, p_cat, p_desc in _CUSTOM_RULES:
if pattern.search(combined):
sev, cat, desc = p_sev, p_cat, p_desc
confidence = 1.0
break

if not desc:
Expand All @@ -346,10 +375,12 @@ def iter_classify(events: Iterable[LogEvent]) -> Iterator[ClassifiedEvent]:
if not desc:
snippet = clean_message[:120].strip() or ev.appname or "General log message"
desc = snippet
confidence = 0.3

# Promote raw severity if syslog says critical but classifier missed it
if ev.severity_raw.lower() in ("crit", "emerg", "alert") and sev == "info":
sev = "high"
confidence = 0.6

yield ClassifiedEvent(
timestamp=ev.timestamp,
Expand All @@ -362,6 +393,7 @@ def iter_classify(events: Iterable[LogEvent]) -> Iterator[ClassifiedEvent]:
action=_action_for(sev, cat),
message=clean_message[:500],
sample_message=clean_message[:200],
confidence=confidence,
)


Expand Down
Loading
Loading