metrics

package
v0.9.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 8, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

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

View Source
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

View Source
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.

View Source
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.

View Source
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 Handler

func Handler() http.Handler

Handler returns the http.Handler serving /metrics in Prometheus text format.

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

func RecordAdmission(source string, admission time.Duration)

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

func RecordPostFlight(time.Duration)

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

func RecordPostFlightTotal(d time.Duration)

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

func RecordServed(source string, status int, total, upstream time.Duration)

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

func RegisterQueueDepth(kind string, fn func() float64)

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

func ReportFlushLag(kind string, seconds float64)

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

func SafeLabel(s string) string

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

func StatusClass(code int) string

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL