Documentation
¶
Overview ¶
Package metrics owns the application's Prometheus instrumentation.
Rationale: kube-prometheus-stack already runs in the cluster and scrapes node/k8s/cnpg/argocd metrics out of the box. Adding a /metrics endpoint to rivolt with an explicit ServiceMonitor lights up per-handler latency, Rivian-result-class counters, and the other app-shaped signals the cluster-level scrapers can't see.
All metrics live on a private *prometheus.Registry rather than the global default. Reasons:
- Tests can spin up a fresh registry per test.
- We don't accidentally collide with go-collector / process-collector registrations from libraries that touch promauto.
- The handler is exported scoped to this registry, so /metrics output is exactly what we asked for.
Naming follows Prometheus conventions: snake_case, base unit suffix (_seconds, _bytes, _total).
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Metrics ¶
type Metrics struct {
// HTTP server side.
HTTPRequestsTotal *prometheus.CounterVec
HTTPRequestDuration *prometheus.HistogramVec
// Rivian upstream classification (transient/outage/rate-limited/
// user-action/unknown — see internal/rivian/errclass.go). Counts
// outcomes per call class, not per error string, so cardinality
// stays bounded.
RivianResultsTotal *prometheus.CounterVec
// Subscription leases owned by this pod. The lease layer
// increments/decrements as it claims and releases.
SubscriptionLeases prometheus.Gauge
// Circuit breaker telemetry. State is a 0/1/2 gauge
// (closed/half_open/open) so Grafana can alert on
// max_over_time != 0. Trips is a counter labelled by reason
// ("rate_limited" | "outage") so we can answer "what's making
// it open" in one PromQL.
RivianBreakerState prometheus.Gauge
RivianBreakerTrips *prometheus.CounterVec
// Global token bucket gating. Counter increments every time
// the bucket says no, partitioned by class so we can answer
// "is priority being starved".
RivianRateLimitBlocked *prometheus.CounterVec
// AI provider spend per user is intentionally NOT exposed here.
// User-id labels would blow up cardinality at 1000 vehicles. The
// existing internal/ai/usage.go writes per-user totals to
// Postgres; surface those via a Grafana SQL data source if/when
// we want to chart them.
AIRequestsTotal *prometheus.CounterVec
// Transactional email outcomes. `provider` is the wired email
// backend (today: "resend"), `status` is "ok" / "error" /
// "rate_limited" / "not_configured". An alert rule watches the
// monthly increase against Resend's free-tier cap (3000) so we
// see the wall coming a few days out, not the morning after.
EmailSendTotal *prometheus.CounterVec
// Signup-to-first-vehicle funnel. One increment per step a user
// reaches (signup_created, rivian_login_attempt, rivian_connected,
// rivian_no_vehicles, ...; see internal/api/funnel.go for the
// full set). `event` is a fixed vocabulary, never a user id, so
// cardinality stays bounded. The per-user trail lives in the
// matching "funnel" log line instead.
FunnelEventsTotal *prometheus.CounterVec
// Completed vehicle sleeps by outcome: "failed" when the car woke
// within a minute of falling asleep, "lasted" otherwise. A rising
// failed share is a car that can't stay asleep (vampire drain);
// the per-vehicle detail is in the "vehicle sleep ended" log line.
VehicleSleepsTotal *prometheus.CounterVec
// contains filtered or unexported fields
}
Metrics is the rivolt-specific instrumentation surface. One instance is built at startup and shared with anything that emits a metric. Keep it small — adding a metric here means it shows up in Grafana, so each one should answer a real question you'd want to ask at 3am.
func New ¶
func New() *Metrics
New constructs a Metrics with all collectors registered against a fresh registry. Pass the returned *Metrics into any package that needs to record a sample. Call Handler() to mount /metrics.
func (*Metrics) HTTPMiddleware ¶
HTTPMiddleware records request count + latency for every handler. Uses chi's RoutePattern (e.g. "/api/state/{vehicleID}") rather than the raw URL so cardinality stays bounded as vehicles scale.
Place AFTER chi route mounting (chi resolves the pattern when the handler runs, not before) — i.e. add it inside Router.Group / on individual subrouters that have routes mounted, OR rely on chi's behaviour of populating RouteContext as routing happens. We mount it at the root using a deferred capture: the recorder reads chi.RouteContext(r.Context()).RoutePattern() AFTER ServeHTTP, by which point chi has filled it in.
func (*Metrics) Handler ¶
Handler returns the http.Handler for /metrics scoped to this Metrics' private registry. Mount under /metrics with no auth — the Prometheus scraper inside the cluster reaches it via the pod IP, and the Ingress doesn't expose it externally (see deploy/helm/rivolt/templates/ingress.yaml).
func (*Metrics) Registry ¶
func (m *Metrics) Registry() *prometheus.Registry
Registry returns the underlying registry. Tests use this to gather expected samples without going through the HTTP handler.