← Volver al listado de tecnologías

Capítulo 22: Observabilidad Distribuida

Por: SiempreListo
sagaobservabilidadopentelemetryprometheusgrafana

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:

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:

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:

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:

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:

  1. Al iniciar: Incrementa started y activeCount
  2. Al completar: Incrementa completed, registra duracion con status ‘completed’
  3. Al fallar: Incrementa failed con el step que fallo, registra duracion con status ‘failed’
  4. Siempre: Decrementa activeCount (en finally)

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:

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:

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:

Las alertas tipicas para sagas:

# 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

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 →