Capítulo 22: Observabilidad Distribuida
Capítulo 22: Observabilidad Distribuida
“No puedes arreglar lo que no puedes ver”
Introduccion
En sistemas distribuidos con sagas, un pedido puede pasar por 5 servicios diferentes. Cuando algo falla, necesitamos responder: ¿Donde fallo? ¿Por que? ¿Cuanto tardo? ¿Esta afectando a otros usuarios?
Observabilidad es la capacidad de entender el estado interno de un sistema a traves de sus salidas externas. Se basa en tres pilares complementarios que veremos en este capitulo.
Herramientas que usaremos:
- OpenTelemetry: Estandar para instrumentacion
- Prometheus: Base de datos de metricas
- Grafana: Visualizacion y dashboards
- Pino: Logging estructurado para Node.js
Pilares de Observabilidad
Los tres pilares proporcionan diferentes perspectivas del sistema:
┌─────────────┬─────────────┬─────────────┐
│ Logs │ Metrics │ Traces │
├─────────────┼─────────────┼─────────────┤
│ Qué pasó │ Cuánto │ Dónde │
│ Errores │ Latencias │ Flujo │
│ Contexto │ Throughput │ Dependencias│
└─────────────┴─────────────┴─────────────┘
OpenTelemetry Setup
OpenTelemetry (OTel) es un estandar open-source para instrumentacion que unifica tracing, metricas y logs. Soporta multiples backends (Jaeger, Zipkin, Datadog).
La configuracion inicial:
- Resource: Identifica el servicio (nombre, version)
- TraceExporter: Envia traces al collector (usamos OTLP HTTP)
- Instrumentations: Auto-instrumenta librerias comunes (HTTP, Express, pg)
Una vez configurado, OTel automaticamente captura traces de llamadas HTTP, queries de base de datos, etc.
// observability/tracing.ts
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 { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'order-service'
}),
traceExporter: new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces'
}),
instrumentations: [getNodeAutoInstrumentations()]
});
sdk.start();
Tracing de Sagas
El tracing permite seguir una solicitud a traves de multiples servicios. Cada operacion crea un span (segmento de tiempo) que se anida en una estructura jerarquica.
Conceptos clave:
- Tracer: Crea spans para operaciones
- Span: Representa una unidad de trabajo con inicio, fin y metadata
- startActiveSpan: Crea un span y lo establece como el span actual
- SpanStatusCode: Indica si la operacion fue OK o ERROR
- recordException: Registra detalles del error para debugging
El span padre saga.execute contiene spans hijos para cada step, creando una vista completa del flujo.
// saga/traced-orchestrator.ts
import { trace, SpanStatusCode, context, propagation } from '@opentelemetry/api';
import { SagaOrchestrator } from './orchestrator';
import { SagaStep, SagaContext } from './types';
const tracer = trace.getTracer('saga-orchestrator');
export class TracedSagaOrchestrator<T extends SagaContext> extends SagaOrchestrator<T> {
async execute(ctx: T): Promise<any> {
return tracer.startActiveSpan('saga.execute', async (span) => {
span.setAttribute('saga.id', this.sagaId);
span.setAttribute('saga.type', 'CREATE_ORDER');
try {
for (const step of this.steps) {
await this.executeTracedStep(step, ctx);
}
span.setStatus({ code: SpanStatusCode.OK });
return { status: 'completed', context: ctx };
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: (error as Error).message });
span.recordException(error as Error);
await this.compensateTraced(ctx);
throw error;
} finally {
span.end();
}
});
}
private async executeTracedStep(step: SagaStep<T>, ctx: T): Promise<void> {
return tracer.startActiveSpan(`saga.step.${step.name}`, async (span) => {
span.setAttribute('step.name', step.name);
try {
await step.execute(ctx);
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR });
span.recordException(error as Error);
throw error;
} finally {
span.end();
}
});
}
private async compensateTraced(ctx: T): Promise<void> {
return tracer.startActiveSpan('saga.compensate', async (span) => {
for (const result of this.state.completedSteps.reverse()) {
const step = this.steps.find(s => s.name === result.name);
if (step) {
await tracer.startActiveSpan(`saga.compensate.${step.name}`, async (stepSpan) => {
await step.compensate(ctx);
stepSpan.end();
});
}
}
span.end();
});
}
}
Metricas con Prometheus
Prometheus es una base de datos de series temporales optimizada para metricas. Los tipos de metricas principales son:
- Counter: Valor que solo incrementa (sagas completadas, errores totales)
- Gauge: Valor que puede subir o bajar (sagas activas actualmente)
- Histogram: Distribucion de valores (duraciones, con percentiles)
Las labels (etiquetas) permiten filtrar y agrupar metricas. Por ejemplo, saga_type permite ver metricas por tipo de saga.
Los buckets del histograma definen los rangos para calcular percentiles (p50, p95, p99).
// observability/metrics.ts
import { Counter, Histogram, Gauge, Registry } from 'prom-client';
const register = new Registry();
export const sagaMetrics = {
started: new Counter({
name: 'saga_started_total',
help: 'Total sagas started',
labelNames: ['saga_type'],
registers: [register]
}),
completed: new Counter({
name: 'saga_completed_total',
help: 'Total sagas completed successfully',
labelNames: ['saga_type'],
registers: [register]
}),
failed: new Counter({
name: 'saga_failed_total',
help: 'Total sagas failed',
labelNames: ['saga_type', 'failed_step'],
registers: [register]
}),
duration: new Histogram({
name: 'saga_duration_seconds',
help: 'Saga execution duration',
labelNames: ['saga_type', 'status'],
buckets: [0.1, 0.5, 1, 2, 5, 10, 30],
registers: [register]
}),
activeCount: new Gauge({
name: 'saga_active_count',
help: 'Currently running sagas',
labelNames: ['saga_type'],
registers: [register]
}),
stepDuration: new Histogram({
name: 'saga_step_duration_seconds',
help: 'Individual step duration',
labelNames: ['saga_type', 'step_name'],
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5],
registers: [register]
})
};
export { register };
Integracion de Metricas
El orquestador instrumentado registra metricas en puntos clave:
- Al iniciar: Incrementa
startedyactiveCount - Al completar: Incrementa
completed, registra duracion con status ‘completed’ - Al fallar: Incrementa
failedcon el step que fallo, registra duracion con status ‘failed’ - Siempre: Decrementa
activeCount(enfinally)
El patron try/catch/finally garantiza que las metricas se registren correctamente incluso con errores.
// saga/metered-orchestrator.ts
import { sagaMetrics } from '../observability/metrics';
export class MeteredSagaOrchestrator<T> {
async execute(ctx: T): Promise<any> {
const sagaType = 'CREATE_ORDER';
const startTime = Date.now();
sagaMetrics.started.inc({ saga_type: sagaType });
sagaMetrics.activeCount.inc({ saga_type: sagaType });
try {
const result = await this.runSteps(ctx);
sagaMetrics.completed.inc({ saga_type: sagaType });
sagaMetrics.duration.observe(
{ saga_type: sagaType, status: 'completed' },
(Date.now() - startTime) / 1000
);
return result;
} catch (error) {
sagaMetrics.failed.inc({
saga_type: sagaType,
failed_step: this.state.currentStep.toString()
});
sagaMetrics.duration.observe(
{ saga_type: sagaType, status: 'failed' },
(Date.now() - startTime) / 1000
);
throw error;
} finally {
sagaMetrics.activeCount.dec({ saga_type: sagaType });
}
}
}
Logging Estructurado
El logging estructurado usa JSON en lugar de texto plano, facilitando busquedas y analisis automatizado.
Pino es una biblioteca de logging para Node.js conocida por su rendimiento. Caracteristicas:
- Child loggers: Crean loggers con contexto adicional (sagaId, sagaType)
- Levels: debug, info, warn, error para filtrar severidad
- Base fields: Campos que aparecen en todos los logs (service, version)
El contexto (sagaId, step name, duracion) en cada log permite correlacionar eventos relacionados.
// observability/logger.ts
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => ({ level: label })
},
base: {
service: 'order-service',
version: process.env.APP_VERSION
}
});
export function createSagaLogger(sagaId: string, sagaType: string) {
return logger.child({ sagaId, sagaType });
}
// Uso en orchestrator
export class LoggedSagaOrchestrator<T> {
private log = createSagaLogger(this.sagaId, 'CREATE_ORDER');
async execute(ctx: T): Promise<any> {
this.log.info({ context: ctx }, 'Saga started');
try {
for (const step of this.steps) {
this.log.info({ step: step.name }, 'Step started');
const stepStart = Date.now();
await step.execute(ctx);
this.log.info({
step: step.name,
duration: Date.now() - stepStart
}, 'Step completed');
}
this.log.info('Saga completed');
return { status: 'completed' };
} catch (error) {
this.log.error({ error }, 'Saga failed');
throw error;
}
}
}
Dashboard Grafana
Grafana visualiza metricas de Prometheus en dashboards interactivos. Cada panel muestra una metrica o combinacion:
- Saga Throughput: Tasa de sagas completadas por minuto (
rate()) - Duration P95: Percentil 95 de duracion (el 95% de sagas son mas rapidas)
- Active Sagas: Numero de sagas ejecutandose ahora
- Failure Rate: Porcentaje de sagas que fallan
Las consultas usan PromQL (Prometheus Query Language), un lenguaje para consultar y transformar metricas.
{
"dashboard": {
"title": "Saga Monitoring",
"panels": [
{
"title": "Saga Throughput",
"type": "graph",
"targets": [{
"expr": "rate(saga_completed_total[5m])",
"legendFormat": "{{saga_type}}"
}]
},
{
"title": "Saga Duration P95",
"type": "graph",
"targets": [{
"expr": "histogram_quantile(0.95, rate(saga_duration_seconds_bucket[5m]))",
"legendFormat": "p95"
}]
},
{
"title": "Active Sagas",
"type": "gauge",
"targets": [{
"expr": "saga_active_count"
}]
},
{
"title": "Failure Rate",
"type": "stat",
"targets": [{
"expr": "rate(saga_failed_total[5m]) / rate(saga_started_total[5m]) * 100"
}]
}
]
}
}
Alertas
Las alertas notifican automaticamente cuando algo sale mal. Se definen con:
- expr: Condicion PromQL que dispara la alerta
- for: Cuanto tiempo debe cumplirse la condicion antes de alertar (evita falsas alarmas)
- severity: Nivel de urgencia (warning, critical)
- annotations: Mensaje descriptivo con variables
Las alertas tipicas para sagas:
- Tasa de fallos alta (>10%)
- Latencia excesiva (p95 > 30s)
- Backlog creciente (muchas sagas activas)
# alerts.yml
groups:
- name: saga-alerts
rules:
- alert: SagaHighFailureRate
expr: rate(saga_failed_total[5m]) / rate(saga_started_total[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "High saga failure rate ({{ $value | humanizePercentage }})"
- alert: SagaSlowExecution
expr: histogram_quantile(0.95, rate(saga_duration_seconds_bucket[5m])) > 30
for: 5m
labels:
severity: warning
annotations:
summary: "Saga P95 latency is {{ $value }}s"
- alert: SagaBacklog
expr: saga_active_count > 100
for: 2m
labels:
severity: warning
annotations:
summary: "{{ $value }} sagas currently running"
Resumen
- OpenTelemetry para tracing distribuido
- Prometheus para metricas
- Pino para logging estructurado
- Grafana para dashboards
- Alertas para deteccion temprana de problemas
Glosario
Observabilidad
Definicion: Capacidad de entender el estado interno de un sistema basandose en sus salidas externas (logs, metricas, traces).
Por que es importante: En sistemas distribuidos, no puedes debuggear con un debugger tradicional. La observabilidad proporciona visibilidad necesaria para diagnosticar problemas.
Ejemplo practico: Una saga falla intermitentemente. Con observabilidad, ves que solo falla cuando el servicio de pagos tiene latencia >2s, identificando la causa raiz.
Distributed Tracing
Definicion: Tecnica para rastrear una solicitud a traves de multiples servicios, creando una vista unificada del flujo completo.
Por que es importante: Cuando un pedido pasa por 5 servicios, tracing muestra exactamente donde esta el cuello de botella o donde ocurrio un error.
Ejemplo practico: El trace de una saga muestra: Order Service (50ms) -> Inventory (200ms) -> Payment (ERROR despues de 5s timeout).
Span
Definicion: Unidad fundamental de tracing que representa una operacion con nombre, tiempo de inicio, duracion y metadata.
Por que es importante: Los spans se anidan creando un arbol que muestra la jerarquia de operaciones y permite identificar que parte de una operacion es lenta.
Ejemplo practico: El span saga.execute tiene hijos saga.step.create_order (100ms) y saga.step.process_payment (2s), mostrando que el pago es lento.
OpenTelemetry
Definicion: Estandar open-source que unifica la recoleccion de traces, metricas y logs, con SDKs para multiples lenguajes y exporters para diferentes backends.
Por que es importante: Evita vendor lock-in - instrumentas una vez y puedes enviar datos a Jaeger, Datadog, New Relic o cualquier backend compatible.
Ejemplo practico: Instrumentas con OTel y exportas a Jaeger en desarrollo. En produccion, cambias solo la configuracion para usar Datadog.
Prometheus
Definicion: Base de datos de series temporales optimizada para metricas, con un lenguaje de consulta (PromQL) y modelo pull donde scrapea endpoints de metricas.
Por que es importante: Es el estandar de facto para metricas en Kubernetes, con excelente integracion con Grafana y soporte para alertas.
Ejemplo practico: Prometheus scrapea /metrics de cada servicio cada 15 segundos, almacenando series como saga_duration_seconds{saga_type="CREATE_ORDER"}.
Histogram
Definicion: Tipo de metrica que agrupa valores en buckets, permitiendo calcular percentiles (p50, p95, p99) y estadisticas de distribucion.
Por que es importante: El promedio puede ocultar problemas; p99 muestra la experiencia del 1% peor de usuarios, crucial para SLAs.
Ejemplo practico: Promedio de duracion es 500ms (parece bien), pero p99 es 10s, indicando que 1 de cada 100 usuarios tiene mala experiencia.
PromQL
Definicion: Lenguaje de consulta de Prometheus para seleccionar, agregar y transformar series de metricas.
Por que es importante: Permite crear consultas sofisticadas para dashboards y alertas, como tasa de errores o percentiles de latencia.
Ejemplo practico: rate(saga_failed_total[5m]) / rate(saga_started_total[5m]) * 100 calcula el porcentaje de fallos en los ultimos 5 minutos.
Logging Estructurado
Definicion: Practica de escribir logs en formato estructurado (JSON) en lugar de texto plano, incluyendo contexto como campos clave-valor.
Por que es importante: Permite busquedas precisas (“todos los logs del saga-123”), agregacion automatica y analisis con herramientas como ELK o Loki.
Ejemplo practico: {"level":"error","sagaId":"123","step":"payment","error":"timeout","duration":5002} vs ERROR: Payment failed for saga 123.
SLA/SLO
Definicion: Service Level Agreement (SLA) es un contrato con usuarios; Service Level Objective (SLO) es el objetivo interno que garantiza cumplir el SLA.
Por que es importante: Define que significa “funciona bien” en terminos medibles (99.9% disponibilidad, p95 < 2s), guiando decisiones de ingenieria.
Ejemplo practico: SLA dice “99.9% de sagas completan en <30s”. Alertas disparan al 99.5% para tener margen de correccion antes de incumplir.
← Capítulo 21: Testing | Capítulo 23: Deployment en Kubernetes →