Volver a la wiki

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:

Resultado en PROD (14-08-2026):


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:


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:


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:


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:


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:

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


Pruebas de cobertura

Véase tests/api/test_itsm_close_cycle.py:


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

Subir