Volver a la wiki

Servicio: Incidencias documentales sin comandos (create_drift_insight)

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:

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ámetroTipoDescripción
targetMonitoringTargetTarget donde anclar la incidencia (FK obligatoria).
anomaly_descriptionstrContexto del drift para el proveedor de IA.
evidencedictDatos frescos (SNMP, perfiles, etc.) para análisis.
subject_labelstrIdentificador legible (p.ej. “Router A @ Rack 5 U42”).
provider_namestrNombre 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)

Aspectocreate_insight()create_drift_insight()
Caso de usoAnomalía operativa del dispositivoDrift en documentación
Ventana mantenimientoSe crea (afecta scheduling)Se omite (no aplica)
ComandosPropuestos por IA (pueden ser varios)Siempre [] (nunca ejecutar)
is_actionableTrue si hay comandosSiempre False
TTL (expires_at)4 horas (por defecto, settings.DEFAULT_INSIGHT_TTL)14 días
RiskLevelCRITICAL/HIGH/MEDIUM/LOW (según IA)Siempre LOW
CategoríaCONNECTIVITY/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:

Proveedor IA: “static” (offline, determinista).

Véase también

Subir