Отладка PHP скриптов с помощью системных логов

Раздел: Разработка на 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 сек.

Логирование в PHP - comments

En
Php log (php)