"""通知渠道封装。 对 email / webhook / dingtalk / wechat 等渠道做统一封装;真实发送通过 urllib 完成(或按需扩展 SDK),默认 InMemorySender 记录发送历史,便于测试。 """ from __future__ import annotations import json import threading import urllib.request from typing import Any, Dict, List, Optional from .models import Alert class ChannelSender: """通知渠道发送器接口。""" def send(self, alert: Alert, target: Optional[str] = None) -> bool: raise NotImplementedError class InMemorySender(ChannelSender): """记录发送历史,不真正外呼。""" def __init__(self, channel: str) -> None: self.channel = channel self.sent: List[Dict[str, Any]] = [] self._lock = threading.Lock() def send(self, alert: Alert, target: Optional[str] = None) -> bool: with self._lock: self.sent.append({"channel": self.channel, "target": target, "alert_id": alert.alert_id}) return True class WebhookSender(ChannelSender): """通用 Webhook(POST JSON)。""" def __init__(self, url: str) -> None: self.url = url def send(self, alert: Alert, target: Optional[str] = None) -> bool: payload = json.dumps(alert.to_dict()).encode("utf-8") req = urllib.request.Request( target or self.url, data=payload, headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=5) as resp: # noqa: S310 return 200 <= resp.status < 300 class Notifier: """通知分发器:按告警渠道列表投递。""" def __init__(self, senders: Optional[Dict[str, ChannelSender]] = None) -> None: self.senders = senders or {} self.sent: List[Dict[str, Any]] = [] self._lock = threading.Lock() def register(self, channel: str, sender: ChannelSender) -> None: self.senders[channel] = sender def notify(self, alert: Alert, channels: Optional[List[str]] = None) -> List[str]: """发送告警,返回成功渠道列表。""" channels = channels or list(self.senders.keys()) delivered: List[str] = [] for channel in channels: sender = self.senders.get(channel) if sender is None: continue ok = sender.send(alert) with self._lock: self.sent.append({"channel": channel, "alert_id": alert.alert_id, "ok": ok}) if ok: delivered.append(channel) return delivered @classmethod def memory(cls, channels: Optional[List[str]] = None) -> "Notifier": """构造内存版 Notifier,便于本地运行/测试。""" channels = channels or ["email", "webhook", "dingtalk", "wechat"] return cls({c: InMemorySender(c) for c in channels})