Spaces:
Sleeping
Sleeping
File size: 3,131 Bytes
0a367ef | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | """Developer alert notifier.
Single source of truth for immediate infrastructure/system error email alerts
sent to the developer team when no Mail ID is available to attribute the error
to. Processing-error alerts (Mail ID available) flow through
processing_error_handler → daily_brief instead.
Callers MUST await send_developer_alert() before continuing so delivery is
confirmed before the loop or job resumes.
"""
from __future__ import annotations
import logging
from app.config import Settings
from app.lib.graph.auth import GraphAuthProvider
from app.lib.graph.client import GraphClient
logger = logging.getLogger(__name__)
def _build_sendmail_payload(
recipients: list[str],
subject: str,
body_html: str,
mailbox_user: str,
) -> tuple[str, dict]:
"""Build a (path, payload) pair ready for GraphClient.post().
Shared by send_developer_alert and scheduler._send_daily_brief to avoid
duplicating the sendMail envelope shape.
"""
path = f"/users/{mailbox_user}/sendMail"
payload = {
"message": {
"subject": subject,
"body": {"contentType": "HTML", "content": body_html},
"toRecipients": [
{"emailAddress": {"address": r}} for r in recipients
],
},
"saveToSentItems": False,
}
return path, payload
async def send_developer_alert(
settings: Settings,
subject: str,
body: str,
) -> None:
"""Send an immediate alert email to DEV_ALERT_RECIPIENTS.
The body is expected to contain both the exception message and the full
stack trace (formatted via traceback.format_exc()) so the developer has
complete context in the email.
Creates its own Graph client on demand — safe to call from the processor
(which has no shared client) and from failure paths where the existing
client may be broken.
If sending fails the exception is logged and swallowed — this function
must never recurse or propagate, as all callers are already in an error
path.
"""
recipients = settings.dev_alert_recipient_list()
if not recipients:
logger.warning(
"send_developer_alert: DEV_ALERT_RECIPIENTS not configured; "
"alert not sent. Subject: %s",
subject,
)
return
client: GraphClient | None = None
try:
auth = GraphAuthProvider(settings)
client = GraphClient(auth)
path, payload = _build_sendmail_payload(
recipients=recipients,
subject=subject,
body_html=f"<pre>{body}</pre>",
mailbox_user=settings.mailbox_user,
)
await client.post(path, payload)
logger.info(
"Developer alert sent to %d recipient(s). Subject: %s",
len(recipients),
subject,
)
except Exception:
logger.exception(
"send_developer_alert: failed to send alert. Subject: %s", subject
)
finally:
if client is not None:
try:
await client.aclose()
except Exception:
pass
|