Escalation policies
Notify wider audiences as a failure persists. Declare a threshold ⇒ group map with escalate([...]): the keys are consecutive-failure counts, the values are named notifiable groups your owner model resolves:
Health::for($team)->monitor(DatabaseCheck::class)
->everyMinute()
->escalate([1 => 'owner', 3 => 'team', 5 => 'oncall'])
->save();use Closure;
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Alerts\Interfaces\HasNotifiablesForAlerts;
use RoundlyConsulting\Alerts\Traits\UsesHealthChecks;
class Team extends Model implements HasNotifiablesForAlerts
{
use UsesHealthChecks; // provides a default notifiablesForAlertGroup()
public function forEachNotifiableForAlerts(Closure $callback): void { /* default group */ }
// Override to map named groups to real notifiables:
public function notifiablesForAlertGroup(string $group): iterable
{
return match ($group) {
'owner' => [$this->owner],
'team' => $this->members,
'oncall' => $this->onCallEngineers(),
default => $this->members,
};
}
}The ResolvesAlertGroups trait, bundled into UsesHealthChecks, returns the default group for any name. Owners need no change unless they want named groups.
How levels advance
- The level is the highest threshold at or below the current consecutive-failure count. It is stored on the alert as escalation_level and resets to 0 on recovery.
- Each newly reached level notifies only its own group(s). If one run jumps several thresholds — say failAfter(3) with [1 => 'owner', 3 => 'team'] — every group in between is notified once.
- HealthCheckEscalated($alert, $fromLevel, $toLevel) fires exactly once per transition. The level is claimed with a compare-and-swap, so two overlapping runs never page a tier twice.
- With a policy, nobody is notified before the first threshold is reached. Start at 1 if the very first alerting failure should reach someone. HealthCheckFailed still fires on every failing run.
- Channels for a level come from notifyVia([...], level: n), where n is the threshold number.
A global default
A check that declares no policy of its own inherits the global default from config('alerts.escalation'), so you can set one policy for the whole application and override it per check:
// config/alerts.php
'escalation' => [1 => 'owner', 3 => 'team', 5 => 'oncall'],The policy is data only — routing to real people always happens through the owner’s notifiablesForAlertGroup().
Reacting to escalation
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Alerts\Events\HealthCheckEscalated;
Event::listen(function (HealthCheckEscalated $event) {
logger()->warning('Health check escalated', [
'check' => $event->alert->healthCheck?->health_check,
'from' => $event->fromLevel,
'to' => $event->toLevel,
]);
});Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.