Загрузить файлы в «alert-processor/app»

This commit is contained in:
2026-08-06 18:34:18 +03:00
parent 5b05a91a15
commit 6454fdea56
5 changed files with 1502 additions and 0 deletions
+269
View File
@@ -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}",
}
)