ADR-019 · Error taxonomy + logging spec (structlog JSON, sampling, retention)¶
Fecha: 2026-04-29 Source:
/srv/projects/cis/cis-plan/DECISIONS.md(do not edit here — re-split desde la fuente)
Contexto
CIS opera 16 servicios live, cada uno con su propia política de logging. Algunos usan print() (cis-isometrico CLI), otros structlog parcial (cis-platform, cis-mailer), otros logging stdlib en plain text (cis-claudia v1, cis-admin legacy). No hay correlación de errores cross-service: un fallo en checkout que toca cis-pagos → cis-platform → cis-mailer produce 3 logs sin request_id compartido, lo que hace imposible reconstruir el flujo cuando un user reporta "no me llegó el mail".
Adicionalmente, el bug del P0.2 (except ImportError: pass en cis-core que ocultó la caída de MCP tools por 8 días) demuestra que la prohibición debe ser explícita en el ADR canónico: silenciar excepciones sin loguearlas es código inseguro en cualquier servicio T0–T2.
La constitución (§10.4 y §10.5) menciona structlog JSON pero no fija las keys obligatorias, las políticas de sampling ni la retención. Este ADR cierra el gap.
Alternativas consideradas
- (a) Plain text + journalctl, sin estandarización
- Pro: cero overhead, journalctl es el default systemd.
-
Contra: imposible parsear cross-service. No se puede correlacionar request_id. No hay queryability estructurada.
-
(b) Structlog JSON + keys canónicas + clasificación de excepciones ← propuesta
- Pro: cada log es una row JSON queryable. journalctl
-o json+jqpermite búsqueda cross-service. Clasificación de excepciones permite alerting selectivo (ServerError → page; ClientError → log nada más). -
Contra: cada servicio debe adoptar la configuración. Costo inicial ~1h por servicio.
-
(c) Stack ELK / OpenTelemetry / Sentry
- Pro: features avanzadas (tracing distribuido, alerting nativo, dashboards).
- Contra: complejidad operacional alta para CIS (otro servicio que mantener). Costo $$. journalctl + cis-monitoreo ya cubre 80% del use case con $0.
Decisión
Logging structlog JSON con keys canónicas obligatorias para todo servicio Python:
{
"timestamp": "2026-04-29T19:23:11.234Z",
"level": "INFO",
"service": "cis-pagos",
"event": "order_created",
"request_id": "req_<uuid>",
"user_id": "<authentik_uuid>",
"session_id": "<authentik_session_id>",
"error_code": null,
"error_class": null,
"duration_ms": 142,
...payload específico
}
Keys obligatorias en cada log: timestamp, level, service, event, request_id. Keys condicionales: user_id y session_id si la request pasó por OIDC; error_code y error_class si level >= WARN.
Sampling:
- DEBUG — 1% en producción (sample por request_id hash). 100% en staging/dev.
- INFO — 100% siempre.
- WARN, ERROR, CRITICAL — 100% siempre.
Retention:
- journalctl local (vps-cis): 90 días default (SystemMaxUse=2G, rotación auto).
- Archive externo (output de journalctl --output=json | gzip cron diario): 365 días en /srv/log-archive/<service>/<yyyy-mm>.json.gz. Compresión típica ~95%.
- Estimación: 16 servicios × ~50MB/día comprimidos → ~24GB/año. Cabe en el VPS.
Taxonomía de excepciones:
class CISError(Exception):
"""Base. Nunca raisearla directamente."""
error_code: str # 'CLIENT.AUTH.EXPIRED', 'SERVER.DB.CONN_LOST', etc.
class ClientError(CISError):
"""Causada por input/estado del cliente. HTTP 4xx. No alertable."""
class ServerError(CISError):
"""Causada por bug interno. HTTP 5xx. Alerta a cis-inbox vía cis-monitoreo."""
class IntegrationError(CISError):
"""Servicio externo (SII, Resend, Flow, Google) caído o rechazó. HTTP 502. Alerta + retry."""
Mapping HTTP en core/py-common/errors.py (ADR-016): ClientError → 400, auth-related ClientError → 401/403, not-found ClientError → 404, ServerError → 500, IntegrationError → 502/503.
Prohibición canon: except ImportError: pass, except Exception: pass, y cualquier except sin al menos log.error(...) o log.exception(...) están prohibidos en código de servicios. Ruff TRY203/TRY004/BLE001 enforcing en pre-commit (ADR-018). Excepción única documentada: try: ... except (KeyboardInterrupt, SystemExit): raise — el silencio es semántico ahí.
Consecuencias
- Positivo: cross-service tracing posible vía
request_id(generado en gatewaycis-platformy propagado por headerX-Request-ID). Alerting selectivo víaerror_classreduce ruido. La prohibición explícita captura el bug del P0.2 a futuro. - Negativo: cada servicio debe adoptar
core/py-common/logging.pyy propagarrequest_id. Costo P0.4 + adopción gradual. - Riesgo: keys obligatorias mal pobladas (
user_idausente en logs autenticados) reducen utilidad. Mitigación: dependency injection de logging en routers FastAPI víaDepends(get_logger)que ya recibe el JWT decoded. - Migración: servicios existentes migran cuando toquen
app/main.pyoapp/logging.py. T0 bloqueante en P0.4. T2 (cis-admin, cis-inbox, cis-pagos, cis-claudia, cis-monitoreo) durante Bloque F. T3 cuando estabilizen. - Compatibilidad:
journalctl -o jsonya funciona; cualquier herramienta de inspección actual sigue válida. La keyeventes estable (no se renombra) — si alguna parsing externa la consume, queda intacto. - Dependencia de ADR-016:
core/py-common/logging.pyes el módulo único que configura structlog. Sin esa lib, cada servicio reimplementa y diverge.