Logging in Laravel
On this page
Laravel wraps the Monolog library behind the Log facade. It ships with several log channels, such as single, daily, slack, syslog, errorlog and stderr, and you can send one log call to several channels at once.
What to log
Log the things you will need when something goes wrong in production:
- Errors and warnings: unexpected failures and the context around them.
- Events: user logins, background jobs finishing, scheduled tasks running.
- Requests: the route, how long it took, and the response status.
- Resource use: slow database queries and cache misses, where they matter.
Log levels
Laravel supports the eight PSR-3 log levels. From least to most severe:
debug: detailed diagnostic information.info: normal events, such as a user logging in.notice: normal but significant events.warning: something unexpected happened, but the application can continue.error: a runtime error that stops one operation.critical: a serious error. The application may not be able to continue.alert: someone must act immediately.emergency: the system is unusable.
Use the level to filter. You rarely want debug in production, because it produces a large amount of output.
Basic usage
The Log facade writes to the default channel:
use Illuminate\Support\Facades\Log;
Log::info('User logged in', ['user_id' => $user->id]);
Log::warning('Disk space running low', ['percent_used' => 87]);
Log::error('Payment failed', ['order_id' => $order->id, 'reason' => $e->getMessage()]);The second argument is a context array. Laravel stores it alongside the message as structured data.
Keep messages fixed and put variables in context
Each log message should be a fixed string you can search for. Avoid joining variables into the message:
Log::info('User logged in ' . $user->id);That message is different for every user, so you cannot search the codebase for it. Pass the variable as context instead:
Log::info('User logged in', ['user_id' => $user->id]);Now a search for User logged in finds the exact line of code that wrote the entry, and your log tool can filter on user_id.
Configuring channels
Channels are defined in config/logging.php:
return [
'default' => env('LOG_CHANNEL', 'stack'),
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'slack'],
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'info',
'days' => 14,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Logs',
'level' => 'critical',
],
],
];The stack channel sends each log call to every channel it lists. Here that means every info entry and above goes to a daily file, and critical errors also post to Slack. The daily driver rotates the file each day and keeps the number of days you set.
To write to one channel without changing the default:
Log::channel('daily')->info('This goes only to the daily file');
Log::channel('slack')->critical('Alerting the team via Slack');In Docker or Kubernetes, use the stderr or errorlog channel. The container runtime captures that output and can forward it to your log store.
Adding context to every log call
Log::withContext attaches context to every later log call in the same request:
Log::withContext([
'request_id' => $requestId,
'user_id' => auth()->id(),
]);
Log::info('Processing order');
Log::error('Something went wrong');Both entries now include request_id and user_id. Call it from a middleware so every request gets an ID you can search for.
Writing a custom channel
If no built-in driver fits, create a custom channel that returns a Monolog logger:
// config/logging.php
'datadog' => [
'driver' => 'custom',
'via' => App\Logging\DatadogLogger::class,
],namespace App\Logging;
use Monolog\Logger;
class DatadogLogger
{
public function __invoke(array $config): Logger
{
$logger = new Logger('datadog');
$logger->pushHandler(new DatadogHandler());
return $logger;
}
}DatadogHandler is your own Monolog handler class that sends records to the service.
Keeping logs useful and affordable
Too much logging slows the app and costs money to store. A few rules keep the volume down:
- Set a level on each channel, so production only keeps what you need.
- Never log passwords, card numbers or other personal data.
- Do not log large objects or whole models. Log the IDs you need to look them up.
- Only include the context attributes that help you debug. Too many make entries hard to read.
Storing and querying logs with Grafana Loki
A log file on one server stops being enough once you run several instances. Hosted options include the ELK stack, Splunk, Sumo Logic and the logging services from AWS, Azure and Google Cloud.
Grafana Loki is one option. It indexes logs by labels instead of full text, which keeps storage cheaper, and it shows logs next to metrics in Grafana dashboards. You can run it locally with Docker:
docker run -d --name=loki -p 3100:3100 grafana/loki:latestAn agent such as Grafana Alloy then ships your log files or container output to it.
You query Loki with LogQL. Fetch every log line for one service:
{job="my-app"}Find lines containing "error":
{job="my-app"} |= "error"Parse logfmt lines and filter on a field:
{job="my-app"} | logfmt | duration > 100msLogs tell you what happened in one request. To track trends such as response times and error rates, you also need metrics and alerts. Monitoring and alerting in Laravel covers those, and you can turn a log pattern into an alert there too.