Alert via Slack
Alert via Slack
Send SLA breach and recovery notifications to a Slack channel with one incoming-webhook URL. No packages needed beyond Laravel's HTTP client.
1. Create a Slack incoming webhook
- In Slack: Apps → Incoming Webhooks → Add to Slack
- Pick a channel (e.g.
#queue-alerts) - Copy the webhook URL
2. Store the URL
.env:
QUEUE_ALERTS_SLACK_WEBHOOK=https://hooks.slack.com/services/T00/B00/xxx
config/services.php:
'queue_alerts' => [
'slack_webhook' => env('QUEUE_ALERTS_SLACK_WEBHOOK'),
],
3. The listener
app/Listeners/NotifyQueueSlack.php:
<?php
namespace App\Listeners;
use Cbox\LaravelQueueAutoscale\Alerting\AlertRateLimiter;
use Cbox\LaravelQueueAutoscale\Events\SlaBreached;
use Cbox\LaravelQueueAutoscale\Events\SlaRecovered;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class NotifyQueueSlack
{
public function __construct(private AlertRateLimiter $limiter) {}
public function onBreach(SlaBreached $event): void
{
if (! $this->limiter->allow("slack:breach:{$event->connection}:{$event->queue}")) {
return;
}
$this->post(sprintf(
":rotating_light: *SLA breach on `%s:%s`* — oldest job %ds (target %ds), %d pending, %d workers",
$event->connection,
$event->queue,
$event->oldestJobAge,
$event->slaTarget,
$event->pending,
$event->activeWorkers,
));
}
public function onRecovery(SlaRecovered $event): void
{
// No rate-limit needed: SlaRecovered only fires on state transition.
$this->post(sprintf(
":white_check_mark: *SLA recovered on `%s:%s`* — pickup back under %ds",
$event->connection,
$event->queue,
$event->slaTarget,
));
}
private function post(string $text): void
{
$url = (string) config('services.queue_alerts.slack_webhook');
if ($url === '') {
return;
}
try {
Http::timeout(5)->post($url, ['text' => $text]);
} catch (\Throwable $e) {
// Never let alerting crash the manager — log and move on.
Log::warning('Slack alert failed', ['error' => $e->getMessage()]);
}
}
}
4. Register it
app/Providers/EventServiceProvider.php:
use Cbox\LaravelQueueAutoscale\Events\SlaBreached;
use Cbox\LaravelQueueAutoscale\Events\SlaRecovered;
use App\Listeners\NotifyQueueSlack;
protected $listen = [
SlaBreached::class => [NotifyQueueSlack::class.'@onBreach'],
SlaRecovered::class => [NotifyQueueSlack::class.'@onRecovery'],
];
Done
Force a breach in a local test to verify:
# Shovel 50 slow jobs (1s each) into the default queue, then watch Slack.
# Run this in tinker; the queued-closure approach avoids needing a job class.
php artisan tinker
>>> for ($i = 0; $i < 50; $i++) { dispatch(function () { sleep(1); }); }
Tuning
- Default rate-limit is 5 minutes per
connection:queue. Change viaQUEUE_AUTOSCALE_ALERT_COOLDOWNorqueue-autoscale.alerting.cooldown_seconds. - For multi-channel routing (e.g.
#critical-alertsfor payments,#queue-noisefor everything else), add a match on$event->queueand use different webhook URLs. - Swap the raw webhook for
laravel/slack-notification-channelif you prefer the Notification abstraction — same rate-limiter pattern, different sender. - Running into Slack rate limits from the webhook itself? That's an upstream concern — our rate limiter dedupes at the event source, but bursts across many queues can still hit Slack's ingest. Consider batching or a dedicated
#per severity tier.