Введение: Почему логирование — основа надежного PHP-приложения
Логирование (log) — это процесс записи событий, происходящих в приложении, в специальный журнал (лог-файл). Для PHP-разработчика грамотное логирование — не просто «плюс в карму», а необходимость. Без него отладка продакшен-ошибок превращается в гадание, а мониторинг производительности становится невозможным. Плохо настроенное логирование может привести к утечке чувствительных данных или, наоборот, к потере критически важной информации.
В этой статье мы разберем лучшие практики логирования в PHP: от выбора библиотеки и уровней логирования до структурирования сообщений и безопасности. Вы узнаете, как превратить разрозненные echo и var_dump в профессиональную систему мониторинга.
1. Выбор библиотеки: PSR-3 и Monolog
В современном PHP-сообществе стандартом де-факто является интерфейс Psr\Log\LoggerInterface (PSR-3). Он определяет 8 методов для разных уровней логирования: emergency, alert, critical, error, warning, notice, info, debug.
Самая популярная реализация PSR-3 — библиотека Monolog. Она гибкая, мощная и поддерживается практически всеми современными фреймворками (Laravel, Symfony, Yii2).
Установка Monolog через Composer
composer require monolog/monolog
Базовый пример использования
<?php
require 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Создаем логгер с именем канала (например, 'my_app')
$log = new Logger('my_app');
// Добавляем обработчик: пишем в файл, уровень - INFO и выше
$log->pushHandler(new StreamHandler(__DIR__ . '/app.log', Logger::INFO));
// Логируем сообщения
$log->info('Пользователь вошел в систему', ['user_id' => 123]);
$log->warning('Неверный пароль', ['attempts' => 3]);
$log->error('Ошибка подключения к БД', ['exception' => $e->getMessage()]);
?>
Обратите внимание: второй аргумент — это контекст (context). В него можно передавать любые дополнительные данные: ID пользователя, IP, стек вызовов и т.д. Никогда не склеивайте данные в строку сообщения — используйте контекст.
2. Уровни логирования и фильтрация
Использование правильных уровней — основа читаемого лога. Общее правило: чем выше уровень, тем меньше сообщений. Вот краткая шпаргалка:
- DEBUG (100) — отладочная информация для разработки. В продакшене обычно отключен.
- INFO (200) — важные события: пользователь зарегистрировался, заказ создан.
- NOTICE (250) — нормальные, но значимые события.
- WARNING (300) — нештатная ситуация, но приложение продолжает работу (например, неудачная попытка входа).
- ERROR (400) — ошибка, требующая вмешательства (не удалось сохранить файл).
- CRITICAL (500) — критическая ошибка (недоступна база данных).
- ALERT (550) — требуется немедленное действие.
- EMERGENCY (600) — система неработоспособна.
Фильтрация в разных средах
В продакшене не нужно писать DEBUG-сообщения — они создают лишнюю нагрузку на диск и затрудняют поиск важных записей. Используйте разные обработчики для разных сред:
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\FingersCrossedHandler;
$log = new Logger('app');
if (getenv('APP_ENV') === 'production') {
// В продакшене пишем только WARNING и выше
$log->pushHandler(new StreamHandler('/var/log/app/error.log', Logger::WARNING));
// FingersCrossedHandler: буферизирует все, но пишет только при ошибке
$log->pushHandler(new FingersCrossedHandler(
new StreamHandler('/var/log/app/debug.log', Logger::DEBUG),
Logger::ERROR
));
} else {
// В разработке пишем всё
$log->pushHandler(new StreamHandler(__DIR__ . '/debug.log', Logger::DEBUG));
}
?>
3. Структурирование логов и обработка ошибок
Плохо: "Ошибка: " . $e->getMessage(). Хорошо: структурированный JSON или четкий формат с контекстом. Monolog по умолчанию выводит строку, но вы можете форматировать вывод с помощью LineFormatter или JsonFormatter.
Пример форматирования в JSON (для анализа в ELK/Graylog)
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;
$log = new Logger('api');
$handler = new StreamHandler('/var/log/app/api.log', Logger::INFO);
$handler->setFormatter(new JsonFormatter());
$log->pushHandler($handler);
$log->info('API запрос', [
'method' => $_SERVER['REQUEST_METHOD'],
'uri' => $_SERVER['REQUEST_URI'],
'duration_ms' => 120
]);
// Вывод: {"message":"API запрос","context":{"method":"GET","uri":"/users","duration_ms":120},"level":200,"level_name":"INFO","channel":"api","datetime":"2024-01-15T10:30:00+00:00"}
?>
Интеграция с обработчиком ошибок PHP
Не полагайтесь только на try-catch. Перехватывайте фатальные ошибки, которые не попадают в блоки catch:
<?php
set_error_handler(function ($severity, $message, $file, $line) use ($log) {
$log->error($message, [
'file' => $file,
'line' => $line,
'severity' => $severity
]);
// Стандартная обработка PHP
return false;
});
register_shutdown_function(function () use ($log) {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
$log->critical($error['message'], [
'file' => $error['file'],
'line' =