ADR-021: Observability: an OpenTelemetry base, a UI stack as an option
Status: Accepted Date: 2026-10-07 Builds on: ADR-020 (addons)
Context
Section titled “Context”Generated services already expose Prometheus metrics (and, per shape, some OpenTelemetry tracing), but nothing collected them and each shape configured telemetry differently (Python used a private OTLP_ENDPOINT, Java shipped the OTel agent disabled, Go only had /metrics). We want one vendor-neutral path, and no obligation to run a particular UI.
Decision
Section titled “Decision”- OpenTelemetry is the base. Declaring
addons.observabilityinapp.yamlrenders, for each environment, an OpenTelemetry Collector (contrib image pinned, non-root numeric user, read-only filesystem, plain manifests, no operator or CRDs) in the application namespace, and injects the standard variables into every service:OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318,OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf,OTEL_SERVICE_NAME,OTEL_RESOURCE_ATTRIBUTES(service.namespace,deployment.environment) andOTEL_SDK_DISABLED=false. A value in a service’s ownenvstill wins. - The collector receives OTLP and scrapes Prometheus. Pipelines for traces, metrics and logs: OTLP in; a Prometheus scrape job per service that declares
metrics:(the path comes fromshapes.ymlruntime.metrics, copied into the explicitservices.yamlentry like port and probes);memory_limiter,resource(adds environment and application) andbatchprocessors;debugplus an optional OTLP/HTTP exporter. - The backend is a choice, not a dependency.
exportTo: <OTLP/HTTP endpoint>sends everything to your own backend (Grafana Cloud, Tempo/Mimir/Loki, Honeycomb, SigNoz, …), withheadersSecret(a Secret whose keyAUTHORIZATIONbecomes theAuthorizationheader; never in git). With neither option the collector only logs, which still proves the wiring. - UI stack option:
ui: lgtm.cluster-upinstalls Grafana’s all-in-oneotel-lgtmimage (Grafana, Tempo, Loki, Prometheus-compatible store) as a shared dev stack in its ownobservabilitynamespace, reachable athttp://grafana.localhost:<port>/, and the collectors export to it. It runs as root with non-persistent storage, so it is for local and demo use only and lives outside the application namespaces (and their policies). Otherui:values can be added later without changing the base. - Config addon, so it is pruned. Unlike databases, the observability Application has
prune: true(a separate ApplicationSet per prune policy), so removing the addon removes the collector. - Template alignment. The Python template now configures tracing from the standard variables (HTTP/protobuf by default,
grpchonoured;OTLP_ENDPOINTkept as a fallback). Java (agent) and web (instrumentation.ts) already used them.
Not included
Section titled “Not included”Alerting and dashboards-as-code (no Prometheus Operator CRDs on purpose), stdout log collection (needs node access; logs arrive only if a service’s SDK pushes them), OpenTelemetry in Go services (metrics are scraped; the Go SDK is a follow-up), tail sampling and HA/multi-tenant backends.