Skip to content

Go OpenTelemetry instrumentation

Go services are instrumented in source. Unlike the JVM or .NET, Go compiles to a static binary with no runtime an agent can attach to and rewrite, so there is no drop-in auto-instrumentation agent. You add the OpenTelemetry SDK once in a small bootstrap file, then wrap your HTTP or gRPC entry points with the contrib instrumentation middleware. This page is the shared setup that every framework guide below builds on.

  • SDK bootstrap — a single otel.go file initialises a TracerProvider, sets the trace-context propagator, and attaches the resource that identifies your service. You call it once from main.
  • Framework middleware — the OpenTelemetry contrib modules wrap your router or gRPC server so every inbound request becomes a span automatically.
  • Custom spans — anywhere you want more detail, you take a tracer from the global provider and start your own spans. See custom instrumentation.

Add the API, the SDK, and the OTLP trace exporter to your module.

Terminal window
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc

Create otel.go next to your main package. It reads the ingest endpoint and token from the standard OTEL_EXPORTER_OTLP_* environment variables, so no credentials live in source.

package main
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"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.37.0"
)
func setupOTel(ctx context.Context, serviceName, environment string) (func(context.Context) error, error) {
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName(serviceName),
attribute.String("service.environment", environment),
),
)
if err != nil {
return nil, err
}
exporter, err := otlptracegrpc.New(ctx)
if err != nil {
return nil, 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 it at the top of main and defer the returned shutdown so the batch processor flushes before the process exits.

func main() {
ctx := context.Background()
shutdown, err := setupOTel(ctx, "checkout-service", "production")
if err != nil {
log.Fatalf("otel setup: %v", err)
}
defer func() {
if err := shutdown(context.Background()); err != nil {
log.Printf("otel shutdown: %v", err)
}
}()
// ... start your server ...
}

The service.name and service.environment attributes are how Osuite groups your traces into a service and separates environments. Skipping the shutdown call is the most common reason the last spans never arrive — see troubleshooting.

Point the exporter at your Osuite region and supply the ingest token.

Terminal window
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.osuite.io:443"
export OTEL_EXPORTER_OTLP_HEADERS="x-osuite-ingest-token=<your-ingest-token>"

The bootstrap above is identical across frameworks. Pick your server to wire in the request middleware.

What you should see

After you wire in a framework and send a request, the service appears in APM within a minute under the service.name you set, tagged with its service.environment.