105 lines
3.4 KiB
Python
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,
|
|
)
|