feat(telemetry): initial stable release v0.9.0

Single-call OTel SDK bootstrap setting all three global providers (traces → Tempo, metrics → Mimir, logs → Loki) over OTLP gRPC.

What's included:
- New(ctx, Config): bootstraps TracerProvider, MeterProvider, and LoggerProvider with OTLP gRPC exporters; sets OTel globals
- W3C TraceContext + Baggage propagation set globally
- Resource tagging: service.name, service.version, deployment.environment merged with SDK defaults
- OTLPInsecure bool for development environments without TLS
- Sequential rollback on partial initialization failure — no dangling exporters on error
- Returns shutdown func(context.Context) error; caller defers in main or wires into launcher BeforeStop
- Tier 5 module: must be imported only by application main packages; zero micro-lib dependencies

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
This commit is contained in:
2026-03-18 14:13:29 -06:00
commit ed4e9ef161
14 changed files with 787 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
# ADR-001: Tier 5 — Application Bootstrap Only
**Status:** Accepted
**Date:** 2026-03-18
## Context
OpenTelemetry is structured around a separation between the **API** (stable, zero-cost when no SDK is wired) and the **SDK** (the real implementation, with exporters, batch processors, and gRPC connections). Any package can import the OTel API and call `otel.Tracer(...)`, `otel.Meter(...)`, etc. at zero runtime cost — these calls are no-ops until an SDK TracerProvider is set as the global.
The question is where in the module tier hierarchy the SDK bootstrap belongs. The options are:
1. Include telemetry bootstrap in each micro-lib that produces signals (e.g., httpserver starts its own SDK).
2. Provide a standalone bootstrap module imported only by application `main` packages.
Option 1 would cause multiple SDK initializations, competing global registrations, and make it impossible for the application to control the exporter endpoint or sampling strategy. It would also force all micro-libs to carry the heavy OTel SDK as a dependency even when the application does not use telemetry.
## Decision
The `telemetry` module is **Tier 5** — the same tier as application bootstrap entry points. It must only be imported by application `main` packages (or equivalent wiring code). It must never be imported by:
- Framework libraries (Tier 03)
- Transport modules (Tier 4)
- Other Tier 5 modules that are not themselves `main`
Micro-libs use only the OTel API packages (`go.opentelemetry.io/otel`, `go.opentelemetry.io/otel/metric`, `go.opentelemetry.io/otel/log`) which default to no-op providers. When an application imports `telemetry` and calls `telemetry.New(...)`, the three global providers are replaced with real SDK providers, and all micro-libs that use the global API automatically emit signals without any change to their code.
## Consequences
- No micro-lib needs to import or configure `telemetry`. The OTel no-op default means libraries compile and run correctly in unit tests without any collector present.
- Applications that do not call `telemetry.New(...)` produce no signals. This is correct — telemetry is opt-in at the application level.
- The `telemetry` module carries heavy SDK dependencies (OTLP gRPC exporters, batch processors). These do not appear in any library's dependency graph.
- Code review must reject any PR that imports `telemetry` from a non-`main` package. This is enforced by convention, not by a build tool currently.
- There is no `launcher.Component` wrapper for telemetry. The caller is responsible for deferring the shutdown function, which flushes all exporters before process exit.