Laravel logging: channels, JSON context, and production configuration

Configure Laravel log channels, exact-level filtering, JSON formatters, and queue-worker logging, then verify fields and alerts after collection.

Best practices
Illustration of application records passing through collection stages

Laravel builds its logging system on Monolog. A channel defines an output, a driver defines how that output works, and the Log facade sends records through the configured channel. The practical choices are where logs go, how context is represented, and how web and queue processes pick up configuration changes.

This guide uses Laravel 12.x with Monolog 3 for the configuration examples. The daily retention option below is days in Laravel 12; newer framework versions may use different options, so compare against the documentation for the deployed version. These are documentation-based examples, not a deployed Laravel-to-Guance test.

Write messages with useful context

Logging configuration lives in config/logging.php. In a standard application, LOG_CHANNEL selects the default channel. Within application code where the order, SKU, exception, and callback data already exist:

use Illuminate\Support\Facades\Log;

Log::info('Order created', [
    'order_id' => $order->id,
    'amount' => $order->amount,
]);
Log::warning('Insufficient stock', ['sku' => $sku]);
Log::error('Payment failed', [
    'order_id' => $order->id,
    'exception' => $e,
]);

// Requires a payments channel in config/logging.php.
Log::channel('payments')->info('Payment callback received', [
    'order_id' => $payload['order_id'] ?? null,
    'status' => $payload['status'] ?? null,
]);

The second argument is the context array. Monolog formats it with the message; a JSON formatter preserves its structure. Use the exception key for a caught exception. Select callback fields deliberately instead of logging a complete payment payload, which may contain credentials or personal data.

Choose an output driver

Driver Behavior Typical use
single Writes one configured file Development, or a host with a deliberate external rotation strategy
daily Rotates dated files through Monolog Host deployments where application-managed rotation fits
syslog / errorlog Sends to syslog or PHP's error log destination Existing system logging integration
stack Combines other channels Intentional delivery to several destinations
monolog Constructs a specified Monolog handler Handler-level customization
custom Calls a factory that creates the logger Handler graphs or specialized integrations

For Laravel 12, the following entries belong inside the configuration array returned by config/logging.php:

'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['daily', 'stderr'],
        'ignore_exceptions' => false,
    ],
    'daily' => [
        'driver' => 'daily',
        'path' => storage_path('logs/laravel.log'),
        'level' => env('LOG_LEVEL', 'info'),
        'days' => 14,
    ],
    'stderr' => [
        'driver' => 'monolog',
        'level' => env('LOG_LEVEL', 'info'),
        'handler' => \Monolog\Handler\StreamHandler::class,
        'with' => ['stream' => 'php://stderr'],
        'formatter' => \Monolog\Formatter\JsonFormatter::class,
    ],
],

The stack offers each record to both destinations, subject to their thresholds. Use it when two outputs are intentional. Collecting both copies into the same backend can create duplicates. A container deployment may use only stderr; a host may collect only the dated files. Laravel 12 logging configuration

Filter exact levels with a custom logger

A channel's ordinary level is a minimum severity. Monolog's FilterHandler can instead accept a specific list. Use a factory for a nested handler rather than putting an instantiated handler object into config/logging.php; configuration needs to survive Laravel's configuration cache.

Add this channel entry:

'alerts' => [
    'driver' => 'custom',
    'via' => \App\Logging\CreateAlertLogger::class,
    'path' => storage_path('logs/critical.log'),
],

Create app/Logging/CreateAlertLogger.php:

<?php

namespace App\Logging;

use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\FilterHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;

class CreateAlertLogger
{
    public function __invoke(array $config): Logger
    {
        $stream = new StreamHandler($config['path'], Level::Debug);
        $stream->setFormatter(new JsonFormatter());
        $filter = new FilterHandler($stream, [
            Level::Critical,
            Level::Alert,
            Level::Emergency,
        ]);

        return new Logger('alerts', [$filter]);
    }
}

Then call Log::channel('alerts')->critical('Payment processing unavailable'). The list deliberately admits Critical, Alert, and Emergency. In this particular case those are also all levels at or above Critical; a list is useful when you need a noncontiguous selection. The Monolog constructor takes the list as minLevelOrList, not an acceptedLevels constructor argument. FilterHandler implementation

Use a tap to set the formatter

A tap customizes the channel after Laravel creates it. Add the following to the daily channel shown above, keeping its path, threshold, and retention settings:

'tap' => [\App\Logging\CustomizeFormatter::class],

Create app/Logging/CustomizeFormatter.php:

<?php

namespace App\Logging;

use Illuminate\Log\Logger;
use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\FormattableHandlerInterface;

class CustomizeFormatter
{
    public function __invoke(Logger $logger): void
    {
        $formatter = new JsonFormatter();
        $formatter->includeStacktraces(true);

        foreach ($logger->getHandlers() as $handler) {
            if ($handler instanceof FormattableHandlerInterface) {
                $handler->setFormatter($formatter);
            }
        }
    }
}

This sets JSON output on handlers that support formatters and includes exception traces in that formatter's output. A tap can also attach processors. Review their fields so secrets and oversized request bodies do not enter every record.

Apply configuration to every process

Set LOG_CHANNEL to the chosen output and use LOG_LEVEL=info when that threshold suits the workload. Keep debug logging targeted and temporary. Choose retention from disk capacity and recovery requirements; Laravel's rotating handler is not an archival service or a guarantee that every event remains available for exactly fourteen days.

Avoid having both the application and an external tool independently rotate the same files. If external rotation is required, define how open file handles and the collector react. Ensure the web server and worker accounts can write their selected destinations.

Refresh cached configuration through the application's deployment process, then restart or gracefully recycle long-running workers. An edited .env file does not by itself change configuration already loaded by a queue worker. Add job_id and queue context where it helps distinguish worker failures from HTTP requests.

Validate collected Laravel records

For host files, configure an absolute path corresponding to storage/logs/laravel-*.log in DataKit log collection. For containers, use the configured stdout/stderr collection path. Select a Pipeline that matches the actual JSON output.

Inspect one exception record and its context. Monolog's level_name and datetime need the appropriate severity and timestamp mapping; job_id may be nested under context, so verify the extracted field rather than assuming it is top-level. Map severity to status and confirm that the stored event time matches the application timestamp.

Test both a web request and a queue job. Once the fields are correct, configure log detection for the relevant queue or service, with a suitable evaluation window and threshold. Retain job_id for investigation instead of combining all error messages into one indistinguishable failure count.

Common questions

Should production always use daily instead of single?

No. daily is useful for application-managed file rotation. A single file can fit an established external rotation strategy, while containers often write to stderr. The important requirement is bounded storage and a collection path that survives rotation.

Where do queue-worker logs go?

They use the channels configured in that worker process. Horizon and queue workers are separate, long-running processes; verify their environment, permissions, and restart behavior independently of the web process.

Where does Laravel report exceptions?

The framework's exception reporting uses the configured logging system, subject to reporting and suppression rules. In newer application skeletons, customize reporting through withExceptions in bootstrap/app.php; older applications may use App\Exceptions\Handler. Follow the structure of the actual application instead of assuming every version has the same Handler file. Laravel exception handling

Can logs include SQL queries?

DB::listen can observe queries, and query logging can be enabled for specific diagnostics. Limit duration and scope in production: volume can grow quickly, and SQL bindings may include sensitive values. Redact or omit values that the investigation does not require.

Related reading

Continue with Monolog and PHP logging. The Log4php guide and microservices logging guide are currently in Chinese.

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting