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.methodinstead ofmethodorhttp_methodserver.addressinstead ofhostorhostnamehttp.response.status_codeinstead ofstatusorcode
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
- Over-instrumenting — Not every function needs a metric. Focus on business-critical paths and system boundaries.
- Ignoring resource attributes — Always set service.name, service.version, and deployment.environment.
- Using synchronous exporters — Always use the async/batching exporter in production.
- 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
