Observabilidad e Instrumentación
Resumen
El código que no se puede observar es código que no se puede operar. La observabilidad es la capacidad de responder "¿qué está haciendo el sistema y por qué?" desde fuera, usando la telemetría que el código emite. La instrumentación no es un complemento posterior al lanzamiento: se escribe junto con la funcionalidad, de la misma manera que las pruebas. Si una funcionalidad se envía sin telemetría, el primer error reportado por un usuario se convierte en arqueología en lugar de una consulta.
Cuándo Usarla
- Construir cualquier funcionalidad que se ejecutará en producción
- Añadir un nuevo servicio, endpoint, trabajo en segundo plano o integración externa
- Un incidente de producción tardó demasiado en diagnosticarse ("no pudimos determinar qué ocurrió")
- Configurar o revisar reglas de alertas
- Revisar un PR que añade E/S, reintentos, colas o llamadas entre servicios
NO para:
- Diagnosticar una falla que está ocurriendo ahora mismo: usa la habilidad
debugging-and-error-recovery(la observabilidad es lo que hace que esa habilidad sea rápida la próxima vez) - Perfilar y optimizar lentitud medida: usa la habilidad
performance-optimization - Listas de verificación de monitoreo del día de lanzamiento y disparadores de reversión: consulta la habilidad
shipping-and-launch; esta habilidad cubre la instrumentación que los alimenta
Proceso
1. Definir "funciona" antes de instrumentar
Telemetría sin una pregunta es ruido. Antes de añadir cualquier instrumentación, escribe de 2 a 4 preguntas que un ingeniero de guardia hará sobre esta funcionalidad:
FEATURE: reintento de pago en checkout
QUESTIONS ON-CALL WILL ASK:
1. ¿Qué fracción de pagos se realizan con éxito en el primer intento vs después del reintento?
2. Cuando un pago falla permanentemente, ¿por qué? (¿error del proveedor? ¿timeout? ¿validación?)
3. ¿El proveedor de pagos es más lento de lo habitual?
→ Cada señal a continuación debe ayudar a responder una de estas.
Si no puedes nombrar las preguntas, no estás listo para instrumentar: registrarás todo y no aprenderás nada.
2. Elegir la señal correcta para cada pregunta
| Señal | Responde | Perfil de costo | Ejemplo |
|---|---|---|---|
| Log estructurado | "¿Qué ocurrió en este caso específico?" | Por evento; crece con el tráfico | payment_failed con código de error del proveedor |
| Métrica | "¿Con qué frecuencia / qué tan rápido, en agregado?" | Fijo por serie; barato de consultar | latencia p99 de llamadas al proveedor |
| Traza | "¿Dónde se fue el tiempo entre servicios?" | Por solicitud; generalmente muestreado | Un checkout lento, desglosado por salto |
Regla general: las métricas te dicen que algo está mal, las trazas te dicen dónde, los logs te dicen por qué.
3. Registro estructurado
Registra eventos, no prosa. Cada línea de log es un objeto JSON con un nombre de evento estable y campos legibles por máquina:
// MAL: interpolación de cadenas — no consultable, inconsistente
logger.info(`Payment ${id} failed for user ${userId} after ${n} retries`);
// BIEN: nombre de evento estable + campos estructurados
logger.warn({
event: 'payment_failed',
paymentId: id,
provider: 'stripe',
errorCode: err.code,
attempt: n,
}, 'payment failed');
Niveles de log — úsalos de manera consistente:
| Nivel | Significado | Acción de guardia |
|---|---|---|
error | Invariante rota; alguien puede necesitar actuar | Investigar |
warn | Degradado pero manejado (reintento exitoso, respaldo usado) | Observar tendencias |
info | Evento de negocio significativo (pedido realizado, trabajo terminado) | Ninguna |
debug | Detalle de diagnóstico | Desactivado en producción por defecto |
Los IDs de correlación son obligatorios. Genera (o acepta) un ID de solicitud en el límite del sistema y adjúnalo a cada línea de log, tramo y llamada saliente. Sin él, no puedes reconstruir una sola solicitud a partir de logs entremezclados:
// Express: logger hijo por solicitud, ID propagado aguas abajo
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] ?? crypto.randomUUID();
req.log = logger.child({ requestId: req.id });
res.setHeader('x-request-id', req.id);
next();
});
Cuando varios puntos de entrada escriben en un solo log, nombra el punto de entrada. Un ID de correlación identifica una ejecución; no dice qué ruta de código la inició. El mismo trabajo alcanzado por un planificador, por un endpoint de reproducción y por una ejecución manual de CLI produce líneas intercambiables en un solo sumidero, por lo que atribuir una línea se reduce a eliminación – leer el historial del planificador, la tabla de procesos, un log de despliegue – y ese argumento solo se sostiene mientras esos registros externos aún existan. Marca el punto de entrada donde comienza la ejecución, junto al ID de correlación, y propaga ambos de la misma manera:
// Un helper para cada punto de entrada: el logger propio de la ejecución lleva ambos campos.
// `entryPoint`, no `source` — ECS reserva `source.*` para campos de red.
export const runLog = (entryPoint: 'scheduler' | 'replay_endpoint' | 'cli', runId: string) =>
logger.child({ entryPoint, requestId: runId });
// tick del planificador -> runLog('scheduler', crypto.randomUUID())
// POST /jobs/:id/replay -> runLog('replay_endpoint', req.id)
// invocación CLI -> runLog('cli', process.env.RUN_ID ?? crypto.randomUUID())
Ambos campos deben cruzar los mismos límites que el ID de correlación – metadatos de cola, encabezados HTTP – o un trabajador re-deriva el punto de entrada y adivina. Un campo que meramente se correlaciona con un punto de entrada es una pista, no una atribución: cualquier cosa que pueda invocar el trabajo puede reproducirlo.
Nunca registres secretos, tokens, contraseñas o PII completa. Esta es una regla estricta de la habilidad security-and-hardening: los pipelines de telemetría son una ruta clásica de fuga de datos. Haz una lista blanca de campos; no registres cuerpos de solicitud completos.
4. Métricas
Para servicios orientados a solicitudes, instrumenta RED en cada endpoint y cada dependencia externa: Rate (solicitudes/segundo), Errors (tasa de fallos), Duration (histograma de latencia, no promedio). Para recursos (colas, pools, hosts), usa USE: Utilization, Saturation, Errors.
Al igual que con el trazado, el camino independiente del proveedor es la API de métricas de OpenTelemetry (mismo SDK y contexto que el paso 5). El siguiente ejemplo usa prom-client de Prometheus — una opción de backend común, no la única; las reglas de RED/USE y cardinalidad son idénticas en ambos casos.
import { Histogram } from 'prom-client';
const httpDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration',
labelNames: ['method', 'route', 'status_class'], // '2xx', no '200'
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});
La cardinalidad es el modo de fallo. Cada combinación única de etiquetas es una serie temporal separada. Las etiquetas deben provenir de conjuntos pequeños y fijos (plantilla de ruta, clase de estado, nombre del proveedor). Nunca uses IDs de usuario, URLs completas, mensajes de error u otros valores no acotados como etiquetas – eso pertenece a los logs y trazas.
OK como etiqueta: route="/api/tasks/:id" status_class="5xx" provider="stripe"
NUNCA una etiqueta: user_id, email, request_id, URL completa, texto del mensaje de error
Nunca promedios, siempre percentiles: un promedio oculta el 1% de usuarios que están teniendo una experiencia terrible. Usa histogramas y lee p50/p95/p99.
5. Trazado distribuido
Usa OpenTelemetry — es el estándar independiente del proveedor, y la auto-instrumentación cubre HTTP, gRPC y clientes de BD comunes con código casi nulo:
// tracing.ts — debe importarse antes que cualquier otra cosa
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
serviceName: 'checkout-service',
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
Añade tramos manuales solo alrededor de unidades de trabajo internas significativas (por ejemplo, applyDiscounts, chargeProvider) y adjunta los atributos por los que la guardia filtrará. Propaga el contexto a través de cada límite asíncrono – encabezados HTTP, metadatos de mensajes de cola – o la traza muere en la brecha. Muestrea basado en cabecera a una tasa baja por defecto; mantén el 100% de los errores si tu backend soporta muestreo de cola.
6. Alertas
Alerta sobre síntomas que sienten los usuarios, no sobre causas:
SÍNTOMA (digno de paginar): CAUSA (panel, no una página):
tasa de error > 1% durante 5 min CPU al 85%
latencia p99 > 2s un pod se reinició
edad de cola > 10 min disco al 70%
Las alertas basadas en causa se disparan cuando no pasa nada y pasan por alto fallos que no predijiste. Las alertas basadas en síntoma se disparan exactamente cuando los usuarios están afectados, independientemente de la causa.
Reglas para cada alerta que crees:
- Debe ser procesable. Si la respuesta es "ignorarlo, se autocura", elimina la alerta.
- Enlaza a un runbook — incluso tres líneas: qué significa, primera consulta a ejecutar, ruta de escalado.
- Tiene un umbral y una duración justificados por el SLO o por datos históricos, no por una suposición.
- Usa solo dos niveles de gravedad: página (afecta al usuario, actúa ahora) y ticket (degradación, actúa esta semana). Un tercer nivel se convierte en ruido que entrena a las personas a ignorar todo.
Escribir Runbooks
La regla 2 anterior requiere que cada alerta enlace a un runbook. El trabajo de un runbook es responder tres preguntas sin que el lector tenga que pensar: qué está pasando, qué revisar primero, y a quién llamar si eso no lo resuelve. Almacena en docs/runbooks/ nombrados según la alerta.
Runbook mínimo viable (tres líneas):
# Runbook: Alta Tasa de Error en /api/tasks
**Significa:** El pool de conexiones de la BD probablemente está agotado, o un mal despliegue.
**Primera comprobación:** `SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend';`
— si el recuento > límite del pool, consulta el Paso 2. (Reemplaza con el equivalente para tu base de datos.)
**Escalar a:** #db-oncall o rotación de guardia de ingeniería.
Cuándo expandir más allá de tres líneas: añade pasos solo cuando la primera comprobación por sí sola no sea suficiente para decidir. Un runbook de cinco pasos que cubra las tres causas más comunes es mejor que un documento de veinte pasos que cubra cada caso extremo y que se lea por encima.
Mantén los runbooks actualizados. Actualiza el runbook como parte del cierre de cada incidente en el que se usó – un runbook desactualizado genera falsa confianza. Si un paso fue incorrecto o faltaba, arréglalo antes de marcar el incidente como resuelto.
7. Verificar la propia telemetría
La instrumentación es código; puede estar mal. Antes de dar por terminado el trabajo, dispara las rutas y mira la salida real:
- Fuerza un error en staging → encuéntralo en los logs por
requestId, confirma que los campos están estructurados (no[object Object]) - Envía tráfico de prueba → confirma que las series métricas aparecen con las etiquetas esperadas y valores sensatos
- Sigue una solicitud entre servicios en la UI de trazado → sin tramos rotos
- Dispara cada nueva alerta una vez (baja el umbral temporalmente) → confirma que llega al canal correcto y que el enlace del runbook funciona
Racionalizaciones Comunes
| Racionalización | Realidad |
|---|---|
| "Añadiré logging después de que funcione" | "Después" se convierte en "después del primer incidente", que es el momento más caro para descubrir que estás ciego. Instrumenta mientras construyes. |
| "Más logs = más observabilidad" | El ruido no estructurado hace que los incidentes sean más lentos, no más rápidos. Tres eventos consultables vencen a trescientas líneas de prosa. |
| "console.log está bien por ahora" | La salida no estructurada no se puede filtrar, correlacionar ni alertar. El logger estructurado cuesta cinco minutos extra una vez. |
| "Podemos simplemente mirar los paneles cuando algo se rompa" | Los paneles construidos sin preguntas definidas te muestran todo excepto la respuesta. Empieza por las preguntas de guardia. |
| "Alerta sobre todo lo importante, lo ajustaremos después" | Un buscapersonas ruidoso entrena a las personas a ignorarlo. El ajuste nunca ocurre; la página real perdida, sí. |
| "El ID de usuario como etiqueta de métrica facilita la depuración" | También hace que tu backend de métricas se caiga. Las búsquedas de alta cardinalidad pertenecen a logs y trazas. |
| "El trazado es excesivo para nuestros dos servicios" | Dos servicios ya significa preguntas de latencia entre servicios que los logs no pueden responder. La auto-instrumentación hace que el costo sea trivial. |
Señales de Alarma
- Un PR de funcionalidad con reintentos, colas o llamadas externas y cero telemetría nueva
- Líneas de log construidas con interpolación de cadenas en lugar de campos estructurados
- Sin ID de correlación/solicitud – cada línea de log es un huérfano
- Un flujo de log alimentado por un planificador, un webhook y ejecuciones manuales, sin un campo que nombre cuál produjo la línea
- Métricas etiquetadas con IDs de usuario, URLs completas o texto de mensajes de error (bomba de cardinalidad)
- Latencia rastreada como un promedio sin percentiles
- Alertas que se disparan a diario y se reconocen sin acción
- Alertas sobre causas (CPU, memoria) que pagan humanos mientras la tasa de error visible para el usuario no está monitoreada
- Secretos, tokens o cuerpos de solicitud completos que aparecen en logs
- "Funciona en mi máquina" como la única evidencia de que una funcionalidad de producción está sana
Verificación
Después de instrumentar una funcionalidad, confirma:
- Las preguntas de guardia para esta funcionalidad están escritas, y cada señal se asigna a una
- Toda la salida de log está estructurada (JSON), con nombres de evento estables y un ID de correlación en cada línea
- Cada sumidero de log escrito por más de un punto de entrada lleva un campo de punto de entrada, establecido donde comienza la ejecución y propagado con el ID de correlación en lugar de inferirse aguas abajo
- No hay secretos, tokens ni PII no redactada en ninguna línea de log (verificación puntual de la salida real)
- Existen métricas RED para cada nuevo endpoint y cada dependencia externa, con conjuntos de etiquetas acotados
- La latencia es un histograma; p95/p99 son consultables
- Una sola solicitud se puede seguir de principio a fin en la UI de trazado sin tramos rotos
- Cada nueva alerta está basada en síntoma, tiene un enlace a runbook y se disparó una vez en prueba
- Una falla inducida en staging se localizó solo a través de telemetría, sin leer el código fuente
Para la versión resumida de esta lista, incluyendo la compuerta de instrumentación previa al lanzamiento, consulta ../../references/observability-checklist.md.