Files
gaokao-volunteer-system/data/notifications/ops_alerts.py
Hermes Agent deee5e538a
Some checks failed
CI / pytest (Python 3.10) (push) Has been cancelled
CI / pytest (Python 3.11) (push) Has been cancelled
CI / pytest (Python 3.12) (push) Has been cancelled
feat(ops): wire SMTP/IM alert sinks and tighten intake validation
2026-06-15 19:36:47 +08:00

105 lines
3.4 KiB
Python

from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from urllib import request as urllib_request
from data.notifications.smtp_sender import SMTPDeliverySender, SMTPSettings
from data.orders.models import utc_now_iso
@dataclass
class OpsAlertResult:
log_written: bool
emails_sent: int = 0
webhooks_sent: int = 0
class WebhookAlertSender:
def __init__(self, urls: list[str]) -> None:
self._urls = urls
def send(self, payload: dict[str, Any]) -> int:
sent = 0
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
for url in self._urls:
req = urllib_request.Request(
url,
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib_request.urlopen(req, timeout=10) as resp: # nosec B310 - runtime-configured webhook sink
if 200 <= resp.status < 300:
sent += 1
return sent
class OpsAlertSink:
def __init__(
self,
*,
log_path: str,
recipients: list[str] | None = None,
email_sender: Any | None = None,
webhook_sender: Any | None = None,
) -> None:
self._log_path = Path(log_path)
self._recipients = recipients or []
self._email_sender = email_sender
self._webhook_sender = webhook_sender
def emit(self, *, alert_type: str, title: str, body: str, details: dict[str, Any]) -> OpsAlertResult:
self._log_path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"created_at": utc_now_iso(),
"alert_type": alert_type,
"title": title,
"body": body,
"details": details,
}
with self._log_path.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(payload, ensure_ascii=False) + "\n")
emails_sent = 0
if self._email_sender is not None and self._recipients:
for recipient in self._recipients:
self._email_sender.send_report_ready(
recipient=recipient,
order_id=str(details.get("order_id") or "ops-alert"),
subject=title,
body=body,
)
emails_sent += 1
webhooks_sent = 0
if self._webhook_sender is not None:
webhooks_sent = self._webhook_sender.send(payload)
return OpsAlertResult(log_written=True, emails_sent=emails_sent, webhooks_sent=webhooks_sent)
def build_alert_sink_from_settings(settings) -> OpsAlertSink:
sender = None
if settings.smtp_host and settings.smtp_sender:
sender = SMTPDeliverySender(
SMTPSettings(
host=settings.smtp_host,
port=settings.smtp_port,
sender=settings.smtp_sender,
username=settings.smtp_username,
password=settings.smtp_password,
use_tls=settings.smtp_use_tls,
use_ssl=settings.smtp_use_ssl,
)
)
webhook_sender = None
if settings.alert_webhook_urls:
webhook_sender = WebhookAlertSender(settings.alert_webhook_urls)
return OpsAlertSink(
log_path=settings.ops_alert_log_path,
recipients=settings.alert_recipients,
email_sender=sender,
webhook_sender=webhook_sender,
)