Files
telemetry/docs/adr/ADR-003-otel-api-vs-sdk-separation.md
Rene Nochebuena ed4e9ef161 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/
2026-03-18 14:13:29 -06:00

44 lines
3.2 KiB
Markdown

# ADR-003: OTel API vs SDK Separation — Global Provider Strategy
**Status:** Accepted
**Date:** 2026-03-18
## Context
OpenTelemetry Go has a two-package model:
- **API packages** (`go.opentelemetry.io/otel`, `.../otel/metric`, `.../otel/log`) — stable, backward-compatible interfaces. When called with no SDK registered, all operations are no-ops with zero allocation.
- **SDK packages** (`go.opentelemetry.io/otel/sdk/...`) — concrete implementations with exporters, processors, samplers. These have real runtime cost and external dependencies.
Micro-libs (httpserver, httpmw, logz, etc.) need to emit spans, metrics, or log records. They must not carry SDK dependencies. The question is how to connect API calls in libraries to the real SDK without importing SDK packages from libraries.
The two main strategies are:
1. **Explicit injection** — each library accepts a `TracerProvider`, `MeterProvider`, or `LoggerProvider` as a constructor argument, and the application injects the real SDK provider.
2. **Global provider** — libraries call `otel.Tracer(...)` / `otel.Meter(...)` / `global.Logger(...)` which consult the process-wide global provider. The application sets that global once at startup.
## Decision
Use the **OTel global provider** strategy. Micro-libs obtain tracers, meters, and loggers from the OTel global API. `telemetry.New(...)` sets all three globals:
```go
otel.SetTracerProvider(tp) // traces
otel.SetMeterProvider(mp) // metrics
global.SetLoggerProvider(lp) // logs (go.opentelemetry.io/otel/log/global)
```
This means:
- Libraries have zero SDK dependency. They only import `go.opentelemetry.io/otel` (and sub-packages for metric/log API).
- Before `telemetry.New` is called, all OTel calls in libraries are no-ops — correct behavior in unit tests and in applications that don't use telemetry.
- After `telemetry.New` is called, all OTel calls in libraries automatically route to the real OTLP exporters with no code change required in the libraries.
Explicit injection was considered but rejected because:
- It forces every library constructor to accept provider arguments even when the application doesn't use telemetry.
- It makes the calling code more verbose (every `New(logger, cfg, tracerProvider, meterProvider, ...)`) without clear benefit in a single-process application.
- The global approach is the design intent of the OTel Go project for application-level bootstrap.
## Consequences
- The global providers are process-global mutable state. Tests that call `telemetry.New` will affect other tests running in the same process if tests run in parallel. The test suite uses a fake collector and short shutdown timeouts to mitigate this.
- If a library is used in a context where the global provider has not been set (e.g., a library test), all OTel calls are no-ops. This is correct and expected.
- Applications that use multiple `telemetry.New` calls (e.g., a misconfigured init) will overwrite the globals. Only one call to `telemetry.New` should occur per process.
- The `go.opentelemetry.io/otel/log/global` package is a separate import from `go.opentelemetry.io/otel` because the log signal API was stabilized later. Libraries using the log API must import the `log/global` sub-package for `global.SetLoggerProvider`.