Files
sift/docs/P2.7.md
T
kyle 4372673184 p2.7: prometheus /metrics
Default GET /metrics is Prometheus text; JSON stays at /metrics.json
and ?format=json. :8443 needs the metrics caller bearer; loopback
stays open. Restricted callers have no per-caller series and no
tavily:restricted gauge.
2026-09-23 21:49:38 -07:00

254 lines
11 KiB
Markdown

# Sift P2.7 — Prometheus `/metrics`
Drafted 2026-09-23 by titan. Reviewer: Claude. Implementer: titan.
Orion pin: straylight, after Claude approves the code.
Kyle decided (2026-09-23, via Claude): aggregated restricted metrics;
bearer on `:8443`; loopback open. Claude: drop `search_rerank_fallback`
from Prometheus; export `sift_self_ip_failures_total`.
Scrape target: `:8443` with `bearer_token_file`.
Builds on live P1/P2/P3 (`e62cfc8`). No nixos edit from titan.
## 1. Scope and exit criteria
In: Prometheus text on `GET /metrics`; keep today's JSON; the series in §4;
duration histograms for fetch, digest, and search; scrape-token auth;
restricted aggregation (no per-caller series, no `tavily:restricted` gauge).
Out: Grafana; a new listen address or firewall (straylight's metrics-plan /
nixos); alert rules; PLAN's "bot-blocked per host" series; P2 §6 recover /
token-ratio / Honcho-context series; Go/process collectors;
`search_rerank_fallback` on Prometheus (stays JSON-only).
**Exit criteria** (evidence in this thread):
1. `make test lint` green (new tests in §7). `make contract` unchanged.
2. `GET /metrics` is Prometheus text 0.0.4 (loopback, or `:8443` with the
`metrics` bearer).
3. `GET /metrics.json` and `GET /metrics?format=json` still return today's
JSON object (existing keys, same types), including `tavily:restricted`
and per-caller maps. `TestMetricsCacheHitMiss` uses `/metrics.json`.
4. After one fleet search, one restricted search, and one digest-fallback,
the Prometheus body has the series in §4 — **no** query text, URL,
hostname, or IP, **no** `caller="<restricted name>"`, **no**
`tavily:restricted`.
5. Non-loopback `/metrics` without the metrics bearer is 401; a fetch
caller bearer is 403; the metrics caller cannot fetch or search (403).
Loopback with no proxy headers stays open.
## 2. Current `/metrics` (verified in tree, not assumed)
`internal/api/api.go` `GET /metrics` is unauthenticated JSON:
```json
{"requests": N, "blocks": N, "scrapes": N, "self_ip_failures": N,
"search_queries": {caller: {source: n}},
"search_credits": {caller: {source: n}},
"search_errors": {source: {kind: n}},
"search_cache_hit": N, "search_cache_miss": N,
"search_rerank_fallback": N,
"search_daily_credits": {source: n},
"search_capped": {source: n}}
```
Digest `Engine.Calls` / `Fallback`, `digest.Gate` inflight/waiters, and
`dcache` hit/miss exist in process memory and were not on JSON. P2.7
exports them (Prometheus + additive JSON fields).
Search `addErr` uses the warning tail as `kind`. JSON keeps that map.
Prometheus does not export `search_errors` (the tail is not a closed set).
Listener stays `127.0.0.1:8890` + existing `tailscale serve` HTTPS `:8443`.
## 3. Wire format
| Request | Response |
|---|---|
| `GET /metrics` | Prometheus text 0.0.4, `Content-Type: text/plain; version=0.0.4; charset=utf-8` |
| `GET /metrics.json` | today's JSON, `Content-Type: application/json` |
| `GET /metrics?format=json` | same JSON as `/metrics.json` |
`format` values other than `json` (including absent) → Prometheus text.
Do not count scrapes of `/metrics`, `/metrics.json`, or `/healthz` in
`sift_requests_total`.
Private `prometheus.Registry` with only the `sift_*` series below. No
client_golang Go/process collectors.
Dep: `github.com/prometheus/client_golang` (vendored).
## 3a. Auth (Kyle, 2026-09-23)
- **Loopback stays open.** `RemoteAddr` is loopback **and** there is no
`X-Forwarded-For` / `X-Forwarded-Proto` / `X-Forwarded-Host` /
`Tailscale-User-Login` / `Tailscale-Headers-Info` **and** `Host` is
loopback or `localhost`. This is `curl http://127.0.0.1:8890/metrics`.
`tailscale serve` on `:8443` sets forwarded / Tailscale headers and a
non-loopback Host, so it is not treated as loopback even though the
upstream is 127.0.0.1:8890.
- **`:8443` requires a bearer.** Dedicated `metrics` caller in `keys.toml`:
`name = "metrics"`, empty workspace, no fetch or search rights.
`sift keys add --caller metrics` forces `workspace = ""`.
- Missing/unknown key → 401. A fetch/search caller's key → 403. The
metrics caller on `/v1/fetch`, `/v2/scrape`, `/v1/search`, `/v2/search`
→ 403 `unauthorized`.
- `/healthz` stays unauthenticated.
- Serve start: WARN if keys.toml has no `metrics` caller (loopback still
works; `:8443` scrapes will 401). WARN if a `metrics` caller somehow
has a workspace (still treated as metrics-only). `CheckMappings` skips
the metrics caller so an empty workspace is not an ERROR.
Straylight's scrape config: `https://orion.scylla-hammerhead.ts.net:8443/metrics`
with `bearer_token_file`. Generating the key is on-box (same as other
callers); do not print it.
## 4. Series
Prefix `sift_`. Counters end `_total`. Histograms are seconds.
**Requests**
- `sift_requests_total{endpoint,code}` counter — `/v1/fetch`, `/v2/scrape`,
`/v1/search`, `/v2/search`. `code` is the HTTP status as a string.
Increment once per finished handler, including 401/403.
**Guard / self-IP**
- `sift_guard_blocks_total` counter — same events as today's `blocks`.
No host, URL, or reason-with-host label.
- `sift_self_ip_failures_total` counter — `selfip.Discoverer.Failures`.
**Digest and Dixie gate** (`internal/digest`)
- `sift_digest_calls_total` counter — Dixie chat actually started
(`Engine.Calls`; cache hits do not increment).
- `sift_digest_fallbacks_total{reason}` counter — closed `reason`:
`queue_full`, `queue_wait`, `canceled`, `embed`, `dixie`, `timeout`,
`empty`, `unset`. Unknown → `other` (never the raw string).
- `sift_dixie_inflight` gauge — `Gate` slots in use.
- `sift_dixie_queue_waiters` gauge — waiters in the sift-side FIFO.
`Gate` grows `Snapshot() (inflight, waiters int)` under the existing mutex.
**Caches**
- `sift_cache_requests_total{cache,result}` counter — `cache` is `digest`,
`embed`, or `search`; `result` is `hit` or `miss`. Search uses the engine
counters already on JSON (`search_cache_hit` / `search_cache_miss`).
**Search cost**
Prometheus cannot mix labeled and unlabeled series under one name, so
totals and per-caller fleet counts are separate:
- `sift_search_queries_total{source}` — **all** callers, including
restricted. No `caller` label.
- `sift_search_queries_caller_total{caller,source}` — **fleet only**.
`permit_restricted` callers are omitted.
- `sift_search_credits_total{source}` — all callers.
- `sift_search_credits_caller_total{caller,source}` — fleet only.
- `sift_search_daily_credits{source}` gauge — current UTC-day total.
**No `tavily:restricted` (or any `*:restricted`) source.** That partition
stays JSON-only.
- `sift_search_capped_total{source}` — same source filter as daily credits.
`source` values are configured names (`tavily`, `brave`, `searxng`, …).
No query, host, or URL.
**Histograms** (no `caller`; include restricted traffic)
- `sift_request_duration_seconds{op}` — `op` is `fetch`, `digest`, or
`search`. One HistogramVec; buckets are the union
`0.1, 0.5, 1, 2, 5, 10, 15, 20, 30, 45` (HistogramVec cannot vary
buckets by label).
- `fetch`: wall time of `/v1/fetch` and `/v2/scrape`.
- `digest`: `digest.Result.DigestMS` on digest attempts; skip cache hits.
- `search`: wall time of `/v1/search` and `/v2/search`.
**JSON additive fields** (same keys as today plus):
- `digest_calls`, `digest_fallbacks` (map reason→n), `dixie_inflight`,
`dixie_queue_waiters`
- `dcache_digest_hit`, `dcache_digest_miss`, `dcache_embed_hit`,
`dcache_embed_miss`
Existing JSON keys stay, including `search_rerank_fallback`,
`search_errors`, per-caller maps, and `tavily:restricted`.
## 5. Labels and redaction
`/metrics` is a second log (same isolation as restricted slog).
- **`caller` only on `*_caller_total` count series**, and only for
callers **not** in `permit_restricted`. Value is `auth.Caller.Name`.
Restricted traffic is in the unlabeled `{source}` totals and in
histograms only.
- **No `host`, `url`, `query`, `q`, `final_url`, `err`, or `msg` labels.**
- **`endpoint`**: the four API routes. **`code`**: three-digit status.
- **`reason` / `cache` / `result` / `op` / `source`**: closed sets from §4.
- **No `search_errors` on Prometheus.**
A test dumps every metric name, label name, and label value from a
handler that ran a restricted search + a blocked fetch, and asserts the
query, the URL, the hostname/IP, and the restricted caller name appear
**nowhere** as a `caller` label (the restricted name must not appear as
any label value). The query/URL/host/IP appear nowhere in the Prometheus
body, including HELP text.
## 6. Code layout
```
internal/metrics Registry, sift_* defs, loopback predicate, Gather
internal/digest Gate.Snapshot; Fallback/Calls already there
internal/auth NameMetrics; Add() clears workspace; MetricsOnly()
internal/api /metrics negotiates format + auth; /metrics.json
handlers observe duration + requests{endpoint,code}
metrics caller rejected on fetch/search
internal/fleetctx CheckMappings skips the metrics caller
cmd/sift serve WARN if no metrics caller
```
JSON continues to read the same atomics/maps the Prometheus collector
scrapes — one set of counters, two encodings.
Do not log `/metrics` scrapes at INFO.
## 7. Tests
- `GET /metrics` from loopback: Prometheus Content-Type; `expfmt` parse;
`HELP`/`TYPE` present for each series in §4.
- `GET /metrics.json` and `?format=json` from loopback: existing JSON keys.
`TestMetricsCacheHitMiss` pointed at `/metrics.json`.
- Non-loopback (or loopback + `X-Forwarded-For`): 401 without bearer; 403
with a fleet key; 200 with the metrics key.
- Metrics caller on `/v1/fetch` and `/v1/search`: 403.
- One fleet `/v1/search` then `sift_search_queries_caller_total{caller,source} == 1`
and `sift_search_queries_total{source} == 1`. Restricted search increments
only the unlabeled total; no `caller="<restricted>"`.
- Digest gate full → `sift_digest_fallbacks_total{reason=...}` and Dixie
gauges in range.
- Restricted + blocked fetch: Prometheus body has no query, URL, host, IP;
no restricted name as a label value; no `tavily:restricted`.
- Histogram: a timed fake fetch observes `_count`.
- Unknown fallback reason coerced to `other`.
- `/metrics` itself does not increment `sift_requests_total`.
No live orion scrape in CI.
## 8. Rollout
1. titan implements on `git.wntrmute.dev:kyle/sift`; evidence in this thread.
2. Claude reviews the code (auth predicate, restricted aggregation, tests).
3. straylight bumps the orion pin once (batch with other approved isolation
patches if any are waiting). Titan does not edit `~/src/nixos`.
4. On-box: `sift keys add --caller metrics --out <root-only file>`; place
the secret for VictoriaMetrics `bearer_token_file`; delete the temp copy.
Do not print the key.
5. straylight's metrics-plan scrapes `:8443/metrics` with that token.
## 9. Open questions
None remaining on this cut. Scrape URL is `:8443` with a bearer (Kyle /
Claude). `self_ip_failures` is on Prometheus; `search_rerank_fallback` is
not.