Cap 4: Metrics

Por: Artiko
opentelemetrymetricscounterhistogramgaugeprometheus

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íncronoAsíncrono equivalente
CounterObservableCounter
UpDownCounterObservableUpDownCounter
GaugeObservableGauge
# 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
TemporalidadCuándo usar
DELTAPrometheus, Datadog — calculan rates en el backend
CUMULATIVESistemas 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)