Task · SFL-0036

obs: local /statusz snapshot endpoint - runtime state is unreachable when OTLP metrics are off or the collector is down

Status
To Do
Labels
followup, phase-2, roadmap
Milestone
Feature roadmap — operator ergonomics & platform capabilities
Updated
2026-08-14

Description

What

The health server exposes exactly two routes and 404s everything else:

Metrics are OTLP push-only and off by default:

The consequence: the process holds a complete picture of its own runtime state and there is no way to read it out of a running container.

State Where it lives Reachable today
Per-lane queue depth / queued_bytes / failing_since src/sf2loki/app.py:95-113, 196-199, 227-230 no
Aggregate sink outage start Pipeline.sink_failing_since, app.py:201-210 only as a rounded duration inside the /readyz 503 body (app.py:626-642)
Last checkpoint commit per key + watermarks app.py:499-549 (_commit, _record_commit_metric) no
Per-org auth failure set App._degraded_orgs, app.py:906, 1270; check at app.py:842-858 only as the first failing org’s name in the /readyz body
Leadership written into a gauge only — app.py:1185, 1199, 1215; never held as a readable attribute no
Egress budget used / paused / day src/sf2loki/egress.py:116-129 (_budget, _used, _paused, _date) only as a pause reason in the /readyz body (egress.py:270-275)

Out-of-process commands are not substitutes:

Why it matters

Failure scenario: Loki pushes stall, or the OTLP collector/gateway is itself the thing that is down or misconfigured — the moment dashboards and alerts go dark. An operator execs into the container and can learn two facts: ok, and ready / a single degradation string. Which lane is backed up, how many bytes are buffered, how long the outage has actually run, where each source’s watermark sits, whether one org out of five is failing auth while the rest are fine, how much of the daily byte budget is spent — all of it exists in memory and is unreachable. Triage falls back to grepping logs for the last emitted lines and inferring state from them.

This is worse for the default deployment than for an instrumented one: telemetry.enabled is False out of the box (config.py:1208), so a fresh install has no metric path at all, and the recommended dashboards/alert pack (deploy/grafana/) depend entirely on OTLP egress plus suffix translation (see closed #58). A zero-dependency local snapshot removes the single point of observability failure and is the first thing to paste into an incident channel or attach to a bug report.

Proposed approach

Add GET /statusz to the existing hand-rolled server, in the same zero-dependency style, gated by config.

1. Snapshot accessors on the state owners (no new I/O, no blocking calls):

2. Compose in App and install a provider on Health, mirroring the set_degraded_check wiring at app.py:1058-1059:

health.set_status_provider(lambda: {...})   # returns a JSON-serialisable dict

Health renders it with json.dumps and serves 200 with Content-Type: application/json. Keep decide() pure: give it a new keyword (e.g. status_body: str | None) so None → the existing 404, "not found" and a string → 200, body. The provider must be synchronous and I/O-free; it runs inside the request handler on the event loop (health.py:118-160).

Suggested shape:

{
  "version": "1.4.0",
  "uptime_seconds": 3612,
  "leader": true,
  "ready": true,
  "degraded_reason": null,
  "orgs": {"prod": true, "sandbox": false},
  "lanes": {
    "streaming": {"queue_depth": 0, "queued_bytes": 0, "n_producers": 1, "failing_since_seconds": null},
    "bulk": {"queue_depth": 812, "queued_bytes": 41943040, "n_producers": 3, "failing_since_seconds": 947}
  },
  "checkpoints": {"pubsub:/event/LoginEventStream": {"value": "…", "committed_ts": 1751500000.0}},
  "egress": {"budget_bytes": 0, "used_bytes": 12938411, "paused": false, "action": "pause", "day": "2026-07-30"}
}

3. Config: add service.status_endpoint: bool = True next to health_addr (config.py:1275). Adding a config field requires just gen-config (regenerates config.example.yaml + docs/config-reference.md) or the drift gate in tests/test_config_artifacts_drift.py fails; the Helm chart’s generated values also carry the field (deploy/helm/values.yaml).

4. sf2loki status [--addr HOST:PORT] CLI verb in src/sf2loki/cli.py (subparser alongside doctor / state): fetch /statusz, pretty-print a table, exit non-zero when unreachable. Optional but cheap, and it is what an operator reaches for first.

Security envelope. The health server binds all interfaces by default (health.py:99-100, health_addr: ":8080" at config.py:1275), so /statusz shares the trust boundary of /readyz — which already emits org names (app.py:855) and budget state (egress.py:274-275) to any caller. Checkpoint values are explicitly non-secret (src/sf2loki/statecmd.py:10). The snapshot must therefore never include tokens, private-key paths, auth headers, endpoint credentials, or raw config; org names, watermarks, and counters only. status_endpoint: false is the escape hatch for deployments that will not expose it.

Rejected alternative: serving a dump of the in-memory metric reader. InMemoryMetricReader.get_metrics_data() is destructive for gauges — a second collect with no new measurement returns nothing (src/sf2loki/obs/metrics.py:212-214) — so a /statusz request would perturb the reader that Metrics.registry and the test suite read through. Typed accessors, not a metric dump.


Imported from GitHub issue #120 on 2026-08-14, when this repo migrated from GitHub Issues to Backlog.md. The original issue has been deleted; its verbatim body, labels and comments are preserved in archive/issues-dump.json (jq '.[] | select(.number == 120)' archive/issues-dump.json).

Filed from the 2026-07-30 full-repo audit (11 finder lanes + adversarial verification per finding).

Acceptance Criteria

Definition of Done

References

View the source file on GitHub