skaisser/howl
Multi-driver Laravel notifier — Discord, Slack, and Telegram with rich embeds.
When something goes wrong, your app should howl into the night.
📖 Full documentation at howl.skaisser.dev →
✨ Why Howl?
A single driver-agnostic API for Discord, Slack, and Telegram. Drop it into any Laravel 12 or 13 app and start delivering rich, structured notifications in minutes.
- 🎯 One fluent API for all three drivers — switch per-call without touching business logic
- 🎨 Rich, native formatting per platform — Discord embeds, Slack Block Kit, Telegram HTML — with mentions, fields, code blocks, buttons, attachments
- 🛰️ Channel failover & fan-out — automatic backup channel dispatch on failure
- 📦 Seven built-in event templates — exceptions, deployments, audits, cron heartbeats, job failures, manual ops, generic info
- 🧪 HowlFake test helper — assert notifications without real HTTP calls; per-driver assertions
- ⚡ Queue-aware with exponential backoff and opt-in Redis rate limiting
- ✅ 100% line coverage across 487 tests, enforced by
pest --coverage --min=100 - 📚 Versioned docs at
howl.skaisser.devplus machine-readablellms.txtfor AI agents
🛠️ Compatibility
| PHP | Laravel | Pest | PHPUnit | Testbench | Status |
|---|---|---|---|---|---|
| 8.3 | 12.x | 3.x | 11.x | 10.x | ✅ |
| 8.3 | 13.x | 4.x | 12.x | 11.x | ✅ |
| 8.4 | 12.x | 3.x | 11.x | 10.x | ✅ |
| 8.4 | 13.x | 4.x | 12.x | 11.x | ✅ |
Composer constraints support all four combinations. CI validates the latest combo (PHP 8.4 × Laravel 13) on every push and PR targeting main; the other rows are validated locally before each release.
📦 Installation
composer require skaisser/howl
php artisan vendor:publish --tag=howl-config
Add your driver credentials to .env:
HOWL_DRIVER=discord # discord | slack | telegram
HOWL_DEFAULT_CHANNEL=errors # primary channel name
# Discord
HOWL_DISCORD_DEFAULT=https://discord.com/api/webhooks/...
# Slack (optional — only if you use the slack driver)
HOWL_SLACK_BOT_TOKEN=xoxb-...
HOWL_SLACK_DEFAULT_CHANNEL=C0XXXXXXX
# Telegram (optional — only if you use the telegram driver)
HOWL_TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
HOWL_TELEGRAM_CHAT_ID=-1001234567890
🚀 Quick Start
use Skaisser\Howl\Facades\Howl;
use Skaisser\Howl\Events\GenericExceptionEvent;
// Direct severity verbs — use config('howl.driver') by default
Howl::error(new GenericExceptionEvent($exception));
Howl::info('Scheduled job completed');
Howl::audit($auditEvent);
// Channel routing — per-call override beats event default beats config
Howl::on('audits')->audit($event);
// Per-call driver override — same payload, different destination
Howl::driver('slack')->info('Deploy succeeded');
Howl::driver('telegram')->error('Database connection lost');
// Chainable: pick driver + channel + severity in one go
Howl::driver('slack')->channel('deployments')->success('v1.2.0 shipped');
📨 Built-in event templates
use Skaisser\Howl\Events\{
GenericExceptionEvent,
DeploymentEvent,
AuditEvent,
CronHeartbeatEvent,
JobRetryExhaustedEvent,
ManualOperationEvent,
GenericInfoEvent,
};
Howl::error(new GenericExceptionEvent($e));
Howl::deployment(new DeploymentEvent(version: 'v1.2.0', env: 'production', commit: 'abc1234'));
Howl::audit(new AuditEvent(actor: $user->email, action: 'role.changed', target: $role));
🛰️ Channel Failover & Fan-Out
Configure a backup channel and pick the mode:
// config/howl.php
'channel' => 'errors',
'channel_backup' => 'errors-backup',
'channel_mode' => 'failover', // try primary; on failure, backup once
// or
'channel_mode' => 'fan_out', // dispatch to BOTH channels in parallel
failover(default): primary first, backup only on failure.trueon first success,falseif both fail.fan_out: dispatch to primary AND backup sequentially.trueif at least one succeeds. Doubles rate-limit consumption — size yourRateLimiter::for()quota accordingly.
🧪 Testing with HowlFake
use Skaisser\Howl\Facades\Howl;
$fake = Howl::fake();
Howl::error('Something broke');
Howl::driver('slack')->info('Deploy started');
// Global assertions
$fake->assertSent(fn ($p) => $p->severity === 'error');
$fake->assertNothingSent(); // negation form
expect($fake->sent())->toHaveCount(2); // count via the sent() accessor
// Per-driver assertions (v1.0+)
$fake->assertSentVia('discord', fn ($p) => $p->severity === 'error');
$fake->assertSentVia('slack', fn ($p) => $p->severity === 'info');
$fake->assertSentViaNothing('telegram');
No real HTTP calls. No mocks of HTTP clients. Drop-in replacement.
⚡ Queue Mode + Rate Limiting
HOWL_QUEUE=true
HOWL_QUEUE_CONNECTION=redis
HOWL_QUEUE_NAME=default
HOWL_RATE_LIMITER_KEY=howl-discord # opt-in Redis rate limiter
// AppServiceProvider::boot()
use Illuminate\Cache\RateLimiter;
use Illuminate\Cache\RateLimiting\Limit;
RateLimiter::for('howl-discord', fn () => Limit::perMinute(28));
SendHowlJob ships with 3 retries + exponential backoff. Queue-failure events always force sync to avoid recursive loops.
📖 Documentation
The full docs site at howl.skaisser.dev covers everything in depth:
- 🚦 Installation & Quick Start
- 🤖 Drivers: Discord · Slack · Telegram
- 🎛️ Configuration reference
- 🧪 HowlFake testing guide
- 📜 API reference
- ⬆️ Upgrade guide v0.x → v1.0
- 📝 Release notes
For AI agents: llms.txt (index) · llms-full.txt (inline)
🤝 Contributing
Issues and pull requests welcome at github.com/skaisser/howl.
Before opening a PR, run the full suite locally:
composer install
vendor/bin/pest --parallel
vendor/bin/pest --coverage --min=100 # enforces 100% line coverage
vendor/bin/pint # code style
📜 License
MIT — see LICENSE. Copyright © Shirleyson Kaisser. // worktree test commit Wed May 13 03:12:54 -03 2026
Related Packages
Send Laravel logs to Discord with deduplication, configurable error grouping, ra...
Real-time error monitoring and alerting for Laravel apps via Discord