NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Alerts for Laravel

Configuration

The published config/alerts.php, without its comment blocks:

<?php

declare(strict_types=1);

use RoundlyConsulting\Alerts\Alert;
use RoundlyConsulting\Alerts\AlertSilence;
use RoundlyConsulting\Alerts\HealthCheck;
use RoundlyConsulting\Alerts\HealthCheckRun;
use RoundlyConsulting\Alerts\Jobs\HealthCheckJob;

return [
    'health-check' => HealthCheck::class,

    // Check classes (or instances) registered globally on boot.
    'checks' => [
        // RoundlyConsulting\Alerts\Checks\DatabaseCheck::class,
    ],

    'alert' => Alert::class,

    // Key type of your notifiable (owner) models: bigint | uuid | ulid
    'key_type' => env('ALERTS_KEY_TYPE', 'bigint'),

    'job' => HealthCheckJob::class,

    // Auto-register the perform command on the scheduler.
    'schedule' => [
        'enabled' => env('ALERTS_SCHEDULE', true),
        'frequency' => env('ALERTS_SCHEDULE_FREQUENCY', 'everyMinute'),
    ],

    // Opt-in JSON status endpoint registered via Health::routes().
    'route' => [
        'uri' => env('ALERTS_ROUTE_URI', 'health'),
        'name' => 'alerts.health',
    ],

    // Master switch for maintenance-window muting + the silence model.
    'silence' => env('ALERTS_SILENCE', true),
    'silence-model' => AlertSilence::class,

    // Run history + latency tracking, retention, and the run model.
    'history' => [
        'enabled' => env('ALERTS_HISTORY', true),
        'retention_days' => env('ALERTS_HISTORY_RETENTION', 30),
        'model' => HealthCheckRun::class,
    ],

    // Optional global default escalation policy (data-only).
    'escalation' => [
        // 1 => 'owner', 3 => 'team', 5 => 'oncall',
    ],
];

Every key

KeyDefaultEnvPurpose
health-checkHealthCheck::class—Model that stores scheduled checks.
checks[]—Check classes (or instances) registered on boot.
alertAlert::class—Model that records alerts.
key_typebigintALERTS_KEY_TYPEKey type of the notifiable_id columns on health_checks, alerts and alert_silences — bigint, uuid or ulid (case-insensitive; anything else throws InvalidConfigurationException). Read when the migrations run, so set it first.
jobHealthCheckJob::class—Queued job dispatched for each due check. A blank value is not set and uses the packaged job.
schedule.enabledtrueALERTS_SCHEDULEAuto-register alerts:perform-health-checks on the scheduler. Pruning is scheduled either way while history is enabled.
schedule.frequencyeveryMinuteALERTS_SCHEDULE_FREQUENCYScheduler method used for the command: everyMinute, everyTwoMinutes, everyThreeMinutes, everyFourMinutes, everyFiveMinutes, everyTenMinutes, everyFifteenMinutes, everyThirtyMinutes, hourly, everyOddHour, everyTwoHours, everyThreeHours, everyFourHours, everySixHours, daily, twiceDaily, weekly, monthly, quarterly or yearly. A blank value is not set (everyMinute); any other name throws when the scheduler is resolved.
route.urihealthALERTS_ROUTE_URIURI of the opt-in JSON status endpoint. A blank value is not set and uses health — never the site root; a non-string value throws.
route.namealerts.health—Route name of the status endpoint. A blank value is not set and uses the default; a non-string value throws.
silencetrueALERTS_SILENCEMaster switch for maintenance-window muting; false ignores every silence.
silence-modelAlertSilence::class—Model that stores mute records.
history.enabledtrueALERTS_HISTORYRecord every run (status + latency) and schedule alerts:prune-runs daily — also when schedule.enabled is off.
history.retention_days30ALERTS_HISTORY_RETENTIONDays of run history kept before alerts:prune-runs deletes them. Must be a whole number of at least 1; blank is not set (30); anything else (five, 5.5, 0) throws instead of pruning, so a typo can never wipe the run history.
history.modelHealthCheckRun::class—Model that records runs.
escalation[]—Global default escalation policy (threshold ⇒ group), applied to any check that declares none of its own.

Environment

ALERTS_KEY_TYPE=bigint
ALERTS_SCHEDULE=true
ALERTS_SCHEDULE_FREQUENCY=everyMinute
ALERTS_ROUTE_URI=health
ALERTS_SILENCE=true
ALERTS_HISTORY=true
ALERTS_HISTORY_RETENTION=30

The switches — schedule.enabled, silence and history.enabled — accept booleans and the usual env strings: 1, true, on and yes turn them on; 0, false, off and no turn them off. A blank value (ALERTS_SCHEDULE=) is not set, so the switch keeps its default (on). Any other value throws InvalidConfigurationException naming the key and the value, so a typo never silently flips a switch.

Every non-boolean setting is read strictly too: a key that is not set — absent, null or blank (a host’s KEY=) — takes its default, and any other invalid value throws InvalidConfigurationException naming the key. Nothing falls back silently.

Keep the checks array to class names. Configured instances such as DatabaseCheck::make('mysql') are better registered from a service provider — objects in a config file stop php artisan config:cache from serializing it.

Swapping models

Every model key may point at your own subclass of the packaged model. Each is resolved through a single seam, so a swapped model is honoured everywhere — relations, actions, commands and the report alike:

namespace App\Models;

use RoundlyConsulting\Alerts\HealthCheck as BaseHealthCheck;

class HealthCheck extends BaseHealthCheck
{
    // your own scopes, relations, observers…
}
// config/alerts.php
'health-check' => App\Models\HealthCheck::class,
'alert' => App\Models\Alert::class,
'silence-model' => App\Models\AlertSilence::class,
'history' => [
    // …
    'model' => App\Models\HealthCheckRun::class,
],

A key that is not set — absent, null or blank — resolves the packaged model. Anything else must be the packaged model or a subclass of it — the package relies on its methods and scopes — otherwise it throws InvalidConfigurationException naming the key. A foreign class is never silently replaced.

Swapping the job

The job key sets the queued job dispatched for each due check. It is dispatched with the HealthCheck row as its only argument; the packaged HealthCheckJob is final, so write your own and run the row with Health::for($row->notifiable)->run($row) — for example to run monitoring on a dedicated queue. The pipeline behind it, RunHealthCheckAction, is internal; don’t call it directly:

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use RoundlyConsulting\Alerts\Facades\Health;
use RoundlyConsulting\Alerts\HealthCheck;

class MonitoringJob implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public function __construct(public HealthCheck $healthCheck)
    {
        $this->onQueue('monitoring');
    }

    public function handle(): void
    {
        // Runs this scheduled row as-is, through the same pipeline as HealthCheckJob.
        Health::for($this->healthCheck->notifiable)->run($this->healthCheck);
    }
}

// config/alerts.php
'job' => App\Jobs\MonitoringJob::class,

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 crypto

By 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.