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

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](https://belvador.dev/en/docs/otel/quickstart#2-set-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

```bash
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:

```go title="otel.go"
package main

import (
	"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

```go title="main.go"
shutdown, err := setupOTel(ctx)
if err != nil {
	log.Fatal(err)
}
defer shutdown(context.Background()) // flushes on exit

logger := 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](https://belvador.dev/en/docs/otel/overview#one-tracing-owner). `sentryotel.NewOtelIntegration()` puts span ids on errors captured with the request's `context.Context` — see [With Sentry SDKs](https://belvador.dev/en/docs/otel/sentry-coexistence#go-sentry-go); that link is not marked tested yet.
