Skip to main content

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


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.

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.

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 otelchi is 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.
Replace go run . with your usual start command (for example go run ./cmd/server or the path to your built binary).
OTEL requires headers to be in comma-separated key=value format as shown in the command above.
OTEL_SERVICE_NAME is also read automatically by resource.WithFromEnv() in the SDK initialization in Step 2 — no code change is needed when deploying to different environments.

Instrument all telemetry together

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