OpenTelemetry Metrics: Best Practices for 2025
Guide

OpenTelemetry Metrics: Best Practices for 2025

Gamunu Balagalla·Nov 28, 2024·10 min read

Why OpenTelemetry in 2025?

OpenTelemetry has crossed the chasm. It's no longer the "promising new standard" — it's the default choice for application instrumentation. The metrics specification reached GA stability in late 2024, and the ecosystem has matured significantly.

If you're starting a new project or re-evaluating your instrumentation strategy, OTel metrics should be your starting point.

The Three Pillars, Unified

OpenTelemetry's value proposition is unifying traces, metrics, and logs under a single SDK. For metrics specifically, this means:

  • Consistent naming across all your telemetry
  • Automatic correlation between metrics and traces
  • Vendor-neutral instrumentation — switch backends without changing application code
  • Rich metadata through resource attributes and baggage propagation

Instrument Types

OTel provides several metric instrument types. Choosing the right one matters:

Counter

For values that only go up. HTTP requests served, errors encountered, bytes processed.

from opentelemetry import metrics

meter = metrics.get_meter("my-service")
request_counter = meter.create_counter(
    "http.server.requests",
    unit="1",
    description="Total HTTP requests served"
)

# In your request handler
request_counter.add(1, {"method": "GET", "route": "/api/users", "status": "200"})

Histogram

For measuring distributions. Request latency, response size, queue depth.

latency_histogram = meter.create_histogram(
    "http.server.duration",
    unit="ms",
    description="HTTP request latency"
)

# In your request handler
start = time.monotonic()
# ... handle request ...
duration = (time.monotonic() - start) * 1000
latency_histogram.record(duration, {"method": "GET", "route": "/api/users"})

UpDownCounter

For values that can increase or decrease. Active connections, items in a queue.

active_connections = meter.create_up_down_counter(
    "http.server.active_connections",
    unit="1",
    description="Currently active HTTP connections"
)

active_connections.add(1)   # connection opened
active_connections.add(-1)  # connection closed

Observable Gauge

For values you want to observe periodically. CPU temperature, memory usage, pool size.

def get_cpu_usage(options):
    options.observe(psutil.cpu_percent(), {})

meter.create_observable_gauge(
    "system.cpu.utilization",
    callbacks=[get_cpu_usage]
)

Best Practices

1. Use Semantic Conventions

OTel defines standard attribute names for common concepts. Use them:

  • http.request.method instead of method or http_method
  • server.address instead of host or hostname
  • http.response.status_code instead of status or code

This ensures your metrics are compatible with OTel-aware tooling and dashboards.

2. Control Cardinality at the Source

Don't add attributes with unbounded values:

# BAD — user_id has millions of possible values
request_counter.add(1, {"user_id": user.id})

# GOOD — bounded cardinality
request_counter.add(1, {"user_tier": user.tier})  # "free", "pro", "enterprise"

3. Use Views to Manage What Gets Exported

OTel Views let you control aggregation and attribute filtering at the SDK level:

from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.view import View, ExplicitBucketHistogramAggregation

view = View(
    instrument_name="http.server.duration",
    aggregation=ExplicitBucketHistogramAggregation(
        boundaries=[5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]
    ),
    attribute_keys=["method", "route", "status"]  # only keep these attributes
)

provider = MeterProvider(views=[view])

4. Export to xScaler Labs via OTLP

xScaler Labs natively supports the OTLP gRPC and HTTP protocols:

from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader

exporter = OTLPMetricExporter(endpoint="https://euw1-01.m.xscalerlabs.com/api/v1/push")
reader = PeriodicExportingMetricReader(exporter, export_interval_millis=60000)
provider = MeterProvider(metric_readers=[reader], views=[view])

Common Mistakes

  1. Over-instrumenting — Not every function needs a metric. Focus on business-critical paths and system boundaries.
  2. Ignoring resource attributes — Always set service.name, service.version, and deployment.environment.
  3. Using synchronous exporters — Always use the async/batching exporter in production.
  4. Forgetting to set units — Always specify units. It prevents dashboard confusion later.

What's Coming in 2025

  • Exemplars GA — Link metrics to specific trace spans for deeper debugging
  • Profiles signal — Continuous profiling as a first-class OTel signal
  • Improved auto-instrumentation — Less code, more coverage