A package to support logging via email in Laravel
shaffe/laravel-mail-log-channel is a Laravel package for a package to support logging via email in laravel.
It currently has 13 GitHub stars and 100.817 downloads on Packagist (latest version 3.3.0).
Install it with composer require shaffe/laravel-mail-log-channel.
Discover more Laravel packages by shaffe
or browse all Laravel packages to compare alternatives.
Last updated
Receive detailed error emails from your Laravel application. Plug it into Laravel's logging stack and get notified when things break β with full context, stack trace, SQL queries, and more.

Exception::context() and log record contextphp artisan mail-log:test to verify your setupcomposer require shaffe/laravel-mail-log-channel
The package auto-registers its service provider.
| Laravel | Package | |:-------------------|:--------| | 10, 11, 12, 13 | ^3.0 | | 5.6 β 13 | ^2.0 | | 5.6 | ^1.0 |
Add a mail channel to config/logging.php and include it in your stack:
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'mail'],
],
'mail' => [
'driver' => 'mail',
'level' => env('LOG_MAIL_LEVEL', 'error'),
'to' => env('LOG_MAIL_ADDRESS'),
],
],
Add the recipient to your .env:
[email protected]
That's it. Unhandled exceptions at or above the configured level will now arrive in your inbox.
All options with their defaults:
'mail' => [
'driver' => 'mail',
'level' => 'error',
// Recipients (see formats below)
'to' => env('LOG_MAIL_ADDRESS'),
// Sender (defaults to mail.from config)
'from' => [
'address' => env('LOG_MAIL_FROM_ADDRESS'),
'name' => env('LOG_MAIL_FROM_NAME', 'Errors'),
],
// Subject line pattern
// Placeholders: %level_name%, %message%, %env%, %context%, %app_name%, %channel%, %datetime%
'subject_format' => '[%level_name%] [%env%] %context% β %message%',
// Throttle identical errors (seconds). Set to 0 or false to disable.
'throttle' => 60,
// Cache store for throttle state (null = default store)
'throttle_cache_store' => null,
// Include last N SQL queries in the email
'log_queries' => true,
// Maximum number of SQL queries to include (default: 10)
'query_limit' => 10,
// Include the incoming request payload (opt-in, off by default).
// Sensitive fields (passwords, tokens, card data) are always redacted.
'log_request_payload' => false,
// Extra field names to redact, merged with the built-in defaults.
'redact_keys' => [],
// Truncate scalar values longer than this many characters.
'payload_max_value_length' => 500,
// Maximum number of fields kept per array level.
'payload_max_keys' => 50,
// Collapse vendor frames in stack trace
'collapse_vendor_frames' => true,
// Custom Mailable class (receives $content and $records)
// 'mailable' => \App\Mail\CustomLogMail::class,
],
The to option accepts several formats:
// Simple string
'to' => '[email protected]',
// Multiple addresses
'to' => ['[email protected]', '[email protected]'],
// With names
'to' => ['[email protected]' => 'Dev Team'],
// Structured
'to' => [
['address' => '[email protected]', 'name' => 'Dev Team'],
['address' => '[email protected]', 'name' => 'Ops'],
],
Route error emails to different recipients based on log level. Levels without a configured recipient (and no default) won't send any email.
'to' => [
'default' => '[email protected]', // fallback for levels not listed
'critical' => '[email protected]', // critical & emergency β on-call
'emergency' => [
['address' => '[email protected]', 'name' => 'On-Call'],
['address' => '[email protected]', 'name' => 'CTO'],
],
'debug' => null, // explicitly suppress debug emails
'info' => '', // same β no email for info
],
Level keys also accept Monolog Level enum values or their numeric equivalents:
use Monolog\Level;
'to' => [
'default' => '[email protected]',
Level::Critical->value => '[email protected]', // 500
'warning' => '[email protected]', // string works too
Level::Debug->value => null, // 100 β suppressed
],
Rules:
default.null, '', or false β no email is sent, even if default exists.default β no email is sent.to works exactly as before.Each level value accepts the same formats as the standard to option (string, array of strings, named array, structured array).
Identical errors are automatically deduplicated to prevent inbox flooding. When the same error occurs multiple times within the throttle window, only the first occurrence sends an email.
Enabled by default with a 60-second window.
Each log record gets a fingerprint to determine uniqueness:
| Record type | Fingerprint components | |:------------|:-----------------------| | Exception | class + code + message + file + line | | Plain message | channel + level + message |
When an error is throttled and then reappears after the window expires, the next email includes a notice indicating how many times the error has occurred since it first appeared, along with the timestamp of the first occurrence.
For example: "β οΈ This error has occurred 47 times since 15 Mar 2025 14:30:00 UTC."
This gives you immediate visibility into the scale of a recurring issue without flooding your inbox.
// Throttle window in seconds (default: 60)
'throttle' => 60,
// Disable throttling
'throttle' => 0,
// Use a specific cache store (useful for multi-server setups)
'throttle_cache_store' => 'redis',
"User 42 not found") produce distinct fingerprints β they won't be incorrectly grouped together.The last N SQL queries leading up to the error are included in the email, with bindings and execution time. This helps understand the database state at the time of failure.
// Number of queries to keep (default: 10)
'query_limit' => 10,
// Disable SQL query logging entirely
'log_queries' => false,
[!WARNING] Query bindings are shown as-is and are not redacted. This is intentional: the whole point of query logging is to reproduce the failing query, and masking the values would defeat it. As a result, a query such as
insert into users (email, password) values (?, ?)will expose its bound values in the email.Treat error emails as confidential: send them to a controlled, access-restricted inbox, and set
'log_queries' => falseif your threat model can't accommodate this. The same applies to the request payload and context sections β redaction there is best-effort on known field names only.
Capture the incoming request body in the error email to help reproduce HTTP failures. Disabled by default because request data is sensitive.
Enable it per channel:
'log_request_payload' => true,
The payload is read with Laravel's request->all(), so it covers both form (application/x-www-form-urlencoded, multipart/form-data) and JSON requests. Nothing is captured on the console or when no request is available.
Sensitive fields are always redacted, recursively and case-insensitively. Their keys remain visible as [REDACTED] so you can tell which fields were present without seeing their values.
The built-in list covers:
| Category | Fields |
|:---------|:-------|
| Auth / secrets | password, password_confirmation, current_password, new_password, secret, token, _token, access_token, refresh_token, api_key, apikey, api_token, authorization, client_secret |
| Card / PCI | card_number, cardnumber, card_no, cc_number, ccnumber, cc_num, cvc, cvv, cvv2, cvc2, card_cvc, card_cvv, security_code, exp_month, exp_year, expiry, expiration, expiration_date, exp_date |
| Stripe | stripe_token, stripetoken, payment_method, payment_intent, setup_intent |
| Braintree / PayPal | payment_method_nonce, nonce |
| Bank details | iban, bic, account_number, routing_number, sort_code |
Add your own field names β they are merged with the defaults, never replacing them:
'redact_keys' => ['ssn', 'national_id'],
Redacted fields remain visible in the email as [REDACTED] so you can see which fields were submitted without exposing their values.
To keep emails readable and within SMTP limits, the payload is bounded:
// Truncate any scalar value longer than this (characters).
// Truncated values are annotated, e.g. "abc⦠[1200 chars total]".
'payload_max_value_length' => 500,
// Keep at most this many fields per array level.
// Omitted fields are reported as "+ N more fields omitted".
'payload_max_keys' => 50,
Files are described, never dumped β only the original name, MIME type, and size are included. File contents are never read or attached.
File paths in error emails are clickable when app.editor is configured. Clicking opens the file at the correct line in your IDE.
// config/app.php
'editor' => 'phpstorm',
Or via .env:
APP_EDITOR=phpstorm
Examples: phpstorm, vscode, vscode-insiders, cursor, sublime, kiro, nova, idea.
Custom URL scheme:
'editor' => [
'href' => 'custom://open?file={file}&line={line}',
],
Remote path remapping (when server paths differ from local):
'editor' => [
'name' => 'phpstorm',
'base_path' => '/local/path/to/project',
],
Each error email includes badges showing the application environment, PHP and Laravel versions, server hostname, peak memory usage, and execution time (time elapsed since LARAVEL_START).
This helps identify errors related to resource exhaustion or slow requests at a glance.
Verify that your mail log channel is properly configured by sending a test email:
php artisan mail-log:test
Options:
# Test with a specific log level
php artisan mail-log:test --level=critical
# Test a specific channel name
php artisan mail-log:test --channel=mail
This sends a fake exception through the configured channel so you can confirm recipients, SMTP settings, and throttle behavior without waiting for a real error.
HtmlFormatter::addRow() has been removedIf you extended HtmlFormatter or parsed the HTML output, review the new format.
This package is a fork of laravel-log-mailer by Steve Porter.
MIT β see LICENSE.