Загрузить файлы в «alert-processor/app»
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.models import NormalizedEvent, NotificationDecision
|
||||
|
||||
|
||||
HIGH_SEVERITIES = {"high", "disaster"}
|
||||
AVERAGE_SEVERITIES = {"average"}
|
||||
LOW_SEVERITIES = {"warning", "information", "not classified", "not_classified", "info"}
|
||||
|
||||
RECOVERY_EVENT_TYPES = {"recovery", "resolved", "resolve", "ok", "clear", "closed"}
|
||||
|
||||
|
||||
def normalize_severity(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return value.strip().lower()
|
||||
|
||||
|
||||
def normalize_event_phase(event_type: str | None, value: str | None = None) -> str:
|
||||
raw = (event_type or "").strip().lower()
|
||||
if raw in RECOVERY_EVENT_TYPES:
|
||||
return "recovery"
|
||||
|
||||
if not raw and str(value or "").strip() == "0":
|
||||
return "recovery"
|
||||
|
||||
return "problem"
|
||||
|
||||
|
||||
def evaluate_event(
|
||||
event: NormalizedEvent,
|
||||
fingerprint: str,
|
||||
repeat_count: int,
|
||||
) -> NotificationDecision:
|
||||
severity = normalize_severity(event.severity)
|
||||
|
||||
if severity in HIGH_SEVERITIES:
|
||||
return NotificationDecision(
|
||||
notify=True,
|
||||
severity=event.severity,
|
||||
channels=["matrix", "mail"],
|
||||
reason="Severity is High/Disaster: mandatory notification",
|
||||
routing_class="high_priority",
|
||||
fingerprint=fingerprint,
|
||||
repeat_count=repeat_count,
|
||||
event_phase="problem",
|
||||
)
|
||||
|
||||
if severity in AVERAGE_SEVERITIES:
|
||||
return NotificationDecision(
|
||||
notify=True,
|
||||
severity=event.severity,
|
||||
channels=["matrix"],
|
||||
reason="Severity is Average: notify by deterministic baseline policy",
|
||||
routing_class="average_priority",
|
||||
fingerprint=fingerprint,
|
||||
repeat_count=repeat_count,
|
||||
event_phase="problem",
|
||||
)
|
||||
|
||||
if severity in LOW_SEVERITIES:
|
||||
return NotificationDecision(
|
||||
notify=False,
|
||||
severity=event.severity,
|
||||
channels=[],
|
||||
reason="Severity is Warning or lower: held for triage/suppress pipeline",
|
||||
routing_class="low_priority",
|
||||
fingerprint=fingerprint,
|
||||
repeat_count=repeat_count,
|
||||
event_phase="problem",
|
||||
)
|
||||
|
||||
return NotificationDecision(
|
||||
notify=False,
|
||||
severity=event.severity,
|
||||
channels=[],
|
||||
reason="Unknown severity: conservative hold until policy is expanded",
|
||||
routing_class="unknown_priority",
|
||||
fingerprint=fingerprint,
|
||||
repeat_count=repeat_count,
|
||||
event_phase="problem",
|
||||
)
|
||||
|
||||
|
||||
def build_recovery_decision(
|
||||
fingerprint: str,
|
||||
repeat_count: int,
|
||||
open_incident_found: bool,
|
||||
previous_severity: str | None,
|
||||
previous_channels: list[str] | None,
|
||||
previous_routing_class: str | None,
|
||||
) -> NotificationDecision:
|
||||
severity_norm = normalize_severity(previous_severity)
|
||||
|
||||
if not open_incident_found:
|
||||
return NotificationDecision(
|
||||
notify=False,
|
||||
severity=previous_severity,
|
||||
channels=[],
|
||||
reason="Recovery received but no matching open incident was found",
|
||||
routing_class="recovery_ignored",
|
||||
fingerprint=fingerprint,
|
||||
repeat_count=repeat_count,
|
||||
event_phase="recovery",
|
||||
open_incident_found=False,
|
||||
recovered_from_severity=previous_severity,
|
||||
)
|
||||
|
||||
if previous_routing_class == "triage_low_priority_notify":
|
||||
return NotificationDecision(
|
||||
notify=True,
|
||||
severity=previous_severity,
|
||||
channels=previous_channels or ["matrix"],
|
||||
reason="Recovery matched a triaged low-severity incident",
|
||||
routing_class="recovery_triage_low_priority",
|
||||
fingerprint=fingerprint,
|
||||
repeat_count=repeat_count,
|
||||
event_phase="recovery",
|
||||
open_incident_found=True,
|
||||
recovered_from_severity=previous_severity,
|
||||
)
|
||||
|
||||
if severity_norm in HIGH_SEVERITIES:
|
||||
return NotificationDecision(
|
||||
notify=True,
|
||||
severity=previous_severity,
|
||||
channels=previous_channels or ["matrix", "mail"],
|
||||
reason="Recovery matched an open High/Disaster incident",
|
||||
routing_class="recovery_high_priority",
|
||||
fingerprint=fingerprint,
|
||||
repeat_count=repeat_count,
|
||||
event_phase="recovery",
|
||||
open_incident_found=True,
|
||||
recovered_from_severity=previous_severity,
|
||||
)
|
||||
|
||||
if severity_norm in AVERAGE_SEVERITIES:
|
||||
return NotificationDecision(
|
||||
notify=True,
|
||||
severity=previous_severity,
|
||||
channels=previous_channels or ["matrix"],
|
||||
reason="Recovery matched an open Average incident",
|
||||
routing_class="recovery_average_priority",
|
||||
fingerprint=fingerprint,
|
||||
repeat_count=repeat_count,
|
||||
event_phase="recovery",
|
||||
open_incident_found=True,
|
||||
recovered_from_severity=previous_severity,
|
||||
)
|
||||
|
||||
return NotificationDecision(
|
||||
notify=False,
|
||||
severity=previous_severity,
|
||||
channels=[],
|
||||
reason="Recovery matched a low-priority incident: no notification by baseline policy",
|
||||
routing_class="recovery_low_priority",
|
||||
fingerprint=fingerprint,
|
||||
repeat_count=repeat_count,
|
||||
event_phase="recovery",
|
||||
open_incident_found=True,
|
||||
recovered_from_severity=previous_severity,
|
||||
)
|
||||
|
||||
|
||||
def decision_supports_suppress(decision: NotificationDecision) -> bool:
|
||||
return decision.routing_class == "average_priority" and decision.notify
|
||||
|
||||
|
||||
def decision_supports_flap_suppress(decision: NotificationDecision) -> bool:
|
||||
return decision.routing_class in {
|
||||
"average_priority",
|
||||
"recovery_average_priority",
|
||||
} and decision.notify
|
||||
|
||||
|
||||
def is_low_severity_problem_candidate(
|
||||
decision: NotificationDecision,
|
||||
) -> bool:
|
||||
return decision.event_phase == "problem" and decision.routing_class == "low_priority"
|
||||
|
||||
|
||||
def apply_suppress_window(
|
||||
decision: NotificationDecision,
|
||||
ttl_seconds: int,
|
||||
) -> NotificationDecision:
|
||||
return decision.model_copy(
|
||||
update={
|
||||
"notify": False,
|
||||
"channels": [],
|
||||
"suppressed": True,
|
||||
"suppress_reason": f"Suppress window active, {ttl_seconds}s remaining",
|
||||
"reason": "Duplicate Average event suppressed by re-notify window",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def apply_flap_suppress(
|
||||
decision: NotificationDecision,
|
||||
event_count: int,
|
||||
window_seconds: int,
|
||||
) -> NotificationDecision:
|
||||
return decision.model_copy(
|
||||
update={
|
||||
"notify": False,
|
||||
"channels": [],
|
||||
"suppressed": True,
|
||||
"flap_detected": True,
|
||||
"flap_event_count": event_count,
|
||||
"flap_reason": f"Flapping detected: {event_count} phase changes within {window_seconds}s",
|
||||
"reason": "Average event suppressed because the series is flapping",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def apply_low_severity_triage(
|
||||
decision: NotificationDecision,
|
||||
verdict: str,
|
||||
classification: str | None,
|
||||
reason: str | None,
|
||||
source: str,
|
||||
) -> NotificationDecision:
|
||||
verdict_norm = (verdict or "hold").strip().lower()
|
||||
classification_norm = (classification or "unknown").strip().lower()
|
||||
reason_text = (reason or "").strip() or "LLM triage applied"
|
||||
|
||||
common = {
|
||||
"triage_applied": True,
|
||||
"triage_source": source,
|
||||
"triage_verdict": verdict_norm,
|
||||
"triage_reason": reason_text,
|
||||
"triage_classification": classification_norm,
|
||||
}
|
||||
|
||||
if verdict_norm == "notify":
|
||||
return decision.model_copy(
|
||||
update={
|
||||
**common,
|
||||
"notify": True,
|
||||
"suppressed": False,
|
||||
"suppress_reason": None,
|
||||
"channels": ["matrix"],
|
||||
"routing_class": "triage_low_priority_notify",
|
||||
"reason": f"LLM triage marked low-severity event as actionable: {reason_text}",
|
||||
}
|
||||
)
|
||||
|
||||
if verdict_norm == "suppress":
|
||||
return decision.model_copy(
|
||||
update={
|
||||
**common,
|
||||
"notify": False,
|
||||
"suppressed": True,
|
||||
"channels": [],
|
||||
"routing_class": "triage_low_priority_suppressed",
|
||||
"reason": "LLM triage suppressed low-severity event",
|
||||
"suppress_reason": reason_text,
|
||||
}
|
||||
)
|
||||
|
||||
return decision.model_copy(
|
||||
update={
|
||||
**common,
|
||||
"notify": False,
|
||||
"suppressed": False,
|
||||
"channels": [],
|
||||
"routing_class": "triage_low_priority_hold",
|
||||
"reason": f"LLM triage kept low-severity event on hold: {reason_text}",
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user