Skip to content

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.
  • logs needs 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.