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 ¶
var AppleLinkOutcomes = []string{"started", "linked", "in_use", "account_linked", "expired", "reauth_refused", "password_needed", "unlinked", "unlink_refused", "provider_error"}
AppleLinkOutcomes are the rivolt_apple_link_total outcomes, all created at 0 at boot: started (a link began), linked, in_use (that Apple ID belongs to another account), account_linked (this account has another Apple ID), expired (no open request, a spent one, or the session ended), reauth_refused (start without the right password), password_needed (the account has no password to link with: Google or GitHub only), unlinked, unlink_refused (the password check refused an unlink), provider_error (Apple or our own failure on the way).
var LeaseFencedKinds = []string{"samples", "drives", "charges", "sends", "redis", "rivian", "vehicle_info"}
LeaseFencedKinds are the write classes rivolt_lease_fenced_total counts.
var LeaseShedChoices = []string{"idle", "awake", "deferred"}
LeaseShedChoices are the shed decisions rivolt_lease_shed_total counts (leases.Coordinator.chooseShed).
var RoutePointRefusals = []string{"fix_offglobe", "fill_globe", "fill_offglobe", "fill_detour", "encode_skip"}
RoutePointRefusals are the rivolt_route_point_refused_total "where" values, all created at 0 at boot: fix_offglobe (a position off the globe), fill_globe (a route-fill with such a vertex, caught by the recorder), fill_offglobe and fill_detour (the routing engine's shape failed its own check: off the globe, or nowhere near the two ends), encode_skip (the encoder's last line).
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
// LeaseGroupsOwned is the number of user-group leases this pod holds (M1 of
// the single-owner design); LeaseFenceWouldReject counts group tokens that
// rivolt_lease_ok would have refused, with nothing acting on it yet.
LeaseGroupsOwned prometheus.Gauge
LeaseFenceWouldReject *prometheus.CounterVec
// LeaseFenced counts owner-side writes refused or skipped because this pod no longer holds the
// group lease, by write class (samples, ...). Alert on > 0 for 10 minutes.
LeaseFenced *prometheus.CounterVec
// RivianScheduleReadTotal counts charging-schedule reads by outcome: ok, retried_ok, dropped (refused
// by the shared budget on every try) and error. Pre-created at 0; no other labels.
RivianScheduleReadTotal *prometheus.CounterVec
// LeaseShed counts the lead-mode shed decisions of a pod over its fair share, by choice: idle
// (a sleeping group), awake (no sleeping one), deferred (every group over the share had an open
// drive or charge, so nothing moved that pass).
LeaseShed *prometheus.CounterVec
// LeaseUnstreamed counts episodes of a lease this pod holds without
// a subscription behind it (the coordinator re-subscribes; alert on
// any increase).
LeaseUnstreamed prometheus.Counter
// LeaseSelfFence counts times this pod stopped every subscription because its group lease
// renewals failed past the local deadline (a slow or unreachable database).
LeaseSelfFence prometheus.Counter
// EntitlementsLookupErrors counts failed reads of an account's Pro entitlement rows. The plan fails open (the account counts as Pro
// where an answer is needed), so a sustained failure is invisible to users and shows only here.
EntitlementsLookupErrors prometheus.Counter
// StripeCancelPending and StripeCancelOldestSeconds are the queue of Stripe cancellations of deleted accounts that have not worked yet:
// how many, and how long the oldest has waited. Anything above 0 for more than an hour means a deleted person may still be billed:
// the alert.
StripeCancelPending prometheus.Gauge
StripeCancelOldestSeconds prometheus.Gauge
// Stripe's webhook endpoint, counts only: StripeWebhookEvents by event type (a fixed set, "other" for any type Rivolt does not handle) and
// outcome (applied, duplicate, ignored, unmapped, stale, rejected); StripeWebhookLag is how long after Stripe created an event Rivolt
// received it (a delivery that Stripe had to retry shows as a long lag). BillingEvents counts billing moments an operator may need to see
// (kind payment_failed: a subscription went past due), pre-created at 0.
StripeWebhookEvents *prometheus.CounterVec
StripeWebhookLag prometheus.Histogram
BillingEvents *prometheus.CounterVec
// Entitlements is the count of entitlement rows by source and status and Accounts the number of accounts, both without the demo account and
// without any user label, refreshed every few minutes; BillingEnabled is 1 when RIVOLT_BILLING is on.
Entitlements *prometheus.GaugeVec
Accounts prometheus.Gauge
BillingEnabled prometheus.Gauge
// LeaseRenew counts lease renew attempts by scope (vehicle: the per-vehicle renew; group: the lead-mode group pass) and
// outcome: ok, lost (the renew worked and something we held was gone), timeout, error. A pod whose renews stop working shows
// here before it self-fences.
LeaseRenew *prometheus.CounterVec
// LeaseAcquireToFrame is the seconds from a vehicle lease being acquired to the first frame
// recorded after it, by state (active: open drive or charge; idle). The flag-on bar is active p95 <= 12 s.
LeaseAcquireToFrame *prometheus.HistogramVec
// ReauthRaiseDeferred counts rejections whose needs_reauth raise was held back because
// the client did not yet know which stored session it was using (the next sync decides).
ReauthRaiseDeferred prometheus.Counter
// RivianAccountMismatch is, per field ("session", "flag"), how many Rivian accounts loaded
// on this pod still disagreed with the database after the last 5-second sync round
// corrected them. Zero when the cross-pod sync works; sustained non-zero (alert: 10 min)
// means a pod is serving state the database does not have. RivianAccountSyncErrorsTotal
// counts rounds that could not read the database at all. No user labels.
RivianAccountMismatch *prometheus.GaugeVec
RivianAccountSyncErrorsTotal prometheus.Counter
// RivianSessionsExpiring is the number of stored Rivian sessions with at most the given
// time left ("14d", "7d", "1d"), from the hourly expiry sweep; RivianSessionsExpired those
// already past their expected end but not yet flagged needs_reauth (should be 0: a non-zero
// value for long means the expiry estimate is wrong or recording stopped unnoticed).
RivianSessionsExpiring *prometheus.GaugeVec
RivianSessionsExpired prometheus.Gauge
// RivianExpiryNoticesTotal counts the expiry warnings by days left (7 or 1) and outcome.
RivianExpiryNoticesTotal *prometheus.CounterVec
// 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
// RivianBudgetFallbackTotal counts outbound Rivian calls gated by the per-process bucket
// because Redis could not serve the global one (by class). A sustained rate means the
// global budget is not being enforced across pods.
RivianBudgetFallbackTotal *prometheus.CounterVec
// PlanQuotaFallbackTotal counts planner-quota checks counted in the process because Redis
// could not serve them (or there is none): the quota is then per pod.
PlanQuotaFallbackTotal prometheus.Counter
// 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
// Authenticated requests by how they were authenticated: cookie, kratos,
// bypass. A fixed vocabulary; no user data.
// (Impersonation is logged but not counted separately: it rides on a
// cookie-authenticated admin.)
AuthRequestsTotal *prometheus.CounterVec
// Server-side funnel events by outcome: recorded, skipped_dnt (the request
// sent Do Not Track / GPC), skipped_disabled, skipped_other, failed. Event
// name and outcome only; no user data.
ServerEventsTotal *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
// Parallax drive-dynamics values ignored instead of applied, by topic:
// cloud replays of a topic's last snapshot (every resubscribe re-sends
// it with its original timestamp) and frames older than a minute. A
// replayed gear D once flapped a charging truck's sessions.
ParallaxReplaysIgnoredTotal *prometheus.CounterVec
// Charges whose energy the recorder replaced with an estimate from the SoC gain because its integral had lost energy
// (a handoff or a restart). No labels: /metrics is public. Every increment is a recorder bug to find.
ChargeEnergyFloorTotal prometheus.Counter
DrivesLocationUnavailableTotal prometheus.Counter
// APNs device registrations refused because the token is registered to another account whose session is live and the
// caller was not the same install, by reason (install_mismatch or install_missing). No token, user or hash: /metrics is public.
APNsRegisterRefusedTotal *prometheus.CounterVec
// Passkey registrations and sign-ins refused, by a fixed reason (ceremony, unknown_credential, assertion,
// no_user_verification, sign_count, registration). No credential, user or address: /metrics is public.
PasskeyRefusedTotal *prometheus.CounterVec
// LogoutForgetAttemptsTotal and LogoutForgetFailuresTotal count the identity provider's "forget" step (Hydra's remembered login,
// Kratos' sessions) wherever Rivolt revokes sessions: by entry point and, for failures, which step failed. No identity labels.
LogoutForgetAttemptsTotal *prometheus.CounterVec
LogoutForgetFailuresTotal *prometheus.CounterVec
// vehicleState frames carrying an otaStatus, by its class (a fixed list of expected words, else "other"): what
// states a software update goes through, which only "Idle" has been seen to say so far.
OTAStatusFramesTotal *prometheus.CounterVec
// Per-wheel tire status on vehicleState frames that carry tire data, by class: low (one of Rivian's low words),
// not_low (normal or ok), other (any other word) or missing (a pressure with no status). A fixed list; no vehicle,
// user or raw word. It tells whether prod trucks ever say low before any alert relies on it.
TireStatusTotal *prometheus.CounterVec
// TireLowWouldSendTotal counts tire-low episodes that would have sent a push (the dry run), by the low word only.
TireLowWouldSendTotal *prometheus.CounterVec
// RequestAcquiresWithoutUser counts request-pool connections handed out for a context with no user, by route pattern.
RequestAcquiresWithoutUser *prometheus.CounterVec
// Demo rate-limit bucket keys by source: "cf" / "lb" when the direct
// peer is a Cloudflare edge / our private load-balancer hop and
// CF-Connecting-IP was used; "xff" when the key is the rightmost
// X-Forwarded-For entry itself (through the Cloudflare tunnel that is
// the visitor, appended by Cloudflare: a per-visitor key); "peer" when
// there was no forwarded address and the connection address is the key
// (a shared bucket behind an unforwarded proxy). Only a high "peer"
// share on a proxied install signals visitors sharing buckets.
DemoClientKeyTotal *prometheus.CounterVec
// Demo requests rejected by a rate limiter (HTTP 429), by route class
// (general, samples, heavy, tiles) and by which budget bound (client
// or global). A sustained global count is the signal to look at; no
// addresses are recorded.
DemoRateLimitedTotal *prometheus.CounterVec
// Failed live recorder writes by kind (drive_upsert, charge_upsert,
// stale_charge). Nonzero means drives or charges are being lost or left
// stale, typically a database outage.
RecorderWriteFailuresTotal *prometheus.CounterVec
// Sign-in lockouts: an email address reached the failed-login limit and is
// refused for the rest of the window. No labels, so no address is recorded.
// A burst is a guessing or targeted-lockout campaign.
LoginLockoutsTotal prometheus.Counter
// FailLimiterFallbackTotal counts failed-login limiter calls Redis could not serve, which
// fell back to the per-pod count (the cross-replica count was unavailable). Alert on a
// sustained rate: the limit is then up to pods x max.
FailLimiterFallbackTotal prometheus.Counter
// RateLimiterFallbackTotal counts requests a rate limiter (per IP or per user) decided from its per-pod
// bucket because Redis could not answer, by limiter name (a fixed set of route names, never an address or a user). Alert on a sustained
// rate: the limits are then up to pods x limit.
RateLimiterFallbackTotal *prometheus.CounterVec
// WebSocket "next" frames dropped because a subscription's 16-slot channel
// was full (a consumer stuck behind a slow database). A missed gear frame
// can merge or lose a drive.
WSFramesDroppedTotal prometheus.Counter
// Non-transactional messages (internal/engage), by kind and outcome (sent,
// failed, already_sent, skipped_*). No user ids in labels.
EngagementSendsTotal *prometheus.CounterVec
// Periodic jobs that run on one replica at a time (internal/singleton), by job and outcome (ran, skipped, error). "skipped" is another
// replica holding the job's lock: normal at N replicas, one per tick per extra pod. "error" is the lock itself failing.
SingletonRunsTotal *prometheus.CounterVec
// The dated Pro notices (internal/launchnotice), by kind and outcome (sent, in_app, skipped, failed, late). "late" counts a founding
// year that had to be extended to keep the 30 days' notice the Terms promise: any nonzero value is worth a look.
DatedNoticesTotal *prometheus.CounterVec
// App Store Server Notifications by outcome (applied, duplicate, ignored, unmapped, stale, rejected); a rejected one is a bad signature or
// a bundle, environment or product that is not this deployment's. Pre-created at 0.
AppleNotificationsTotal *prometheus.CounterVec
// AppleLinkTotal counts Link Apple to my account requests by outcome (see appleLinkOutcomes in internal/api).
AppleLinkTotal *prometheus.CounterVec
// RoutePointRefusedTotal counts route points kept out of a stored drive route, by where (see RoutePointRefusals).
RoutePointRefusedTotal *prometheus.CounterVec
// BillingNonUSTotal counts Stripe customers whose billing address is outside the US (Rivolt Pro is sold in the US only), once per customer
// and country per process. The account keeps Pro; the operator refunds and cancels, so any increase is worth an alert. Starts at 0.
BillingNonUSTotal prometheus.Counter
// Claims left in 'sending' past 15 minutes: a crash between claim and
// delivery. Never resent, so a nonzero value is how a lost message shows.
EngagementStuckSending prometheus.Gauge
// vehicle_state partitions dropped by sample retention.
SamplePartitionsDropped prometheus.Counter
// Archive objects that exist but fail to read back (corrupt, truncated):
// for a thinned month, full-resolution data lost. Alert on any.
ArchiveReadCorruptTotal prometheus.Counter
// Archive objects flagged for re-archiving because their month's rows no
// longer matched them (a write during a month's first archive).
ArchiveRepairsTotal prometheus.Counter
// Telemetry archive objects (one per user, vehicle and month) by outcome
// (verified, failed), and the bytes of the verified ones.
ArchiveObjectsTotal *prometheus.CounterVec
ArchiveBytesTotal prometheus.Counter
// Archive deletions (account deletion, history reset): objects deleted,
// and requests pending longer than archive.DeleteWithin (alert on > 0).
ArchiveObjectsDeletedTotal prometheus.Counter
ArchiveDeletionsOverdue prometheus.Gauge
// Unix time of the deletion job's last completed run, set to the start
// time when the job starts: an alert on its age catches a pod whose runs
// never complete (the overdue gauge is only set by a completed run).
// 0 while the archive is off.
ArchiveDeletionsLastRun prometheus.Gauge
// 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) RegisterDBPool ¶ added in v0.40.8
RegisterDBPool exposes a database/sql pool's saturation under name ("system"): the connections in use and the limit, and how many callers had to wait for one and for how long. Waits are the signal that the pool is too small for the load; a sustained rate after the pool was cut to 10 per pod means raise RIVOLT_DB_MAX_CONNS, not that the database is slow. No labels beyond the pool's name.
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.