Skip to content

OpenTelemetry for Go

One setup function that reads the standard OTEL_* variables, otelhttp for servers and clients, otelslog for logs with traces, and flushing on shutdown.

Markdown
On this page

Go has no zero-code agent, so you add one setup function and wrap your HTTP server and client. Exporters, resource and sampling still come from the standard variables — nothing Belvador-specific is compiled in.

Tested on 2026-10-24 with go 1.27.3, go.opentelemetry.io/otel v1.47.0, otelhttp v0.72.0, otelslog v0.21.0: traces passed, logs passed, logs carry the request's trace and span.

Install#

Terminal
go get go.opentelemetry.io/otel \  go.opentelemetry.io/otel/sdk \  go.opentelemetry.io/otel/sdk/log \  go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \  go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp \  go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp \  go.opentelemetry.io/contrib/bridges/otelslog

Set up once#

Add this file to your main package. It builds the trace and log pipelines from the environment and returns one function that flushes both:

otel.go
package mainimport (	"context"	"errors"	"go.opentelemetry.io/otel"	"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"	"go.opentelemetry.io/otel/log/global"	"go.opentelemetry.io/otel/propagation"	sdklog "go.opentelemetry.io/otel/sdk/log"	"go.opentelemetry.io/otel/sdk/resource"	sdktrace "go.opentelemetry.io/otel/sdk/trace")func setupOTel(ctx context.Context) (func(context.Context) error, error) {	res, err := resource.New(ctx, resource.WithFromEnv(), resource.WithTelemetrySDK(), resource.WithHost())	if err != nil {		return nil, err	}	traceExp, err := otlptracehttp.New(ctx)	if err != nil {		return nil, err	}	logExp, err := otlploghttp.New(ctx)	if err != nil {		return nil, err	}	tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(traceExp), sdktrace.WithResource(res))	lp := sdklog.NewLoggerProvider(sdklog.WithProcessor(sdklog.NewBatchProcessor(logExp)), sdklog.WithResource(res))	otel.SetTracerProvider(tp)	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))	global.SetLoggerProvider(lp)	return func(ctx context.Context) error { return errors.Join(tp.Shutdown(ctx), lp.Shutdown(ctx)) }, nil}

Use it#

main.go
shutdown, err := setupOTel(ctx)if err != nil {	log.Fatal(err)}defer shutdown(context.Background()) // flushes on exitlogger := otelslog.NewLogger("my-service")client := &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}srv := &http.Server{Addr: ":8080", Handler: otelhttp.NewHandler(mux, "server")}// Inside a handler: pass r.Context() so the log line carries the request's span.logger.WarnContext(r.Context(), "payment declined", "order", orderID)
  • otelhttp.NewHandler creates a server span per request and continues the caller's trace from traceparent.

  • otelhttp.NewTransport creates a client span per outgoing request and sends traceparent, so the next service joins the same trace.

  • otelslog turns log/slog calls into OpenTelemetry log records. Use the …Context methods (InfoContext, WarnContext, ErrorContext) with the request's context: that is how the record learns the trace id and span id. A plain logger.Warn(...) is still sent, without them.

Shutdown#

Call the returned function before the process exits — the batch processors hold up to a few seconds of data. Handle SIGTERM (as signal.NotifyContext does) so a container stop or a rolling deploy flushes instead of dropping the last batch.

Other libraries#

The same pattern covers gRPC (otelgrpc), database drivers and frameworks with their own contrib packages under go.opentelemetry.io/contrib/instrumentation/…. Only net/http and log/slog are part of what Belvador runs.

With Sentry's Go SDK#

Keep sentry-go for errors with tracing off, so OpenTelemetry is the only tracing owner. sentryotel.NewOtelIntegration() puts span ids on errors captured with the request's context.Context — see With Sentry SDKs; that link is not marked tested yet.

Spotted something wrong or missing? Tell us: hello@belvador.dev