or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

attributes.mdbridges.mdcontext-propagation.mdexporters-other.mdexporters-otlp.mdindex.mdlogging.mdmetrics.mdsdk-log.mdsdk-metric.mdsdk-resource.mdsdk-trace.mdsemantic-conventions.mdtracing.md
tile.json

metrics.mddocs/

Metrics Collection API

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.

Package Import

import "go.opentelemetry.io/otel/metric"

Overview

The metrics API is organized around three main concepts:

  1. MeterProvider: Factory for creating Meters
  2. Meter: Creates instruments for a specific instrumentation scope
  3. Instruments: Record measurements (synchronous or asynchronous)

Instrument Categories

Synchronous Instruments (record during execution):

  • Counter (Int64Counter, Float64Counter)
  • UpDownCounter (Int64UpDownCounter, Float64UpDownCounter)
  • Histogram (Int64Histogram, Float64Histogram)
  • Gauge (Int64Gauge, Float64Gauge)

Asynchronous Instruments (observe via callback):

  • ObservableCounter (Int64ObservableCounter, Float64ObservableCounter)
  • ObservableUpDownCounter (Int64ObservableUpDownCounter, Float64ObservableUpDownCounter)
  • ObservableGauge (Int64ObservableGauge, Float64ObservableGauge)

Quick Start

Basic Example

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"),
	))
}

Core Interfaces

MeterProvider

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
}

Creating a MeterProvider

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()
}

MeterOption

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) MeterOption

Example:

meter := provider.Meter(
	"github.com/myorg/myapp",
	metric.WithInstrumentationVersion("1.0.0"),
	metric.WithSchemaURL("https://opentelemetry.io/schemas/1.20.0"),
)

Meter

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)
}

Synchronous Instruments

Counter

Records non-decreasing values (always increases).

Int64Counter

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),
))

Float64Counter

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"),
))

UpDownCounter

Records values that can increase or decrease.

Int64UpDownCounter

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)

Float64UpDownCounter

type Float64UpDownCounter interface {
	// Add records a change to the counter (can be positive or negative).
	Add(ctx context.Context, incr float64, options ...AddOption)
}

Histogram

Records distribution of values.

Int64Histogram

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"),
))

Float64Histogram

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)

Gauge

Records instantaneous values (introduced in newer versions).

Int64Gauge

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)

Float64Gauge

type Float64Gauge interface {
	// Record records the instantaneous value.
	Record(ctx context.Context, value float64, options ...RecordOption)
}

Asynchronous Instruments

ObservableCounter

Records non-decreasing cumulative values via callback.

Int64ObservableCounter

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
	}),
)

Float64ObservableCounter

type Float64ObservableCounter interface {
	Float64Observable
}

ObservableUpDownCounter

Records cumulative values that can increase or decrease via callback.

Int64ObservableUpDownCounter

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
	}),
)

Float64ObservableUpDownCounter

type Float64ObservableUpDownCounter interface {
	Float64Observable
}

ObservableGauge

Records instantaneous values via callback.

Int64ObservableGauge

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
	}),
)

Float64ObservableGauge

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
	}),
)

Callbacks

Registering Callbacks

Single Instrument Callback

// 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
	}),
)

Multi-Instrument Callback

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()

Callback Types

// 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

Observer Interfaces

// 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)
}

Measurement Options

Common Options

// InstrumentOption applies to all instruments
type InstrumentOption interface {
	// Implementation specific
}

// Available instrument options
func WithDescription(desc string) InstrumentOption
func WithUnit(u string) InstrumentOption

Measurement Options

// 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
}

Attribute Options

// Add attributes to measurements
func WithAttributes(attributes ...attribute.KeyValue) MeasurementOption

// Use pre-computed attribute set (more efficient)
func WithAttributeSet(set attribute.Set) MeasurementOption

Example:

// 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))

Histogram Options

// Configure histogram bucket boundaries
func WithExplicitBucketBoundaries(bounds ...float64) HistogramOption

Example:

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,
	),
)

Registration

Registration Interface

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()

Complete Usage Examples

HTTP Server Metrics

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)
}

Database Metrics

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...))
}

Runtime Metrics

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
}

Best Practices

1. Choose the Right Instrument Type

// 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")

2. Use Semantic Conventions

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),
))

3. Reuse Attribute Sets

// 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))

4. Handle Errors Appropriately

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
}

5. Use Descriptive Names and Units

// Good
duration, _ := meter.Float64Histogram(
	"http.server.duration",
	metric.WithDescription("HTTP server response time"),
	metric.WithUnit("s"),
)

// Bad
duration, _ := meter.Float64Histogram("duration")

6. Configure Histogram Boundaries

// 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,
	),
)

7. Unregister Callbacks When Done

reg, _ := meter.RegisterCallback(callback, instruments...)
defer reg.Unregister()

Related Documentation

  • SDK Metric: SDK implementation with aggregation and export
  • Exporters: Sending metrics to backends
  • Attributes: Working with attributes
  • Semantic Conventions: Standard metric names