A provider 2xx means it accepted the message, not that anyone received it. Twilio answers 201 Created and the carrier may refuse seconds later, and nothing watching HTTP status codes will ever know. That is not hypothetical: the ledger shows 48 consecutive messages to one number, every one undelivered, going back to January - including a daily send for seven weeks. Each was recorded upstream as a success, and each was billed. Two decisions worth keeping. We poll rather than take a StatusCallback, because this relay binds to loopback on purpose and no carrier can reach it - polling costs one API call per run and keeps that property. And we reconcile against the provider's ledger rather than our own record, because our own record is exactly what was wrong, and it only knows about messages we sent; Twilio's knows about the ones another service sent too, which is how those 48 would have been caught. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
399487862c
commit
349f03015f
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check what the carrier did with the messages we handed over.
|
||||
|
||||
Run on a schedule. Exits non-zero when anything was accepted and then not
|
||||
delivered, so a cron mailer or a supervisor surfaces it — the point being that
|
||||
somebody hears about it. Forty-eight consecutive failures went unnoticed for
|
||||
seven months because nothing ever asked this question.
|
||||
|
||||
python3 scripts/reconcile.py # last 24 hours
|
||||
python3 scripts/reconcile.py --hours 168 # last week
|
||||
python3 scripts/reconcile.py --email [email protected] # and post the report
|
||||
|
||||
Reporting by email goes through this relay's own send path, so the alert
|
||||
travels the one channel that is known to work. If SMS is the thing that is
|
||||
broken, an SMS alert about it would be the last thing to arrive.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.config import Settings # noqa: E402
|
||||
from app.reconcile import describe, fetch, reconcile, window_start # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--hours", type=int, default=24, help="how far back to look")
|
||||
parser.add_argument("--email", help="send the report to this address as well")
|
||||
parser.add_argument("--quiet-when-clean", action="store_true",
|
||||
help="print nothing when every message was delivered")
|
||||
args = parser.parse_args()
|
||||
|
||||
settings = Settings()
|
||||
if not (settings.twilio_sid and settings.twilio_token):
|
||||
print("no Twilio credentials configured; nothing to reconcile", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
messages = fetch(account_sid=settings.twilio_sid, auth_token=settings.twilio_token,
|
||||
since=window_start(args.hours), timeout=settings.timeout)
|
||||
report = reconcile(messages)
|
||||
text = describe(report)
|
||||
|
||||
if report.ok and args.quiet_when_clean:
|
||||
return 0
|
||||
print(text)
|
||||
|
||||
if args.email and report.failed:
|
||||
# Only on failure: a daily "everything is fine" email is a thing people
|
||||
# filter, and then the one that matters is filtered too.
|
||||
from app.providers import send_email
|
||||
|
||||
outcome = send_email(
|
||||
to=args.email, subject=f"SMS not delivered: {len(report.failed)} in the last {args.hours}h",
|
||||
body=text, api_key=settings.sendgrid_key, sender=settings.email_from,
|
||||
timeout=settings.timeout)
|
||||
print(f"report emailed: provider status {outcome.status}", file=sys.stderr)
|
||||
|
||||
return 1 if report.failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user