Servicio: Incidencias documentales sin comandos (create_drift_insight)
Archivo: monitoring/services/insight_service.py (~95 LOC nuevas)
Módulo: monitoring.services
Introducido: v1.61.0 (2026-07-17)
Propósito
Crear incidencias AIInsight especializadas para drifts documentales:
- Sin ventana de mantenimiento (no afecta operativa del dispositivo).
- Sin comandos ejecutables (nunca muestra botón Apply).
- TTL largo (14 días en vez de 4 horas por defecto).
- Pensadas para resolver coregiend planos, no dispositivos.
Complementa a create_insight() (para anomalías operativas), compartiendo lógica de análisis IA sin duplicación.
Signature
async def create_drift_insight(
*,
target: MonitoringTarget,
anomaly_description: str,
evidence: dict,
subject_label: str,
provider_name: str = "static",
) -> AIInsight:
"""
Ticket ITSM de drift documental (Barra de Integridad F3): un veredicto
humano "Real change" en la cola de reconciliación abre esta incidencia con
la explicación de la IA.
Variante deliberada de create_insight — mismas piezas, tres diferencias:
- Sin maintenance-window: el drift no es una anomalía operativa del equipo.
- Sin comandos (commands=[] SIEMPRE, ignore lo que proponga la IA): la
resolución es corregir el plano, no ejecutar nada en el dispositivo —
así el ticket nunca muestra el botón Apply (is_actionable=False).
- Caducidad larga (DRIFT_TICKET_TTL_DAYS, no las 4h por defecto del modelo).
"""
Parámetros
| Parámetro | Tipo | Descripción |
|---|---|---|
target | MonitoringTarget | Target donde anclar la incidencia (FK obligatoria). |
anomaly_description | str | Contexto del drift para el proveedor de IA. |
evidence | dict | Datos frescos (SNMP, perfiles, etc.) para análisis. |
subject_label | str | Identificador legible (p.ej. “Router A @ Rack 5 U42”). |
provider_name | str | Nombre del proveedor IA (default: “static” offline). |
Flujo interno
1. Validaciones y contexto
org = target.organization
rate_error = await _check_rate_limit(org.id, target.id)
if rate_error:
logger.warning(f"Rate limited: {rate_error} (drift ticket, target={target.name})")
raise ValueError(rate_error)
device_context = {
"id": target.id,
"org_id": org.id,
"name": target.name,
"vendor": target.config.get("vendor", "unknown"),
"device_type": target.config.get("device_type", "unknown"),
}
Rate limit: Mismo control que create_insight() (máx X incidencias por target en Y tiempo).
2. Análisis IA con fallback
Usa la nueva función extraída _analyze_with_fallback() (compartida con create_insight):
async def _analyze_with_fallback(
provider_name,
*,
device_context,
snmp_data,
ssh_logs,
anomaly_description,
):
"""Llama al proveedor de IA con fallback a reglas estáticas."""
provider = get_provider(provider_name)
try:
result = await provider.analyze_incident(
device_context=device_context,
snmp_data=snmp_data,
ssh_logs=ssh_logs,
anomaly_description=anomaly_description,
)
except Exception as e:
if provider_name != "static":
logger.warning(f"{provider_name} failed ({e}), falling back to static rules")
provider = get_provider("static")
result = await provider.analyze_incident(...)
else:
raise
return provider, result
En create_drift_insight():
provider, result = await _analyze_with_fallback(
provider_name,
device_context=device_context,
snmp_data=evidence,
ssh_logs="", # Sin logs SSH para drifts
anomaly_description=anomaly_description,
)
3. Creación del AIInsight (campos críticos)
insight = await AIInsight.objects.acreate(
organization=org,
target=target,
# Resumen y diagnóstico IA
summary=f"Documentation drift: {subject_label}"[:200],
root_cause=result.diagnosis.root_cause,
confidence_score=result.diagnosis.confidence_score,
osi_layer=result.diagnosis.osi_layer,
telemetry_insight=result.diagnosis.telemetry_insight,
# Indicación de resolución
action_label="Update the rack plan to match reality (resolving re-arms verification)",
# Diferencias vs. create_insight()
risk_level=AIInsight.RiskLevel.LOW,
impact_description=result.recommendation.impact_description,
commands=[], # ← SIEMPRE vacío
rollback_commands=[], # ← SIEMPRE vacío
status=AIInsight.Status.PENDING,
category=AIInsight.Category.OTHER,
ai_provider=provider.name,
# Datos crudos para auditoría
raw_snmp_data=evidence,
raw_ssh_logs="",
anomaly_trigger=anomaly_description,
# TTL largo
expires_at=timezone.now() + timedelta(days=DRIFT_TICKET_TTL_DAYS),
)
Constante:
DRIFT_TICKET_TTL_DAYS = 14
4. Auditoría y broadcasting
await AIInsightAuditLog.objects.acreate(
insight=insight,
action="created",
details={"provider": provider.name, "source": "integrity_drift"},
)
logger.info(f"Drift insight created: {insight.summary} (target={target.name}, provider={provider.name})")
# Notificar clientes conectados via WebSocket (Observatory)
try:
from monitoring.consumers import broadcast_insight
await broadcast_insight(
org_id=org.id,
insight_id=insight.id,
summary=insight.summary,
risk_level=insight.risk_level,
target_name=target.name,
status=insight.status,
)
except Exception as e:
logger.debug(f"WebSocket broadcast skipped: {e}")
Propiedades emergentes
is_actionable (property en AIInsight):
@property
def is_actionable(self) -> bool:
return bool(self.commands)
Para drifts: SIEMPRE False (no hay comandos).
Impacto en UI: El botón Apply en Observatory nunca se muestra para incidencias de drift.
Reutilización: _analyze_with_fallback()
Extraída de create_insight() para evitar duplicación:
async def _analyze_with_fallback(
provider_name,
*,
device_context,
snmp_data,
ssh_logs,
anomaly_description,
) -> tuple[AIProvider, AIInsightResult]:
"""Llama al proveedor de IA con fallback a reglas estáticas. Devuelve (provider, result)."""
provider = get_provider(provider_name)
try:
result = await provider.analyze_incident(...)
except Exception as e:
if provider_name != "static":
logger.warning(f"{provider_name} failed ({e}), falling back to static rules")
provider = get_provider("static")
result = await provider.analyze_incident(...)
else:
raise
return provider, result
Antes (F1): 24 líneas de try/except duplicadas en create_insight().
Ahora (F3): Centralizado en _analyze_with_fallback(), utilizado por ambas.
Cambio en create_insight():
# Antes (F1)
provider = get_provider(provider_name)
try:
result = await provider.analyze_incident(...)
except Exception as e:
if provider_name != "static":
logger.warning(f"{provider_name} failed ({e}), falling back...")
provider = get_provider("static")
result = await provider.analyze_incident(...)
else:
raise
# Ahora (F3)
provider, result = await _analyze_with_fallback(
provider_name,
device_context=device_context,
snmp_data=snmp_data,
ssh_logs=ssh_logs,
anomaly_description=anomaly_description,
)
Diferencias vs. create_insight() (operativo)
| Aspecto | create_insight() | create_drift_insight() |
|---|---|---|
| Caso de uso | Anomalía operativa del dispositivo | Drift en documentación |
| Ventana mantenimiento | Se crea (afecta scheduling) | Se omite (no aplica) |
| Comandos | Propuestos por IA (pueden ser varios) | Siempre [] (nunca ejecutar) |
| is_actionable | True si hay comandos | Siempre False |
| TTL (expires_at) | 4 horas (por defecto, settings.DEFAULT_INSIGHT_TTL) | 14 días |
| RiskLevel | CRITICAL/HIGH/MEDIUM/LOW (según IA) | Siempre LOW |
| Categoría | CONNECTIVITY/PERFORMANCE/HARDWARE/… | Siempre OTHER |
| Resumen | “Device unreachable”, “High CPU”, etc. | “Documentation drift: {subject}” |
Integración
Llamada desde racks/services/integrity_itsm.open_drift_ticket()
insight = async_to_sync(create_drift_insight)(
target=target,
anomaly_description=description,
evidence=evidence or {},
subject_label=label,
provider_name=get_default_provider_name(),
)
ack.insight = insight
ack.save(update_fields=["insight"])
Contexto: La tarea Huey open_integrity_drift_ticket(ack_id) corre en proceso síncrono; usa async_to_sync() para llamar a esta función async.
Testing
3 de 8 tests de test_integrity_itsm.py validan este servicio:
- ✅ Creación de ticket sin comandos y con TTL largo.
- ✅ Recomputo de contexto fresco del sujeto.
- ✅ Idempotencia por
ack_id.
Proveedor IA: “static” (offline, determinista).
Véase también
- [[feature—racks—barra-integridad-f3]]
- [[entity—racks—service—integrity-itsm]]
- [[entity—monitoring—model—aiinsight]]
- [[entity—monitoring—model—monitoring-target]]