Saltar al contenido

Observabilidad más allá del monitoreo: trazas, logs y métricas

Cómo construir un stack de observabilidad que conecta trazas distribuidas, logs estructurados y métricas para depurar producción más rápido.

5 min de lectura
Diagrama de los tres pilares de la observabilidad que conecta trazas, logs y métricas en una vista unificada

El monitoreo te dice que algo está mal. La observabilidad te dice por qué. Esa diferencia importa cuando estás depurando un incidente de producción a las 2 de la madrugada. Si tus dashboards muestran una tasa de errores elevada pero no puedes rastrear una sola solicitud fallida a través de tu sistema, tienes monitoreo sin observabilidad.

Los tres pilares —trazas, logs y métricas— no son útiles de forma aislada. Su verdadero valor está en la correlación: vincular un pico en una métrica con las trazas que lo causaron, y esas trazas con las líneas de log que explican la causa raíz.

Métricas: el punto de partida

Las métricas son números agregados a lo largo del tiempo. Te dicen qué está pasando a nivel de sistema, pero no por qué.

tstypescript
// ❌ Only tracking high-level metrics
const requestCount = new Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
});
 
app.use((req, res, next) => {
  requestCount.inc();
  next();
});
// You know requests increased, but not which endpoints or status codes
tstypescript
// ✅ Metrics with dimensions for drill-down
import { Counter, Histogram } from 'prom-client';
 
const requestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration in seconds',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
});
 
const requestErrors = new Counter({
  name: 'http_request_errors_total',
  help: 'HTTP request errors',
  labelNames: ['method', 'route', 'error_type'],
});
 
app.use((req, res, next) => {
  const start = process.hrtime.bigint();
 
  res.on('finish', () => {
    const duration = Number(process.hrtime.bigint() - start) / 1e9;
    const route = req.route?.path ?? 'unknown';
 
    requestDuration
      .labels(req.method, route, String(res.statusCode))
      .observe(duration);
 
    if (res.statusCode >= 400) {
      requestErrors
        .labels(req.method, route, String(res.statusCode))
        .inc();
    }
  });
 
  next();
});

Con métricas etiquetadas puedes responder preguntas como "¿qué endpoint está lento?" o "¿qué códigos de error están aumentando?" directamente desde los datos de la métrica. Usa histogramas para la latencia (te dan percentiles), contadores para totales y gauges para valores actuales como la profundidad de la cola.

Logging estructurado

Los logs sin estructura no se pueden consultar a escala. Los logs estructurados son buscables, filtrables y se pueden correlacionar con las trazas.

tstypescript
// ❌ Unstructured log lines
console.log(`User ${userId} created order ${orderId} - total: $${total}`);
console.log(`ERROR: Payment failed for order ${orderId}`);
// Good luck searching for "all payment failures over $100 in the last hour"
tstypescript
// ✅ Structured JSON logs with consistent fields
import pino from 'pino';
 
const logger = pino({
  level: process.env.LOG_LEVEL ?? 'info',
  formatters: {
    level: (label) => ({ level: label }),
  },
  timestamp: pino.stdTimeFunctions.isoTime,
});
 
// Application code
logger.info({
  event: 'order_created',
  userId,
  orderId,
  total,
  currency: 'USD',
  itemCount: items.length,
});
 
logger.error({
  event: 'payment_failed',
  orderId,
  userId,
  amount: total,
  errorCode: 'card_declined',
  provider: 'stripe',
  traceId: req.headers['x-trace-id'],
});

Los logs estructurados tienen dos ventajas: puedes consultarlos (event=payment_failed AND amount>100) y puedes incluir IDs de traza que vinculan cada línea de log con las trazas distribuidas.

Tracing distribuido

Una traza sigue una única solicitud a través de cada servicio que toca. Cada segmento es un span. Los spans forman un árbol que muestra en qué se fue el tiempo.

tstypescript
import { trace, SpanStatusCode } from '@opentelemetry/api';
 
const tracer = trace.getTracer('order-service');
 
async function createOrder(userId: string, items: CartItem[]) {
  return tracer.startActiveSpan('createOrder', async (span) => {
    try {
      span.setAttribute('user.id', userId);
      span.setAttribute('order.item_count', items.length);
 
      // Child span: validate inventory
      const inventory = await tracer.startActiveSpan(
        'validateInventory',
        async (childSpan) => {
          try {
            const result = await inventoryService.check(items);
            childSpan.setAttribute('inventory.available', result.allAvailable);
            return result;
          } finally {
            childSpan.end();
          }
        }
      );
 
      if (!inventory.allAvailable) {
        span.setStatus({
          code: SpanStatusCode.ERROR,
          message: 'Items out of stock',
        });
        throw new Error('Items out of stock');
      }
 
      // Child span: process payment
      const payment = await tracer.startActiveSpan(
        'processPayment',
        async (childSpan) => {
          try {
            const result = await paymentService.charge(userId, total);
            childSpan.setAttribute('payment.provider', 'stripe');
            childSpan.setAttribute('payment.amount', total);
            return result;
          } finally {
            childSpan.end();
          }
        }
      );
 
      span.setAttribute('order.id', payment.orderId);
      return { orderId: payment.orderId, status: 'confirmed' };
    } catch (err) {
      span.recordException(err as Error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      throw err;
    } finally {
      span.end();
    }
  });
}

La traza que genera este código muestra: createOrder (350ms) → validateInventory (50ms) + processPayment (280ms). Si el pago es lento, ves exactamente dónde está el cuello de botella y puedes profundizar en los spans del servicio de pagos.

Correlacionar los tres pilares

El verdadero valor aparece cuando vinculas métricas, logs y trazas entre sí. Una alerta de métrica te lleva a las trazas, y esas trazas te llevan a líneas de log específicas.

tstypescript
// Middleware that connects all three pillars
import { trace, context } from '@opentelemetry/api';
 
function observabilityMiddleware(req: Request, res: Response, next: NextFunction) {
  const span = trace.getActiveSpan();
  const traceId = span?.spanContext().traceId ?? 'no-trace';
 
  // Attach trace ID to logger — all log lines include it
  req.log = logger.child({ traceId, requestId: req.id });
 
  // Attach trace ID to response headers — clients can report it
  res.setHeader('X-Trace-Id', traceId);
 
  const start = process.hrtime.bigint();
 
  res.on('finish', () => {
    const duration = Number(process.hrtime.bigint() - start) / 1e9;
 
    // Metric with trace exemplar
    requestDuration
      .labels(req.method, req.route?.path ?? 'unknown', String(res.statusCode))
      .observe(duration);
 
    // Structured log with trace correlation
    req.log.info({
      event: 'request_completed',
      method: req.method,
      path: req.path,
      statusCode: res.statusCode,
      duration,
    });
  });
 
  next();
}

El flujo de correlación:

  1. Alerta de métrica: la latencia P99 de /api/orders supera los 2s
  2. Búsqueda de trazas: se buscan trazas hacia /api/orders con una duración > 2s en los últimos 10 minutos
  3. Análisis de spans: el span processPayment está consistentemente en 1.8s (normalmente 200ms)
  4. Profundización en logs: se filtran los logs por traceId y aparecen errores payment_timeout desde el endpoint europeo de Stripe

Alertar sobre las señales correctas

Genera alertas sobre síntomas (el impacto que percibe el usuario), no sobre causas (el uso de CPU). Usa el método RED para servicios y el método USE para recursos.

ymlyaml
# ❌ Alerting on causes — noisy and often irrelevant
groups:
  - name: infrastructure
    rules:
      - alert: HighCPU
        expr: cpu_usage > 80
        # CPU at 81% with no user impact → false alarm at 3am
 
# ✅ Alerting on symptoms — user-facing impact
groups:
  - name: service-health
    rules:
      - alert: HighErrorRate
        expr: |
          sum(rate(http_request_errors_total{status_code=~"5.."}[5m]))
          /
          sum(rate(http_requests_total[5m])) > 0.01
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Error rate exceeds 1% for 5 minutes"
 
      - alert: HighLatency
        expr: |
          histogram_quantile(0.99,
            rate(http_request_duration_seconds_bucket[5m])
          ) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "P99 latency exceeds 2s for 5 minutes"

La cláusula for: 5m evita que se generen alertas por picos transitorios. Una sola solicitud lenta es ruido. Cinco minutos de latencia elevada sí es un problema real.

Conclusiones clave

  1. Las métricas muestran qué está pasando — usa histogramas y contadores etiquetados con dimensiones para poder profundizar
  2. Los logs estructurados explican por qué — usa logging en JSON con nombres de campo consistentes e IDs de traza
  3. Las trazas muestran en qué se va el tiempo — instrumenta los límites entre servicios y las operaciones costosas como spans
  4. La correlación es el multiplicador — vincula métricas → trazas → logs mediante IDs de traza propagados entre servicios
  5. Alerta sobre síntomas, no sobre causas — las tasas de error y los percentiles de latencia importan más que el uso de CPU
  6. Usa la cláusula for — exige violaciones sostenidas antes de alertar para evitar ruido por picos transitorios
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX