Problem/Motivation
Alerting is currently hard-wired to email. AlertService::checkAndAlert() both decides whether an alert should fire (the threshold-crossing logic) and how it is delivered (it calls MailManagerInterface::mail() directly and loops over alerts.emails). The two responsibilities are entangled in one class.
This has real limitations:
- Teams that manage AI budgets in Slack, Microsoft Teams, PagerDuty, or a custom monitoring system cannot receive alerts without patching the module. Webhook/Slack delivery is on the roadmap but has no code - it only exists as an example in
ai_metering.api.php. - Adding a new delivery mechanism means editing
AlertServiceitself, so every channel a site needs has to live in this module rather than in the site's or a contrib module's own code. - The delivery logic is not independently testable - a channel cannot be unit-tested without exercising the threshold math around it.
Delivery is a good fit for Drupal's plugin system: the service should own the decision, and each channel should own its own delivery.
Proposed Resolution
Split alert delivery into a plugin type so AlertService is reduced to detection only.
- Define an
AlertChannelplugin type - anAlertChannelInterface, anAlertChannelBase, an attribute/annotation, and a plugin manager (ai_metering.alert_channel). Plugins are discovered fromsrc/Plugin/AlertChannel/. - Introduce a small value object (for example
AlertEvent) carrying the alert payload thatcheckAndAlert()already assembles:uid,username,threshold,pct_used,tokens_used,budget.AlertChannelInterface::send(AlertEvent $event): voidreceives it. - Refactor
AlertServiceto keep only the once-per-threshold-crossing detection logic. After a crossing is detected it builds theAlertEventand dispatches it to every enabled channel via the plugin manager, instead of callingMailManagerInterfaceitself. - Move the existing email behaviour into an
emailAlertChannelplugin. It wraps the currentMailManagerInterfacecall, thealerts.emailsrecipient loop, the per-addressFILTER_VALIDATE_EMAILcheck, and the failure logging. Behaviour is preserved so existing sites see no change. - Add a
webhookAlertChannelplugin that POSTs a JSON body built from theAlertEventto a configured endpoint via thehttp_clientservice, with a Slack-compatible payload shape (atextsummary plus the structured fields) so it works with Slack and Teams incoming webhooks out of the box. Failures are logged and never interrupt the metering request. - Extend
ai_metering.settingsand its schema with per-channel enable flags and channel config - for the webhook plugin at minimum aurl, an optional secret/header, and a timeout. The settings form gains a section per channel. - Tests: a unit test per channel (email delivery, webhook payload shape and enabled/disabled behaviour) plus an
AlertServicetest asserting it dispatches to enabled channels exactly once per crossing and does no delivery itself.
Result: AlertService owns when, plugins own how, and sites can add channels without touching this module.
AI Assistance
Yes, AI was used to explore the issue.
Issue fork ai_metering-3608699
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
codeitwisely commentedComment #5
codeitwisely commented