Laravel Logging — the complete guide
Laravel's logging system is built on top of Monolog, a battle-tested PHP logging library. The idea is simple: you write a log message, Laravel routes it to one or more channels, and each channel decides what to do with it — write it to a file, push it to Slack, send it to a cloud service, etc.
All logging configuration lives in config/logging.php. The most important key is default, which you control via the LOG_CHANNEL (or LOG_STACK) environment variable in your .env file.
// config/logging.php
'default' => env('LOG_CHANNEL', 'stack'),
Writing a log entry is as simple as:
use Illuminate\Support\Facades\Log;
Log::info('User logged in', ['user_id' => $user->id]);
Log::error('Payment failed', ['order_id' => $order->id, 'reason' => $e->getMessage()]);
Or using the global helper:
logger('Something happened');
logger()->error('Something broke', ['context' => 'here']);
Log levels
Laravel follows the PSR-3 log level standard. Levels are ordered by severity — when you set a minimum level, only messages at that level or higher are recorded.
| Level | Method | When to use |
|---|---|---|
emergency |
Log::emergency() |
System is completely unusable |
alert |
Log::alert() |
Needs immediate action (e.g. DB down) |
critical |
Log::critical() |
Critical conditions, component unavailable |
error |
Log::error() |
Runtime errors that don't require immediate action |
warning |
Log::warning() |
Exceptional occurrences that aren't errors |
notice |
Log::notice() |
Normal but significant events |
info |
Log::info() |
Interesting events, user actions |
debug |
Log::debug() |
Detailed diagnostic information |
Severity order (highest to lowest): emergency > alert > critical > error > warning > notice > info > debug
Built-in channels
Laravel ships with several channels out of the box. You can use them as-is or extend them.
| Channel | Driver | What it does |
|---|---|---|
stack |
stack |
Aggregates multiple channels into one |
single |
single |
Writes to a single file (storage/logs/laravel.log) |
daily |
daily |
Rotates log files daily, keeps 14 days by default |
slack |
slack |
Sends log messages to a Slack webhook |
papertrail |
monolog |
Streams logs to Papertrail via UDP/TCP |
stderr |
monolog |
Writes to stderr — useful in Docker/CI |
syslog |
syslog |
Writes to the system syslog |
errorlog |
errorlog |
Writes using PHP's error_log() |
null |
monolog |
Discards all log messages — handy in testing |
LOG_STACK
When you use the stack channel as your default, the LOG_STACK environment variable controls which channels are bundled into that stack. This is the most flexible setup for production.
# .env
# Use only the daily file channel
LOG_STACK=daily
# Write to daily files AND push critical errors to Slack
LOG_STACK=daily,slack
# Use a single file (simplest, good for small projects)
LOG_STACK=single
The stack channel in config/logging.php picks up LOG_STACK like this:
// config/logging.php
'stack' => [
'driver' => 'stack',
'channels' => explode(',', env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
A few practical combinations:
- Local development:
LOG_STACK=single— one file, no rotation noise - Staging:
LOG_STACK=daily— daily rotation so you can check specific days - Production:
LOG_STACK=daily,slack— file for history, Slack for immediate alerts on errors - Docker / containers:
LOG_STACK=stderr— writes to stdout/stderr, picked up by the container log driver
LOG_LEVEL
LOG_LEVEL sets the minimum severity that a channel will record. Messages below this level are silently dropped.
# .env
# Only record errors and above (error, critical, alert, emergency)
LOG_LEVEL=error
# Record everything including debug output
LOG_LEVEL=debug
# Good balance for production
LOG_LEVEL=warning
This is applied per channel in config/logging.php:
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14,
],
Setting LOG_LEVEL=debug in production will generate a lot of noise and eat disk space fast. Use warning or error on production servers and only drop to debug when actively investigating an issue.
Creating a custom channel
There are two common approaches: adding a new named channel to config/logging.php, or using a Monolog handler directly.
1. A dedicated channel for a specific feature
Useful when you want to keep, say, payment logs completely separate from your main app log:
// config/logging.php
'channels' => [
// ... existing channels ...
'payments' => [
'driver' => 'daily',
'path' => storage_path('logs/payments.log'),
'level' => 'debug',
'days' => 30,
],
],
Then in your code:
Log::channel('payments')->info('Charge succeeded', [
'amount' => $charge->amount,
'customer' => $charge->customer,
]);
2. A custom Monolog handler
When you need something beyond the built-in drivers, use the monolog driver and pass any Monolog handler:
// config/logging.php
'gelf' => [
'driver' => 'monolog',
'handler' => \Monolog\Handler\GelfHandler::class,
'with' => [
'publisher' => new \Gelf\Publisher(
new \Gelf\Transport\UdpTransport('logs.example.com', 12201)
),
],
],
3. A fully custom channel via a factory
For total control, use the custom driver and point it to a factory class:
// config/logging.php
'custom-channel' => [
'driver' => 'custom',
'via' => \App\Logging\CreateCustomLogger::class,
],
// app/Logging/CreateCustomLogger.php
namespace App\Logging;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;
class CreateCustomLogger
{
public function __invoke(array $config): Logger
{
$handler = new StreamHandler(storage_path('logs/custom.log'));
$handler->setFormatter(new JsonFormatter());
return new Logger('custom', [$handler]);
}
}
Stacking channels
The stack driver lets you combine multiple channels so a single Log::info() call writes to all of them simultaneously. You can define multiple named stacks for different environments or use cases.
// config/logging.php
'production' => [
'driver' => 'stack',
'channels' => ['daily', 'slack'],
'ignore_exceptions' => false,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log',
'emoji' => ':boom:',
'level' => env('LOG_LEVEL', 'critical'), // Only push critical and above to Slack
],
The ignore_exceptions flag controls what happens if one channel in the stack fails (e.g. Slack is unreachable). Set it to true if you don't want a logging failure to break your application.
You can also log to a specific channel or stack on the fly:
// Log to a specific channel, ignoring the default
Log::channel('slack')->critical('Database unreachable');
// Log to multiple channels at once
Log::stack(['daily', 'slack'])->error('Something went wrong');
Tips & tricks
Add context to every log message in a request
Use Log::withContext() in a middleware to attach user or request context to every log entry automatically:
// app/Http/Middleware/LogContext.php
public function handle(Request $request, Closure $next): Response
{
if ($user = $request->user()) {
Log::withContext([
'user_id' => $user->id,
'email' => $user->email,
]);
}
return $next($request);
}
Share context for the entire request lifecycle
Log::shareContext() goes one step further and shares context across all channels, including those created via Log::channel():
Log::shareContext([
'request_id' => (string) Str::uuid(),
]);
Tap into Monolog to add processors
Monolog processors let you add extra data to every log record automatically. Use the tap key to customize a channel without switching drivers:
// config/logging.php
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14,
'tap' => [\App\Logging\AddEnvironmentData::class],
],
// app/Logging/AddEnvironmentData.php
namespace App\Logging;
use Illuminate\Log\Logger;
use Monolog\Processor\WebProcessor;
class AddEnvironmentData
{
public function __invoke(Logger $logger): void
{
foreach ($logger->getHandlers() as $handler) {
$handler->pushProcessor(new WebProcessor());
}
}
}
Use a separate channel for slow queries
// app/Providers/AppServiceProvider.php
DB::listen(function (QueryExecuted $query) {
if ($query->time > 1000) { // ms
Log::channel('slow-queries')->warning('Slow query detected', [
'sql' => $query->sql,
'bindings' => $query->bindings,
'time_ms' => $query->time,
]);
}
});
Suppress logs in tests
Set LOG_CHANNEL=null in phpunit.xml or your testing .env.testing to discard all log output during test runs:
<!-- phpunit.xml -->
<env name="LOG_CHANNEL" value="null"/>
Troubleshooting
Nothing is being logged
- Check that
LOG_LEVELisn't set too high. If it'serror,Log::info()andLog::debug()will be silently dropped. - Make sure
storage/logsis writable:chmod -R 775 storage/logs - Verify your
LOG_CHANNEL/LOG_STACKvalue actually exists as a key in thechannelsarray inconfig/logging.php. - Run
php artisan config:clear— a cached config won't pick up.envchanges.
Logs end up in the wrong file
When using the stack channel, double-check which channels are included. LOG_STACK takes a comma-separated list — no spaces around commas:
# Correct
LOG_STACK=daily,slack
# Wrong — the space will cause the channel name to be " slack" (not found)
LOG_STACK=daily, slack
Slack notifications not arriving
- Verify
LOG_SLACK_WEBHOOK_URLis set and the webhook is still active in your Slack app settings. - The default Slack channel level is
critical. Lower it toerrororwarningif you're testing:'slack' => [ 'driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), 'level' => 'error', // was 'critical' ], - Make sure your server can reach Slack's API — check firewall rules and
ignore_exceptionsto surface any connection errors.
Daily log files not rotating
- The
dailydriver creates a new file each day — files are namedlaravel-YYYY-MM-DD.log. If you only see one file, the app may not have been running on subsequent days. - The
daysoption controls retention, not rotation. Rotation happens automatically on each new day. - If you're on a shared host or NFS mount, check that file locking works correctly — Monolog uses
StreamHandlerwhich acquires file locks.
Log file grows without limit
You're probably using the single driver in production. Switch to daily and set a reasonable days value, or configure log rotation at the OS level with logrotate.
Seeing "failed to open stream: Permission denied"
Your web server user (typically www-data) doesn't have write access to storage/logs. Fix it with:
chown -R www-data:www-data storage/logs
chmod -R 775 storage/logs
Context data is missing from log entries
Make sure you pass context as the second argument — it must be an associative array:
// Correct
Log::info('Order created', ['order_id' => $order->id]);
// Wrong — this passes a string, not context
Log::info('Order created', $order->id);