Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
89c1b94
docs: claude mcp integration
furyfist Jul 24, 2026
62343ed
docs: implementation plan v1
furyfist Jul 24, 2026
9fb20b6
docs: implementation plan v2
furyfist Jul 24, 2026
34b603d
add metrics pipeline to demo otel agent
furyfist Jul 24, 2026
4e133e1
tighten agent sending queue for overflow determinism
furyfist Jul 24, 2026
6d9678f
add metrics pipeline to demo otel gateway
furyfist Jul 24, 2026
8bd835f
add Claude Code telemetry env script
furyfist Jul 24, 2026
2ba7647
document real telemetry setup in demo README
furyfist Jul 24, 2026
25200e5
parameterize overflow chaos scenario duration and rate
furyfist Jul 24, 2026
dada814
add overflow scenario to chaos runner
furyfist Jul 24, 2026
18ff2e0
add overflow scenario test
furyfist Jul 24, 2026
c82419b
pin demo physics values in DECISIONS
furyfist Jul 24, 2026
f7c2036
add ui_base_url to HTTP config
furyfist Jul 24, 2026
7578801
add config test for ui_base_url default
furyfist Jul 24, 2026
91d10c7
add evidence link builder to httpapi
furyfist Jul 24, 2026
0057f11
add trust window and param constants to httpapi
furyfist Jul 24, 2026
56f372a
implement trust derivation in httpapi
furyfist Jul 24, 2026
5d9495e
register trust route
furyfist Jul 24, 2026
655b32c
add trust verdict table tests
furyfist Jul 24, 2026
2a51793
add trust error response tests
furyfist Jul 24, 2026
d50ce10
add trust recommendation template tests
furyfist Jul 24, 2026
c6c77c0
add MCP server module with single trust tool
furyfist Jul 24, 2026
da2cefb
add mcp command to CLI
furyfist Jul 24, 2026
3301f3c
add MCP passthrough tests
furyfist Jul 24, 2026
3950dbd
add MCP configuration file
furyfist Jul 24, 2026
d64f7e6
verify and pin SigNoz traces explorer URL format
furyfist Jul 24, 2026
ec8d8c5
add time bounds to traces explorer link builder
furyfist Jul 24, 2026
365e24b
add view evidence button to incident page
furyfist Jul 24, 2026
6feaa36
add signoz links unit tests
furyfist Jul 24, 2026
409b268
add three-minute demo walkthrough
furyfist Jul 24, 2026
be73025
update README with demo quickstart
furyfist Jul 24, 2026
8294813
pin Claude Code CLI version for M1 demo telemetry
furyfist Jul 24, 2026
94398c9
align overflow scenario expectation with frozen contiguous gap shape
furyfist Jul 24, 2026
2e1a1fb
fix trust endpoint recommendation template, incidents cap, and eviden…
furyfist Jul 24, 2026
d03a610
add time bounds to traces explorer link builder
furyfist Jul 24, 2026
86ebf3c
fix walkthrough hardcoded numbers, PII checklist, and stream scoping
furyfist Jul 24, 2026
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
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"spanledger": {
"command": "python",
"args": ["-m", "spanledger", "mcp"]
}
}
}
4 changes: 3 additions & 1 deletion DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ Changelog of targeted deviations from PROJECT_PLAN.md (per §13.3).
- 2026-07-21 — **`spanledger_probe_e2e_latency_seconds` now populated** by `verify.LatencySampler`, implementing spike S1's method: poll the Query API for a freshly-emitted probe until it is visible, then record `emit→queryable` at poll resolution. Deliberately independent of `maturity_delay` (which floors loss verdicts at 30s but must not floor latency) — it polls a just-sent probe immediately. Honest by construction: a real measured delay; if a sampled probe never becomes visible within the timeout (e.g. during an outage) nothing is recorded, never a guessed value. Kept inside `verify.py` (not a new top-level module) to preserve the §6 module list. Live-verified 2026-07-21 (~6s emit→queryable).
- 2026-07-21 — **M4 correlation hint built (first cut), gated behind `correlation.enabled` (default off).** On a loss finding it queries the `otelcol_*` loss counters over the gap window and attaches the largest positive delta as `correlation_hint` (`{metric, delta, hop}`); `hop` is the coarse component parsed from the metric name (receiver/processor/exporter). Requires the user to scrape collector self-metrics (`docs/setup-collector-metrics.md`), so it is opt-in — off by default to avoid wasted queries. The v5 metrics query shape (`signal: metrics`, `requestType: scalar`, `increase`/`sum` aggregation) is **validated live against the running SigNoz** (accepted, returns success), but the *hint end-to-end* is not yet exercised because the demo pipeline does not scrape otelcol metrics. Strictly off the detection path: any query failure/empty result degrades to no hint via `select_top_anomaly`, never to a false or missing loss verdict. Per-collector `hop` attribution (vs component-name) is a V2 job (the conservation ledger).
- 2026-07-22 — **V2 Phase 1 (`events.py`): `ReliabilityEvent.to_json()` omits `severity`/`links` for the four V1 finding classes**, deviating from the PHASE_1_FOUNDATION.md module-3 prose ("new fields severity, links added top-level"). The two testable, higher-priority requirements in the same document — the golden-file lock (`test/golden/finding_v1.json`) and "`/findings` byte-identical to the golden file" (P1-7 gate) — both require the wire form to stay exactly the V1 §4.2 shape with no additions. `severity` and `links` remain ordinary attributes on the `ReliabilityEvent` object (used internally, e.g. `recovery.links == [loss_id]`); they just aren't injected into `to_json()`'s flattened dict. No behavior change to `/findings` or the golden fixture; this only resolves an internal doc inconsistency in favor of the frozen wire contract.
- 2026-07-22 — **`entry_refused` and `verification_stalled` event classes are declared but not constructed in Phase 1**, matching V1's own precedent (both existed only as unused constants in `findings.py` before this refactor — no runner code path ever built one). `backend_unreachable` **is** newly wired (`EventEngine.report_backend_unreachable`, called from `runner._verify_stream` on `SignozQueryError`) since Phase 1's edge case 2 explicitly requires it and has a test. Its payload is a minimal ad hoc shape (`id`, `stream`, `epoch`, `class`, `detail`) rather than the full V1 §4.2 finding schema — there is no golden fixture for it (only `loss` has one), so there is no historical byte-shape to preserve. Extending `entry_refused`/`verification_stalled` with real call sites, and giving `backend_unreachable` a fuller schema if `/api/v2/events` consumers need one, is a V2 job.
- 2026-07-24 — **M2 demo physics values pinned:** `toxic_type: bandwidth`, `proxy: gateway`, `rate: 8 KB/s`, `duration: 90s`, `agent_queue_size: 64`, `agent_max_elapsed_time: 30s`, `probe_interval: 1s`. These values guarantee deterministic overflow loss within 90s while keeping delivery ratio >= 0.80 and preserving silent loss semantics.
- 2026-07-24 — **Claude Code CLI version pinned for the demo: `2.1.204`.** Per plan Part 7 risk #6 (beta trace/log/metric span shape can change between releases): do not upgrade the Claude Code CLI used for demo recording past this version without re-running the M1 acceptance checklist. Note: this pins the CLI version only. The signal-presence portion of the M1 acceptance checklist (`claude_code.interaction` spans, `claude_code.user_prompt` events, `claude_code.session.count` in SigNoz Traces/Logs/Metrics Explorer) requires a live SigNoz instance on `signoz-network` (external, per the 2026-07-17 SigNoz entry above) and has not been run in this environment — it remains an outstanding manual step for an operator with the demo stack up.

15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,17 @@ python -m spanledger run --config spanledger.yaml
```

Status: `curl localhost:8231/status` · Findings: `curl localhost:8231/findings` ·
CLI: `python -m spanledger report`
Trust API: `curl "localhost:8231/api/v2/trust?from=...&to=..."` ·
MCP Server: `python -m spanledger mcp` · CLI: `python -m spanledger report`

## Demo Quickstart (Claude Code + MCP Trust Layer)

To run the live demo with real Claude Code telemetry and active loss verification:

1. Enable Claude Code telemetry export: `. demo/claude-code-env.sh`
2. Start the demo stack: `docker compose -f demo/compose.yaml up -d`
3. Run spanLedger: `python -m spanledger run --config demo/spanledger.demo.yaml`
4. Follow the step-by-step 3-minute script in [demo/walkthrough.md](demo/walkthrough.md).

## Tests

Expand All @@ -28,6 +38,7 @@ ruff check .

## Layout

`spanledger/` core modules (config, probe, registry, verify, findings, signoz, report, httpapi) ·
`spanledger/` core modules (config, probe, registry, verify, findings, signoz, report, httpapi, mcp) ·
`assets/` SigNoz dashboard + alerts · `demo/` compose stack, traffic app, chaos scripts ·
`spikes/` M0 spike scripts · `docs/` architecture and specs.

12 changes: 12 additions & 0 deletions demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,15 @@ otel-gateway → SigNoz. spanLedger injects probes at the agent and verifies in
- `reset.sh` — restore the stack between takes

Every number shown in the demo must be reproducible by running these scripts.

## Real telemetry (Claude Code)

To send live Claude Code telemetry through the audited pipeline:

```sh
. demo/claude-code-env.sh
```

This sets up OTLP export to `http://localhost:14317` (the entry agent).
Ensure privacy flags remain unset to avoid logging prompts or API bodies.

10 changes: 6 additions & 4 deletions demo/chaos/overflow.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#!/usr/bin/env sh
# Sustained overload via toxiproxy bandwidth limit -> queue overflow -> periodic striped drops.
# Sustained overload via toxiproxy bandwidth limit -> queue overflow -> the queue stays
# full for the duration, so drops land as one contiguous run (plan Part 2 physics).
set -eu
DURATION="${1:-120}"
DURATION="${1:-90}"
RATE="${2:-8}"
curl -s -X POST localhost:8474/proxies/gateway/toxics \
-d '{"name":"slow","type":"bandwidth","attributes":{"rate":8}}' >/dev/null
echo "bandwidth throttled for ${DURATION}s"
-d "{\"name\":\"slow\",\"type\":\"bandwidth\",\"attributes\":{\"rate\":${RATE}}}" >/dev/null
echo "bandwidth throttled for ${DURATION}s at ${RATE}KB/s"
sleep "$DURATION"
curl -s -X DELETE localhost:8474/proxies/gateway/toxics/slow >/dev/null
echo "throttle removed"
7 changes: 4 additions & 3 deletions demo/chaos/run_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,11 @@ def _fault_oomcrash():


def _fault_overflow():
# Throttle the gateway link so the agent's queue backs up and overflows -> striped drops.
# Throttle the gateway link so the agent's queue backs up and stays full for the
# duration -> one contiguous run of drops (plan Part 2 physics, frozen).
r = httpx.post(
f"{TOXIPROXY}/proxies/gateway/toxics",
json={"type": "bandwidth", "attributes": {"rate": 1}},
json={"type": "bandwidth", "attributes": {"rate": 8}},
timeout=10,
)
name = r.json().get("name", "bandwidth_downstream") if r.status_code < 300 else None
Expand Down Expand Up @@ -149,7 +150,7 @@ class Scenario:
"SIGTERM mid-load (opentelemetry-collector #13853)"),
"oomcrash": Scenario(_fault_oomcrash, Expectation("loss", ("contiguous",)),
"OOM/SIGKILL with in-memory queue"),
"overflow": Scenario(_fault_overflow, Expectation("loss", ("striped", "scattered")),
"overflow": Scenario(_fault_overflow, Expectation("loss", ("contiguous",)),
"sustained overload -> queue overflow"),
}

Expand Down
14 changes: 14 additions & 0 deletions demo/claude-code-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env sh
# Claude Code Telemetry Configuration for spanLedger demo.
# Note: Keep privacy flags OFF (do NOT set OTEL_LOG_USER_PROMPTS,
# OTEL_LOG_ASSISTANT_RESPONSES, OTEL_LOG_TOOL_CONTENT, or OTEL_LOG_RAW_API_BODIES).

export CLAUDE_CODE_ENABLE_TELEMETRY=1
export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 # required for traces
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:14317
export OTEL_TRACES_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=otlp
export OTEL_METRIC_EXPORT_INTERVAL=10000 # 10s, not the 60s default
export OTEL_RESOURCE_ATTRIBUTES=service.name=claude-code-demo
12 changes: 12 additions & 0 deletions demo/otel/agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ exporters:
endpoint: toxiproxy:24317 # gateway via toxiproxy (fault injection point)
tls:
insecure: true
# Bounded queue + bounded retry ceiling: makes overflow reachable and loss
# guaranteed (not probabilistic) under the Part 2 bandwidth throttle.
sending_queue:
enabled: true
queue_size: 64
retry_on_failure:
enabled: true
max_elapsed_time: 30s

service:
pipelines:
Expand All @@ -23,3 +31,7 @@ service:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
4 changes: 4 additions & 0 deletions demo/otel/gateway.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ service:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
153 changes: 64 additions & 89 deletions demo/walkthrough.md
Original file line number Diff line number Diff line change
@@ -1,93 +1,68 @@
# Demo walkthrough

The operator journey Phase 2 exists to tell: an alert fires, you land on the SLO
dashboard, you drill into the finding, you pivot to the actual traces, you decide
whether to roll back, and you watch recovery close the loop. Every command below is
real — reproducible by a judge running the same steps.

Automated version: `python demo/chaos/run_scenarios.py --walkthrough` runs this exact
gateway-kill script with narration printed at each step, timed for a live recording
so the presenter talks over it instead of typing (see the script's `run_walkthrough`).

## Prerequisites

- Demo stack up: `docker compose -f demo/compose.yaml up -d` (SigNoz on
`signoz-network`, `SIGNOZ_API_KEY` exported).
- Dashboards and alerts rendered and imported:
```sh
spanledger assets render --config demo/spanledger.demo.yaml --out /tmp/rendered
python demo/validate_assets.py --rendered /tmp/rendered
```
- `spanledger run --config demo/spanledger.demo.yaml` running against the stack.

## 1. Alert fires

Kill the gateway: `docker stop spanledger-demo-otel-gateway-1` (or let
`run_scenarios.py --walkthrough` do it on a timer). Within a few minutes,
`burn-fast.json` + `burn-fast-1h.json` (both labeled `spanledger.policy: fast-burn`)
should both be firing in SigNoz's alert list — narrate that the two-window pairing
(D14) is what's supposed to co-fire before you'd page on it for real.

## 2. Land on the SLO dashboard

Open **Delivery SLO & Error Budget** (`slo-error-budget.json`). Point at:

- **Error budget remaining** dropping through the same 0.25/0.10/0 ladder the
`budget_warning`/`budget_exhausted` events fire on.
- **Burn rates** climbing past the 14.4 / 6.0 threshold lines.
- **Formula SLI** (panel 3) tracking panel 2's `spanledger_sli_ratio` gauge —
"computed two different ways, same answer" is the audit-the-auditor moment
(success criterion 2).
- **Unknown/low-confidence strip** staying near zero — the loss is real loss, not
probes stuck in the maturity settling window.

## 3. Drill into the finding

The **Findings & budget events** table on the same dashboard shows the `loss` event.
Open it — the payload has `gap_shape: "contiguous"`, a `loss_onset` timestamp, and
(new in Phase 2, D15) a **`traces_filter`** field:

```
spanledger.stream = 'gateway-a' AND spanledger.seq >= <a> AND spanledger.seq <= <b>
```

## 4. Ledger-less honesty (deferred)

spanLedger V2 tells you a stream lost data and roughly when — it does not yet tell
you *which hop*. That per-hop conservation ledger (comparing an otelcol panel's own
counters against spanLedger's probe-verified floor) is explicitly a Phase 3 feature.
Say so plainly here rather than implying more precision than the product has today.

## 5. Traces evidence

Copy the `traces_filter` string from step 3, paste it into SigNoz's **Traces
Explorer** query bar. You'll see the surrounding real traffic around the missing
probe sequence range — the honest floor ("this many probes, this range, this
confidence") next to the real spans that were flowing at the time.

## 6. Rollback (if this were a real regression)

This is also where a deploy marker earns its keep: if the loss lines up with a
recent `deploy_marker` event (visible on **Deployment Regression**'s marker-lane
table), that's your rollback signal. Post one for the walkthrough itself so the
marker lane isn't empty:
# 3-Minute Demo Walkthrough Script

Every command and number shown below is reproducible by running the demo stack.

## Timed Run-of-Show (3 Minutes)

### 0:00–0:45 — The Baseline & Real Workload
1. Boot the stack with Claude Code telemetry enabled:
```sh
. demo/claude-code-env.sh
python -m spanledger run --config demo/spanledger.demo.yaml
```
2. Point out live `claude_code.*` spans flowing in SigNoz alongside synthetic probes.
3. Show `GET /api/v2/trust?from=...&to=...&stream=gateway-a` returning `trustworthy: true`,
`100.0% delivery`. **Scope every trust query to `stream=gateway-a`** — the demo
config also runs `gateway-b` (plan §7, non-determinism #10); an unscoped query
aggregates both streams and the numbers won't match the single-stream narrative.

### 0:45–1:30 — Silent Loss Injection (Overflow Scenario)
1. Trigger the overflow chaos scenario:
```sh
sh demo/chaos/overflow.sh 90 8
```
2. Claude Code keeps coding with **zero error output**.
3. SigNoz continues ingesting telemetry, but backend exporter queue fills up and drops spans silently.

### 1:30–2:15 — The MCP Trust Layer in Action
1. Ask Claude Code (or query `/api/v2/trust?...&stream=gateway-a`): *"Why are my traces
missing?"*
2. MCP Tool `spanledger_check_telemetry_trust` is invoked **first**.
3. Response returns (the exact numbers come from this run, never state them in
advance — plan §2's honesty note: guaranteed is *that* loss occurs and *that*
spanLedger reports the exact range it observed, not a predetermined figure):
- `trustworthy: false`
- `verified_ratio` below `1.0` — read the live value off the response
- `extrapolated_user_spans_lost` on the incident — a nonzero live estimate, not a
fixed number
- `probable_cause`: `contiguous` queue overflow
- Prose recommendation warning the developer before application diagnosis.

### 2:15–2:45 — Verification & Evidence Link
1. Navigate to the incident in spanLedger UI or click the `signoz_url` deep link.
2. Click **View Evidence**.
3. SigNoz Traces Explorer opens pre-filtered with `traces_filter` and time-bounded to `affected_window`.
4. The platform confirms the absence of probe spans in the lost sequence range.

### 2:45–3:00 — Recovery & Wrap-up
1. Throttle lifts automatically after 90 seconds.
2. A `recovery` event registers, linked back to the loss incident ID.
3. Trust status recovers to `trustworthy: true`.

## Pre-Recording PII Checklist

All four must be true **before any recording** — check them for real each time, not
once:

- [ ] Use throwaway Anthropic/Groq account
- [ ] `OTEL_LOG_USER_PROMPTS` unset
- [ ] `OTEL_LOG_ASSISTANT_RESPONSES` unset
- [ ] `OTEL_LOG_RAW_API_BODIES` unset

Verify the three env vars with:

```sh
spanledger mark-deploy --label "walkthrough recording" --api http://localhost:8231
env | grep -c OTEL_LOG_
```

## 7. Recovery

Restart the gateway: `docker start spanledger-demo-otel-gateway-1`. Within a couple
of verification cycles:

- A `recovery` event appears, `links` pointing at the `loss` event's id it closed.
- **Error budget remaining** stops dropping; **burn rates** fall back toward 0.
- The fast-burn alert pair clears once both windows recover and stay recovered for
`2 * verify.poll_interval` (the hysteresis re-arm window, module 5).

## Status

Written and internally consistent (real event fields, real metric names, the real
`traces_filter` string), but **not rehearsed end-to-end by a human** in this
implementation session — see `docs/v2/PENDING_WORK.md`.
→ must print `0`.
Loading
Loading