OpenTelemetry Go implementation providing APIs for distributed tracing, metrics, and logging to instrument applications and send telemetry data to observability platforms
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()Install with Tessl CLI
npx tessl i tessl/golang-go-opentelemetry-io--otel