MidТеория7 min

Логирование

Каналы логов (single, daily, slack, stack, syslog), создание кастомных каналов, контекст, Monolog в Laravel 11

Логирование в Laravel

Laravel использует библиотеку Monolog для логирования, предоставляя удобную абстракцию с поддержкой множества каналов и драйверов. Конфигурация логирования находится в config/logging.php.

Конфигурация каналов

// config/logging.php
return [
    'default' => env('LOG_CHANNEL', 'stack'),

    'deprecations' => [
        'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
        'trace' => env('LOG_DEPRECATIONS_TRACE', false),
    ],

    'channels' => [
        // Stack - combines multiple channels
        'stack' => [
            'driver' => 'stack',
            'channels' => explode(',', env('LOG_STACK', 'single')),
            'ignore_exceptions' => false,
        ],

        // Single file
        'single' => [
            'driver' => 'single',
            'path' => storage_path('logs/laravel.log'),
            'level' => env('LOG_LEVEL', 'debug'),
            'replace_placeholders' => true,
        ],

        // Daily rotation
        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => env('LOG_LEVEL', 'debug'),
            'days' => env('LOG_DAILY_DAYS', 14),
            'replace_placeholders' => true,
        ],

        // Slack notifications
        'slack' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'username' => 'Laravel Log',
            'emoji' => ':boom:',
            'level' => env('LOG_LEVEL', 'critical'),
            'replace_placeholders' => true,
        ],

        // Papertrail
        'papertrail' => [
            'driver' => 'monolog',
            'level' => env('LOG_LEVEL', 'debug'),
            'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
            'handler_with' => [
                'host' => env('PAPERTRAIL_URL'),
                'port' => env('PAPERTRAIL_PORT'),
                'connectionString' => 'tls://' . env('PAPERTRAIL_URL') . ':' . env('PAPERTRAIL_PORT'),
            ],
            'processors' => [PsrLogMessageProcessor::class],
        ],

        // Standard error output
        'stderr' => [
            'driver' => 'monolog',
            'level' => env('LOG_LEVEL', 'debug'),
            'handler' => StreamHandler::class,
            'formatter' => env('LOG_STDERR_FORMATTER'),
            'with' => [
                'stream' => 'php://stderr',
            ],
            'processors' => [PsrLogMessageProcessor::class],
        ],

        // Syslog
        'syslog' => [
            'driver' => 'syslog',
            'level' => env('LOG_LEVEL', 'debug'),
            'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
            'replace_placeholders' => true,
        ],

        // Error log
        'errorlog' => [
            'driver' => 'errorlog',
            'level' => env('LOG_LEVEL', 'debug'),
            'replace_placeholders' => true,
        ],

        // Null channel (discard all logs)
        'null' => [
            'driver' => 'monolog',
            'handler' => NullHandler::class,
        ],

        // Emergency fallback
        'emergency' => [
            'path' => storage_path('logs/laravel.log'),
        ],
    ],
];

Основные драйверы каналов

single

Записывает все логи в один файл:

'single' => [
    'driver' => 'single',
    'path' => storage_path('logs/laravel.log'),
    'level' => 'debug',
],

daily

Создаёт новый файл каждый день с автоматической ротацией:

'daily' => [
    'driver' => 'daily',
    'path' => storage_path('logs/laravel.log'),
    'level' => 'debug',
    'days' => 14, // Keep logs for 14 days
],
// Creates files: laravel-2026-02-22.log, laravel-2026-02-21.log, etc.

stack

Комбинирует несколько каналов. Лог-сообщение отправляется во ВСЕ каналы стека:

'stack' => [
    'driver' => 'stack',
    'channels' => ['daily', 'slack'],
    'ignore_exceptions' => false,
],

slack

Отправляет логи в Slack-канал через webhook:

'slack' => [
    'driver' => 'slack',
    'url' => env('LOG_SLACK_WEBHOOK_URL'),
    'username' => 'Production Alerts',
    'emoji' => ':rotating_light:',
    'level' => 'critical', // Only critical and above
],

syslog

Отправляет логи в системный syslog:

'syslog' => [
    'driver' => 'syslog',
    'level' => 'warning',
    'facility' => LOG_USER,
],

Использование логирования

Фасад Log

use Illuminate\Support\Facades\Log;

// PSR-3 log levels
Log::emergency('System is unusable.');
Log::alert('Action must be taken immediately.');
Log::critical('Critical conditions.');
Log::error('Runtime errors.');
Log::warning('Exceptional occurrences that are not errors.');
Log::notice('Normal but significant events.');
Log::info('Interesting events.');
Log::debug('Detailed debug information.');

Контекстные данные

Второй аргумент - массив контекстных данных:

Log::info('User logged in.', [
    'user_id' => $user->id,
    'email' => $user->email,
    'ip' => request()->ip(),
]);

Log::error('Payment failed.', [
    'order_id' => $order->id,
    'gateway' => 'stripe',
    'error_code' => $e->getCode(),
    'message' => $e->getMessage(),
]);

Log::warning('Slow query detected.', [
    'query' => $query,
    'time_ms' => $executionTime,
    'connection' => $connectionName,
]);

Запись в конкретный канал

// Write to specific channel
Log::channel('slack')->critical('Server disk space is low.', [
    'disk_usage' => '95%',
    'server' => gethostname(),
]);

// Write to multiple channels (stack on-the-fly)
Log::stack(['daily', 'slack'])->error('Payment gateway down.', [
    'gateway' => 'stripe',
    'downtime_start' => now()->toIso8601String(),
]);

// Write to custom channel
Log::channel('orders')->info('Order placed.', [
    'order_id' => $order->id,
    'total' => $order->total,
]);

Хелпер logger()

// Quick logging
logger('Debug message');
logger()->info('User action performed.', ['user_id' => $user->id]);
logger()->error('Something failed.', ['error' => $e->getMessage()]);

Контекст логирования (Laravel 11+)

Laravel 11 добавил механизм контекста, который автоматически включается во все последующие записи лога:

use Illuminate\Log\Context\Repository;
use Illuminate\Support\Facades\Context;

// Add context that persists across all log entries in this request
Context::add('request_id', (string) Str::uuid());
Context::add('user_id', auth()->id());

// Now ALL log entries will include request_id and user_id
Log::info('Processing order.'); // Includes request_id and user_id
Log::info('Order completed.');  // Also includes request_id and user_id

Контекст в middleware

declare(strict_types=1);

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Str;

final class AddLogContext
{
    public function handle(Request $request, Closure $next): mixed
    {
        Context::add('request_id', (string) Str::uuid());
        Context::add('url', $request->fullUrl());
        Context::add('method', $request->method());

        if ($request->user()) {
            Context::add('user_id', $request->user()->id);
            Context::add('user_email', $request->user()->email);
        }

        return $next($request);
    }
}

Stacked контекст

// Push values to a stack within context
Context::push('breadcrumbs', 'homepage');
Context::push('breadcrumbs', 'category');
Context::push('breadcrumbs', 'product');

// Retrieve stacked values
$breadcrumbs = Context::get('breadcrumbs');
// ['homepage', 'category', 'product']

Контекст через очереди

Контекст автоматически передаётся в queued jobs:

// In controller
Context::add('request_id', $requestId);
Context::add('user_id', auth()->id());

// Dispatch job - context is automatically passed
ProcessOrder::dispatch($order);

// In the job, the context is available
// Log entries in the job will include request_id and user_id

Создание кастомных каналов

Кастомный канал через Monolog

// config/logging.php
'channels' => [
    'orders' => [
        'driver' => 'daily',
        'path' => storage_path('logs/orders.log'),
        'level' => 'info',
        'days' => 30,
    ],

    'api' => [
        'driver' => 'daily',
        'path' => storage_path('logs/api.log'),
        'level' => 'debug',
        'days' => 7,
    ],

    'security' => [
        'driver' => 'stack',
        'channels' => ['daily-security', 'slack'],
    ],

    'daily-security' => [
        'driver' => 'daily',
        'path' => storage_path('logs/security.log'),
        'level' => 'warning',
        'days' => 90,
    ],

    'performance' => [
        'driver' => 'single',
        'path' => storage_path('logs/performance.log'),
        'level' => 'info',
    ],
],

Кастомный Monolog Handler

// config/logging.php
'channels' => [
    'telegram' => [
        'driver' => 'monolog',
        'handler' => \App\Logging\TelegramHandler::class,
        'handler_with' => [
            'token' => env('TELEGRAM_BOT_TOKEN'),
            'chatId' => env('TELEGRAM_CHAT_ID'),
        ],
        'level' => 'critical',
    ],
],
declare(strict_types=1);

namespace App\Logging;

use Monolog\Handler\AbstractProcessingHandler;
use Monolog\LogRecord;

final class TelegramHandler extends AbstractProcessingHandler
{
    public function __construct(
        private readonly string $token,
        private readonly string $chatId,
        int|string $level = 'DEBUG',
        bool $bubble = true,
    ) {
        parent::__construct($level, $bubble);
    }

    protected function write(LogRecord $record): void
    {
        $text = sprintf(
            "*%s* [%s]\n```\n%s\n```",
            $record->level->name,
            $record->datetime->format('Y-m-d H:i:s'),
            $record->formatted ?? $record->message,
        );

        Http::post("https://api.telegram.org/bot{$this->token}/sendMessage", [
            'chat_id' => $this->chatId,
            'text' => $text,
            'parse_mode' => 'Markdown',
        ]);
    }
}

Кастомизация канала через tap

// config/logging.php
'daily' => [
    'driver' => 'daily',
    'path' => storage_path('logs/laravel.log'),
    'level' => 'debug',
    'days' => 14,
    'tap' => [\App\Logging\CustomizeFormatter::class],
],
declare(strict_types=1);

namespace App\Logging;

use Illuminate\Log\Logger;
use Monolog\Formatter\LineFormatter;

final class CustomizeFormatter
{
    /**
     * Customize the given logger instance.
     */
    public function __invoke(Logger $logger): void
    {
        foreach ($logger->getHandlers() as $handler) {
            $handler->setFormatter(new LineFormatter(
                format: "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n",
                dateFormat: 'Y-m-d H:i:s',
                allowInlineLineBreaks: true,
                ignoreEmptyContextAndExtra: true,
            ));
        }
    }
}

Уровни логирования (PSR-3)

emergency → alert → critical → error → warning → notice → info → debug

Когда вы устанавливаете level в конфигурации канала, записываются только сообщения этого уровня и выше:

// level: 'warning' → will log: warning, error, critical, alert, emergency
// level: 'debug'   → will log: ALL levels
// level: 'error'   → will log: error, critical, alert, emergency

Логирование запросов к базе данных

// In AppServiceProvider::boot()
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

DB::listen(function ($query) {
    if ($query->time > 100) { // Slow queries (>100ms)
        Log::channel('performance')->warning('Slow query detected.', [
            'sql' => $query->sql,
            'bindings' => $query->bindings,
            'time_ms' => $query->time,
        ]);
    }
});

Практический пример: структурированное логирование

declare(strict_types=1);

namespace App\Services;

use App\Models\Order;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\Log;

final class OrderService
{
    public function processOrder(Order $order): void
    {
        // Set context for all subsequent log entries
        Context::add('order_id', $order->id);
        Context::add('customer_id', $order->customer_id);

        Log::channel('orders')->info('Starting order processing.');

        try {
            $this->validateInventory($order);
            Log::channel('orders')->info('Inventory validated.');

            $this->chargePayment($order);
            Log::channel('orders')->info('Payment charged.', [
                'amount' => $order->total,
                'currency' => $order->currency,
            ]);

            $this->fulfillOrder($order);
            Log::channel('orders')->info('Order fulfilled.');

        } catch (\Throwable $e) {
            Log::channel('orders')->error('Order processing failed.', [
                'error' => $e->getMessage(),
                'step' => $this->getCurrentStep(),
            ]);

            // Also alert via Slack for critical orders
            if ($order->total > 1000) {
                Log::channel('slack')->critical('High-value order failed!', [
                    'order_id' => $order->id,
                    'total' => $order->total,
                    'error' => $e->getMessage(),
                ]);
            }

            throw $e;
        }
    }
}

Проверь себя

Как Laravel 11 Context автоматически передаёт данные в очередь?

Если канал настроен с level: 'warning', какие уровни логов будут записаны?

Чем Log::channel('slack')->critical('msg') отличается от Log::stack(['daily', 'slack'])->critical('msg')?

Что делает опция 'tap' в конфигурации канала логирования?

Какой драйвер канала логирования комбинирует несколько каналов, отправляя сообщение во все сразу?