Go
This guide walks you through setting up OpenTelemetry in your Go application and exporting traces to CtrlB. Unlike some languages, Go has no zero-code auto-instrumentation agent — you initialize the SDK explicitly in code and wrap your HTTP handlers, gRPC servers, and database clients using contrib middleware packages from the opentelemetry-go-contrib repository.
OpenTelemetry SDK status (mid-2026): Go trace instrumentation is Stable. See the OpenTelemetry language status table.
Prerequisites
- Go 1.21+
- CtrlB OTLP (ingestion host, auth, stream, service name): See Prerequisites
Step 1. Install Dependencies
You can install OpenTelemetry in two ways — pull in all common contrib packages to get started quickly, or cherry-pick only what your stack needs for production.
A: Install all common instrumentation packages (recommended for getting started)
Run the following go get commands to add the core SDK, OTLP HTTP exporter, propagators, and the most widely used contrib instrumentations:
go get go.opentelemetry.io/otel \
go.opentelemetry.io/otel/sdk/trace \
go.opentelemetry.io/otel/sdk/resource \
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \
go.opentelemetry.io/otel/propagation \
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp \
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
Then run go mod tidy to align dependency versions.
B: Install specific instrumentation packages (recommended for production)
If your application only uses certain frameworks or transports, install only what you need:
# Core SDK and exporter (always required)
go get go.opentelemetry.io/otel \
go.opentelemetry.io/otel/sdk/trace \
go.opentelemetry.io/otel/sdk/resource \
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \
go.opentelemetry.io/otel/propagation
# net/http server and client instrumentation
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
# Gin web framework
go get go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin
# Echo web framework
go get go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho
# gRPC
go get go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
To browse the full list of available Go instrumentation packages, see the opentelemetry-go-contrib instrumentation directory and the OpenTelemetry Registry for Go.
Step 2. Configure OpenTelemetry Instrumentation
Go requires explicit SDK initialization. The recommended pattern is to set up a TracerProvider once at startup, register it globally, and defer a graceful shutdown.
A. Initialize the SDK (required for all apps)
Create an otel.go (or equivalent) file in your application:
package main
import (
"context"
"errors"
"fmt"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
// initTracer initializes a TracerProvider that exports spans via OTLP HTTP.
// It reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS from
// the environment, so no endpoint or auth is hard-coded here.
func initTracer(ctx context.Context) (func(context.Context) error, error) {
exporter, err := otlptracehttp.New(ctx)
if err != nil {
return nil, fmt.Errorf("creating OTLP trace exporter: %w", err)
}
res, err := resource.New(ctx,
resource.WithFromEnv(), // picks up OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES
resource.WithTelemetrySDK(),
resource.WithProcess(),
resource.WithOS(),
resource.WithHost(),
resource.WithSchemaURL(semconv.SchemaURL),
)
if err != nil && !errors.Is(err, resource.ErrPartialResource) {
return nil, fmt.Errorf("creating resource: %w", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
return tp.Shutdown, nil
}
Call initTracer from your main function and defer the returned shutdown. Use a non-canceled context for shutdown so the exporter can flush (for example context.Background() rather than a signal-canceled context):
import (
"context"
"log"
)
func main() {
ctx := context.Background()
shutdown, err := initTracer(ctx)
if err != nil {
log.Fatalf("failed to initialize tracer: %v", err)
}
defer func() {
shutdownCtx := context.Background()
if err := shutdown(shutdownCtx); err != nil {
log.Printf("error shutting down tracer: %v", err)
}
}()
// ... start your server
}
B. Instrument net/http servers and clients
Wrap your http.Handler with otelhttp.NewHandler and wrap outgoing client transports with otelhttp.NewTransport:
import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
// Server — wrap the entire mux or individual handlers
http.Handle("/api/v1/items", otelhttp.NewHandler(itemsHandler, "items"))
// Or wrap the default mux
http.ListenAndServe(":8080", otelhttp.NewHandler(http.DefaultServeMux, "server"))
// Client — wrap the transport so outgoing requests carry trace context
client := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
C. Instrument gRPC servers and clients (optional)
Use the otelgrpc stats handlers to instrument gRPC without modifying individual RPCs:
import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
// Server
grpcServer := grpc.NewServer(
grpc.StatsHandler(otelgrpc.NewServerHandler()),
)
// Client
conn, err := grpc.NewClient(target,
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
)
D. Instrument Gin or Echo (optional)
For Gin, add the middleware once after creating the engine:
import (
"github.com/gin-gonic/gin"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)
r := gin.Default()
r.Use(otelgin.Middleware("<service_name>"))
For Echo:
import (
"github.com/labstack/echo/v4"
"go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"
)
e := echo.New()
e.Use(otelecho.Middleware("<service_name>"))
Chi users: There is no official OpenTelemetry contrib package for Chi. A community package
otelchiis available and follows the same middleware pattern.
Step 3. Configure Exporter for CtrlB
Once your application is instrumented, configure the OTLP exporter to send telemetry data directly to CtrlB.
Set the following environment variables before starting your app:
OTEL_EXPORTER=otlp \
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \
OTEL_SERVICE_NAME=<service_name> \
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://<INGESTION_HOST>/api/default/v1/traces \
OTEL_METRICS_EXPORTER=none \
OTEL_LOGS_EXPORTER=none \
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <API_TOKEN>,stream-name=<STREAM_NAME>" \
go run .
<service_name>is the name of your service as it should appear in CtrlB.
Replacego run .with your usual start command (for examplego run ./cmd/serveror the path to your built binary).
OTEL requires headers to be in comma-separatedkey=valueformat as shown in the command above.
OTEL_SERVICE_NAMEis also read automatically byresource.WithFromEnv()in the SDK initialization in Step 2 — no code change is needed when deploying to different environments.
The block above sends traces only. To export traces, metrics, and logs from the same application, use the shared OTLP endpoint and headers instead of the signal-specific endpoint above:
OTEL_EXPORTER_OTLP_ENDPOINT=https://<INGESTION_HOST>/api/default \
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://<INGESTION_HOST>/api/default/<STREAM_NAME>/_otel/v1/logs \
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <API_TOKEN>,stream-name=<STREAM_NAME>"
Step 4. Use OpenTelemetry Collector (Optional)
For production setups, it's recommended to send traces through an OpenTelemetry Collector before CtrlB. This enables buffering, batching, retries, and multi-sink routing.
See OpenTelemetry Collector for Traces for configuration details.
Troubleshooting
Spans are created but not exported
Ensure initTracer is called before any other code creates spans, and that the shutdown function is deferred (not called eagerly). The batcher flushes on shutdown — if the process exits without calling Shutdown, buffered spans are dropped.
Trace context is not propagated across HTTP calls
Two things must both be true: (1) the otelhttp.NewTransport wrapper must be used on outgoing clients, and (2) otel.SetTextMapPropagator must be called with a composite propagator that includes propagation.TraceContext{}. Calling otel.SetTracerProvider alone is not enough.
go: inconsistent vendoring or version conflicts
Run go mod tidy after adding any new go.opentelemetry.io packages. All go.opentelemetry.io/otel/* modules must use compatible versions — mixing minor versions (e.g. v1.27 and v1.24) causes build errors.
resource.WithFromEnv() not picking up OTEL_SERVICE_NAME
Verify the environment variable is exported in the shell or process environment before the binary starts. In Docker or Kubernetes, confirm the variable is set in the container's env block, not just in the host shell.
See Also
- See Validate Tracing Instrumentation for validation steps and other common issues faced.