Plan de Instrumentación Generado

NutriFlow — Observabilidad e Instrumentación en Producción

Plan completo: logging estructurado · métricas RED/USE · trazado OpenTelemetry · alertas por síntomas

Servicio checkout-service / subscription-worker
Stack Node.js · TypeScript · Express
Proveedor MercadoPago
Fecha 2026-06-15

Contexto del Problema

El módulo de Plan de Pagos de NutriFlow recibe quejas de "el pago no funcionó" sin ninguna telemetría que permita diagnosticar la causa. El equipo opera ciego: solo hay console.log no estructurados, sin correlation IDs ni dashboards. Este plan establece la instrumentación mínima viable para producción desde el primer deploy.

1Preguntas de On-Call — Antes de Instrumentar
Pregunta 1

¿Qué porcentaje de cobros iniciales fallan y cuál es la razón (timeout, fondos insuficientes, tarjeta inválida)?

métrica log
Pregunta 2

¿Cuánto tarda MercadoPago en responder en p95/p99? ¿Hay degradación durante horas pico?

métrica traza
Pregunta 3

¿El worker nocturno procesó todos los registros o hubo errores silenciosos que dejaron suscripciones en estado incorrecto?

log métrica alerta
Pregunta 4

¿Una solicitud de pago fallida puede rastrearse end-to-end (log → traza → span de proveedor) sin leer el código fuente?

traza log
2Mapa de Señales — Log vs Métrica vs Traza
Pregunta Señal Qué responde Ejemplo concreto
¿Por qué falló este cobro específico? log "¿Qué pasó en este caso?" payment_failed con errorCode: MP_INSUFFICIENT_FUNDS
¿Cuántos cobros fallan por hora? métrica "¿Con qué frecuencia?" subscriptions_payment_total{status="failed"}
¿MercadoPago está más lento de lo normal? métrica "¿Cuánto tarda, en agregado?" Histograma mercadopago_request_duration_seconds p99
¿Dónde se fue el tiempo en un pago lento? traza "¿Dónde exactamente?" Span applyDiscountchargeProviderupdateSubscription
¿El worker acabó con errores? log + alerta "¿Qué pasó?" + "¿Me avisa?" worker_run_completed con failedCount > 0 → alerta ticket
3Logging Estructurado — Código TypeScript
📄 src/middleware/request-context.ts
TS
// Correlation ID por request — obligatorio en toda la stack
import pino from 'pino';
import crypto from 'crypto';

export const logger = pino({ level: 'info', formatters: { level: (label) => ({ level: label }) } });

export function requestContextMiddleware(req, res, next) {
  const requestId = req.headers['x-request-id'] ?? crypto.randomUUID();
  req.id  = requestId;
  req.log = logger.child({ requestId, service: 'checkout-service' });
  res.setHeader('x-request-id', requestId);
  next();
}
📄 src/services/subscription.service.ts
TS
async function createSubscription(req: Request, dto: CreateSubscriptionDto) {
  const { log } = req;

  log.info({ event: 'subscription_create_started', clinicId: dto.clinicId,
    planId: dto.planId, amount: dto.amountCents }, 'subscription create started');

  try {
    const charge = await mercadopago.charge({ amount: dto.amountCents, token: dto.cardToken });

    log.info({ event: 'payment_succeeded', clinicId: dto.clinicId,
      chargeId: charge.id, provider: 'mercadopago', attempt: 1
    }, 'initial payment succeeded');

    return await db.createSubscription({ ...dto, chargeId: charge.id, status: 'active' });

  } catch (err) {
    log.warn({ event: 'payment_failed', clinicId: dto.clinicId,
      provider: 'mercadopago', errorCode: err.code,  // MP_INSUFFICIENT_FUNDS, MP_INVALID_CARD...
      errorType: err.type, attempt: 1
    }, 'initial payment failed');     // warn: degraded pero manejado
    throw err;
  }
}

// Worker nocturno — siempre loguear resumen al final
async function processExpiredSubscriptions() {
  const start = Date.now();
  let processed = 0, failed = 0;

  for (const sub of await db.findExpiring()) {
    try {
      await renewSubscription(sub.id);
      processed++;
    } catch (e) {
      failed++;
      logger.error({ event: 'subscription_renewal_error', subscriptionId: sub.id,
        errorCode: e.code }, 'renewal failed');
    }
  }

  logger.info({ event: 'worker_run_completed', processedCount: processed,
    failedCount: failed, durationMs: Date.now() - start }, 'worker finished');
}
4Métricas RED/USE — Endpoints y Dependencias
Rate (pagos/min)
47.3
subscriptions created · últimos 5 min
Error Rate
3.2%
MP_INSUFFICIENT_FUNDS · 68% del total errors
Duration p99 MercadoPago
1.84s
SLO objetivo: < 1.5s · violación activa
📄 src/metrics/index.ts
TS
import { Histogram, Counter, register } from 'prom-client';

// RED: Rate + Errors + Duration por endpoint
export const httpDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'Latencia de requests HTTP',
  labelNames: ['method', 'route', 'status_class'],  // '2xx' no '200' — cardinalidad fija
  buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});

// RED específico de MercadoPago — dependencia externa crítica
export const mpDuration = new Histogram({
  name: 'mercadopago_request_duration_seconds',
  help: 'Latencia de llamadas al proveedor MercadoPago',
  labelNames: ['operation', 'status'],   // 'charge'|'refund' · 'success'|'failed'
  buckets: [0.1, 0.25, 0.5, 1, 1.5, 3, 5],
});

export const paymentTotal = new Counter({
  name: 'subscriptions_payment_total',
  help: 'Total de intentos de cobro por resultado',
  labelNames: ['status', 'error_code'],   // error_code: MP_INSUFFICIENT_FUNDS, etc.
});

export const workerErrors = new Counter({
  name: 'subscription_worker_errors_total',
  help: 'Errores del worker de renovación nocturna',
  labelNames: ['error_code'],
});

// NUNCA como label: userId, email, requestId, URL completa, mensaje de error libre
5Trazado Distribuido — OpenTelemetry
📄 src/tracing.ts — importar ANTES que cualquier otro módulo
TS
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';

const sdk = new NodeSDK({
  serviceName: 'checkout-service',
  traceExporter: new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT }),
  instrumentations: [getNodeAutoInstrumentations()],  // cubre HTTP, PG, Redis automáticamente
});
sdk.start();
📄 src/services/subscription.service.ts — spans manuales
TS
import { trace, context } from '@opentelemetry/api';

const tracer = trace.getTracer('checkout-service');

async function createSubscription(req, dto) {
  return tracer.startActiveSpan('subscription.create', async (span) => {
    span.setAttributes({
      'clinic.id': dto.clinicId,
      'subscription.plan_id': dto.planId,
      'payment.amount_cents': dto.amountCents,
    });

    const charge = await tracer.startActiveSpan('mercadopago.charge', async (mpSpan) => {
      try {
        const result = await mercadopago.charge(dto);
        mpSpan.setAttributes({ 'mp.charge_id': result.id, 'mp.status': 'success' });
        return result;
      } catch (e) {
        mpSpan.setAttributes({ 'mp.error_code': e.code, 'mp.status': 'failed' });
        mpSpan.recordException(e);
        throw e;
      } finally {
        mpSpan.end();
      }
    });

    span.end();
    return charge;
  });
}
6Reglas de Alerta — Síntomas, No Causas
PAGE

Tasa de error de pagos elevada

rate(subscriptions_payment_total{status="failed"}[5m]) / rate(subscriptions_payment_total[5m]) > 0.05 for: 5m
Runbook: 1) Revisar logs con event=payment_failed agrupados por errorCode. 2) Comprobar dashboard MercadoPago. 3) Si error_code = MP_PROVIDER_DOWN → escalar a payments@nutriflow.com
PAGE

Latencia p99 de MercadoPago sobre SLO

histogram_quantile(0.99, mercadopago_request_duration_seconds_bucket) > 1.5 for: 10m
Runbook: 1) Consultar tracing UI filtrando por service=checkout-service, span=mercadopago.charge. 2) Comparar latencia de las últimas 24h en el dashboard. 3) Si es generalizado → activar fallback o comunicar degradación.
TICKET

Worker de renovación con errores

increase(subscription_worker_errors_total[1d]) > 0
Runbook: 1) Buscar logs con event=subscription_renewal_error del último run. 2) Listar subscriptionId afectados. 3) Revisar BD — ¿estado inconsistente? 4) Reprocesar manualmente o reintentar en próximo run.
TICKET

Worker nocturno no completó en ventana esperada

absent(worker_run_completed{job="subscription-worker"}[2h]) during: 02:00–05:00 UTC
Runbook: 1) Verificar que el cron job se disparó en el scheduler. 2) Revisar logs de contenedor para fallos de inicio. 3) Si proceso colgado → kill + restart manual.
7Checklist de Verificación Pre-Deploy
!Red Flags Encontrados en el Código Original
Logs con interpolación de string: `Payment ${id} failed for user ${userId}` — no queryable.
Sin correlation IDs: cada línea de log es un huérfano, imposible reconstruir una request.
console.log no estructurado: no se puede filtrar, correlacionar ni alertar sobre él.
Cero métricas en el worker nocturno — errores silenciosos durante semanas.
Sin tracing: la latencia de MercadoPago era invisible; p99 desconocido.
Alertas inexistentes — los usuarios eran el mecanismo de detección de incidentes.