Diseñando Sistemas Observables desde el Inicio
Diseñando Sistemas Observables desde el Inicio
El costo de la observabilidad retroactiva
Hay dos formas de tener un sistema observable: diseñarlo para serlo desde el principio, o intentar añadir observabilidad a un sistema existente.
La segunda opción es dramáticamente más costosa. Cuando un sistema no fue diseñado con observabilidad en mente:
- Los servicios no propagan el contexto de trazas
- Los logs son texto plano sin estructura
- Las operaciones de negocio no tienen métricas
- Agregar instrumentación requiere cambiar código en decenas de lugares
El costo de instrumentar retroactivamente un sistema maduro puede ser meses de trabajo de ingeniería. El costo de diseñar con observabilidad desde el inicio es marginal — quizás un 10-15% de overhead en el tiempo de desarrollo.
Este capítulo te da el framework para hacer lo segundo.
La observabilidad como requisito no funcional
Los requisitos no funcionales (NFRs) son las propiedades del sistema que definen cómo funciona, en contraste con los funcionales que definen qué hace.
Seguridad, performance, escalabilidad, mantenibilidad — estos son NFRs universalmente aceptados. La observabilidad debe estar en esa lista.
Definiendo el contrato de observabilidad de un servicio
Antes de construir un servicio, define explícitamente:
# observability-contract.yaml
service: payment-service
version: 1.0
metrics:
- name: payments_processed_total
type: counter
labels: [status, gateway, currency]
description: "Total number of payment processing attempts"
- name: payment_duration_seconds
type: histogram
buckets: [0.1, 0.5, 1, 2, 5, 10]
labels: [gateway, status]
description: "Payment processing duration"
logs:
required_fields: [timestamp, level, service, trace_id, span_id]
business_events:
- payment_initiated
- payment_succeeded
- payment_failed
- payment_retried
traces:
auto_instrumented:
- HTTP incoming/outgoing
- Database queries
- Queue operations
manual_spans:
- processPayment (business operation)
- validatePaymentData
- chargeGateway
slo:
availability:
target: 99.9%
sli: "rate(payments_processed_total{status='success'}[5m]) / rate(payments_processed_total[5m])"
latency:
target_p99_ms: 3000
sli: "histogram_quantile(0.99, payment_duration_seconds)"
Este contrato se incluye en el diseño técnico del servicio, se revisa en code review, y se valida en CI.
Patrones de instrumentación
Patrón 1: Middleware de observabilidad
Para APIs, el middleware es el lugar correcto para instrumentar la mayoría de las señales:
// observability.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { trace, metrics, context } from '@opentelemetry/api';
const meter = metrics.getMeter('api-gateway');
const requestCounter = meter.createCounter('http_requests_total');
const requestDuration = meter.createHistogram('http_request_duration_seconds');
export function observabilityMiddleware(req: Request, res: Response, next: NextFunction) {
const startTime = Date.now();
const span = trace.getActiveSpan();
// Enriquecer span con contexto de la request
if (span) {
span.setAttribute('http.method', req.method);
span.setAttribute('http.url', req.url);
span.setAttribute('http.user_agent', req.headers['user-agent'] ?? '');
// Contexto de negocio si está disponible
if (req.user) {
span.setAttribute('user.id', req.user.id);
span.setAttribute('user.tier', req.user.tier);
}
}
res.on('finish', () => {
const duration = (Date.now() - startTime) / 1000;
const attributes = {
'http.method': req.method,
'http.status_code': String(res.statusCode),
'http.route': req.route?.path ?? 'unknown',
};
requestCounter.add(1, attributes);
requestDuration.record(duration, attributes);
});
next();
}
Patrón 2: Wrapper de operaciones de negocio
Para lógica de negocio, un wrapper que instrumenta automáticamente:
// tracing.utils.ts
import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
const tracer = trace.getTracer('business-operations');
export async function withSpan<T>(
name: string,
fn: () => Promise<T>,
attributes: Record<string, string | number | boolean> = {}
): Promise<T> {
return tracer.startActiveSpan(name, { kind: SpanKind.INTERNAL }, async (span) => {
Object.entries(attributes).forEach(([key, value]) => {
span.setAttribute(key, value);
});
try {
const result = await fn();
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: (error as Error).message });
span.recordException(error as Error);
throw error;
} finally {
span.end();
}
});
}
// Uso en código de negocio:
async function processOrder(orderId: string) {
return withSpan('processOrder', async () => {
const order = await getOrder(orderId);
await withSpan('validateInventory', () => checkInventory(order), {
'order.item_count': order.items.length
});
await withSpan('chargePayment', () => chargePayment(order), {
'payment.amount_cents': order.totalCents,
'payment.gateway': order.paymentGateway
});
return order;
}, { 'order.id': orderId });
}
Patrón 3: Logger contextual
Un logger que automáticamente incluye el contexto de traza activo:
// logger.ts
import { trace } from '@opentelemetry/api';
import pino from 'pino';
const baseLogger = pino({ level: process.env.LOG_LEVEL ?? 'info' });
type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
function createLogMethod(level: LogLevel) {
return (message: string, data?: Record<string, unknown>) => {
const span = trace.getActiveSpan();
const traceContext = span?.spanContext() ? {
trace_id: span.spanContext().traceId,
span_id: span.spanContext().spanId,
} : {};
baseLogger[level]({ ...traceContext, ...data }, message);
};
}
export const logger = {
trace: createLogMethod('trace'),
debug: createLogMethod('debug'),
info: createLogMethod('info'),
warn: createLogMethod('warn'),
error: createLogMethod('error'),
fatal: createLogMethod('fatal'),
};
// Uso: el trace_id se inyecta automáticamente en cada log
logger.info('Payment processed', { orderId, amountCents, gateway });
logger.error('Payment failed', { orderId, error: err.message, attempt });
Observabilidad en diferentes capas del sistema
APIs y servicios HTTP
flowchart TD
REQ[Request HTTP] --> MW[Middleware de Observabilidad]
MW --> |Crea span, extrae trace context| HANDLER[Request Handler]
HANDLER --> |Enriquece span| DB[Database Query]
HANDLER --> |Propaga trace context| DOWNSTREAM[Downstream Service]
DB --> |Span de DB automático| RESP[Response]
RESP --> MW
MW --> |Emite: counter, histogram| METRICS[Metrics Backend]
MW --> |Emite: logs con trace_id| LOGS[Log Backend]
MW --> |Cierra span| TRACES[Trace Backend]
Sistemas de colas y workers
Los sistemas asíncronos tienen el desafío de que la traza se “rompe” cuando el mensaje pasa por una cola. La solución es incluir el contexto de traza en el mensaje:
// Producer: inyecta contexto de traza en el mensaje
async function publishPaymentJob(orderId: string) {
const span = trace.getActiveSpan();
const traceContext = {
traceId: span?.spanContext().traceId,
traceFlags: span?.spanContext().traceFlags,
};
await queue.publish('payment-jobs', {
orderId,
_telemetry: traceContext // Incluir en el payload
});
logger.info('Payment job published', { orderId });
}
// Consumer: restaura el contexto de traza desde el mensaje
async function processPaymentJob(message: QueueMessage) {
const parentContext = message._telemetry;
// Crear un nuevo span con link a la traza original
const span = tracer.startSpan('processPaymentJob', {
links: parentContext ? [{ context: parentContext }] : []
});
await context.with(trace.setSpan(context.active(), span), async () => {
try {
await doProcessPayment(message.orderId);
span.setStatus({ code: SpanStatusCode.OK });
} catch (err) {
span.setStatus({ code: SpanStatusCode.ERROR });
span.recordException(err);
} finally {
span.end();
}
});
}
Bases de datos y queries
La instrumentación automática de bases de datos con OpenTelemetry captura:
- Duración de la query
- Tipo de operación (SELECT, INSERT, etc.)
- Tabla afectada
- Número de rows afectadas
Pero falta el contexto de negocio. Agrega esto manualmente cuando sea relevante:
async function getUserOrders(userId: string): Promise<Order[]> {
const span = trace.getActiveSpan();
span?.setAttribute('query.purpose', 'fetch_user_orders');
span?.setAttribute('query.user_id', userId);
const startTime = Date.now();
const orders = await db.query('SELECT * FROM orders WHERE user_id = $1', [userId]);
span?.setAttribute('query.result_count', orders.length);
span?.setAttribute('query.duration_ms', Date.now() - startTime);
return orders;
}
Checklist de observabilidad para producción
Este checklist cubre lo mínimo que un servicio debe tener antes de ir a producción:
Logs
- Todos los logs emiten JSON estructurado
- Cada log incluye
timestamp(UTC, ISO 8601),level,service,environment - Logs de operaciones de negocio importantes en nivel INFO
- Logs de errores incluyen stack trace y contexto completo
-
trace_idyspan_idpresentes en cada log - Sin datos sensibles (passwords, tokens, PII) en logs
Métricas
- Contador de requests totales con labels (status, endpoint)
- Histograma de latencia por endpoint
- Métricas de negocio de las operaciones críticas del servicio
- Métricas de recursos (CPU, memoria) via exporters estándar
- Sin etiquetas de alta cardinalidad (user_id, request_id)
Trazas
- Instrumentación automática configurada (HTTP, DB, queues)
- Spans manuales en las operaciones de negocio principales
- Atributos de negocio en spans críticos (user_id, order_id, etc.)
- Context propagation habilitada (W3C TraceContext headers)
- Sampling configurado apropiadamente para el volumen de producción
SLOs
- SLI de disponibilidad definido y medido
- SLI de latencia definido y medido
- SLO establecido con baseline histórico o conservador
- Alertas de burn rate configuradas (al menos P1 y P2)
- Dashboard de SLO accesible a todo el equipo
Alertas
- Al menos una alerta de síntoma para el servicio (no solo causas)
- Runbook vinculado a cada alerta
- Alertas testeadas (se dispararon en staging al introducir el fallo)
- Severidad y routing correctamente configurados
Documentación operativa
- Runbook de los incidentes más comunes
- Architecture diagram actualizado con dependencias del servicio
- Contacto del equipo owner en los metadatos del servicio
Anti-patrones de diseño que impiden la observabilidad
Anti-patrón 1: Logging de todo o de nada
Algunos equipos logean absolutamente todo (incluyendo cada línea de código de debug) o prácticamente nada (solo errores críticos). Ambos extremos son ineficientes.
La regla: logea eventos, no estados. “User authentication succeeded” (evento) es útil. “User object: {id: 1, name: ‘Juan’, role: ‘admin’, preferences: {…}}” (estado completo) es ruido.
Anti-patrón 2: Asumir que el sistema funciona si no hay errores
Un sistema puede estar procesando transacciones a 10% de su velocidad normal, sin errores, y ser un incidente serio. Sin métricas de throughput, no sabrías hasta que los usuarios se quejaran.
Siempre mide el volumen de operaciones completadas, no solo la tasa de error.
Anti-patrón 3: Health checks que no son representativos
// ❌ Health check inútil - solo verifica que el proceso está corriendo
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// ✅ Health check real - verifica las dependencias críticas
app.get('/health', async (req, res) => {
const checks = await Promise.allSettled([
db.query('SELECT 1'),
redis.ping(),
stripe.ping(),
]);
const healthy = checks.every(c => c.status === 'fulfilled');
const details = {
database: checks[0].status,
cache: checks[1].status,
payment_gateway: checks[2].status,
};
res.status(healthy ? 200 : 503).json({
status: healthy ? 'healthy' : 'degraded',
checks: details
});
});
Anti-patrón 4: Trazas solo en el happy path
Es tentador solo añadir spans a los flujos que “importan”. Pero los flujos de error son exactamente los que más necesitan trazas detalladas.
// ❌ Solo el happy path está instrumentado
async function processPayment(order) {
return withSpan('processPayment', async () => {
const result = await stripe.charge(order);
return result;
// Si esto falla, no hay información adicional del error
});
}
// ✅ Paths de error también instrumentados
async function processPayment(order) {
return withSpan('processPayment', async (span) => {
span.setAttribute('payment.gateway', order.gateway);
span.setAttribute('payment.amount_cents', order.amountCents);
try {
const result = await stripe.charge(order);
span.setAttribute('payment.transaction_id', result.id);
return result;
} catch (error) {
// Información detallada del error en el span
span.setAttribute('error.type', error.constructor.name);
span.setAttribute('error.stripe_code', error.code);
span.setAttribute('error.decline_code', error.decline_code);
span.recordException(error);
throw error;
}
});
}
El futuro: observabilidad continua
La frontera actual de la observabilidad va más allá del debugging reactivo:
Anomaly Detection automática: Sistemas de ML que aprenden el comportamiento normal y alertan en anomalías sin necesitar umbrales manuales.
Observabilidad en CI/CD: Ejecutar tests de performance y observabilidad en cada PR. Si el p99 de una función crítica aumenta un 20%, el PR es rechazado automáticamente.
Correlación automática de cambios: Relacionar automáticamente un deploy con un cambio en las métricas. “El error rate aumentó justo después de este PR” sin que nadie tenga que buscarlo.
Análisis de causa raíz asistido por IA: Herramientas que, dado un incidente, sugieren automáticamente las causas más probables basadas en los datos de telemetría.
Cierre: los principios que perviven
Más allá de las herramientas específicas — que cambiarán en los próximos años — estos principios son permanentes:
-
La observabilidad es una propiedad del sistema, no una herramienta: Instrumenta bien y puedes cambiar de backend cuando quieras.
-
Los tres pilares son complementarios: Logs para el detalle, métricas para las tendencias, trazas para el flujo. Necesitas los tres.
-
Alerta en síntomas, investiga causas: Las alertas deben corresponder a experiencia del usuario, no a estados de la infraestructura.
-
Los SLOs son el lenguaje común entre ingeniería y negocio: Permiten tomar decisiones objetivas sobre el balance entre innovación y confiabilidad.
-
La cultura blameless es prerequisito: Sin ella, los errores se ocultan y no se aprende de ellos.
-
La observabilidad es una inversión que se paga sola: Equipos que invierten en ella reducen su MTTD/MTTR drásticamente y tienen ingenieros más felices.
Referencias finales del tutorial
- Observability Engineering — Charity Majors, Liz Fong-Jones, George Miranda (O’Reilly, 2022)
- Google SRE Book — gratuito online
- Google SRE Workbook — gratuito online
- OpenTelemetry Documentation
- CNCF Observability TAG Whitepaper
- Grafana Observatory — Observability Glossary
- Brendan Gregg — Systems Performance (2nd Edition)
- Honeycomb Learning Center
- The New Stack — Observabilidad