from __future__ import annotations import asyncio import json import logging import smtplib import ssl import types from email.message import EmailMessage import httpx from jinja2 import Template logger = logging.getLogger(__name__) async def dispatch_notifications( smtp_cfg: dict, webhook_cfg: dict, alert_data: dict, ) -> dict: """Dispatch all configured notification channels. Returns results dict.""" results = {} if smtp_cfg.get("host") and smtp_cfg.get("recipients"): results["email"] = await _send_email(smtp_cfg, alert_data) if webhook_cfg.get("url"): results["webhook"] = await _send_webhook(webhook_cfg, alert_data) return results async def _send_email(cfg: dict, alert: dict) -> dict: try: msg = EmailMessage() msg["Subject"] = f"[Steward] {alert['state']} — {alert['rule_name']}" msg["From"] = cfg.get("username", "steward@localhost") msg["To"] = ", ".join(cfg["recipients"]) msg.set_content( f"Alert: {alert['state']}\n" f"Rule: {alert['rule_name']}\n" f"Resource: {alert['resource']}\n" f"Metric: {alert['metric']} = {alert['value']}\n" f"Threshold: {alert['threshold']}\n" f"Time: {alert['timestamp']}\n" ) loop = asyncio.get_running_loop() await loop.run_in_executor(None, _smtp_send_sync, cfg, msg) return {"sent": True, "error": None} except Exception as exc: logger.error(f"Email notification failed: {exc}") return {"sent": False, "error": str(exc)} def _smtp_send_sync(cfg: dict, msg: EmailMessage) -> None: ctx = ssl.create_default_context() if cfg.get("tls", True) else None with smtplib.SMTP(cfg["host"], cfg.get("port", 587)) as smtp: if ctx: smtp.starttls(context=ctx) if cfg.get("username"): smtp.login(cfg["username"], cfg.get("password", "")) smtp.send_message(msg) async def _send_webhook(cfg: dict, alert: dict) -> dict: template_str = cfg.get( "template", '{"content": "{{ alert.state }} — {{ alert.rule_name }}"}', ) # Render Jinja2 template with alert as a namespace object (attribute access) try: alert_ns = types.SimpleNamespace(**alert) rendered = Template(template_str).render(alert=alert_ns) except Exception as exc: error = f"Template render failed: {exc}" logger.error(error) return {"sent": False, "error": error} # Validate JSON before sending try: json.loads(rendered) except json.JSONDecodeError as exc: error = f"Rendered webhook template is not valid JSON: {exc}" logger.error(error) return {"sent": False, "error": error} try: async with httpx.AsyncClient(timeout=10) as client: response = await client.post( cfg["url"], content=rendered, headers={"Content-Type": "application/json"}, ) response.raise_for_status() return {"sent": True, "error": None} except Exception as exc: error = str(exc) logger.error(f"Webhook notification failed: {error}") return {"sent": False, "error": error}