Skip to content

Plataforma técnica · Orchestrator

Observabilidad

Orchestrator Observabilidad Audit

La observabilidad del Orchestrator es infraestructura cross-cutting: vive fuera del dominio Command y atraviesa todas las peticiones HTTP. Se compone de dos subsistemas complementarios:

ComponenteVive enNaturaleza
auditLoggerlib/audit.tsMiddleware + API explícita.
metricsMiddlewaremiddleware/metrics.tsMiddleware + API explícita.
MonitoringService (dashboard)domain/command/MonitoringService.tsConsumidor: lee los agregados que estos dos producen.

El Monitoring Dashboard del Command Center es la capa de presentación sobre esta infraestructura — no la implementa, solo la lee desde monitoring.v_dashboard_summary.

Ambos subsistemas son singletons que se enganchan al pipeline de middleware de Express. Mantienen estado en memoria (cola/contadores) y persisten por lotes a tablas PostgreSQL bajo el esquema monitoring en nostromo_command.

flowchart TB

    %% =========================
    %% Pila de Middleware Express
    %% =========================
    subgraph Express["Pila de Middleware Express"]
        TrustProxy[trust proxy]
        HTTPS[Redirección HTTPS]
        Helmet[seguridad helmet]
        Limiter[globalLimiter]
        CORS[cors]
        Morgan[registro morgan]
        RequestMW[requestMiddleware]
        MetricsMW[metricsMiddleware]
        AuditMW[auditMiddleware]
        Routes[Rutas API]

        TrustProxy --> HTTPS
        HTTPS --> Helmet
        Helmet --> Limiter
        Limiter --> CORS
        CORS --> Morgan
        Morgan --> RequestMW
        RequestMW --> MetricsMW
        MetricsMW --> AuditMW

    end
    %% =========================
    %% Bases de datos
    %% =========================
    subgraph DBs["PostgreSQL · nostromo_command"]
        AuditDB[(monitoring.audit_log)]
        MetricsDB[(monitoring.system_metrics)]
    end

    %% =========================
    %% Sistema de Auditoría
    %% =========================
    subgraph AuditSys["Sistema de Auditoría"]
        AuditLogger[AuditLogger Singleton]
        AuditAPI[API auditLog]
        AuditBuffer[Cola en Memoria]
        AuditTimer[Flush 5s o 50 entradas]

        AuditLogger --> AuditAPI
        AuditAPI --> AuditBuffer
        AuditBuffer --> AuditTimer
        AuditTimer -->|INSERT por lotes| AuditDB
    end

    %% =========================
    %% Sistema de Métricas
    %% =========================
    subgraph MetricsSys["Sistema de Métricas"]
        MetricsCollector[MetricsCollector Singleton]
        MetricsAPI[API metrics]
        MetricsStore[Counters / Gauges / Histograms]
        MetricsTimer[Flush 60s]
        MetricsEndpoint[GET /metrics]

        MetricsCollector -->|Sistema cada 30s| MetricsStore
        MetricsAPI --> MetricsStore
        MetricsStore --> MetricsTimer
        MetricsTimer -->|INSERT por lotes| MetricsDB
        MetricsCollector --> MetricsEndpoint
    end

    MetricsMW -->|Rastrear petición| MetricsCollector
    AuditMW -->|Registrar mutación| AuditLogger
    Routes -->|Llamadas explícitas| AuditAPI
    Routes -->|Llamadas explícitas| MetricsAPI
    AuditMW --> Routes

El orden importa:

  1. Antes de autenticación: métricas y audit capturan todos los intentos, incluso fallos de login. El auditMiddleware excluye explícitamente /api/auth/login (que se loguea manualmente con éxito/fallo desde el handler).
  2. Después de requestIdMiddleware: ambas operan con req.id (correlación).
  3. MetricsMW antes que AuditMW: para medir el rendimiento “puro” sin contar el costo del audit.

Ambos sistemas registran un hook process.on('beforeExit') para vaciar el buffer:

shutdown
process.on('beforeExit', async () => {
await auditLogger.shutdown(); // Flush entradas pendientes
await metrics.shutdown(); // Flush métricas pendientes
});

Garantiza que un SIGTERM ordenado (Docker stop, systemd, Cloudflare cycle) no pierda los últimos eventos.

auditExample.ts
import { auditLog, extractAuditContext } from '@/lib/audit';
const ctx = extractAuditContext(req);
await centralPool.query('INSERT INTO employees ...');
auditLog.insert('remuneraciones.employees', newEmployee, ctx);
metricsExample.ts
const timer = createTimer();
const result = await payrollEngine.calculate(input);
timer.observe('payroll_calculation_duration_ms', {
employee_count: input.employees.length
});