Files
telemetry/docs/adr/ADR-003-otel-api-vs-sdk-separation.md

44 lines
3.2 KiB
Markdown
Raw Normal View History

# 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`.