Prometheus & OpenTelemetry Client Libraries
Production Go services expose Prometheus metrics for dashboards and OpenTelemetry traces for request latency debugging.
Search across all documentation pages
Production Go services expose Prometheus metrics for dashboards and OpenTelemetry traces for request latency debugging.
The client libraries sit at the HTTP/gRPC edge and wrap outbound calls without replacing your logging stack.
Quick-reference recipe card - copy-paste ready.
import (
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
http.Handle("/metrics", promhttp.Handler())import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
handler := otelhttp.NewHandler(mux, "api")When to reach for this:
/metrics or an OTel collector receives OTLPpackage main
import (
"context"
"log"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
)
var (
httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request latency",
Buckets: prometheus.DefBuckets,
}, []string{"method", "route", "status"})
)
func initTracer() trace.TracerProvider {
tp := sdktrace.NewTracerProvider()
otel.SetTracerProvider(tp)
return tp
}
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
next.ServeHTTP(ww, r)
route := chi.RouteContext(r.Context()).RoutePattern()
httpDuration.WithLabelValues(r.Method, route, http.StatusText(ww.Status())).Observe(time.Since(start).Seconds())
})
}
func main() {
tp := initTracer()
defer func() { _ = tp.Shutdown(context.Background()) }()
r := chi.NewRouter()
r.Use(metricsMiddleware)
r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })
r.Handle("/metrics", promhttp.Handler())
root := otelhttp.NewHandler(r, "api")
log.Fatal(http.ListenAndServe(":8080", root))
}What this demonstrates:
method, route, status)promhttp.Handler on a dedicated path for scraperspromhttp exposes text exposition format.TracerProvider, exporters (OTLP gRPC/HTTP), and resource attributes (service.name).traceparent headers.| Type | Use | Example |
|---|---|---|
| Counter | Monotonic totals | http_requests_total |
| Gauge | Point-in-time values | go_goroutines, queue depth |
| Histogram | Latency distributions | http_request_duration_seconds |
| Summary | Client-side quantiles | Use histogram + Prometheus queries instead when possible |
exporter, _ := otlptracegrpc.New(ctx)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("orders-api"),
)),
)
otel.SetTracerProvider(tp)service.name with deployment labels for trace search.promauto only when a single registry is fine; libraries should accept prometheus.Registerer parameters.go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc interceptors similarly./users/123 per request explodes series. Fix: use chi.RoutePattern() or templated route names.prometheus.Registerer into constructors.Shutdown on SIGTERM with graceful drain timeout./metrics leaks operational detail. Fix: separate listener or network policy on admin port.
| Alternative | Use When | Don't Use When |
|---|---|---|
expvar | Tiny internal debug counters | You need histograms and Grafana |
| StatsD/DogStatsD clients | Org mandates Datadog agent | Platform standardized on Prometheus |
| Vendor APM agents | Fully managed tracing | You want OTLP portability |
| Logs only | Early prototype | Production SLOs require metrics |
Many teams keep Prometheus client for scraping while adopting OTel for traces; OTel metrics maturity varies - follow platform guidance.
Use contrib middleware packages or wrap the engine as http.Handler with otelhttp at the mount point.
Add unary/stream interceptors from otelgrpc and grpc-prometheus alongside HTTP instrumentation.
That section covers slog, RED dashboards, and health probes; this page focuses on Prometheus and OTel client libraries.
Start with DefBuckets for APIs; tune histogram buckets when SLO thresholds cluster away from defaults.
Use a custom registry in tests, gather metrics with Gather(), and assert counter/histogram values with testify.
Yes - inject trace_id into slog/zap fields via OTel baggage or span context in middleware.
At minimum service.name and service.version; add deployment.environment for multi-env filtering.
controller-runtime exposes metrics for Kubernetes operators; same cardinality rules apply.
Keep OTel SDK wiring in main or internal/telemetry; business packages depend on interfaces, not exporters.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 19, 2026