Decisions

ADR-032: `app_info` gauge as the service-identity signal

Prometheus metrics answer "how is the service behaving", but several operational questions are about the service *itself*: is it reachable right now, what version is running, on which release track, did the canary…

#032

ADR-032: app_info gauge as the service-identity signal

Status: Accepted Date: 2026-04-21

Context

Prometheus metrics answer "how is the service behaving", but several operational questions are about the service itself: is it reachable right now, what version is running, on which release track, did the canary actually get deployed, did a rollback take effect. Log lines carry this data (observ stamps service, version, commit, release_track on every line — see ADR-004) but log queries are poorly suited to "is the service currently emitting anything" checks. Specifically:

  • Alerts on log absence require Loki-side alerting, which Phase 5 adds but Phase 2b cannot rely on.
  • Dashboard panels that ask "what version is running where" need a metric to plot — logs would need a separate data source panel and an aggregation over the time window.
  • Scrape-level monitoring (Blackbox exporter, Alertmanager's up{} series) tells you whether the scrape succeeded, not whether the service passed its own wire-up.

The industry pattern for this is a build-identity gauge: a metric whose value is the constant 1 and whose labels carry the identity. Queries of the form sum by (version) (app_info{service="distill"}) answer "how many distinct versions of distill are currently reporting", and absent(app_info{service="distill", release_track="canary"}) answers "did the canary stop reporting". Examples in the wild: Kubernetes uses kube_pod_info; node_exporter uses node_uname_info; the CNCF Observability SIG documents the pattern under "info metric".

Phase 2b is adding metrics to gyrum-go. The decision is whether to include a build-identity gauge in the standard set and, if so, under what name and label shape.

Decision

The metrics sub-package registers an app_info gauge at init, value 1, with labels service, release_track, version, commit, go_version — all sourced from observ.GetBuildInfo(). It is part of the standard metric catalogue, alongside the three HTTP series.

app_info{service="distill", release_track="stable",
         version="1.2.3", commit="abcd123", go_version="go1.26.1"} 1

The gauge is registered unconditionally by the sub-package — no consumer-side wiring needed, no "if configured then emit" branches. A service that imports pkg/observ/metrics gets app_info for free.

Label shape matches the log-line shape (service, version, commit, release_track) one-for-one, so operators who learn the log schema do not learn a second schema for metrics. go_version is metrics-only — the logger does not emit it per-line (would be noise) but it is valuable on a metric for detecting stale-binary incidents.

Name is app_info, not gyrum_app_info or service_info. Prefixes would make the metric prometheus-registry-unique for little gain; the service label already carries the identity. The CNCF pattern is app_info or build_info; app_info is shorter and aligns with the majority of in-the-wild examples.

Consequences

  • "Is the service up and reporting" has a metric answer. Phase 5 alerts against absent(app_info{service="distill"}) fire when a service stops scraping for any reason: process dead, network partitioned, scrape target mis-registered. No log-query-based liveness rule needed.
  • Version rollout is observable. During a deploy, count by (version) (app_info{service="distill"}) shows both old and new versions in parallel, converging to one when the rollout finishes. A rollback that failed to take effect appears as "new version label still present"; a partial rollout appears as both labels coexisting past the expected window.
  • Canary comparison works mechanically. Split by release_track="canary" vs release_track="stable" on any HTTP metric joined with app_info to confirm the canary is actually receiving traffic: http_requests_total{release_track="canary"} > 0 presupposes app_info{release_track="canary"} is reporting.
  • Cardinality cost is low and bounded. One series per {service, release_track, version, commit, go_version} combo. In practice this is one series per running replica set at a time; during a deploy it briefly doubles. Well inside the "small number a human can name" ceiling from ADR-005.
  • Label set cannot grow silently. Like any other metric, app_info labels are set at registration. Adding a new label (e.g. build_host) requires an ADR amendment — this metric is stable by design because alerts and dashboards will reference it.
  • Stale go_version is detectable. A service pinned to an old Go minor shows up distinctly: group by (go_version) (app_info{service="distill"}) reveals the split. Useful when the distill canary accidentally shipped on the stable builder image.

Alternatives considered

  • No build-identity metric; rely on log queries for version questions. Works for post-incident investigation, fails for alerts and dashboards. The Phase 5 alert tranche specifically needs "service missing" to be a metric; log-absence alerts arrive later and cost more to operate. Rejected on capability gap.
  • Use build_info as the name. Valid alternative; used by the Go collector itself (go_build_info). Rejected because the namespace collision with runtime metrics is mildly awkward — operators searching for "build info" would find both, and the CNCF convention leans toward app_info for application-level identity.
  • One metric per label dimension (app_version, app_commit, ...). Avoids the info-metric pattern entirely. Rejected: the pattern is multiple series with no shared join key, which makes "version by release_track" a cross-series join the query engine does poorly. Info metrics exist precisely so this class of query is cheap.
  • Emit only at startup, not as a gauge. A counter set once at boot would leak memory in prometheus (counters are never forgotten). A gauge is the right shape — the value is constant 1 and the series is live while the process is live.
  • Put go_version on every HTTP series for "stale binary" queries. Explodes cardinality for a single investigation dimension. app_info owns go_version; joins via service, release_track keep the HTTP series clean.

Supersedes: none Superseded by: