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"vsrelease_track="stable"on any HTTP metric joined withapp_infoto confirm the canary is actually receiving traffic:http_requests_total{release_track="canary"} > 0presupposesapp_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_infolabels 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_infoas 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 towardapp_infofor 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_versionon every HTTP series for "stale binary" queries. Explodes cardinality for a single investigation dimension.app_infoownsgo_version; joins viaservice, release_trackkeep the HTTP series clean.
Supersedes: none Superseded by: