Observabilidad Más Allá del Logging: Lo que Desearía Haber Sabido Antes
Resumen
La observabilidad no se trata de recopilar más datos — se trata de poder responder preguntas que no sabías que ibas a hacer cuando todo estaba bien. Estructura tus logs como JSON desde el primer día (te lo suplico), instrumenta con OpenTelemetry para trazado distribuido, rastrea métricas RED para servicios y métricas USE para infraestructura, y define SLOs antes de configurar alertas. Los tres pilares — logs, métricas, trazas — son inútiles a menos que estén correlacionados. Y por todo lo que es sagrado, cada alerta necesita un runbook.
Durante los primeros dos años de mi carrera, toda mi estrategia de observabilidad fue console.log. No estoy exagerando. Cuando algo se rompía en producción, hacía SSH al servidor, le hacía tail a los logs, y grepeaba la palabra "error." A veces me ponía elegante y grepeaba "ERROR" en mayúsculas, porque seguro los importantes estarían en mayúsculas. (No lo estaban.)
¡Funcionaba! Más o menos. De la misma manera que la cinta adhesiva en una tubería con fugas "funciona" — justo hasta que la presión del agua cambia y estás parado en un charco preguntándote dónde salió todo mal.
El sistema que finalmente me quebró fue un pipeline de inferencia de IA sirviendo predicciones en tiempo real. La latencia se disparaba aleatoriamente. Los usuarios reportaban resultados incorrectos de forma intermitente. Los logs decían que todo estaba bien. Tenía monitoreo que verificaba "¿está el servidor arriba?" y siempre lo estaba. ¡El servidor estaba arriba! Solo que estaba... mal. A veces. Para algunos usuarios. De maneras que no podía reproducir. El problema estaba en algún lugar de la interacción entre seis servicios, dos modelos de ML y un feature store, y no tenía absolutamente ninguna forma de trazar una única solicitud a través de esa cadena. Básicamente estaba haciendo arqueología — tamizando timestamps entre seis archivos de logs diferentes, intentando reconstruir qué le había pasado a la Solicitud #LoQueSea a las 14:32:07. Me tomó cuatro días encontrar un bug que, con trazado apropiado, habría tomado cuatro minutos.
Ahí fue cuando aprendí la diferencia entre logging y observabilidad. Y esa diferencia, déjame decirte, es la diferencia entre "puedo ver qué pasó" y "puedo entender por qué pasó."
Los Tres Pilares (Y Por Qué Deben Estar Conectados)
Todo el mundo habla de los tres pilares de la observabilidad: logs, métricas y trazas. Has visto los posts de blog. Has visto el diagrama de Venn. Puede que incluso tengas los tres configurados en tu sistema ahora mismo. Pero esto es lo que la mayoría de las guías se pierden completamente — y esto es lo que me habría ahorrado meses de frustración: estos pilares son casi inútiles de forma aislada. Su poder viene de la correlación.
Tener logs sin poder conectarlos a las trazas es como tener una novela de detectives donde todos los capítulos están revueltos. Tienes toda la información, técnicamente, pero buena suerte averiguando quién fue el culpable.
┌─────────────────────────────────────────────────────────────────┐
│ The Three Pillars — Connected │
├─────────────────────────────────────────────────────────────────┤
│ │
│ METRICS TRACES LOGS │
│ ──────── ────── ──── │
│ "What is "Where did "What │
│ happening?" time go?" happened?" │
│ │
│ Error rate Request flow Detailed │
│ Latency p99 across services event record │
│ Throughput Bottleneck Stack traces │
│ identification Business events │
│ │
│ ┌──────────────────────────────┐ │
│ │ CORRELATION │ │
│ │ │ │
│ │ trace_id ←→ log entry │ │
│ │ trace_id ←→ metric tag │ │
│ │ request_id ←→ all three │ │
│ │ │ │
│ │ "Show me the logs and │ │
│ │ metrics for THIS specific │ │
│ │ slow request" │ │
│ └──────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
La clave es un ID de correlación compartido — típicamente el trace ID de OpenTelemetry — que aparece en cada entrada de log, cada etiqueta de métrica y cada span de traza. Cuando una métrica muestra un pico de latencia, deberías poder hacer clic para ver las trazas específicas que fueron lentas, y desde esas trazas a las líneas de log de cada servicio involucrado. Debería sentirse como hacer zoom en un mapa: empezar a nivel ciudad (métricas), hacer zoom al barrio (trazas), y luego leer las señales de la calle (logs).
¿Sin esta correlación? Solo estás mirando tres dashboards separados intentando alinear timestamps a ojo. He hecho esto. Por horas. Mientras había un incidente en curso. Es como intentar armar un rompecabezas donde las piezas están en tres cuartos diferentes y no te dejan llevarlas de un lado al otro.
Empieza con la Correlación
Antes de invertir en dashboards sofisticados, asegúrate de que cada línea de log incluya trace_id y span_id. Este único cambio transforma tu experiencia de depuración más que cualquier compra de herramienta.
Logging Estructurado: Deja de Usar Printf
El primer pilar que hay que hacer bien es el logging, porque es el que ya estás haciendo. Probablemente mal. (No te sientas atacado — yo lo hice mal por años, y tengo un doctorado. La educación no te inmuniza contra el console.log("AQUI 2 !!!!") a las 11 PM.)
El tema con los logs no estructurados: son datos de solo escritura. Los escribes para tu comodidad — esa sensación cálida y reconfortante de "voy a poder ver qué pasó." Y luego intentas consultarlos a escala y descubres que parsear User usr_123 created order ord_456 for $99.99 a través de 500 millones de líneas de log con una regex es más o menos tan divertido como hacer tu declaración de impuestos en números romanos.
// Unstructured — useless at scale
console.log(`User ${userId} created order ${orderId} for $${total}`);
// Output: "User usr_123 created order ord_456 for $99.99"
// Good luck parsing that with a regex across 500 million log lines
// Structured — queryable, filterable, aggregatable
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level(label) { return { level: label }; },
},
timestamp: pino.stdTimeFunctions.isoTime,
});
// Every log entry is a JSON object
logger.info({
event: 'order_created',
user_id: 'usr_123',
order_id: 'ord_456',
total: 99.99,
currency: 'USD',
items_count: 3,
trace_id: context.traceId,
duration_ms: 145,
}, 'Order created successfully');
// Output:
// {
// "level": "info",
// "time": "2026-02-22T10:30:00.000Z",
// "event": "order_created",
// "user_id": "usr_123",
// "order_id": "ord_456",
// "total": 99.99,
// "currency": "USD",
// "items_count": 3,
// "trace_id": "abc123def456",
// "duration_ms": 145,
// "msg": "Order created successfully"
// }Ahora puedo consultar: "Muéstrame todas las órdenes de más de $500 que tardaron más de 200ms en la última hora." Intenta hacer eso con logs estilo printf. Dale. Te espero. (Voy a estar esperando mucho tiempo, porque es básicamente imposible sin querer lanzar tu laptop al océano.)
El cambio a logging estructurado es uno de esos cambios que se sienten como overhead cuando estás escribiendo el código y se sienten como un superpoder cuando estás depurando a las 2 AM. La primera vez que corrí una consulta en Kibana que filtró por event: "order_created" AND total > 500 AND duration_ms > 200 y obtuve resultados en milisegundos, sentí que había estado viviendo en una cueva y alguien acababa de mostrarme la electricidad.
Disciplina de Niveles de Log
Solo registra a nivel ERROR si es algo que necesita atención humana. He visto sistemas donde el 40% del volumen de logs eran mensajes de nivel ERROR para condiciones esperadas como "usuario no encontrado" en un endpoint de búsqueda. Ese ruido hace invisibles los errores reales.
Historia real: una vez heredé un servicio donde cada respuesta 404 se logueaba a nivel ERROR. El endpoint de búsqueda retornaba 404 cuando no había resultados, lo cual era... la mayoría de las búsquedas. El dashboard de errores era una pared sólida de rojo. Los errores reales — fallas de conexión a la base de datos, OOM kills, las cosas que importan — eran completamente invisibles en el ruido. El equipo había aprendido a ignorar el dashboard de errores por completo. Cuando ocurrió una caída real de la base de datos, nadie notó las alertas por 45 minutos porque se habían condicionado a tratar el dashboard de errores como una lámpara de lava: siempre moviéndose, nunca significando nada.
Qué Registrar (Y Qué No)
┌─────────────────────────────────────────────────────────────────┐
│ Logging Decision Guide │
├───────────────────────────┬─────────────────────────────────────┤
│ DO Log: │ DON'T Log: │
├───────────────────────────┼─────────────────────────────────────┤
│ • Business events │ • PII (names, emails, SSNs) │
│ (order created, paid) │ • Authentication tokens/secrets │
│ • State transitions │ • Full request/response bodies │
│ • Integration calls │ (log summaries instead) │
│ (duration, status) │ • Expected conditions at ERROR │
│ • Error context (what │ level (404s, validation fails) │
│ was the request?) │ • High-frequency health checks │
│ • Performance data │ • Duplicate info already in traces │
│ (latency, queue depth) │ • Temporary debug logs (remove!) │
└───────────────────────────┴─────────────────────────────────────┘
Ese "temporary debug logs (remove!)" es un ataque personal contra mi yo del pasado. Una vez dejé un logger.debug("checking inventory", { items }) en código de producción que logueaba el payload completo de inventario — incluyendo descripciones de productos — para cada orden. Nuestra factura de almacenamiento de logs se triplicó. En un mes. Nuestro equipo de infraestructura me mandó un mensaje de Slack que era simplemente un screenshot del dashboard de facturación con un solo signo de interrogación. Me merecía ese signo de interrogación.
Trazado Distribuido con OpenTelemetry
El trazado distribuido cambió la manera en que depuro sistemas de producción. No estoy siendo dramático. Es la mejora individual más grande a mi capacidad de respuesta ante incidentes en la última década. En vez de correlacionar timestamps entre seis flujos de logs diferentes mientras entrecierro los ojos frente al monitor y murmuro "este parece que pasó más o menos al mismo tiempo que aquel," veo el recorrido completo de la solicitud en una sola vista. De principio a fin. Cada salto entre servicios. Cada llamada a la base de datos. Cada búsqueda en caché. Con tiempos.
OpenTelemetry (OTel) es el estándar ahora, y digo "ahora" porque si empezaste este viaje hace unos años, puede que tengas cicatrices de la división OpenTracing/OpenCensus. Buenas noticias: se fusionaron. Malas noticias: si tienes instrumentación vieja de Jaeger por ahí, es hora de migrar. (Tuve que hacer esta migración. No fue divertido. Pero valió la pena.)
OTel es agnóstico de proveedor, bien soportado, y — esta es la parte que realmente me importa — la instrumentación que escribas hoy funcionará con Jaeger, Datadog, Honeycomb, Grafana Tempo, o lo que sea que estés usando el próximo año cuando tu empresa inevitablemente cambie de proveedor de observabilidad. Y van a cambiar. Siempre cambian.
// Basic OpenTelemetry setup for Node.js
// tracing.ts — load this before anything else
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION }
from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: 'order-service',
[ATTR_SERVICE_VERSION]: '1.4.2',
environment: process.env.NODE_ENV,
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
}),
instrumentations: [
getNodeAutoInstrumentations({
// Auto-instrument HTTP, Express, PostgreSQL, Redis, etc.
'@opentelemetry/instrumentation-fs': { enabled: false },
}),
],
});
sdk.start();La auto-instrumentación te da trazas a través de llamadas HTTP, consultas a base de datos y búsquedas en caché sin cambios de código. Solo eso ya vale la configuración. Pero el valor real — lo que te hace sentir como si tuvieras visión de rayos X de tu sistema de producción — viene de spans personalizados alrededor de tu lógica de negocio:
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('order-service');
async function processOrder(order: Order) {
return tracer.startActiveSpan('process_order', async (span) => {
span.setAttribute('order.id', order.id);
span.setAttribute('order.total', order.total);
span.setAttribute('order.items_count', order.items.length);
try {
// Each step gets its own child span automatically
const inventory = await checkInventory(order.items);
const payment = await chargePayment(order);
// Custom span for ML-based fraud check
await tracer.startActiveSpan('fraud_check', async (fraudSpan) => {
fraudSpan.setAttribute('model.version', 'fraud-v3.2');
const score = await fraudModel.predict(order);
fraudSpan.setAttribute('fraud.score', score);
fraudSpan.setAttribute('fraud.flagged', score > 0.8);
fraudSpan.end();
});
await fulfillOrder(order, inventory);
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
span.recordException(error);
throw error;
} finally {
span.end();
}
});
}Ese span fraud_check es un ejemplo perfecto de por qué la instrumentación personalizada importa. La auto-instrumentación me habría mostrado "ocurrió una llamada HTTP" o "se cargó un modelo." Pero no me diría el score de fraude, la versión del modelo, o si la orden fue marcada. Cuando nuestra detección de fraude empezó a retornar scores raros después de una actualización del modelo, pude buscar fraud.score > 0.9 AND model.version = 'fraud-v3.2' e inmediatamente ver qué órdenes estaban afectadas. ¿Sin ese span personalizado? Habría estado de vuelta grepeando archivos de log y reconstruyendo líneas de tiempo a mano. Nunca más.
Estrategia de Muestreo de Trazas
No traces el 100% de las solicitudes en producción — es costoso e innecesario. Muestrea el 100% de errores y solicitudes lentas, el 10-20% del tráfico normal, y el 100% de operaciones específicas que estés investigando. El muestreo basado en cola (tail-based sampling) de OpenTelemetry te permite decidir después de que la solicitud se complete.
Un comentario sobre el muestreo, porque esta también la aprendí de la manera cara: nuestro primer despliegue de OTel trazaba el 100% de las solicitudes. En staging. Funcionó perfecto. Luego lo prendimos en producción y nuestro backend de trazas se cayó en menos de una hora. Resulta que el 100% de muestreo sobre 50,000 solicitudes por minuto genera un MONTÓN de datos. ¿Quién lo hubiera dicho? (Todos lo sabían. Yo debería haberlo sabido. La documentación literalmente dice "no hagas esto." Lo hice de todos modos. Clásico.)
Métricas que Importan: RED y USE
Este es un patrón que he visto en cada empresa donde he trabajado: alguien configura Prometheus y Grafana, se emociona, e instrumenta TODO. Uso de CPU. Uso de memoria. Pausas del garbage collector. Conteo de threads. Conteo de file descriptors. Tamaños de connection pools. Ratios de aciertos de caché. Generaciones del heap de la JVM. La temperatura del cuarto de servidores (bueno, quizás no esa, pero no me sorprendería).
Luego ocurre un incidente, y todos se quedan mirando 47 dashboards y nadie puede descifrar qué métrica realmente importa.
Los métodos RED y USE resuelven esto. Te dan un marco para lo que realmente deberías medir, para que puedas dejar de jugar "¿cuál de estas 200 gráficas es relevante ahora mismo?" durante una caída.
┌─────────────────────────────────────────────────────────────────┐
│ Metrics Frameworks │
├─────────────────────────────────────────────────────────────────┤
│ │
│ RED Method (for request-driven services): │
│ ────────────────────────────────────────── │
│ R — Rate: Requests per second │
│ E — Errors: Failed requests per second │
│ D — Duration: Latency distribution (p50, p95, p99) │
│ │
│ USE Method (for infrastructure resources): │
│ ────────────────────────────────────────── │
│ U — Utilization: % of resource capacity in use │
│ S — Saturation: Queue depth / work waiting │
│ E — Errors: Error count for the resource │
│ │
│ Apply RED to: API endpoints, background jobs, ML inference │
│ Apply USE to: CPU, memory, disk, network, DB connections │
│ │
└─────────────────────────────────────────────────────────────────┘
RED para tus servicios, USE para tu infraestructura. Eso es todo. Ese es el tweet. (Digo, hay más matices, pero solo ese encuadre te lleva al 80% del camino.)
En la práctica, esto es lo que instrumento para cada servicio desde el día uno — no "eventualmente" o "cuando tengamos tiempo," sino antes de que el servicio llegue a producción:
import { metrics } from '@opentelemetry/api';
const meter = metrics.getMeter('order-service');
// RED metrics for the order endpoint
const requestCounter = meter.createCounter('http.requests.total', {
description: 'Total HTTP requests',
});
const requestDuration = meter.createHistogram('http.request.duration_ms', {
description: 'HTTP request duration in milliseconds',
unit: 'ms',
});
const errorCounter = meter.createCounter('http.errors.total', {
description: 'Total HTTP errors',
});
// Middleware that captures RED metrics automatically
function metricsMiddleware(req: Request, res: Response, next: NextFunction) {
const start = performance.now();
res.on('finish', () => {
const duration = performance.now() - start;
const labels = {
method: req.method,
path: req.route?.path || 'unknown',
status_code: res.statusCode.toString(),
};
requestCounter.add(1, labels);
requestDuration.record(duration, labels);
if (res.statusCode >= 500) {
errorCounter.add(1, labels);
}
});
next();
}Trampa de los Buckets del Histograma
Los buckets de histograma por defecto rara vez son correctos para tu servicio. Si tu API típicamente responde en 5-50ms, los buckets por defecto (hasta 10s) agruparán todo tu tráfico normal en un solo bucket. Configura los buckets basándote en tu distribución de latencia real: [5, 10, 25, 50, 100, 250, 500, 1000, 2500].
¿Lo de los buckets del histograma? Déjame contarte de la vez que pasé DOS HORAS durante un incidente mirando un histograma de latencia que mostraba "todo está en el bucket de 0-1s" y pensando "genial, ¡la latencia está bien!" No estaba bien. Nuestro p99 se había disparado a 800ms. Pero como los límites de los buckets por defecto eran [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10], todo desde 0ms hasta 1000ms estaba en el mismo bucket. El histograma era técnicamente correcto — las solicitudes estaban por debajo de 1 segundo — pero estaba escondiendo un aumento de 16x en la latencia de cola. Configura tus buckets, gente. Configura. Tus. Buckets.
Alertas Sin Fatiga
La fatiga de alertas es real, es peligrosa, y va a destruir silenciosamente tu rotación de guardia desde adentro. Lo he visto pasar. Cuando tu ingeniero de guardia ignora la alerta número 50 de la semana — porque las 49 anteriores fueron falsos positivos o ruido no accionable — la número 51 podría ser una caída real que cuesta dinero real y afecta usuarios reales.
La regla que sigo ahora (después de aprenderlo por las malas, obviamente — hay un tema recurrente aquí): cada alerta debe ser accionable, y cada página debe requerir una decisión humana dentro de 15 minutos. Si puede esperar hasta la mañana, no es una página. Si no hay nada que un humano pueda hacer al respecto, no es una página. Si el sistema debería auto-recuperarse, no es una página — es un elemento de monitoreo. Simple, ¿verdad? Te sorprendería cuántos equipos despiertan a su guardia por "el uso de disco está al 82%." ¿Qué se supone que haga el ingeniero de guardia a las 3 AM? ¿Ir a comprar un disco duro más grande?
┌─────────────────────────────────────────────────────────────────┐
│ Alerting Hierarchy │
├─────────────────────────────────────────────────────────────────┤
│ │
│ PAGE (wake someone up): │
│ • Error rate > 5% for 5 minutes │
│ • p99 latency > 2s for 10 minutes │
│ • SLO burn rate consuming daily budget in 1 hour │
│ • Zero successful requests for 2 minutes │
│ │
│ TICKET (fix during business hours): │
│ • Error rate > 1% for 30 minutes │
│ • Disk usage > 80% │
│ • Certificate expiring in < 14 days │
│ • Dependency deprecation warnings │
│ │
│ DASHBOARD ONLY (informational): │
│ • CPU utilization trends │
│ • Request rate changes │
│ • Cache hit ratios │
│ • Individual 500 errors (already counted in error rate) │
│ │
└─────────────────────────────────────────────────────────────────┘
La lección más grande — y no puedo enfatizar esto lo suficiente — alerta sobre síntomas, no sobre causas. "La tasa de error está por encima del 5%" es accionable. "El CPU está al 80%" generalmente no lo es, porque quizás así se ve tu servicio bajo carga normal en horario de oficina. Si el CPU alto está causando problemas, la alerta de tasa de error o la de latencia se van a activar de todos modos. No necesitas una alerta de CPU como intermediario; necesitas alertas que te digan que los usuarios están afectados.
Una vez trabajé en un equipo que tenía 147 reglas de alerta. Ciento cuarenta y siete. La mayoría eran basadas en causas: "disco acercándose al 70%," "uso de memoria por encima del 60%," "conteo de threads aumentando." La persona de guardia recibía unas 8 páginas por día en un día tranquilo. Habían desarrollado una respuesta pavloviana al vibrar de su teléfono — no urgencia, sino resignación. Cuando reescribimos las alertas para ser basadas en síntomas y orientadas a SLOs, bajamos a unas 3 páginas por semana. El mismo sistema. Los mismos modos de fallo. Solo mejor relación señal-ruido. Los ingenieros de guardia volvieron a dormir. La moral mejoró de forma medible.
# Prometheus alerting rules example
groups:
- name: order-service-slos
rules:
# Alert on high error rate (symptom-based)
- alert: HighErrorRate
expr: |
sum(rate(http_errors_total{service="order-service"}[5m]))
/
sum(rate(http_requests_total{service="order-service"}[5m]))
> 0.05
for: 5m
labels:
severity: page
annotations:
summary: "Order service error rate above 5%"
runbook: "https://wiki.internal/runbooks/order-service-errors"
# Alert on SLO burn rate (proactive)
- alert: SLOBurnRateHigh
expr: |
slo:burn_rate:5m{service="order-service"} > 14.4
and
slo:burn_rate:1h{service="order-service"} > 14.4
for: 2m
labels:
severity: page
annotations:
summary: "Order service burning through error budget too fast"Los Runbooks Son Innegociables
Cada alerta debe enlazar a un runbook. Cuando te despiertan a las 3 AM, no deberías tener que pensar en qué verificar primero. El runbook debería listar: qué significa la alerta, qué verificar, causas comunes y cómo mitigar.
Una vez me despertaron por una alerta que decía "SLO burn rate high." Eso era todo. Sin enlace a runbook. Sin contexto. Sin primeros pasos sugeridos. Pasé los primeros 20 minutos del incidente simplemente averiguando qué significaba la alerta. ¡Veinte minutos! ¡Durante un incidente activo! Lo del runbook no es opcional — es la diferencia entre una resolución de 20 minutos y un maratón de 60 minutos de "¿qué estoy mirando siquiera?" Escribe el runbook cuando crees la alerta. No "después." No "cuando tengamos tiempo." Ahora. Tu yo de las 3 AM del futuro te lo agradecerá.
SLOs, SLIs y Presupuestos de Error
Los SLOs (Service Level Objectives) transformaron cómo mi equipo piensa sobre la confiabilidad. Antes de los SLOs, nuestro objetivo de confiabilidad era el vago e imposible "hacer todo lo más confiable posible." Lo cual suena noble pero es operacionalmente inútil. ¿Qué tan confiable es "lo más posible"? ¿100%? (Eso no es posible.) ¿99.99%? (Eso cuesta una fortuna.) ¿99.9%? (Puede que esté bien.) Sin un número, "confiable" es solo un sentimiento, y los sentimientos no te ayudan a tomar decisiones de ingeniería.
Con SLOs, tienes un objetivo concreto y — esta es la parte mágica — un presupuesto para fallar.
┌─────────────────────────────────────────────────────────────────┐
│ SLO Framework │
├─────────────────────────────────────────────────────────────────┤
│ │
│ SLI (Indicator): What you measure │
│ "Proportion of requests that return successfully │
│ within 500ms" │
│ │
│ SLO (Objective): Your target for the SLI │
│ "99.9% of requests succeed within 500ms over │
│ a 30-day rolling window" │
│ │
│ Error Budget: How much failure you can afford │
│ 0.1% = ~43 minutes of downtime per month │
│ or ~4,320 failed requests per 4.32M total │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Error Budget Remaining: 67% ████████████░░░░░░ │ │
│ │ Days into window: 15/30 │ │
│ │ Status: HEALTHY — safe to deploy │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ When budget is exhausted: │
│ → Freeze non-critical deployments │
│ → Focus engineering on reliability improvements │
│ → Conduct incident reviews for budget-burning events │
│ │
└─────────────────────────────────────────────────────────────────┘
El concepto de presupuesto de error es lo que hace prácticos a los SLOs en vez de aspiracionales. Sin él, la confiabilidad es una religión — "debemos tener cero errores, cero downtime, cero problemas." Eso no es ingeniería; eso es pensamiento mágico. Con un presupuesto de error, la confiabilidad se convierte en un trade-off de ingeniería — "tenemos espacio para X fallos este mes, así que podemos lanzar esta funcionalidad y aceptar un poco de riesgo, O ya quemamos nuestro presupuesto y es hora de pausar el trabajo de features y enfocarnos en estabilidad."
Este es genuinamente el cambio de mentalidad más poderoso que he experimentado en mi carrera. Convierte el argumento de "moverse rápido vs. ser confiable" de un debate filosófico a una conversación respaldada por datos. ¿Producto quiere lanzar una feature riesgosa? Revisa el presupuesto de error. ¿Queda bastante presupuesto? Lánzala. ¿El presupuesto está flaco? Quizás endurecemos el despliegue o esperamos al próximo mes. Sin sentimientos heridos, sin guerras de territorios — solo matemáticas.
Para mis servicios de inferencia de IA, defino dos SLOs:
// SLO definitions for an AI inference service
const slos = {
availability: {
sli: 'Proportion of inference requests that return a valid prediction',
target: 0.999, // 99.9%
window: '30d',
// Excludes: client errors (4xx), scheduled maintenance
},
latency: {
sli: 'Proportion of inference requests completing within threshold',
target: 0.99, // 99%
threshold_ms: 500,
window: '30d',
// p99 latency must stay under 500ms
},
};¿Por qué dos SLOs? Porque un servicio puede estar "disponible" (retornando respuestas) mientras es inaceptablemente lento, y puede ser "rápido" mientras retorna basura. Aprendí esto cuando nuestro servicio de inferencia estaba técnicamente respondiendo al 100% de las solicitudes — solo que el 5% de esas respuestas eran la predicción de fallback por defecto del modelo porque el feature store estaba expirando. Disponibilidad: perfecta. Utilidad: cuestionable. Necesitas ambas dimensiones.
Diseño de Dashboards que Funciona
Un muro de dashboards no es observabilidad. Es decoración de interiores. He visto NOCs (Network Operations Centers) con 30 pantallas mostrando gráficos hermosos en glorioso color de alta definición que nadie — NADIE — mira durante un incidente real. Miran Slack. Miran la alerta. Miran el único panel de Grafana que tienen en favoritos. Las otras 29 pantallas son protectores de pantalla caros.
Cada dashboard debería responder una pregunta específica. Si no puedes articular qué pregunta responde un dashboard, bórralo. (Sí, de verdad.) Mantengo tres niveles, y podo sin piedad cualquier cosa que no encaje:
┌─────────────────────────────────────────────────────────────────┐
│ Dashboard Hierarchy │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Level 1: Service Health (the "glance" dashboard) │
│ ───────────────────────────────────────────── │
│ • One row per service │
│ • SLO status (green/yellow/red) │
│ • Error budget remaining │
│ • Current error rate and p99 latency │
│ Purpose: "Is anything on fire right now?" │
│ │
│ Level 2: Service Deep Dive │
│ ───────────────────────── │
│ • RED metrics over time │
│ • Latency heatmap │
│ • Error breakdown by type │
│ • Dependency health │
│ • Recent deployments overlay │
│ Purpose: "This service has a problem, what kind?" │
│ │
│ Level 3: Investigation │
│ ──────────────────── │
│ • Trace search and exploration │
│ • Log search with filters │
│ • Database query performance │
│ • Infrastructure metrics (USE) │
│ Purpose: "I know the problem area, show me details" │
│ │
└─────────────────────────────────────────────────────────────────┘
El Nivel 1 es el que miro primero durante cualquier incidente. Si todas las filas están en verde, el problema probablemente no está en nuestros servicios (revisa el CDN, el DNS, las cosas fuera de tu radio de impacto). Si una fila está en rojo, hago clic al Nivel 2 de ese servicio. El Nivel 2 me dice qué tipo de problema — ¿son errores? ¿latencia? ¿una dependencia? Luego el Nivel 3 es donde pasa el verdadero trabajo de detective.
Esta jerarquía se mapea al flujo natural de respuesta ante incidentes: "¿Hay algo mal?" -> "¿Qué servicio es?" -> "¿Qué específicamente está roto?" Si tus dashboards no siguen este flujo, la gente se los va a saltar completamente e ir directo a grepear logs. Lo he visto pasar cien veces.
Marcadores de Despliegue
Siempre superpone marcas de tiempo de despliegue en tus dashboards de métricas. La causa más común de problemas en producción es "desplegamos algo." Poder correlacionar visualmente un pico de latencia con un despliegue ahorra minutos preciosos durante incidentes.
Lo de los marcadores de despliegue no es opcional. Yo estimaría que el 70% de los incidentes de producción que he investigado fueron causados por, correlacionados con, o empeorados por un despliegue reciente. Poder mirar una gráfica de latencia e inmediatamente ver "ah, hubo un deploy 12 minutos antes de este pico" vale su peso en oro. Sin ese marcador, pasarías 20 minutos corriendo git log y buscando en Slack "¿alguien desplegó algo?" mientras el reloj del incidente sigue corriendo.
Guardias que No Queman a la Gente
La observabilidad se trata, en última instancia, de personas. Ya sé que eso suena como algo que leerías en un póster motivacional en un WeWork, pero lo digo en serio. La mejor instrumentación del mundo no ayuda si tu ingeniero de guardia está quemado por falsas alarmas, privado de sueño por páginas innecesarias, y sin el contexto que necesita para realmente arreglar problemas cuando son reales.
He sido ese ingeniero quemado. He tenido rotaciones de guardia donde temía tanto la semana que me afectaba el trabajo la semana anterior. Nadie hace buena ingeniería cuando está ansioso por el vibrar de su teléfono. Arreglar esto no es simplemente "nice to have" — es un tema de retención, un tema de calidad, y honestamente, un tema de decencia humana.
Prácticas que mejoraron materialmente la calidad de las guardias en mis equipos:
-
Cada página recibe un postmortem sin culpas si dura más de 15 minutos. No para castigar a nadie — la palabra "sin culpas" está haciendo trabajo crítico en esa oración — sino para mejorar el sistema. Si la misma alerta se dispara tres veces en un mes, el sistema tiene un bug, no el ingeniero.
-
La entrega de guardia incluye contexto: qué alertas se activaron esta semana, qué está actualmente degradado, qué despliegues están en curso. He visto entregas que eran literalmente "buena suerte." Eso no es una entrega, es un abandono. Escribe un párrafo. Comparte los tickets abiertos. Menciona la cosa que ha estado inestable. Toma 10 minutos y ahorra horas.
-
Guardia en sombra para nuevos miembros del equipo: empárejalos con un ingeniero experimentado en su primera rotación. Observan, hacen preguntas, y construyen confianza antes de volar solos. Tirar a un ingeniero junior a la guardia sin sombra es como conseguir tanto una mala respuesta a incidentes COMO un currículum actualizado en LinkedIn.
-
Presupuesto de toil: si más del 30% del tiempo de guardia se gasta en tareas manuales repetitivas, es una señal para automatizar. Rastréalo. "Reinicié el servidor de caché 4 veces esta semana" no es una experiencia de guardia — es un cron job que no se ha escrito todavía.
-
Tiempo libre compensatorio: si alguien es despertado a las 3 AM y pasa dos horas arreglando un problema, debería tomarse tiempo libre al día siguiente. La guardia no es trabajo gratis. Ya sé que esto suena obvio, pero he trabajado en empresas donde no era la norma, y la diferencia en moral entre "tómate la mañana libre" y "te vemos en el standup a las 9" es enorme.
El Camino de Madurez en Observabilidad
No necesitas implementar todo lo de este artículo de una vez. Por favor no lo intentes. He visto equipos intentar la "gran reforma de observabilidad de un golpe" y terminar con un setup de OpenTelemetry a medio configurar, tres librerías de logging diferentes, y una instancia de Grafana de la que nadie sabe la contraseña. (Ojalá estuviera bromeando.)
Esta es la progresión que recomiendo, basada en ver a múltiples equipos pasar por esto:
Fase 1: Fundamentos (semana 1-2)
- Logging JSON estructurado con IDs de correlación
- Endpoints básicos de health check
- Dashboards de tasa de error y latencia
Solo esto va a transformar tu depuración. En serio. Si no haces nada más, haz esto. El salto de console.log a logging estructurado con trace IDs es la mejora individual con mejor relación costo-beneficio de todo este artículo.
Fase 2: Trazado (semana 3-4)
- Auto-instrumentación con OpenTelemetry
- Spans personalizados para lógica de negocio crítica
- Correlación traza-a-log
Aquí es donde las cosas empiezan a sentirse como magia. La primera vez que haces clic en una traza y ves el flujo completo de la solicitud a través de cinco servicios con desglose de tiempos... te preguntarás cómo depurabas antes sin esto.
Fase 3: SLOs (mes 2)
- Definir SLIs y SLOs para cada servicio
- Seguimiento de presupuesto de error
- Alertas basadas en síntomas vinculadas a SLOs
Aquí es donde ocurre el cambio cultural. Pasas de "confiabilidad basada en vibras" a "confiabilidad basada en datos." Producto e ingeniería dejan de discutir sobre si lanzar o estabilizar, porque el presupuesto de error responde la pregunta por ellos.
Fase 4: Cultura (continuo)
- Runbooks para cada alerta
- Postmortems sin culpas
- Mejoras en la calidad de las guardias
- Reuniones regulares de revisión de SLOs
Esta fase nunca termina, y está bien. Es mantenimiento, no un proyecto. Revisa tus SLOs trimestralmente. Actualiza tus runbooks cuando las cosas cambien. Sigue haciendo que la guardia sea menos horrible.
Las herramientas importan menos que las prácticas. Lo voy a decir otra vez porque es lo más contraintuitivo de este artículo: las herramientas importan menos que las prácticas. He visto equipos con contratos de Datadog de seis cifras que todavía depuran haciendo SSH a los servidores y dándole tail a los logs, y equipos corriendo Prometheus + Grafana + Jaeger con presupuesto mínimo que resuelven incidentes en minutos. La diferencia siempre es cultura y disciplina, no tecnología. La plataforma cara no ayuda si nadie escribe logs estructurados. El stack open-source funciona genial si la gente realmente instrumenta su código y escribe runbooks.
Empieza con logs estructurados e IDs de correlación. Todo lo demás se construye sobre esa base. Y la próxima vez que algo se rompa a las 3 AM — porque se va a romper, esa es la naturaleza de los sistemas distribuidos — te agradecerás por haber invertido en observabilidad antes de necesitarla. O más exactamente, pensarás "al menos puedo averiguar qué está pasando" en vez de "no tengo idea de qué está pasando y quiero llorar." Ambas son emociones válidas a las 3 AM. Pero solo una de ellas lleva a una resolución.
Frequently Asked Questions
No te pierdas nada
Artículos sobre IA, ingeniería y las lecciones que aprendo construyendo cosas. Sin spam, lo prometo.
Osvaldo Restrepo
Senior Full Stack AI & Software Engineer. Building production AI systems that solve real problems.