Laravel Discord Logger
Send Laravel logs to Discord with deduplication, configurable error grouping, rate limiting, async delivery and a graceful no-op when the webhook URL is missing.
#Laravel Discord Logger
Send Laravel logs to Discord — built for production. A Monolog channel with deduplication, configurable error grouping, rate limiting, async delivery with 429 backoff, and a graceful no-op when the webhook URL is missing.
#Installation
composer require jeffersongoncalves/laravel-discord-logger
Publish the config (optional):
php artisan vendor:publish --tag="laravel-discord-logger-config"
#Configuration
Add a channel to config/logging.php:
'discord' => [ 'driver' => 'custom', 'via' => \JeffersonGoncalves\DiscordLogger\Logger::class, 'level' => env('LOG_DISCORD_LEVEL', 'error'), 'url' => env('LOG_DISCORD_WEBHOOK_URL'), ],
Stack it onto your default channel so errors fan out:
'stack' => [ 'driver' => 'stack', 'channels' => ['single', 'discord'], 'ignore_exceptions' => false, ],
Set the webhook (leave empty in local/testing — nothing will be sent, nothing will break):
LOG_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/xxx/yyy
#How the improvements work
#Error grouping (fingerprint)
config/discord-logger.php → grouping.strategy:
message— group by message textlevel_message— group by level + messageexception(default) — group by exception class + file + line- a
callbackfn (\Monolog\LogRecord $record): stringfor full control
With grouping.normalize on, volatile tokens (numbers, UUIDs, hashes) are stripped before hashing, so User 123 not found and User 456 not found collapse together.
#Deduplication
Within deduplication.window seconds, only the first occurrence of a fingerprint is sent. Repeats are counted silently. When the window closes, a single summary (🔁 occurred N×) is delivered — turn it off with deduplication.summary => false.
#Rate limiting
rate_limit.global— hard cap on total messages app-widerate_limit.per_fingerprint— extra guard against a single looping error
#Async delivery
Delivery runs through a queued job by default (queue.enabled). Discord 429s are retried respecting Retry-After; bad webhooks (4xx) fail fast instead of looping. Set queue.enabled => false to send inline (best-effort, errors swallowed).
This means a queue worker must be running (
php artisan queue:work, Horizon, or supervisor) forLog::debug/info/error(...)calls to actually reach Discord.discord-logger:testsends inline and bypasses the queue entirely, so it will succeed even with no worker running — don't use it alone to confirm real logging works. If you don't run a worker (or don't want to), setDISCORD_LOGGER_QUEUE=false.
#State store
Dedup + rate-limit counters live in the cache store named by store (null = default). Use Redis in production for atomic counters.
#Per-level webhooks & mentions
Route a level to its own channel and ping someone when it matters:
'webhooks' => [ 'EMERGENCY' => env('DISCORD_LOGGER_WEBHOOK_ALERTS'), 'CRITICAL' => env('DISCORD_LOGGER_WEBHOOK_ALERTS'), ], 'mentions' => [ 'EMERGENCY' => '@here', 'CRITICAL' => '<@&123456789012345678>', // role id ],
allowed_mentions is set automatically, so @here / @everyone / role / user pings actually fire.
#Context redaction
Sensitive data is masked before it ever reaches Discord, via two complementary strategies:
- Key matching (
redact) — case-insensitive key fragments whose values are masked. Defaults coverpassword,secret,token,authorization,api_key,apikey. Scrubbing recurses into nested arrays and the public properties of objects carried in the context. - Value patterns (
redact_value_patterns) — PCRE regexes matched against scalar values, so a secret leaking under an innocuous key (e.g.url,auth) — or inside the message text and the stacktrace — is still caught. Defaults coverBearertokens and JWTs; leave the array empty to disable value-based redaction.
#Fallback channel & safety
The handler never throws and is guarded against logging-while-logging recursion. If delivery throws, the error is swallowed; set fallback_channel (e.g. 'single') to record those failures instead of losing them.
#Troubleshooting
discord-logger:test works, but Log::debug/info/error(...) never shows up in Discord.
The test command sends inline, straight to the webhook. Real log calls go through a queued job by default (see Async delivery), so nothing is delivered until a queue worker processes it. Fixes:
- Run a worker:
php artisan queue:work(or Horizon/supervisor in production), or - Set
DISCORD_LOGGER_QUEUE=falseto send inline instead (no worker needed).
This is especially easy to hit on a fresh Laravel install (Laravel 11+, including 13): QUEUE_CONNECTION defaults to database, but no worker runs automatically — jobs just sit pending in the jobs table.
Also check the channel's level in config/logging.php — if it's set above the level you're logging at (e.g. level => 'error' while calling Log::debug(...)), Monolog filters the record before it ever reaches the handler.
#Commands
# Publish the config + optionally star the repo php artisan discord-logger:install # Send a test message to verify the webhook php artisan discord-logger:test --channel=discord
#Testing
composer test
#Changelog
Please see CHANGELOG for more information on what has changed recently.
#Contributing
Please see CONTRIBUTING for details.
#Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
#Credits
#License
The MIT License (MIT). See License File.