Files
FabledSteward/roundtable/core/notifications.py
T
2026-04-13 17:10:31 -04:00

101 lines
3.2 KiB
Python

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"[Fabled Scryer] {alert['state']}{alert['rule_name']}"
msg["From"] = cfg.get("username", "fabledscryer@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}