Wire OTLP logs, metrics & traces¶
All three signal packages share the same shape — NewProvider(ctx, res, settings,
…opts) — so wiring the full stack is three calls against one resource. This guide
also shows resolving one shared endpoint across the signals with per-signal overrides.
One resource, three providers¶
res := otelcore.Resource("my-service", "1.4.0")
s := otelcore.Settings{Enabled: true, Endpoint: "https://collector.internal:4318"}
tp, err := tracing.NewProvider(ctx, res, s, tracing.WithSampling(0.25))
mp, err := metrics.NewProvider(ctx, res, s, metrics.WithInterval(30*time.Second))
lp, err := logs.NewProvider(ctx, res, s)
Register them with the global API and remember to shut each down on exit (they flush buffered telemetry):
otel.SetTracerProvider(tp)
otel.SetMeterProvider(mp)
global.SetLoggerProvider(lp) // go.opentelemetry.io/otel/log/global
defer func() {
_ = tp.Shutdown(ctx)
_ = mp.Shutdown(ctx)
_ = lp.Shutdown(ctx)
}()
Bridge logs into slog¶
For logs, logs.Handler
turns a LoggerProvider into a ready slog.Handler, so your existing slog call
sites export to OTel without change:
lp, _ := logs.NewProvider(ctx, res, s)
logger := slog.New(logs.Handler(lp, "my-service"))
logger.Info("service started") // ← exported via OTLP
Signal options¶
tracing.WithSampling(ratio)— parent-based ratio sampling (e.g.0.1= 10%).metrics.WithInterval(d)— the periodic reader's export interval.logsneeds no options for the common case.
One endpoint, per-signal overrides¶
When most signals share a collector but one differs (say traces go to a separate
tail-sampling endpoint), resolve each signal's Settings from a shared config plus a
per-signal override with
otelcore.ResolveSettings:
shared := otelcore.Config{Endpoint: "https://collector.internal:4318"}
// Traces override only the endpoint; everything else inherits the shared config.
traceSignal := otelcore.SignalConfig{Enabled: true, Endpoint: "https://tail-sampler:4318"}
traceSettings := otelcore.ResolveSettings(shared, traceSignal, otelcore.SignalOverrides{
Endpoint: true, // only Endpoint was explicitly set for traces
})
// traceSettings.Endpoint == "https://tail-sampler:4318"; Headers/Insecure inherit shared.
The SignalOverrides
mask records which fields the per-signal section actually set, so an unset field
never clobbers the shared value with a zero. You decide how a field counts as "set"
(a config key present, a non-empty flag, an env var) and express it as the mask — the
core takes no dependency on any configuration container.
Enabled and the empty endpoint¶
Settings.Enabled is yours to honour — skip building a provider when a signal is
disabled. An empty Endpoint on an enabled signal is not an error: the exporter
falls back to OTEL_EXPORTER_OTLP_*. See the
config model for the full resolution order.