CreaRack-SL

Cola de auditoría C (task #286): ITSM, SLA, notificaciones y tareas de fondo — marcar y notificar en una sola transacción, ventanas de mantenimiento con fail-open, secretos en el log

Cuándo

04-09-2026 · commit 63b62635 (PR #498, v1.106.0). Tercera entrega de la “cola de auditoría” del task #286 sobre el dominio monitoring — la primera (v1.104.0, “cola auditoría A”) cubrió sondas y targets, la segunda (v1.105.0, “cola auditoría B”) cerró CNS e insights de IA; esta cierra ITSM, SLA, notificaciones y tareas de fondo con 13 hallazgos MEDIA propios más 14 adicionales de una revisión en Opus sobre el mismo PR. 41 tests nuevos en tests/api/test_audit_monitoring_c.py.

Síntomas visibles

  1. check_sla_breaches() marcaba TODOS los breaches y enviaba TODOS los webhooks dentro de la misma transacción: un envío lento la mantenía abierta minutos, y cualquier fallo (incluso dentro de NotificationLog.objects.create) revertía todos los flags sla_*_breached ya puestos esa pasada mientras los webhooks que ya habían disparado no se podían “des-enviar” — los mismos insights se re-notificaban cada minuto.
  2. check_escalations() tenía el mismo patrón (marcar + notificar en una transacción) más una N+1: policy.levels.filter(...) descartaba el prefetch_related y relanzaba una query por insight × política cada 2 minutos.
  3. Un AlertEvent nunca se auto-resolvía al cesar la condición — solo un clic humano en “Resolver” ponía resolved_at, así que una alerta transitoria quedaba “activa” en el panel para siempre; el evento agrupado (“N dispositivos afectados”) tampoco se cerraba al caer por debajo del umbral de agrupación, contando el mismo incidente dos veces en una recuperación parcial.
  4. Una ventana de mantenimiento solo suprimía notificaciones si estaba vinculada explícitamente al target — una ventana org-wide (sin targets) suprimía insights pero no el canal de notificaciones.
  5. error_message de una notificación fallida guardaba el texto crudo de la excepción; para Slack/Teams ese texto incluye la URL completa del webhook (el secreto), servida en claro por /notifications/log a cualquier usuario con itsm:view.
  6. El chequeo de ventana de mantenimiento en dispatch_notification envolvía la comprobación en un except: pass — si fallaba, la notificación se enviaba igualmente, abriendo una vía para saltarse la supresión.
  7. Un channel_type desconocido en NotificationChannel.config (fila corrupta o vieja) no llamaba a ningún transporte pero tampoco marcaba fallo: la notificación se perdía sin rastro.
  8. Ventanas de mantenimiento sin exigir zona horaria ni end_at > start_at.
  9. NotificationLog crecía sin purga — y cada chequeo de rate-limit (_tenant_rate_limited) hace COUNT sobre esa tabla.
  10. Las tareas periódicas de Huey de este fichero (SLA, escalado, patrones, expiración, purga, reconciliación) leen/escriben tablas con RLS sin pasar por el middleware que fija app.current_org_id — dependían de un default de rol implícito y no declarado.
  11. detect_recurring_patterns corría todas las organizaciones dentro de una sola transacción: una excepción en la org 3 dejaba la transacción “necesita rollback” y la query de la org 4 reventaba con TransactionManagementError, matando la pasada entera en vez de saltarse solo la org fallida.
  12. Acknowledge-all sin tope de tamaño ni aislamiento por insight.

Causa raíz

El mismo patrón de las dos entregas anteriores de esta cola (A y B): operación de base de datos y llamada de red externa comparten una transacción larga, así que un fallo de red revierte estado que ya era correcto y un éxito de red no se puede deshacer si algo posterior falla. A eso se suma guardas de seguridad con except: pass (fail-open en vez de fail-closed) y tablas de auditoría/rastro sin política de retención desde el día uno.

Fix aplicado

Commit 63b626352ba4a1971d121299719507e341a5f3b4 (PR #498):

  • SLA: collect_sla_breach_candidates() separa el marcado masivo del backlog (rápido, solo DB — lo más viejo de 1 hora se marca sin notificar) de breach_ack_event()/breach_resolve_event(), que notifican un insight a la vez, cada uno en su propia transacción corta, y solo marcan sla_*_breached si dispatch_notification confirma el envío.
  • Escalado: determine_and_mark_escalations() (marcar) y notify_escalation() (notificar) separados igual que SLA; _get_next_escalation_level/get_escalation_status consumen el prefetch_related en memoria en vez de relanzar queries.
  • Alertas: _resolve_open_event()/_resolve_open_events_bulk() cierran el AlertEvent (o el agrupado) cuando la condición deja de cumplirse, con un único mensaje WS alert_resolved por lote en vez de uno por target.
  • Ventanas de mantenimiento: active_maintenance_windows(target, suppress_field=...) unifica la consulta para insights y notificaciones — la supresión de notificaciones ahora cubre también las ventanas org-wide.
  • notification_service: _safe_error_message() guarda solo el tipo de excepción + status HTTP, nunca la URL; el chequeo de ventana de mantenimiento es ahora fail-closed CON traza (NotificationLog(status="failed")) si la comprobación en sí falla; un channel_type desconocido marca status="failed" en vez de perderse.
  • Migración 0034: índice (channel, created_at) sobre NotificationLog + tarea purge_notification_logs (diaria, 02:45, por lotes de 5000) que borra logs de más de 90 días.
  • Las tareas de monitoring/tasks.py envuelven su lectura/escritura en rls_bypass() explícito; detect_recurring_patterns aísla cada organización en su propio transaction.atomic() (savepoint) para que el fallo de una org no tumbe el resto.
  • Acknowledge-all: tope de 500 insights + savepoint por insight.
  • log_action añadido en las mutaciones de alerts/knowledge/groups (ITSM).
  • 41 tests nuevos (tests/api/test_audit_monitoring_c.py). Revisado en Opus. De paso corrige una afirmación errónea del CHANGELOG de la v1.103.0 (decía que Huey estaba “inerte” frente a RLS; no lo estaba — llevaba un default de rol implícito).

Lecciones

Separar “marcar” (DB) de “notificar” (I/O externo) en transacciones distintas es el patrón que se repite en las tres entregas de esta cola (A, B, C): meter una llamada de red dentro de una transacción larga convierte cualquier fallo de red en una reversión de estado que ya era correcto, y cualquier éxito de red en algo que no se puede deshacer si algo posterior falla. Y un except: pass alrededor de una guarda de seguridad es indistinguible de “todo va bien” para quien lee el log — tiene que fallar cerrado y dejar rastro.

Preventivos futuros

  • Revisar el resto de tareas Huey de otros dominios por el mismo patrón marcar+notificar en una única transacción.
  • Cualquier tabla de eventos/auditoría nueva necesita política de retención desde el día 1, no cuando ya pesa.
  • Continuar la cola de auditoría del task #286 sobre los dominios de monitoring que falten.

Véase también

  • [[entity—monitoring—service—notification-service]]
  • [[concept—monitoring—itsm]]
  • [[feature—monitoring—itsm-ciclo-cierre]]
  • [[decision—20260814—itsm-closed-at-semantics]]
  • [[incident—20260904—cola-auditoria-b-cns-insights]]
  • [[feature—monitoring—auditoria-suprema-etapa-3-sa4-sa5]]
  • [[crearack—monitoring—itsm]]