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
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 deNotificationLog.objects.create) revertía todos los flagssla_*_breachedya 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.check_escalations()tenía el mismo patrón (marcar + notificar en una transacción) más una N+1:policy.levels.filter(...)descartaba elprefetch_relatedy relanzaba una query por insight × política cada 2 minutos.- Un
AlertEventnunca se auto-resolvía al cesar la condición — solo un clic humano en “Resolver” poníaresolved_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. - 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.
error_messagede 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/loga cualquier usuario conitsm:view.- El chequeo de ventana de mantenimiento en
dispatch_notificationenvolvía la comprobación en unexcept: pass— si fallaba, la notificación se enviaba igualmente, abriendo una vía para saltarse la supresión. - Un
channel_typedesconocido enNotificationChannel.config(fila corrupta o vieja) no llamaba a ningún transporte pero tampoco marcaba fallo: la notificación se perdía sin rastro. - Ventanas de mantenimiento sin exigir zona horaria ni
end_at > start_at. NotificationLogcrecía sin purga — y cada chequeo de rate-limit (_tenant_rate_limited) haceCOUNTsobre esa tabla.- 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. detect_recurring_patternscorrí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 conTransactionManagementError, matando la pasada entera en vez de saltarse solo la org fallida.- 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) debreach_ack_event()/breach_resolve_event(), que notifican un insight a la vez, cada uno en su propia transacción corta, y solo marcansla_*_breachedsidispatch_notificationconfirma el envío. - Escalado:
determine_and_mark_escalations()(marcar) ynotify_escalation()(notificar) separados igual que SLA;_get_next_escalation_level/get_escalation_statusconsumen elprefetch_relateden memoria en vez de relanzar queries. - Alertas:
_resolve_open_event()/_resolve_open_events_bulk()cierran elAlertEvent(o el agrupado) cuando la condición deja de cumplirse, con un único mensaje WSalert_resolvedpor 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; unchannel_typedesconocido marcastatus="failed"en vez de perderse. - Migración 0034: índice
(channel, created_at)sobreNotificationLog+ tareapurge_notification_logs(diaria, 02:45, por lotes de 5000) que borra logs de más de 90 días. - Las tareas de
monitoring/tasks.pyenvuelven su lectura/escritura enrls_bypass()explícito;detect_recurring_patternsaísla cada organización en su propiotransaction.atomic()(savepoint) para que el fallo de una org no tumbe el resto. - Acknowledge-all: tope de 500 insights + savepoint por insight.
log_actionañ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
monitoringque 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]]