Skip to content
Draft
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: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Supported formats include ISO-8601 application logs, syslog, nginx access logs,

## Evidence-backed reasoning

The optional `--explain` flag turns existing detector output into concise, user-facing evidence statements. Each statement identifies its direct or statistical basis: parsed error level, message-length z-score with its configured threshold, or observed error count/rate in a complete analysis window. LogSight does not infer an incident root cause, use an LLM, send logs externally, or report a model-confidence score.
The optional `--explain` flag turns existing detector output into concise, user-facing evidence statements. Each statement identifies its direct or statistical basis: parsed error level, message-length z-score with its configured threshold, or observed error count/rate in a complete analysis window. Each statement also reports a deterministic support level: `single-signal` for one detector signal and `corroborated` when the same entry meets both error-level and statistical criteria. LogSight does not infer an incident root cause, use an LLM, send logs externally, or report a model-confidence score.

```bash
logsight analyze application.log --window 200 --spike-threshold 0.20 --explain
Expand Down
4 changes: 2 additions & 2 deletions logsight/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ def _print_explanations(explanations: list[EvidenceExplanation]) -> None:
for explanation in explanations:
evidence = ", ".join(explanation.evidence)
console.print(
f" [{explanation.evidence_strength}] {escape(explanation.summary)} "
f"([dim]{escape(evidence)}[/dim])"
f" support={explanation.evidence_strength};{explanation.support_level} — "
f"{escape(explanation.summary)} ({escape(evidence)})"
)


Expand Down
11 changes: 11 additions & 0 deletions logsight/reasoning.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from logsight.analyzer import AnomalyReport, ErrorRateSpike

EvidenceStrength = Literal["direct", "statistical"]
SupportLevel = Literal["single-signal", "corroborated"]


@dataclass(frozen=True)
Expand All @@ -21,6 +22,7 @@ class EvidenceExplanation:
category: str
summary: str
evidence_strength: EvidenceStrength
support_level: SupportLevel
evidence: tuple[str, ...]


Expand All @@ -42,6 +44,11 @@ def explain_report(
"no root cause is inferred."
),
evidence_strength="direct",
support_level=(
"corroborated"
if "message_length_zscore" in finding.reasons
else "single-signal"
),
evidence=(
f"level={entry.level.value}",
f"message_length={len(entry.message)}",
Expand All @@ -57,6 +64,9 @@ def explain_report(
"statistical threshold; no root cause is inferred."
),
evidence_strength="statistical",
support_level=(
"corroborated" if "error_level" in finding.reasons else "single-signal"
),
evidence=(
f"zscore={finding.message_length_zscore:.3f}",
f"threshold={report.zscore_threshold:.3f}",
Expand All @@ -74,6 +84,7 @@ def explain_report(
"threshold; no root cause is inferred."
),
evidence_strength="statistical",
support_level="single-signal",
evidence=(
f"errors={spike.error_count}",
f"entries={spike.total}",
Expand Down
1 change: 1 addition & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,5 @@ def test_explain_uses_detector_evidence(self, tmp_path):
assert result.exit_code == 0
assert "Evidence-backed findings" in result.output
assert "level=ERROR" in result.output
assert "single-signal" in result.output
assert "no root cause is inferred" in " ".join(result.output.split())
10 changes: 10 additions & 0 deletions tests/test_reasoning.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def test_explains_direct_error_evidence_without_causal_claim():
assert len(explanations) == 1
assert explanations[0].category == "error-level"
assert explanations[0].evidence_strength == "direct"
assert explanations[0].support_level == "single-signal"
assert "level=ERROR" in explanations[0].evidence
assert "no root cause is inferred" in explanations[0].summary

Expand Down Expand Up @@ -48,3 +49,12 @@ def test_returns_no_explanations_when_detector_has_no_findings():
report = detect_anomalies([_entry("INFO", "healthy")])

assert explain_report(report) == []


def test_marks_two_detector_signals_as_corroborated():
entries = [_entry("INFO", "normal")] * 20 + [_entry("ERROR", "x" * 500)]
report = detect_anomalies(entries, zscore_threshold=2.0)

explanations = explain_report(report)

assert any(item.support_level == "corroborated" for item in explanations)