The OpenTelemetry Go metrics API provides interfaces and types for recording measurements about your application's performance and behavior. It supports various instrument types for different measurement patterns.
import "go.opentelemetry.io/otel/metric"The metrics API is organized around three main concepts:
Synchronous Instruments (record during execution):
Asynchronous Instruments (observe via callback):
package main
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
)
func main() {
// Get a meter
meter := otel.Meter("my-service")
// Create a counter
counter, err := meter.Int64Counter(
"requests.count",
metric.WithDescription("Total number of requests"),
metric.WithUnit("{request}"),
)
if err != nil {
panic(err)
}
// Record a measurement
ctx := context.Background()
counter.Add(ctx, 1, metric.WithAttributes(
attribute.String("method", "GET"),
attribute.String("path", "/api/users"),
))
}type MeterProvider interface {
// Meter returns a unique Meter scoped to be used by instrumentation code
// to collect metrics. The scope and identity of that instrumentation code
// is uniquely defined by the name and options passed.
Meter(name string, opts ...MeterOption) Meter
}import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/sdk/metric"
)
// Set up global meter provider
func setupMetrics() {
mp := metric.NewMeterProvider(
metric.WithReader(reader),
metric.WithResource(resource),
)
otel.SetMeterProvider(mp)
}
// Access global meter provider
func getMeterProvider() metric.MeterProvider {
return otel.GetMeterProvider()
}type MeterOption interface {
// Has unexported methods.
}
// WithInstrumentationVersion sets the instrumentation version.
func WithInstrumentationVersion(version string) MeterOption
// WithSchemaURL sets the schema URL for the Meter.
func WithSchemaURL(schemaURL string) MeterOption
// WithInstrumentationAttributes sets the instrumentation attributes.
func WithInstrumentationAttributes(attr ...attribute.KeyValue) MeterOptionExample:
meter := provider.Meter(
"github.com/myorg/myapp",
metric.WithInstrumentationVersion("1.0.0"),
metric.WithSchemaURL("https://opentelemetry.io/schemas/1.20.0"),
)type Meter interface {
// Int64 synchronous instruments
Int64Counter(name string, options ...Int64CounterOption) (Int64Counter, error)
Int64UpDownCounter(name string, options ...Int64UpDownCounterOption) (Int64UpDownCounter, error)
Int64Histogram(name string, options ...Int64HistogramOption) (Int64Histogram, error)
Int64Gauge(name string, options ...Int64GaugeOption) (Int64Gauge, error)
// Float64 synchronous instruments
Float64Counter(name string, options ...Float64CounterOption) (Float64Counter, error)
Float64UpDownCounter(name string, options ...Float64UpDownCounterOption) (Float64UpDownCounter, error)
Float64Histogram(name string, options ...Float64HistogramOption) (Float64Histogram, error)
Float64Gauge(name string, options ...Float64GaugeOption) (Float64Gauge, error)
// Int64 asynchronous instruments
Int64ObservableCounter(name string, options ...Int64ObservableCounterOption) (Int64ObservableCounter, error)
Int64ObservableUpDownCounter(name string, options ...Int64ObservableUpDownCounterOption) (Int64ObservableUpDownCounter, error)
Int64ObservableGauge(name string, options ...Int64ObservableGaugeOption) (Int64ObservableGauge, error)
// Float64 asynchronous instruments
Float64ObservableCounter(name string, options ...Float64ObservableCounterOption) (Float64ObservableCounter, error)
Float64ObservableUpDownCounter(name string, options ...Float64ObservableUpDownCounterOption) (Float64ObservableUpDownCounter, error)
Float64ObservableGauge(name string, options ...Float64ObservableGaugeOption) (Float64ObservableGauge, error)
// RegisterCallback registers a callback that will be called during collection.
RegisterCallback(callback Callback, instruments ...Observable) (Registration, error)
}Records non-decreasing values (always increases).
type Int64Counter interface {
// Add records a change to the counter.
Add(ctx context.Context, incr int64, options ...AddOption)
}Example:
// Create counter
counter, err := meter.Int64Counter(
"api.requests",
metric.WithDescription("Total API requests"),
metric.WithUnit("{request}"),
)
if err != nil {
panic(err)
}
// Record measurement
counter.Add(ctx, 1, metric.WithAttributes(
attribute.String("method", "POST"),
attribute.String("endpoint", "/api/users"),
attribute.Int("status", 200),
))type Float64Counter interface {
// Add records a change to the counter.
Add(ctx context.Context, incr float64, options ...AddOption)
}Example:
// Create counter for bytes
bytesCounter, err := meter.Float64Counter(
"bytes.transferred",
metric.WithDescription("Bytes transferred"),
metric.WithUnit("By"),
)
// Record bytes
bytesCounter.Add(ctx, 1024.5, metric.WithAttributes(
attribute.String("direction", "upload"),
))Records values that can increase or decrease.
type Int64UpDownCounter interface {
// Add records a change to the counter (can be positive or negative).
Add(ctx context.Context, incr int64, options ...AddOption)
}Example:
// Create updown counter for active connections
activeConns, err := meter.Int64UpDownCounter(
"connections.active",
metric.WithDescription("Active connections"),
metric.WithUnit("{connection}"),
)
// Increment when connection opens
activeConns.Add(ctx, 1)
// Decrement when connection closes
activeConns.Add(ctx, -1)type Float64UpDownCounter interface {
// Add records a change to the counter (can be positive or negative).
Add(ctx context.Context, incr float64, options ...AddOption)
}Records distribution of values.
type Int64Histogram interface {
// Record adds an additional value to the distribution.
Record(ctx context.Context, value int64, options ...RecordOption)
}Example:
// Create histogram for request duration
requestDuration, err := meter.Int64Histogram(
"request.duration",
metric.WithDescription("Request duration in milliseconds"),
metric.WithUnit("ms"),
metric.WithExplicitBucketBoundaries(10, 50, 100, 250, 500, 1000, 2500, 5000),
)
// Record duration
start := time.Now()
// ... handle request ...
duration := time.Since(start).Milliseconds()
requestDuration.Record(ctx, duration, metric.WithAttributes(
attribute.String("endpoint", "/api/users"),
))type Float64Histogram interface {
// Record adds an additional value to the distribution.
Record(ctx context.Context, value float64, options ...RecordOption)
}Example:
// Create histogram for response times in seconds
responseTime, err := meter.Float64Histogram(
"http.server.duration",
metric.WithDescription("HTTP server response time"),
metric.WithUnit("s"),
metric.WithExplicitBucketBoundaries(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
)
// Record response time
start := time.Now()
// ... handle request ...
duration := time.Since(start).Seconds()
responseTime.Record(ctx, duration)Records instantaneous values (introduced in newer versions).
type Int64Gauge interface {
// Record records the instantaneous value.
Record(ctx context.Context, value int64, options ...RecordOption)
}Example:
// Create gauge for queue size
queueSize, err := meter.Int64Gauge(
"queue.size",
metric.WithDescription("Current queue size"),
metric.WithUnit("{item}"),
)
// Record current size
queueSize.Record(ctx, currentQueueSize)type Float64Gauge interface {
// Record records the instantaneous value.
Record(ctx context.Context, value float64, options ...RecordOption)
}Records non-decreasing cumulative values via callback.
type Int64ObservableCounter interface {
Int64Observable
}Example:
// Create observable counter with callback
uptime, err := meter.Int64ObservableCounter(
"process.uptime",
metric.WithDescription("Process uptime in seconds"),
metric.WithUnit("s"),
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
uptimeSeconds := int64(time.Since(startTime).Seconds())
o.Observe(uptimeSeconds)
return nil
}),
)type Float64ObservableCounter interface {
Float64Observable
}Records cumulative values that can increase or decrease via callback.
type Int64ObservableUpDownCounter interface {
Int64Observable
}Example:
import "runtime"
// Create observable updown counter for heap memory
heapMemory, err := meter.Int64ObservableUpDownCounter(
"process.runtime.go.mem.heap_alloc",
metric.WithDescription("Bytes of allocated heap objects"),
metric.WithUnit("By"),
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
var m runtime.MemStats
runtime.ReadMemStats(&m)
o.Observe(int64(m.HeapAlloc))
return nil
}),
)type Float64ObservableUpDownCounter interface {
Float64Observable
}Records instantaneous values via callback.
type Int64ObservableGauge interface {
Int64Observable
}Example:
// Create observable gauge for CPU usage
cpuUsage, err := meter.Int64ObservableGauge(
"system.cpu.utilization",
metric.WithDescription("CPU utilization percentage"),
metric.WithUnit("%"),
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
usage := getCPUUsage() // Your function to get CPU usage
o.Observe(int64(usage))
return nil
}),
)type Float64ObservableGauge interface {
Float64Observable
}Example:
// Create observable gauge for temperature
temperature, err := meter.Float64ObservableGauge(
"system.cpu.temperature",
metric.WithDescription("CPU temperature"),
metric.WithUnit("Cel"),
metric.WithFloat64Callback(func(_ context.Context, o metric.Float64Observer) error {
temp := getTemperature() // Your function to get temperature
o.Observe(temp)
return nil
}),
)// Register callback during instrument creation
gauge, err := meter.Int64ObservableGauge(
"memory.usage",
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
usage := getMemoryUsage()
o.Observe(usage)
return nil
}),
)import "runtime"
// Create instruments
heapAlloc, _ := meter.Int64ObservableUpDownCounter("heap.alloc")
gcCount, _ := meter.Int64ObservableCounter("gc.count")
// Register callback for multiple instruments
registration, err := meter.RegisterCallback(
func(_ context.Context, o metric.Observer) error {
var m runtime.MemStats
runtime.ReadMemStats(&m)
o.ObserveInt64(heapAlloc, int64(m.HeapAlloc))
o.ObserveInt64(gcCount, int64(m.NumGC))
return nil
},
heapAlloc,
gcCount,
)
// Unregister when no longer needed
defer registration.Unregister()// Generic callback for multiple instruments
type Callback func(context.Context, Observer) error
// Int64-specific callback
type Int64Callback func(context.Context, Int64Observer) error
// Float64-specific callback
type Float64Callback func(context.Context, Float64Observer) error// Generic observer
type Observer interface {
ObserveInt64(instrument Int64Observable, value int64, options ...ObserveOption)
ObserveFloat64(instrument Float64Observable, value float64, options ...ObserveOption)
}
// Int64-specific observer
type Int64Observer interface {
Observe(value int64, options ...ObserveOption)
}
// Float64-specific observer
type Float64Observer interface {
Observe(value float64, options ...ObserveOption)
}// InstrumentOption applies to all instruments
type InstrumentOption interface {
// Implementation specific
}
// Available instrument options
func WithDescription(desc string) InstrumentOption
func WithUnit(u string) InstrumentOption// For synchronous Add operations
type AddOption interface {
// Implementation specific
}
// For synchronous Record operations
type RecordOption interface {
// Implementation specific
}
// For asynchronous Observe operations
type ObserveOption interface {
// Implementation specific
}// Add attributes to measurements
func WithAttributes(attributes ...attribute.KeyValue) MeasurementOption
// Use pre-computed attribute set (more efficient)
func WithAttributeSet(set attribute.Set) MeasurementOptionExample:
// Using WithAttributes
counter.Add(ctx, 1, metric.WithAttributes(
attribute.String("method", "GET"),
attribute.String("path", "/api/users"),
attribute.Int("status", 200),
))
// Using WithAttributeSet (more efficient for repeated use)
attrs := attribute.NewSet(
attribute.String("method", "GET"),
attribute.String("path", "/api/users"),
)
counter.Add(ctx, 1, metric.WithAttributeSet(attrs))// Configure histogram bucket boundaries
func WithExplicitBucketBoundaries(bounds ...float64) HistogramOptionExample:
histogram, err := meter.Float64Histogram(
"http.server.duration",
metric.WithUnit("s"),
metric.WithExplicitBucketBoundaries(
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
),
)type Registration interface {
// Unregister removes the callback registration.
Unregister() error
}Example:
// Register callback
reg, err := meter.RegisterCallback(callback, instrument1, instrument2)
if err != nil {
panic(err)
}
// Unregister when done
defer reg.Unregister()import (
"net/http"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
)
type MetricsMiddleware struct {
requestCounter metric.Int64Counter
requestDuration metric.Float64Histogram
activeRequests metric.Int64UpDownCounter
}
func NewMetricsMiddleware() (*MetricsMiddleware, error) {
meter := otel.Meter("http-server")
requestCounter, err := meter.Int64Counter(
"http.server.request.count",
metric.WithDescription("Total HTTP requests"),
metric.WithUnit("{request}"),
)
if err != nil {
return nil, err
}
requestDuration, err := meter.Float64Histogram(
"http.server.duration",
metric.WithDescription("HTTP request duration"),
metric.WithUnit("s"),
metric.WithExplicitBucketBoundaries(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0),
)
if err != nil {
return nil, err
}
activeRequests, err := meter.Int64UpDownCounter(
"http.server.active_requests",
metric.WithDescription("Active HTTP requests"),
metric.WithUnit("{request}"),
)
if err != nil {
return nil, err
}
return &MetricsMiddleware{
requestCounter: requestCounter,
requestDuration: requestDuration,
activeRequests: activeRequests,
}, nil
}
func (m *MetricsMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
start := time.Now()
// Track active requests
m.activeRequests.Add(ctx, 1)
defer m.activeRequests.Add(ctx, -1)
// Create response writer wrapper to capture status code
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
// Handle request
next.ServeHTTP(rw, r)
// Record metrics
duration := time.Since(start).Seconds()
attrs := []attribute.KeyValue{
semconv.HTTPMethod(r.Method),
semconv.HTTPRoute(r.URL.Path),
semconv.HTTPStatusCode(rw.statusCode),
}
m.requestCounter.Add(ctx, 1, metric.WithAttributes(attrs...))
m.requestDuration.Record(ctx, duration, metric.WithAttributes(attrs...))
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}import (
"database/sql"
"time"
)
type DatabaseMetrics struct {
queryDuration metric.Float64Histogram
queryCounter metric.Int64Counter
activeConns metric.Int64ObservableUpDownCounter
maxOpenConns metric.Int64ObservableGauge
}
func SetupDatabaseMetrics(db *sql.DB) (*DatabaseMetrics, error) {
meter := otel.Meter("database")
queryDuration, err := meter.Float64Histogram(
"db.client.operation.duration",
metric.WithDescription("Database operation duration"),
metric.WithUnit("s"),
)
if err != nil {
return nil, err
}
queryCounter, err := meter.Int64Counter(
"db.client.operation.count",
metric.WithDescription("Database operations"),
metric.WithUnit("{operation}"),
)
if err != nil {
return nil, err
}
activeConns, err := meter.Int64ObservableUpDownCounter(
"db.client.connections.usage",
metric.WithDescription("Active database connections"),
metric.WithUnit("{connection}"),
)
if err != nil {
return nil, err
}
maxOpenConns, err := meter.Int64ObservableGauge(
"db.client.connections.max",
metric.WithDescription("Maximum open connections"),
metric.WithUnit("{connection}"),
)
if err != nil {
return nil, err
}
// Register callback for connection metrics
_, err = meter.RegisterCallback(
func(_ context.Context, o metric.Observer) error {
stats := db.Stats()
o.ObserveInt64(activeConns, int64(stats.InUse))
o.ObserveInt64(maxOpenConns, int64(stats.MaxOpenConnections))
return nil
},
activeConns,
maxOpenConns,
)
if err != nil {
return nil, err
}
return &DatabaseMetrics{
queryDuration: queryDuration,
queryCounter: queryCounter,
}, nil
}
func (m *DatabaseMetrics) RecordQuery(ctx context.Context, operation string, duration time.Duration, err error) {
attrs := []attribute.KeyValue{
attribute.String("db.operation", operation),
}
if err != nil {
attrs = append(attrs, attribute.Bool("error", true))
}
m.queryCounter.Add(ctx, 1, metric.WithAttributes(attrs...))
m.queryDuration.Record(ctx, duration.Seconds(), metric.WithAttributes(attrs...))
}import "runtime"
func SetupRuntimeMetrics() error {
meter := otel.Meter("runtime")
// Memory metrics
_, err := meter.Int64ObservableGauge(
"runtime.go.mem.heap_alloc",
metric.WithDescription("Bytes of allocated heap objects"),
metric.WithUnit("By"),
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
var m runtime.MemStats
runtime.ReadMemStats(&m)
o.Observe(int64(m.HeapAlloc))
return nil
}),
)
if err != nil {
return err
}
// Goroutine count
_, err = meter.Int64ObservableGauge(
"runtime.go.goroutines",
metric.WithDescription("Number of goroutines"),
metric.WithUnit("{goroutine}"),
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
o.Observe(int64(runtime.NumGoroutine()))
return nil
}),
)
if err != nil {
return err
}
// GC count
_, err = meter.Int64ObservableCounter(
"runtime.go.gc.count",
metric.WithDescription("Number of GC cycles"),
metric.WithUnit("{count}"),
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
var m runtime.MemStats
runtime.ReadMemStats(&m)
o.Observe(int64(m.NumGC))
return nil
}),
)
return err
}// Counter - for values that only increase
requestCount := meter.Int64Counter("requests.count")
// UpDownCounter - for values that increase and decrease
activeConnections := meter.Int64UpDownCounter("connections.active")
// Histogram - for measuring distributions
requestDuration := meter.Float64Histogram("request.duration")
// Gauge - for point-in-time measurements
queueSize := meter.Int64Gauge("queue.size")
// ObservableGauge - for async point-in-time measurements
cpuUsage := meter.Float64ObservableGauge("cpu.usage")import semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
counter.Add(ctx, 1, metric.WithAttributes(
semconv.HTTPMethod("GET"),
semconv.HTTPRoute("/api/users/:id"),
semconv.HTTPStatusCode(200),
))// Pre-compute attribute sets for better performance
commonAttrs := attribute.NewSet(
attribute.String("service", "api"),
attribute.String("version", "1.0.0"),
)
// Reuse in measurements
counter.Add(ctx, 1, metric.WithAttributeSet(commonAttrs))counter, err := meter.Int64Counter("my.counter")
if err != nil {
// Log error but don't fail - metrics are not critical path
log.Printf("Failed to create counter: %v", err)
// Use a no-op counter or handle gracefully
}// Good
duration, _ := meter.Float64Histogram(
"http.server.duration",
metric.WithDescription("HTTP server response time"),
metric.WithUnit("s"),
)
// Bad
duration, _ := meter.Float64Histogram("duration")// Configure boundaries appropriate for your use case
histogram, _ := meter.Float64Histogram(
"request.duration",
metric.WithUnit("s"),
metric.WithExplicitBucketBoundaries(
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
),
)reg, _ := meter.RegisterCallback(callback, instruments...)
defer reg.Unregister()