crewshipd exposes a Prometheus text-format endpoint at GET /metrics on the
main HTTP port. It serves two groups of series: process gauges (uptime, memory,
goroutines, WebSocket connections) and domain metrics — the counters and gauges
an operator alerts on.
Authorization
/metrics is not public:
- Requests from loopback (the true client IP, X-Forwarded-For aware) are always allowed — the typical node-local Prometheus or sidecar scrape.
- Remote scrapers must send
Authorization: Bearer <token>matching theCREWSHIP_METRICS_TOKENenvironment variable. - With no token configured, non-loopback requests get a
404. - The
404is the answer for every method, not justGET— an unauthorizedPOST /metricslooks exactly like an unauthorizedGET, so a scanner cannot use a method probe to confirm the endpoint exists. Authorized callers still get an honest405 Method Not Allowed(withAllow: GET, HEAD) for anything other thanGET/HEAD.
Process metrics
Every series carries a
hostname label.
Domain metrics
Assignments and queue
Queue depth is deliberately aggregated, not labeled per crew — crews are
user-created and unbounded, and per-crew labels would grow the series set
without limit. The three aggregates cover the alerting cases: total backlog
growing, backlog spreading across crews, and a single crew wedged
(
depth_max climbing while depth is flat).
Pipeline runs
Agent runs
Alert on failure rate with the usual counter recipe:
rate() /
increase() treat that as a normal counter reset.
LLM cost (paymaster)
Provider label values are capped (overflow folds into
provider="other") so
the series set stays bounded. Spend-rate alert:
Containers
Database
Issue sessions and deliveries — the §19.3 service levels
These series answer the four questions the Issues & Routines PRD (§19.3, §24.1) says are actually measured for the mention → wake → reply loop: delivery, continuation, duplication, and human comprehension. Before this section existed, none of them could be computed at all — there was no percentile capability anywhere increwshipd (no Prometheus client,
no histograms, SQLite has no PERCENTILE_CONT) — so every percentile below
is computed in Go, from real write-path timestamps, over a bounded window of
the most recent 500 rows. A quantile series is absent when its window has
zero samples — never a fabricated 0 — while its own sample-count series is
always present, because a count of zero is a real, computed answer.
Delivery — “did the mention reach an agent, and how fast”
Duplication — “did one event ever produce two runs”
Continuation — “does the agent pick up where it left off, in a bounded pack”
Human comprehension — “how often does a run need a human, and does routing hold”
Scheduling — “did the routine fire when it was due”
Freshness and cost
The DB-derived block is computed from indexed counts and cached for 15 seconds — scraping more often than that returns the same snapshot. At typical 15–60s scrape intervals this is invisible; it exists so a scraper retry storm (or an abusive client that got hold of the token) cannot turn/metrics into a query amplifier.
For traces and OTLP export, see OTLP setup.