Documentation
¶
Overview ¶
Package metrics owns the Prometheus registry and the /metrics HTTP handler. Consumers declare their own metrics (typically in their package's metrics.go), register them via Register, and use prometheus.Counter/Gauge/Histogram normally.
Conventions ¶
Every metric must set Namespace to metrics.Namespace ("relay") and pick a Subsystem matching the package name (e.g. "eventlog", "auth", "usage").
Naming:
- Counters end in _total (prometheus convention; client_golang enforces this)
- Gauges describe a current value (e.g. queue_depth)
- Histograms end in _seconds or _bytes
Labels: use sparingly. High-cardinality labels (e.g. per-request IDs) explode memory. Prefer separate counters over a label with O(N) values.
Index ¶
- Constants
- Variables
- func Handler() http.Handler
- func InflightDec(source string)
- func InflightInc(source string)
- func ProviderKeyDown(reason string)
- func RecordAdmission(source string, admission time.Duration)
- func RecordLost(kind string)
- func RecordPostFlight(time.Duration)
- func RecordPostFlightTotal(d time.Duration)
- func RecordServed(source string, status int, total, upstream time.Duration)
- func Register(collectors ...prometheus.Collector)
- func RegisterQueueDepth(kind string, fn func() float64)
- func ReportFlushLag(kind string, seconds float64)
- func SafeLabel(s string) string
- func SetInflightLimit(n int)
- func StatusClass(code int) string
- func UpstreamConn(reused bool)
Constants ¶
const Namespace = "relay"
Namespace is the shared Prometheus namespace for every Relay metric. Concrete metrics SHOULD set this as their Namespace and pick a Subsystem matching their package (e.g. "eventlog", "auth", "usage").
Variables ¶
var ( // RecordsLost counts background records dropped because a bounded // emitter queue was full (usage rows, payload captures). The async // contract mandates this counter: a drop you can't see is a billing // or audit hole. `kind` is the emitter: "usage" | "payload". RecordsLost = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: Namespace, Name: "records_lost_total", Help: "Background records dropped due to a full emitter queue, by kind (usage/payload).", }, []string{"kind"}, ) // FlushLag is the age of the oldest un-flushed WAL segment, by kind // (usage/payload). RecordsLost fires only when data is already gone // and emit_queue_depth watches the in-memory channel — but a sink // outage (ClickHouse down) grows the on-disk WAL for hours with both // of those green. This gauge answers "how stale is my usage/billing // data": 0 = fully drained; rising = the sink is not accepting. FlushLag = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: Namespace, Name: "flush_lag_seconds", Help: "Age in seconds of the oldest un-flushed WAL segment, by kind (usage/payload). 0 = drained; rising = the sink is not accepting data.", }, []string{"kind"}, ) // ProviderKeysDown counts the times a pooled provider key was placed // into cooldown by a failure — the leading indicator of heading // toward "no healthy keys in pool". `reason` is the failure class // (auth/rate_limit/server_error/network/local_rl). // // A counter, not a gauge: breaker state lives in shared kv, so a // per-pod gauge of "keys down right now" would be inconsistent across // the fleet. Trip counts sum cleanly. A faithful global gauge is // deferred (would need a kv scan — not lightweight). ProviderKeysDown = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: Namespace, Name: "provider_keys_down_total", Help: "Times a pooled provider key was put into cooldown by a failure, by reason.", }, []string{"reason"}, ) )
Health signals: the two "am I about to have a problem" metrics — silent data loss (Q3) and provider keys dying (Q4). Both are one-liner emitters the owning packages call at the relevant moment; nothing here reaches back into them.
var ( // RequestsTotal answers "how much traffic, how many errors" (Q1/Q2). // `status` is a bounded class (2xx/4xx/5xx), never the raw code. RequestsTotal = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: Namespace, Name: "requests_total", Help: "Total requests handled, by source runner and status class (2xx/4xx/5xx).", }, []string{"source", "status"}, ) // RequestSeconds is total end-to-end time (Q2 — the "whose time is // it" numerator). Wide buckets span cached/error responses through // long generation runs. RequestSeconds = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Namespace: Namespace, Name: "request_seconds", Help: "End-to-end request latency (handler entry to response closed).", Buckets: []float64{ 0.001, 0.005, 0.01, 0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 30, 60, 120, }, }, []string{"source"}, ) // OverheadSeconds is THE wedge metric (Q1): Relay's own time, total // minus the upstream call. The performance contract puts a hard SLO // here (p99 < 10ms live). Tight sub-second buckets: 100µs → 500ms. OverheadSeconds = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Namespace: Namespace, Name: "overhead_seconds", Help: "Relay overhead = total handler time minus the upstream call. Observed only when upstream was reached.", Buckets: []float64{0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.25, 0.5}, }, []string{"source"}, ) // InflightRequests answers "how many streams are open right now / // where does concurrency plateau" (Q5). A streamed request counts // until its body closes (post-flight fires at Close). InflightRequests = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: Namespace, Name: "inflight_requests", Help: "Currently-open requests, by source runner. Streams count until body close.", }, []string{"source"}, ) // InflightLimit is the configured admission cap — the denominator that // turns InflightRequests into headroom ("how close am I to shedding"). // Without it the cap lives only in an env var and no dashboard can // plot inflight/limit or alert on approach. Constant after boot. InflightLimit = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: Namespace, Name: "inflight_limit", Help: "Configured per-pod in-flight admission cap. Compare against relay_inflight_requests for shed headroom.", }, ) // AdmissionSeconds is Timing.Start → upstream handoff: auth + // rate-limit reserve + key selection — the Redis-contention proxy // (Q5). Skipped when the request never reached upstream. Same // sub-second bucket style as OverheadSeconds: 100µs → 250ms. AdmissionSeconds = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Namespace: Namespace, Name: "admission_seconds", Help: "Time from request accept to upstream handoff (auth + rate-limit reserve + key selection). Observed only when upstream was reached.", Buckets: []float64{0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.25}, }, []string{"source"}, ) // PostFlightSeconds is the duration of one full detached post-flight // goroutine — token extraction, the observer fan-out (Registry.Finalize), // and the rate-limit / host-health commit phase that runs after it. Same // Q5 question ("how long does relay's bookkeeping run after a request"), // now including the Valkey commit RTTs that dominate it. PostFlightSeconds = prometheus.NewHistogram( prometheus.HistogramOpts{ Namespace: Namespace, Name: "post_flight_seconds", Help: "Duration of the detached post-flight goroutine (token extraction + observer fan-out + rate-limit/host-health commits) per request.", Buckets: []float64{0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5}, }, ) )
The request-flow metrics (Rate / Errors / Duration) plus the Relay-vs- upstream split.
Labelled by `source` (the runner: pipeline/proxy/ws/batch) — the lowest-cardinality dimension already on the lifecycle Context. Wire `shape` is deliberately NOT a label: it isn't carried on the Context and plumbing it would touch the inference handler. Add it the day the question "openai vs anthropic traffic split" is actually asked.
var UpstreamConnections = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: Namespace, Name: "upstream_connections_total", Help: "Upstream connections obtained per attempt, by kind (new = dialed, reused = keep-alive pool). new ≈ request rate means connection churn.", }, []string{"kind"}, )
UpstreamConnections answers "is Relay re-dialing the provider on every request" — the connection-churn fear. The stdlib's MaxIdleConnsPerHost default of 2 silently cost ~40ms of upstream p50 before the 2026-07-08 campaign found it by profiling; a new:reused ratio approaching the request rate names that failure in one glance. `kind` is "new" (dialed) or "reused" (from the keep-alive pool).
Functions ¶
func InflightDec ¶ added in v0.3.0
func InflightDec(source string)
InflightDec marks a request finalized. Callers must pair with a prior InflightInc for the same request or the gauge goes negative.
func InflightInc ¶ added in v0.3.0
func InflightInc(source string)
InflightInc marks a request admitted. Hot path: O(1), allocation-free.
func ProviderKeyDown ¶
func ProviderKeyDown(reason string)
ProviderKeyDown is the one-liner keypool calls when a key transitions into cooldown.
func RecordAdmission ¶ added in v0.3.0
RecordAdmission is the one-liner the post-flight metrics observer calls when the request reached upstream (Timing.Upstream.Start was stamped).
func RecordLost ¶
func RecordLost(kind string)
RecordLost is the one-liner an emitter calls at its drop site.
func RecordPostFlight ¶ added in v0.3.0
RecordPostFlight was the lifecycle.Registry finalize-observer feed for post_flight_seconds, timing only the Registry.Finalize fan-out. The metric now spans the whole detached post-flight goroutine — including the rate-limit / host-health commit phase that runs *after* Finalize returns, which an observer firing inside Finalize cannot see. The runners record it via RecordPostFlightTotal; this callback is retained as a deliberate no-op so the existing SetFinalizeObserver wiring can't double-count the histogram. Drop the SetFinalizeObserver call when convenient.
func RecordPostFlightTotal ¶ added in v0.9.8
RecordPostFlightTotal records the wall-clock duration of one detached post-flight goroutine (token extraction + observer fan-out + commits), called by the runners once the goroutine finishes.
func RecordServed ¶
RecordServed is the one-liner the post-flight metrics observer calls once per finalized request. total is the full handler time; upstream is the upstream-call duration (zero when upstream wasn't reached, in which case the overhead split is not observed — it'd be meaningless).
func Register ¶
func Register(collectors ...prometheus.Collector)
Register adds collectors to the shared registry. Panics on duplicate registration (consistent with prometheus.MustRegister).
func RegisterQueueDepth ¶ added in v0.3.0
RegisterQueueDepth exposes a bounded emitter's queue depth as relay_emit_queue_depth{kind=...} — the leading signal for the drops RecordsLost counts after the fact. Same kinds as records_lost_total. fn is sampled at scrape time (chan len is concurrency-safe). Call once per kind at boot, where the emitter is constructed.
func ReportFlushLag ¶ added in v0.9.8
ReportFlushLag is the one-liner a WAL queue calls after each flush pass with the age of its oldest remaining segment (0 when drained).
func SafeLabel ¶
SafeLabel returns "unknown" if s is empty, preventing empty-string Prometheus labels.
func SetInflightLimit ¶ added in v0.9.8
func SetInflightLimit(n int)
SetInflightLimit publishes the admission cap. Called once at boot where the Admission semaphore is constructed.
func StatusClass ¶
StatusClass returns a bounded status label ("2xx", "4xx", "5xx", etc.) for an HTTP status code. Using a class avoids high-cardinality label explosion.
func UpstreamConn ¶ added in v0.9.8
func UpstreamConn(reused bool)
UpstreamConn is the one-liner the upstream transport's httptrace hook calls when a connection is handed to an attempt.
Types ¶
This section is empty.