Skip to main content

OpenTelemetry for Go

OpenTelemetry for Go uses an explicit SDK model — there's no auto-instrumentation agent. You add a small amount of setup code once, then sprinkle in middleware or driver wrappers wherever you want spans.

Go is explicit on purpose

Unlike Java or Python, Go doesn't have bytecode manipulation — so there's no "magic" auto-instrumentation. In practice this is a one-time setup per service. Once wired up, the same SDK middleware covers every HTTP request, and driver wrappers handle every database call automatically.


Before You Begin​

You'll need:

  • A Go application (Go 1.20+)
  • The URL of your CodeXray collector (e.g., https://your-codexray.example.com)
  • A project API key from the CodeXray UI (Project Settings → API Keys)

Step 1 — Add the OpenTelemetry Dependencies​

go get \
go.opentelemetry.io/otel \
go.opentelemetry.io/otel/sdk \
go.opentelemetry.io/otel/sdk/trace \
go.opentelemetry.io/otel/sdk/resource \
go.opentelemetry.io/otel/exporters/otlp/otlptrace \
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \
go.opentelemetry.io/otel/propagation \
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp

The first block is the SDK + OTLP exporter. The last line adds the HTTP middleware that captures inbound/outbound HTTP traffic.


Step 2 — Initialise the Tracer​

Create a small telemetry.go (or similar) alongside main.go:

package main

import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"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.24.0"
)

func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
// OTLP exporter — reads OTEL_EXPORTER_OTLP_* env vars
exporter, err := otlptrace.New(ctx, otlptracehttp.NewClient())
if err != nil {
return nil, err
}

res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName("your-service-name"),
),
)
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, nil
}

Call initTracer from main():

ctx := context.Background()
tp, err := initTracer(ctx)
if err != nil { log.Fatal(err) }
defer tp.Shutdown(ctx)

Step 3 — Configure via Environment Variables​

The OTLP exporter reads these automatically:

export OTEL_SERVICE_NAME="your-service-name"
export OTEL_EXPORTER_OTLP_ENDPOINT="https://your-codexray.example.com"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_EXPORTER_OTLP_HEADERS="x-api-key=your-project-api-key"
VariablePurpose
OTEL_SERVICE_NAMEThe name that appears in CodeXray's Applications list
OTEL_EXPORTER_OTLP_ENDPOINTYour CodeXray collector URL (no trailing slash)
OTEL_EXPORTER_OTLP_PROTOCOLhttp/protobuf (recommended) or grpc
OTEL_EXPORTER_OTLP_HEADERSAuthentication — your project API key as x-api-key

Step 4 — Instrument Your HTTP Handlers​

Wrap your handlers with otelhttp.NewHandler — one line per route, or once at the server level:

import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"

func main() {
// ... initTracer call ...

mux := http.NewServeMux()
mux.Handle("/hello", otelhttp.NewHandler(
http.HandlerFunc(helloHandler),
"hello",
))
http.ListenAndServe(":8080", mux)
}

With a router (chi, gorilla/mux, gin)​

Middleware is one line:

// chi
r := chi.NewRouter()
r.Use(func(next http.Handler) http.Handler {
return otelhttp.NewHandler(next, "http.server")
})

// gin
r := gin.Default()
r.Use(otelgin.Middleware("your-service-name"))

// gorilla/mux
r := mux.NewRouter()
r.Use(otelmux.Middleware("your-service-name"))

Each router has its own contrib package — otelgin, otelmux, otelecho, etc. See OpenTelemetry Go Contrib for the full list.


Step 5 — Instrument Outbound HTTP Calls​

Wrap your HTTP client's transport with otelhttp.NewTransport:

client := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}

// Every call made with this client now produces spans
resp, err := client.Get("https://api.example.com/data")

Step 6 — Instrument Database Calls​

Wrap your database driver with the OTel SQL wrapper:

go get github.com/uptrace/opentelemetry-go-extra/otelsql
import (
_ "github.com/lib/pq"
"github.com/uptrace/opentelemetry-go-extra/otelsql"
)

db, err := otelsql.Open("postgres", dsn)

Every SQL query now produces a span with the statement, target database, and latency. Works identically for MySQL, SQLite, and any other database/sql driver.

For gRPC, MongoDB, Redis, and Kafka, similar wrappers exist under OpenTelemetry Go Contrib.


Additional Resource Attributes​

Attach environment, version, and any custom labels to every span:

res, _ := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName("your-service-name"),
semconv.ServiceVersion("1.3.0"),
semconv.DeploymentEnvironment("production"),
),
)

Or set OTEL_RESOURCE_ATTRIBUTES as an env var for the same effect without a redeploy:

export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.3.0"

Verifying It's Working​

  1. Build and run your service with the OTel env vars set
  2. Hit an instrumented endpoint (any route wrapped with otelhttp.NewHandler)
  3. In CodeXray, open Applications → find your service → Traces tab
  4. Traces should appear within a minute

If nothing shows up, the OTel SDK logs export failures explicitly — watch your service's stderr for OTel messages.


Custom Spans for Business Logic​

Add spans around your own code whenever you want richer detail:

import "go.opentelemetry.io/otel"

tracer := otel.Tracer("my-service")

func processOrder(ctx context.Context, orderID string) error {
ctx, span := tracer.Start(ctx, "processOrder")
defer span.End()

span.SetAttributes(attribute.String("order.id", orderID))
// ... your logic, passing ctx down ...
return nil
}

Propagate ctx through every call — the SDK uses it to keep spans joined to their parent trace.


Next Steps​