Cap 4: Metrics
El modelo de métricas de OTel
OTel desacopla cómo mides (instrumento) de cómo reportas (aggregation + temporality). Esto permite usar la misma instrumentación con Prometheus, Datadog o cualquier backend.
flowchart LR
I[Instrumento\nCounter/Gauge/Histogram] -->|registra medición| SDK
SDK -->|agrega| AGG[Aggregation\nSum/LastValue/ExplicitBuckets]
AGG -->|con temporalidad| EXP[Exporter]
EXP -->|formato del backend| B[Prometheus / OTLP / Datadog]
Tipos de instrumentos
Counter — conteo monotónico
Cuenta eventos que solo aumentan. Nunca decrece (si necesitas decremento, usa UpDownCounter).
from opentelemetry import metrics
meter = metrics.get_meter("order-service", "1.0.0")
# Crear el instrumento una vez
requests_counter = meter.create_counter(
name="http.server.request.count",
description="Número de requests HTTP recibidas",
unit="1",
)
# Usar en cada request
def handle_request(method: str, route: str, status: int):
requests_counter.add(1, {
"http.request.method": method,
"http.route": route,
"http.response.status_code": status,
})
Casos de uso: requests totales, errores totales, bytes enviados, eventos procesados.
UpDownCounter — conteo no monotónico
Puede subir o bajar. Para cantidades que fluctúan.
active_connections = meter.create_up_down_counter(
name="db.client.connection.count",
description="Conexiones activas al pool",
unit="1",
)
def on_connection_open():
active_connections.add(1, {"db.system": "postgresql"})
def on_connection_close():
active_connections.add(-1, {"db.system": "postgresql"})
Casos de uso: conexiones activas, items en cola, sesiones concurrentes.
Histogram — distribución de valores
Mide la distribución de valores numéricos. Ideal para latencias y tamaños.
import time
request_duration = meter.create_histogram(
name="http.server.request.duration",
description="Duración de requests HTTP",
unit="s",
)
def handle_request(handler):
start = time.perf_counter()
try:
response = handler()
status = response.status_code
except Exception:
status = 500
raise
finally:
duration = time.perf_counter() - start
request_duration.record(duration, {
"http.request.method": request.method,
"http.route": request.route,
"http.response.status_code": status,
})
Casos de uso: latencia de requests, tamaño de payloads, duración de jobs.
Los histogramas permiten calcular percentiles (p50, p95, p99) en el backend.
Gauge — valor instantáneo
Mide un valor en un punto en el tiempo — no suma ni cuenta.
# Gauge asíncrono (callback) — más común para recursos del sistema
import psutil
cpu_usage = meter.create_observable_gauge(
name="system.cpu.utilization",
description="Uso de CPU",
unit="1", # 0 a 1
)
def observe_cpu(options):
yield metrics.Observation(
psutil.cpu_percent() / 100,
{"cpu.state": "user"},
)
cpu_usage.set_callback(observe_cpu)
Casos de uso: uso de CPU/memoria, temperatura, nivel de batería, número de workers activos.
Instrumentos asíncronos (Observable)
Para valores que es más eficiente observar periódicamente que registrar en cada operación:
| Síncrono | Asíncrono equivalente |
|---|---|
Counter | ObservableCounter |
UpDownCounter | ObservableUpDownCounter |
Gauge | ObservableGauge |
# ObservableCounter para métricas del sistema
net_bytes = meter.create_observable_counter(
name="system.network.io",
unit="By",
)
def observe_network(options):
stats = psutil.net_io_counters()
yield metrics.Observation(stats.bytes_sent, {"direction": "transmit"})
yield metrics.Observation(stats.bytes_recv, {"direction": "receive"})
net_bytes.set_callback(observe_network)
Temporalidad
La temporalidad define si las métricas reportan valores acumulados o deltas:
graph LR
subgraph Delta
D1[t=0: 0] --> D2[t=1: +5]
D2 --> D3[t=2: +3]
D3 --> D4[t=3: +8]
end
subgraph Cumulative
C1[t=0: 0] --> C2[t=1: 5]
C2 --> C3[t=2: 8]
C3 --> C4[t=3: 16]
end
| Temporalidad | Cuándo usar |
|---|---|
DELTA | Prometheus, Datadog — calculan rates en el backend |
CUMULATIVE | Sistemas que esperan contadores que solo crecen |
# Configurar temporalidad por exporter
from opentelemetry.sdk.metrics.export import AggregationTemporality
exporter = OTLPMetricExporter(
preferred_temporality={
Counter: AggregationTemporality.DELTA,
Histogram: AggregationTemporality.DELTA,
}
)
Cardinalidad
La cardinalidad es la cantidad de combinaciones únicas de atributos. Alta cardinalidad = problema de rendimiento.
# ❌ ALTA CARDINALIDAD — user_id tiene millones de valores
requests_counter.add(1, {
"user.id": user_id, # ← millones de series
"http.route": "/api/orders",
})
# ✅ BAJA CARDINALIDAD — atributos con pocos valores posibles
requests_counter.add(1, {
"http.request.method": "GET", # pocos valores
"http.route": "/api/orders", # rutas parametrizadas
"http.response.status_code": 200, # ~10 valores
})
Regla práctica: cada atributo con más de ~100 valores únicos es un riesgo de cardinalidad.
Views — personalizar aggregations
Las Views permiten cambiar cómo se agrega una métrica sin modificar la instrumentación:
from opentelemetry.sdk.metrics import MeterProvider, View
from opentelemetry.sdk.metrics.export import ExplicitBucketHistogramAggregation
# Cambiar los buckets del histogram para latencias HTTP
custom_view = View(
instrument_name="http.server.request.duration",
aggregation=ExplicitBucketHistogramAggregation(
boundaries=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
),
)
# Eliminar una métrica que no necesitas
drop_view = View(
instrument_name="internal.debug.*",
aggregation=DropAggregation(),
)
provider = MeterProvider(views=[custom_view, drop_view])
Nomenclatura de métricas
OTel recomienda una convención jerárquica con punto como separador:
{namespace}.{subject}.{measure}[.{unit}]
# Ejemplos:
http.server.request.duration → latencia de servidor HTTP
http.client.request.duration → latencia de cliente HTTP
db.client.operation.duration → latencia de operaciones DB
system.memory.usage → uso de memoria del sistema
process.runtime.jvm.memory.usage → memoria JVM
Export de métricas
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
exporter = OTLPMetricExporter(endpoint="http://localhost:4317")
reader = PeriodicExportingMetricReader(
exporter,
export_interval_millis=60_000, # exportar cada 60s
)
provider = MeterProvider(
resource=resource,
metric_readers=[reader],
)
metrics.set_meter_provider(provider)