Отладка PHP скриптов с помощью системных логов
Основы логирования в PHP
Логирование помогает отслеживать выполнение скриптов, ошибки и отладочную информацию. В PHP есть несколько способов организовать запись логов. Рассмотрим наиболее эффективные варианты.
Как организовать гибкое логирование с разными уровнями и каналами?
Библиотека Monolog
Monolog – это стандартное решение для логирования в современных PHP приложениях. Она поддерживает множество обработчиков (файлы, базы данных, почта, Slack) и уровни логирования (debug, info, notice, warning, error, critical, alert, emergency).
Установка через Composer:
composer require monolog/monologПример базовой настройки записи в файл:
require 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('my_app');
$log->pushHandler(new StreamHandler('var/log/app.log', Logger::DEBUG));
$log->info('Запрос обработан', ['user_id' => 42]);
$log->error('Ошибка соединения с БД', ['code' => 500]);Каждый обработчик можно настроить независимо: уровень, формат, файл ротации. Monolog решает проблему структурированности записей и конкуренции доступа к файлам (используется блокировка внутри StreamHandler).
Типичные проблемы:
- Ошибка установки – не указаны права на запись в директорию логов. Решение: создать директорию и установить права 775 или 777 для окружения разработки.
- Конфликт версий – требуется PHP 7.2+ и совместимые зависимости. Рекомендуется использовать последнюю стабильную версию Composer.
- Игнорирование уровней – не все ошибки будут записаны, если уровень обработчика установлен выше. Необходимо явно указать Logger::DEBUG для отлова всего.
Как записывать сообщения в системный журнал или файл без внешних зависимостей?
Функция error_log
Встроенная функция позволяет отправлять сообщения в системный syslog, в указанный файл или по email. Простой пример:
error_log('Пользователь залогинился', 3, '/var/log/myapp.log');Параметр 3 указывает на файл. Для записи в syslog используется тип 0. Функция не требует дополнительных библиотек, но формат записи неструктурированный, и нет управления уровнем. Для отладки на сервере с общей файловой системой могут возникнуть проблемы с правами.
Типичные ошибки:
- Отсутствие прав на запись в указанный файл. Решение: проверить владельца процесса и права директории.
- Смешивание сообщений – функция не поддерживает уровни, все пишется в один поток. Для разделения приходится использовать разные файлы вручную.
- При использовании syslog сообщения попадают в системный журнал (например, /var/log/messages), что перегружает администратора. Не рекомендуется на продакшене.
Как быстро записать данные в лог без дополнительных инструментов?
Прямая запись через file_put_contents
Самый простой способ – дописать строку в файл:
file_put_contents('log.txt', date('Y-m-d H:i:s') . ' - ' . 'Сообщение' . PHP_EOL, FILE_APPEND | LOCK_EX);Флаг LOCK_EX устанавливает исключающую блокировку, предотвращая одновременную запись несколькими процессами. Однако блокировка не гарантирует атомарность для больших объемов. Этот метод подходит для простых скриптов и быстрого прототипирования.
Типичные проблемы:
- Конкуренция – при высокой нагрузке блокировка может снизить производительность или вызвать взаимоблокировки. Решение: перейти на Monolog или использовать очереди.
- Отсутствие структуры – записи хранятся в виде обычного текста без разбора. Для анализа требуется сложный парсинг.
- Проблемы с правами – если файл не существует, file_put_contents создаст его, но права могут не совпадать с ожидаемыми. Явно задавать chmod после создания.
Как перехватывать все ошибки PHP и записывать их в лог?
Собственный обработчик ошибок (set_error_handler)
Можно переопределить стандартное поведение при ошибках и записывать их в созданный лог.
function my_error_handler($severity, $message, $file, $line) {
$logMessage = "[$severity] $message in $file:$line";
error_log($logMessage, 3, '/var/log/errors.log');
// можно также вывести пользователю стандартную страницу
return true;
}
set_error_handler('my_error_handler');
echo 1/0; // вызовет ошибку Division by zeroОбработчик ловит не все типы ошибок: фатальные (E_ERROR) и ошибки времени компиляции (E_COMPILE_ERROR) не перехватываются. Для них требуется register_shutdown_function и проверка последней ошибки через error_get_last(). Это усложняет реализацию.
Типичные ошибки:
- Фатальные ошибки не попадают в лог – скрипт завершается до вызова обработчика. Решение: дополнительно настроить shutdown-функцию.
- Поведение зависит от error_reporting – если уровень строгий, многие сообщения могут быть проигнорированы. Необходимо выставить error_reporting(-1) при разработке.
- Рекурсия – если внутри обработчика происходит ошибка, можно зациклиться. Использовать try-catch или guard.
Расширенные примеры логирования
Monolog с несколькими обработчиками и форматированием
Покажем конфигурацию, которая одновременно пишет в файл с подробным форматом и отправляет срочные уведомления по email.
require 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\NativeMailerHandler;
use Monolog\Formatter\LineFormatter;
$log = new Logger('production');
// Обработчик для файла с ротацией даты
$fileHandler = new StreamHandler(__DIR__ . '/logs/app_'.date('Y-m-d').'.log', Logger::DEBUG);
$fileHandler->setFormatter(new LineFormatter("[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"));
$log->pushHandler($fileHandler);
// Обработчик для email – только ошибки и выше
$mailHandler = new NativeMailerHandler(
'admin@example.com',
'Критическая ошибка в приложении',
'errors@example.com',
Logger::ERROR
);
$log->pushHandler($mailHandler);
// Логирование с контекстом
$log->info('Разработчик запросил страницу', ['uri' => $_SERVER['REQUEST_URI']]);
$log->error('Исключение в модуле', ['exception' => $e->getMessage()]);Результат в файле (pre.ex_r):
[2025-02-24 14:30:01] production.INFO: Разработчик запросил страницу {"uri":"/index.php"} []
[2025-02-24 14:30:02] production.ERROR: Исключение в модуле {"exception":"Connection refused"} []Логирование с использованием процессоров для добавления данных
Processors позволяют обогащать каждую запись, например, IP-адресом или идентификатором пользователя.
use Monolog\Processor\WebProcessor;
use Monolog\Processor\IntrospectionProcessor;
$log->pushProcessor(new WebProcessor());
$log->pushProcessor(new IntrospectionProcessor(Logger::WARNING)); // добавляет имя класса и строку
$log->warning('Время выполнения превысило 2 секунды');Вывод (pre.ex_r):
[2025-02-24 14:32:10] production.WARNING: Время выполнения превысило 2 секунды {"ip":"192.168.1.1","method":"GET","url":"/slow.php"} {"class":"App\\Controller\\IndexController","function":"index","line":45}Логирование исключений в Monolog
Monolog умеет форматировать объекты Throwable как строку.
try {
throw new \InvalidArgumentException('Неверный ID пользователя');
} catch (\Throwable $e) {
$log->error($e->getMessage(), ['exception' => $e]);
}В файле появится полное исключение с трейсом (pre.ex_r):
[2025-02-24 14:35:00] production.ERROR: Неверный ID пользователя {"exception":"[object] (InvalidArgumentException(code: 0): Неверный ID пользователя at /var/www/index.php:15)"} []Собственный обработчик ошибок с захватом фатальных
Пример более полной реализации, которая ловит и фатальные ошибки через shutdown.
set_error_handler(function($severity, $message, $file, $line) {
$logLine = "[$severity] $message в файле $file на строке $line";
file_put_contents('/var/log/errors.txt', $logLine . PHP_EOL, FILE_APPEND | LOCK_EX);
});
register_shutdown_function(function() {
$lastError = error_get_last();
if ($lastError !== null && in_array($lastError['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
$logLine = "[FATAL] {$lastError['message']} в файле {$lastError['file']} на строке {$lastError['line']}";
file_put_contents('/var/log/errors.txt', $logLine . PHP_EOL, FILE_APPEND | LOCK_EX);
}
});При ошибке типа 'Call to undefined function' будет записано (pre.ex_r):
[FATAL] Call to undefined function undefinedFunction() в файле /var/www/fatal.php на строке 3
Сравнение производительности: file_put_contents vs Monolog
Для оценки быстродействия можно провести микро-тест. Monolog с одним обработчиком показывает сопоставимую скорость, но при большом количестве запросов (10000+) накладные расходы на классы и автозагрузку становятся заметными. Однако для реальных приложений гибкость и структурированность Monolog оправдывают затраты.
$start = microtime(true);
for ($i=0; $i<1000; $i++) {
file_put_contents('/dev/null', "$i", FILE_APPEND | LOCK_EX);
}
echo 'Время file_put_contents: ' . (microtime(true) - $start) . ' сек.';Время file_put_contents: 0.045 сек.
$log->pushHandler(new StreamHandler('/dev/null', Logger::DEBUG));
$start = microtime(true);
for ($i=0; $i<1000; $i++) {
$log->addRecord(Logger::DEBUG, $i);
}
echo 'Время Monolog: ' . (microtime(true) - $start) . ' сек.';Время Monolog: 0.067 сек.