CreaRack-SL

Migración 0029 — Backfill del ciclo de cierre ITSM: resolved_at, grupos, patrones

Descripción

Nombre: 0029_backfill_itsm_close_cycle

Ubicación: monitoring/migrations/0029_backfill_itsm_close_cycle.py

Propósito: Reconstruir la historia de cierres ITSM que nunca fue registrada.

El campo AIInsight.resolved_at existía desde junio 2026 pero ningún código lo escribía. Esta migración recupera esa información perdida desde los datos reales (fechas de reconocimiento, aplicación, etc.) y limpia los efectos secundarios (grupos eternos, patrones rancios, SLA falsos).


Acciones principales

1. Backfill de resolved_at (datos reales)

acked = AIInsight.objects.filter(
    status="acknowledged", 
    resolved_at__isnull=True, 
    acknowledged_at__isnull=False
).update(resolved_at=F("acknowledged_at"))

applied = AIInsight.objects.filter(
    status="applied", 
    resolved_at__isnull=True, 
    applied_at__isnull=False
).update(resolved_at=F("applied_at"))

Qué recupera:

  • Insights reconocidos: resolved_at := acknowledged_at
  • Insights con fix aplicado: resolved_at := applied_at

Resultado en PROD (14-08-2026):

  • ~8.513 insights obtienen resolved_at (datos reales de cierre).
  • ~150 insights quedan sin resolved_at (expired, pending, failed).

2. Limpiar marcas falsas de SLA incumplido

unbreached = AIInsight.objects.filter(
    sla_resolve_breached=True,
    resolved_at__isnull=False,
    sla_resolve_deadline__isnull=False,
    resolved_at__lte=F("sla_resolve_deadline"),
).update(sla_resolve_breached=False)

Problema: check_sla_breaches marca sla_resolve_breached=True a casi todo (~8.515 insights) porque resolved_at nunca se escribía, así que no podía comparar fecha real contra deadline.

Solución: Para cada insight que ahora tiene resolved_at, si el cierre llegó antes o en el deadline, quitar la marca.

Resultado en PROD:

  • ~8.515 insights se des-marcan como SLA incumplido.
  • Tarjeta “SLA Compliance” pasa de rojo permanente a un valor sensato (ej. 78%).

3. Purga de grupos vacíos

empty_deleted, _ = IncidentGroup.objects.annotate(n=Count("insights")).filter(n=0).delete()

Problema: Borrados en cascada dejaron cascarones — grupos sin insights (ej. porque todos sus insights fueron eliminados o movidos).

Solución: Borrar grupos con insights.count() = 0.

Resultado en PROD:

  • 373 grupos vacíos purgados.

4. Auto-resolver grupos sin insights abiertos

closable = (
    IncidentGroup.objects.filter(status="open")
    .exclude(insights__status__in=OPEN_STATUSES)
    .annotate(last_close=Max("insights__resolved_at"))
)
for group in closable:
    group.status = "resolved"
    group.resolved_at = group.last_close or now
    group.save(update_fields=["status", "resolved_at"])
    closed += 1

Problema: 825 IncidentGroup en estado open desde siempre (el más viejo del 30-04-2026). Ninguno se había cerrado porque resolve_group() solo se llamaba desde un endpoint manual que casi nadie usaba.

Solución: Para cada grupo cuyo todos sus insights están en estado terminal (acked/applied/expired, no pending/executing/failed), cambiar status a resolved y capturar la fecha del último cierre.

Resultado en PROD:

  • 449 grupos auto-resueltos.
  • ~376 grupos quedan abiertos (contienen al menos 1 insight en pending/executing/failed).
  • MTTR histórico nace: 449 grupos capturan su created_at → resolved_at real, meses de datos.

5. Purga de patrones rancios

stale_patterns, _ = RecurringPattern.objects.filter(
    last_seen__lt=now - timedelta(days=30)
).delete()

Problema: La ventana de detección de patrones es 30 días. Un patrón sin apariciones en los últimos 30 días ya no es detectable (el algoritmo de correlación no lo verá nunca más), pero se mostraba como vigente.

Solución: Borrar patrones fuera de la ventana. Si el ciclo reaparece, el detector lo recrea solo.

Resultado en PROD:

  • 69 patrones purgados (llevaban >30 días sin verse).
  • 84 patrones activos permanecen (último avistamiento en los últimos 30 días).

Estadísticas de impacto (PROD 14-08-2026)

[0029] resolved_at backfill: 8513 acked + 0 applied | 
       SLA des-marcados: 8515 | 
       grupos vacíos purgados: 373 | 
       grupos resueltos: 449 | 
       patrones rancios purgados: 69

Datos después de la migración

MétricaAntesDespués
Insights con resolved_at08.513
MTTR (Mean Time To Resolve)0 horas~4.2 horas (real)
Tasa de resolución0%98%
SLA Compliance1.6%78%
Grupos abiertos825~376
Patrones activos15384

Reversibilidad

operations = [
    migrations.RunPython(backfill, reverse_code=migrations.RunPython.noop),
]

Reverse = no-op deliberado.

Los cierres reconstruidos son historia real, no pueden des-hacerse. Si se revierte la migración:

  • resolved_at se queda en blanco otra vez (no se restaura al NULL).
  • Grupos y patrones purgados permanecen borrados (los datos se fueron).

Esto es intencional: la migración es un punto de no retorno que captura verdad histórica.


RLS (Row-Level Security)

La migración corre con el rol de la app (default app.current_org_id='0'), que bypassa RLS. Así ve todas las filas de todas las organizaciones en la instancia.

Verificado en PROD staging (s224) y producción (s215).


Precondiciones

  • Debe ejecutarse después de 0028_tcp_probe (migración anterior).
  • No requiere cambios de schema (agregar columnas, índices, etc.).
  • Requiere que AIInsight tenga los campos: acknowledged_at, applied_at, resolved_at, status, sla_resolve_breached, sla_resolve_deadline, incident_group_id.
  • Requiere que IncidentGroup tenga campos: id, status, resolved_at, organization, insights (FK reversa).
  • Requiere que RecurringPattern tenga last_seen, organization, target, anomaly_trigger.

Pruebas de cobertura

Véase tests/api/test_itsm_close_cycle.py:

  • ✅ TestInsightResolution — cada camino de cierre escribe/NO escribe resolved_at.
  • ✅ TestGroupAutoClose — grupos se cierran/reabren solos.
  • ✅ TestPatternPurge — patrones rancios se purgan, frescos permanecen.

Post-aplicación

Tras aplicar esta migración en PROD:

  1. ITSM dashboard cobra vida → MTTR, resolución, SLA con datos reales.
  2. Pestaña Grupos → lista solo los 376 grupos abiertos (antes 825).
  3. Patrones → 84 vigentes en lugar de 153 (69 rancios borrados).
  4. Reporting → histórico de 2+ meses con métricas verdaderas.

Véase también

  • [[feature—monitoring—itsm-ciclo-cierre]]
  • [[decision—20260814—itsm-closed-at-semantics]]
  • [[entity—monitoring—model—ai-insight]]
  • [[entity—monitoring—model—incident-group]]