Go
This guide walks you through setting up OpenTelemetry in your Go application and exporting metrics 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 metrics 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 metric exporter, and the most widely used contrib instrumentations:
go get go.opentelemetry.io/otel \
go.opentelemetry.io/otel/sdk/metric \
go.opentelemetry.io/otel/sdk/resource \
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp \
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 metric exporter (always required)
go get go.opentelemetry.io/otel \
go.opentelemetry.io/otel/sdk/metric \
go.opentelemetry.io/otel/sdk/resource \
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp
# 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 MeterProvider once at startup, register it globally, and defer a graceful shutdown. Metrics use a periodic reader that collects and exports on an interval, rather than the batch processor used for spans.
A. Initialize the SDK (required for all apps)
Create an otel.go (or equivalent) file in your application:
package main
import (
"context"
"errors"
"fmt"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
// initMeter initializes a MeterProvider that exports metrics 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 initMeter(ctx context.Context) (func(context.Context) error, error) {
exporter, err := otlpmetrichttp.New(ctx)
if err != nil {
return nil, fmt.Errorf("creating OTLP metric 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)
}
mp := sdkmetric.NewMeterProvider(
sdkmetric.WithResource(res),
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(
exporter,
sdkmetric.WithInterval(15*time.Second),
)),
)
otel.SetMeterProvider(mp)
return mp.Shutdown, nil
}
Call initMeter from your main function and defer the returned shutdown. Use a non-canceled context for shutdown so the reader can flush its final export (for example context.Background() rather than a signal-canceled context):
import (
"context"
"log"
)
func main() {
ctx := context.Background()
shutdown, err := initMeter(ctx)
if err != nil {
log.Fatalf("failed to initialize meter: %v", err)
}
defer func() {
shutdownCtx := context.Background()
if err := shutdown(shutdownCtx); err != nil {
log.Printf("error shutting down meter: %v", err)
}
}()
// ... start your server
}
With the MeterProvider registered globally, create instruments once and record from anywhere in your code. Create instruments at package level (or on a struct), not per request:
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
)
var meter = otel.Meter("<service_name>")
var (
orders, _ = meter.Int64Counter("orders.processed",
metric.WithDescription("Number of orders processed"),
metric.WithUnit("{order}"),
)
latency, _ = meter.Float64Histogram("payment.duration",
metric.WithDescription("Payment processing duration"),
metric.WithUnit("ms"),
)
)
// Record from anywhere in your request path:
func processOrder(ctx context.Context, elapsedMillis float64) {
orders.Add(ctx, 1, metric.WithAttributes(attribute.String("payment.provider", "stripe")))
latency.Record(ctx, elapsedMillis)
}
Keep attribute cardinality low — every unique attribute combination becomes its own time series.
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 to record outgoing request metrics
client := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
otelhttprecords standard HTTP server metrics — request duration and counts — through the globalMeterProviderautomatically once the provider from step A is registered. You do not need to define these instruments by hand.
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_METRICS_ENDPOINT=https://<INGESTION_HOST>/api/default/v1/metrics \
OTEL_TRACES_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 metrics 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 metrics through an OpenTelemetry Collector before CtrlB. This enables buffering, batching, retries, and multi-sink routing.
See OpenTelemetry for Metrics for configuration details.
Troubleshooting
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.