Загрузить файлы в «alert-processor/app/notifications»
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# empty
|
||||
@@ -0,0 +1,115 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from app.mail_notifier import MailNotifier
|
||||
from app.matrix_notifier import MatrixNotifier
|
||||
from app.models import NotificationDecision, ProcessorForwardEnvelope
|
||||
from app.notifications.formatter import NotificationFormatter
|
||||
|
||||
|
||||
@dataclass
|
||||
class DispatchReport:
|
||||
attempted: bool = False
|
||||
|
||||
matrix_attempted: bool = False
|
||||
matrix_sent: bool = False
|
||||
matrix_event_id: str | None = None
|
||||
matrix_error: str | None = None
|
||||
|
||||
matrix_image_attempted: bool = False
|
||||
matrix_image_sent: bool = False
|
||||
matrix_image_event_id: str | None = None
|
||||
matrix_image_mxc_uri: str | None = None
|
||||
matrix_image_error: str | None = None
|
||||
|
||||
mail_attempted: bool = False
|
||||
mail_sent: bool = False
|
||||
mail_error: str | None = None
|
||||
|
||||
errors: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class NotificationDispatcher:
|
||||
def __init__(
|
||||
self,
|
||||
matrix_notifier: MatrixNotifier | None = None,
|
||||
mail_notifier: MailNotifier | None = None,
|
||||
) -> None:
|
||||
self.matrix_notifier = matrix_notifier
|
||||
self.mail_notifier = mail_notifier
|
||||
|
||||
async def dispatch(
|
||||
self,
|
||||
envelope: ProcessorForwardEnvelope,
|
||||
decision: NotificationDecision,
|
||||
) -> DispatchReport:
|
||||
report = DispatchReport()
|
||||
|
||||
if not decision.notify or decision.suppressed:
|
||||
return report
|
||||
|
||||
report.attempted = True
|
||||
|
||||
if "matrix" in decision.channels and self.matrix_notifier is not None:
|
||||
report.matrix_attempted = True
|
||||
|
||||
matrix_body = NotificationFormatter.format_matrix_message(
|
||||
envelope=envelope,
|
||||
decision=decision,
|
||||
)
|
||||
matrix_result = await self.matrix_notifier.send_message(matrix_body)
|
||||
|
||||
report.matrix_sent = matrix_result.ok
|
||||
report.matrix_event_id = matrix_result.event_id
|
||||
report.matrix_error = matrix_result.error
|
||||
if matrix_result.error:
|
||||
report.errors.append(f"matrix: {matrix_result.error}")
|
||||
|
||||
# Если есть отрисованный PNG, отправляем его вторым сообщением
|
||||
graph_image_path = envelope.event.graph_image_path
|
||||
if graph_image_path and Path(graph_image_path).exists():
|
||||
report.matrix_image_attempted = True
|
||||
|
||||
image_body = f"{envelope.event.trigger_name or 'Graph'} graph"
|
||||
image_result = await self.matrix_notifier.send_image(
|
||||
file_path=graph_image_path,
|
||||
body=image_body,
|
||||
)
|
||||
|
||||
report.matrix_image_sent = image_result.ok
|
||||
report.matrix_image_event_id = image_result.event_id
|
||||
report.matrix_image_mxc_uri = image_result.content_uri
|
||||
report.matrix_image_error = image_result.error
|
||||
if image_result.error:
|
||||
report.errors.append(f"matrix_image: {image_result.error}")
|
||||
|
||||
if "mail" in decision.channels and self.mail_notifier is not None:
|
||||
report.mail_attempted = True
|
||||
|
||||
subject = NotificationFormatter.format_mail_subject(
|
||||
envelope=envelope,
|
||||
decision=decision,
|
||||
)
|
||||
body = NotificationFormatter.format_mail_body(
|
||||
envelope=envelope,
|
||||
decision=decision,
|
||||
)
|
||||
|
||||
attachments: list[str] = []
|
||||
if envelope.event.graph_image_path:
|
||||
attachments.append(envelope.event.graph_image_path)
|
||||
|
||||
mail_result = await self.mail_notifier.send_message(
|
||||
subject=subject,
|
||||
body=body,
|
||||
attachments=attachments,
|
||||
)
|
||||
|
||||
report.mail_sent = mail_result.ok
|
||||
report.mail_error = mail_result.error
|
||||
if mail_result.error:
|
||||
report.errors.append(f"mail: {mail_result.error}")
|
||||
|
||||
return report
|
||||
@@ -0,0 +1,192 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.config import settings
|
||||
from app.models import NotificationDecision, ProcessorForwardEnvelope
|
||||
|
||||
|
||||
class NotificationFormatter:
|
||||
@staticmethod
|
||||
def format_matrix_message(
|
||||
envelope: ProcessorForwardEnvelope,
|
||||
decision: NotificationDecision,
|
||||
) -> str:
|
||||
e = envelope.event
|
||||
ctx = e.zabbix_context or {}
|
||||
|
||||
lines = [
|
||||
f"[{decision.severity or 'UNKNOWN'}] {decision.event_phase.upper()}",
|
||||
f"Host: {e.host or 'unknown'}",
|
||||
f"Service: {e.service or 'unknown'}",
|
||||
f"Trigger: {e.trigger_name or 'unknown'}",
|
||||
f"Reason: {decision.reason}",
|
||||
f"Fingerprint: {decision.fingerprint or 'unknown'}",
|
||||
f"Repeat count: {decision.repeat_count}",
|
||||
]
|
||||
|
||||
if decision.triage_applied:
|
||||
lines.append(
|
||||
f"Triage: {decision.triage_verdict or 'unknown'}"
|
||||
f" ({decision.triage_classification or 'unknown'}, source={decision.triage_source or 'unknown'})"
|
||||
)
|
||||
if decision.triage_reason:
|
||||
lines.append(f"Triage reason: {decision.triage_reason}")
|
||||
|
||||
if decision.correlation_applied:
|
||||
lines.append(
|
||||
f"Correlation: role={decision.correlation_role or 'unknown'}"
|
||||
f", kind={decision.correlation_kind or 'unknown'}"
|
||||
)
|
||||
if decision.root_cause_candidate:
|
||||
lines.append(
|
||||
f"RCA: root cause candidate (related alerts in window: {decision.correlated_event_count})"
|
||||
)
|
||||
if decision.parent_event_id:
|
||||
lines.append(f"Parent event: {decision.parent_event_id}")
|
||||
if decision.correlation_reason:
|
||||
lines.append(f"Correlation reason: {decision.correlation_reason}")
|
||||
|
||||
if decision.recovered_from_severity:
|
||||
lines.append(f"Recovered from severity: {decision.recovered_from_severity}")
|
||||
|
||||
if decision.flap_detected and decision.flap_reason:
|
||||
lines.append(f"Flap: {decision.flap_reason}")
|
||||
|
||||
if decision.suppressed and decision.suppress_reason:
|
||||
lines.append(f"Suppress: {decision.suppress_reason}")
|
||||
|
||||
if ctx.get("last_value"):
|
||||
lines.append(f"Last value: {ctx['last_value']}")
|
||||
if ctx.get("item_key"):
|
||||
lines.append(f"Item key: {ctx['item_key']}")
|
||||
if ctx.get("resolved_opdata"):
|
||||
lines.append(f"OpData: {ctx['resolved_opdata']}")
|
||||
|
||||
if decision.remediation_summary:
|
||||
lines.append(f"Remediation: {decision.remediation_summary}")
|
||||
|
||||
if decision.remediation_steps:
|
||||
lines.append("Steps:")
|
||||
for idx, step in enumerate(decision.remediation_steps, start=1):
|
||||
lines.append(f"{idx}. {step}")
|
||||
|
||||
if decision.remediation_commands:
|
||||
lines.append("Commands:")
|
||||
for cmd in decision.remediation_commands:
|
||||
lines.append(f"- {cmd}")
|
||||
|
||||
if e.event_url:
|
||||
lines.append(f"Zabbix Event: {e.event_url}")
|
||||
elif e.zabbix_url:
|
||||
lines.append(f"Zabbix Event: {e.zabbix_url}")
|
||||
|
||||
if e.graph_url:
|
||||
lines.append(f"Graph: {e.graph_url}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def format_mail_subject(
|
||||
envelope: ProcessorForwardEnvelope,
|
||||
decision: NotificationDecision,
|
||||
) -> str:
|
||||
e = envelope.event
|
||||
phase = decision.event_phase.upper()
|
||||
severity = decision.severity or e.severity or "UNKNOWN"
|
||||
host = e.host or "unknown-host"
|
||||
trigger = e.trigger_name or "unknown-trigger"
|
||||
return f"{settings.mail_subject_prefix} [{severity}] {phase} {host} :: {trigger}"
|
||||
|
||||
@staticmethod
|
||||
def format_mail_body(
|
||||
envelope: ProcessorForwardEnvelope,
|
||||
decision: NotificationDecision,
|
||||
) -> str:
|
||||
e = envelope.event
|
||||
ctx = e.zabbix_context or {}
|
||||
|
||||
lines = [
|
||||
f"Severity: {decision.severity or e.severity or 'UNKNOWN'}",
|
||||
f"Phase: {decision.event_phase}",
|
||||
f"Host: {e.host or 'unknown'}",
|
||||
f"Service: {e.service or 'unknown'}",
|
||||
f"Trigger: {e.trigger_name or 'unknown'}",
|
||||
f"Reason: {decision.reason}",
|
||||
f"Fingerprint: {decision.fingerprint or 'unknown'}",
|
||||
f"Repeat count: {decision.repeat_count}",
|
||||
f"Correlation ID: {e.correlation_id}",
|
||||
f"Event ID: {e.event_id or 'unknown'}",
|
||||
]
|
||||
|
||||
if decision.triage_applied:
|
||||
lines.append(
|
||||
f"Triage: {decision.triage_verdict or 'unknown'}"
|
||||
f" ({decision.triage_classification or 'unknown'}, source={decision.triage_source or 'unknown'})"
|
||||
)
|
||||
if decision.triage_reason:
|
||||
lines.append(f"Triage reason: {decision.triage_reason}")
|
||||
|
||||
if decision.correlation_applied:
|
||||
lines.append(
|
||||
f"Correlation: role={decision.correlation_role or 'unknown'}"
|
||||
f", kind={decision.correlation_kind or 'unknown'}"
|
||||
)
|
||||
if decision.root_cause_candidate:
|
||||
lines.append(
|
||||
f"RCA: root cause candidate (related alerts in window: {decision.correlated_event_count})"
|
||||
)
|
||||
if decision.parent_event_id:
|
||||
lines.append(f"Parent event: {decision.parent_event_id}")
|
||||
if decision.correlation_reason:
|
||||
lines.append(f"Correlation reason: {decision.correlation_reason}")
|
||||
|
||||
if decision.recovered_from_severity:
|
||||
lines.append(f"Recovered from severity: {decision.recovered_from_severity}")
|
||||
|
||||
if decision.flap_detected and decision.flap_reason:
|
||||
lines.append(f"Flap reason: {decision.flap_reason}")
|
||||
|
||||
if decision.suppressed and decision.suppress_reason:
|
||||
lines.append(f"Suppress reason: {decision.suppress_reason}")
|
||||
|
||||
if ctx.get("last_value"):
|
||||
lines.append(f"Last value: {ctx['last_value']}")
|
||||
if ctx.get("item_name"):
|
||||
lines.append(f"Item name: {ctx['item_name']}")
|
||||
if ctx.get("item_key"):
|
||||
lines.append(f"Item key: {ctx['item_key']}")
|
||||
if ctx.get("resolved_opdata"):
|
||||
lines.append(f"OpData: {ctx['resolved_opdata']}")
|
||||
if ctx.get("trigger_comments"):
|
||||
lines.append(f"Trigger comments: {ctx['trigger_comments']}")
|
||||
|
||||
if decision.remediation_summary:
|
||||
lines.append("")
|
||||
lines.append(f"Remediation: {decision.remediation_summary}")
|
||||
|
||||
if decision.remediation_steps:
|
||||
lines.append("Steps:")
|
||||
for idx, step in enumerate(decision.remediation_steps, start=1):
|
||||
lines.append(f"{idx}. {step}")
|
||||
|
||||
if decision.remediation_commands:
|
||||
lines.append("Commands:")
|
||||
for cmd in decision.remediation_commands:
|
||||
lines.append(f"- {cmd}")
|
||||
|
||||
if decision.correlation_applied:
|
||||
lines.append(
|
||||
f"Correlation: role={decision.correlation_role or 'unknown'}"
|
||||
f", kind={decision.correlation_kind or 'unknown'}"
|
||||
f", source={decision.correlation_source or 'unknown'}"
|
||||
f", confidence={decision.correlation_confidence or 'unknown'}"
|
||||
)
|
||||
|
||||
if e.event_url:
|
||||
lines.append(f"Zabbix Event URL: {e.event_url}")
|
||||
elif e.zabbix_url:
|
||||
lines.append(f"Zabbix Event URL: {e.zabbix_url}")
|
||||
|
||||
if e.graph_url:
|
||||
lines.append(f"Graph URL: {e.graph_url}")
|
||||
|
||||
return "\n".join(lines)
|
||||
Reference in New Issue
Block a user