Техники трассировки фатальных сбоев в PHP7: от перехвата до логирования

Раздел: Ошибки PHP -> Фатальные ошибки PHP

Трассировка фатальных ошибок в PHP7: основные методы

Фатальные ошибки (E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR) в PHP7 приводят к немедленному завершению скрипта. Для отладки необходимо получить трассировку стека вызовов в момент ошибки. В статье рассмотрены несколько подходов к перехвату и логированию таких ошибок.

Как перехватить фатальную ошибку и получить ее стек вызовов?

Основной метод - комбинация функций register_shutdown_function и error_get_last. После регистрации callback будет вызван при любом завершении скрипта, включая фатальные ошибки. Внутри callback проверяется последняя ошибка через error_get_last.

<?php
register_shutdown_function(function() {
    $error = error_get_last();
    if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
        // Логирование ошибки
        $message = sprintf(
            "Фатальная ошибка: %s в файле %s на строке %d\nТип: %d",
            $error['message'],
            $error['file'],
            $error['line'],
            $error['type']
        );
        file_put_contents('/path/to/log.txt', $message . PHP_EOL, FILE_APPEND);
    }
});

// Пример фатальной ошибки
trigger_error('Тестовая фатальная ошибка', E_USER_ERROR);
?>

Php 7 trace fatal error (трассировка фатальной ошибки php7)

Пояснение шагов:

  1. Регистрируется shutdown-функция, которая будет выполнена после завершения любого скрипта.
  2. Внутри функции error_get_last() возвращает массив с информацией о последней произошедшей ошибке.
  3. Проверяется тип ошибки - если это фатальная (E_ERROR и т.д.), выполняется логирование.
  4. Важно: trigger_error с уровнем E_USER_ERROR также считается фатальной ошибкой (E_USER_ERROR = 256) и перехватится этим методом.

Типичные проблемы и их решения:

  • Ошибка не логируется - проверьте, что shutdown-функция зарегистрирована до возникновения ошибки (например, в начале скрипта). Убедитесь, что путь к лог-файлу доступен для записи.
  • Ошибки парсинга (E_PARSE) - регистрация shutdown-функции происходит после выполнения всего кода, но если синтаксическая ошибка в самом файле, скрипт не выполняется, и shutdown не вызывается. Для таких случаев нужно использовать внешние инструменты (например, php -l).
  • shutdown-функция вызывается и при нормальном завершении - в этом случае error_get_last() может вернуть null или предыдущую ошибку. Обязательно проверяйте наличие ошибки и её тип.

Как получить детальный стек вызовов с помощью Xdebug?

Xdebug предоставляет расширенные возможности трассировки. Для фатальных ошибок можно включить сбор стека вызовов с помощью настроек xdebug.collect_params, xdebug.show_exception_trace и функции xdebug_get_function_stack() внутри shutdown-функции.

<?php
// Включение трассировки Xdebug (настройки в php.ini или ini_set)
ini_set('xdebug.collect_params', '4');
ini_set('xdebug.show_exception_trace', '1');
ini_set('xdebug.auto_trace', '0');

register_shutdown_function(function() {
    $error = error_get_last();
    if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
        // Получение стека вызовов (требуется Xdebug)
        if (function_exists('xdebug_get_function_stack')) {
            $stack = xdebug_get_function_stack();
            // Логирование в файл
            $log = "Фатальная ошибка: {$error['message']}\nФайл: {$error['file']}:{$error['line']}\nСтек вызовов:\n";
            foreach ($stack as $frame) {
                $log .= sprintf("  %s(%s) called at [%s:%s]\n",
                    $frame['function'] ?? 'unknown',
                    implode(', ', $frame['params'] ?? []),
                    $frame['file'] ?? 'unknown',
                    $frame['line'] ?? '0'
                );
            }
            file_put_contents('/path/to/xdebug_log.txt', $log . PHP_EOL, FILE_APPEND);
        }
    }
});
?>

Пояснение: Функция xdebug_get_function_stack() возвращает массив кадров стека, где каждый кадр содержит имя функции, параметры, файл и строку. Параметры отображаются в зависимости от значения xdebug.collect_params.

Проблемы и их решения:

  • Xdebug не установлен - функция xdebug_get_function_stack не существует. В этом случае можно использовать debug_backtrace(), но внутри shutdown-функции при фатальной ошибке стек может быть пуст. Рекомендуется проверять наличие расширения.
  • Излишнее потребление памяти - Xdebug может замедлять работу. Включайте его только при отладке.

Как настроить логирование фатальных ошибок через php.ini?

Стандартные директивы log_errors и error_log позволяют записывать все ошибки, включая фатальные, в файл на сервере. Это простейший способ без программирования.

; В php.ini или через .htaccess
log_errors = On
error_log = /path/to/php_errors.log
display_errors = Off ; для production

После настройки все ошибки PHP будут записываться в указанный лог-файл. Формат записи содержит тип, сообщение, файл и строку, но не содержит полный стек вызовов (за исключением случаев с Xdebug).

Проблемы:

  • Нет доступа к php.ini - на общих хостингах можно использовать ini_set('log_errors', '1') в начале скрипта, но это не перехватит ошибки парсинга до выполнения. Для таких ошибок нужно смотреть логи веб-сервера.
  • Ошибки не отображаются в логе - проверьте права на запись в файл error_log и путь.

Почему set_error_handler не ловит фатальные ошибки и как использовать set_exception_handler?

В PHP7 фатальные ошибки (E_ERROR) можно перехватить с помощью конструкции try-catch, так как они стали объектами класса Error, реализующего интерфейс Throwable. Однако это работает только для ошибок, которые могут быть пойманы в контексте выполнения. Некоторые фатальные ошибки (например, E_PARSE) не поддаются catch.

<?php
set_exception_handler(function($exception) {
    $message = 'Необработанное исключение или ошибка: ' . $exception->getMessage() . 
               ' в файле ' . $exception->getFile() . ' на строке ' . $exception->getLine();
    file_put_contents('/path/to/exceptions.log', $message . PHP_EOL, FILE_APPEND);
});

// Пример фатальной ошибки, которую можно поймать
try {
    undefinedFunction(); // Ошибка "Call to undefined function" - это E_ERROR
} catch (\Error $e) {
    // Ошибка будет обработана здесь
    echo 'Поймана ошибка: ' . $e->getMessage();
}
?>

Обратите внимание, что set_exception_handler ловит только исключения и ошибки, которые не были пойманы в try-catch. Для ошибок, возникающих в глобальном коде (например, вне try), обработчик сработает, но для ошибок парсинга он не вызывается.

Проблемы:

  • Не перехватываются ошибки парсинга - код не выполняется, поэтому set_exception_handler не срабатывает. Единственный способ - проверка через php -l или логи сервера.
  • Путаница с set_error_handler - set_error_handler обрабатывает нефатальные ошибки (E_WARNING, E_NOTICE и др.), но не E_ERROR. Для фатальных нужно использовать try-catch или shutdown.

Как получить трассировку вызовов через debug_backtrace внутри shutdown-функции?

Функция debug_backtrace() возвращает стек вызовов на момент её вызова. Внутри shutdown-функции она может показать только саму shutdown-функцию, но не место возникновения ошибки. Поэтому её использование ограничено.

<?php
register_shutdown_function(function() {
    $error = error_get_last();
    if ($error) {
        $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 10);
        $log = "Ошибка: {$error['message']}\n";
        $log .= "Текущий стек (shutdown):\n";
        foreach ($backtrace as $i => $frame) {
            $log .= sprintf("#%d %s(%s) called at [%s:%s]\n",
                $i,
                $frame['function'] ?? 'unknown',
                implode(', ', $frame['args'] ?? []),
                $frame['file'] ?? 'unknown',
                $frame['line'] ?? '0'
            );
        }
        file_put_contents('/path/to/debug_log.txt', $log . PHP_EOL, FILE_APPEND);
    }
});
// Фатальная ошибка
foo();
?>

Результат покажет только кадры внутри shutdown-функции, а не точку вызова foo(). Это делает метод бесполезным для трассировки фатальной ошибки. Предпочтительнее использовать Xdebug или комбинировать с error_get_last.

Проблема: debug_backtrace в shutdown не отражает контекст ошибки. Чтобы получить настоящий стек, нужно до возникновения ошибки захватить контекст, что невозможно при фатальной ошибке.

Пример
<?php
// Полный обработчик ошибок для production
class ErrorHandler {
    private static $logFile = '/var/log/php_errors.log';
    private static $email = 'admin@example.com';

    public static function register() {
        set_exception_handler([self::class, 'handleException']);
        set_error_handler([self::class, 'handleError']);
        register_shutdown_function([self::class, 'handleShutdown']);
    }

    public static function handleException($e) {
        self::log($e->getMessage(), $e->getFile(), $e->getLine(), get_class($e), $e->getTraceAsString());
        self::sendEmail($e);
    }

    public static function handleError($severity, $message, $file, $line) {
        // Обработка нефатальных ошибок (E_WARNING и т.д.)
        self::log($message, $file, $line, $severity);
        return true; // предотвращаем стандартный обработчик
    }

    public static function handleShutdown() {
        $error = error_get_last();
        if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
            self::log($error['message'], $error['file'], $error['line'], $error['type']);
            // Попытка получить стек через xdebug
            if (function_exists('xdebug_get_function_stack')) {
                $stack = xdebug_get_function_stack();
                self::appendStackToLog($stack);
            }
            self::sendEmail($error);
        }
    }

    private static function log($message, $file, $line, $type, $trace = '') {
        $date = date('Y-m-d H:i:s');
        $entry = "[$date] Тип: $type | $message в $file:$line\n";
        if ($trace) {
            $entry .= "Стек:\n$trace\n";
        }
        file_put_contents(self::$logFile, $entry, FILE_APPEND | LOCK_EX);
    }

    private static function appendStackToLog($stack) {
        $entry = "Стек вызовов (Xdebug):\n";
        foreach ($stack as $frame) {
            $entry .= sprintf("  %s(%s) at %s:%s\n",
                $frame['function'] ?? 'unknown',
                implode(', ', $frame['params'] ?? []),
                $frame['file'] ?? 'unknown',
                $frame['line'] ?? '0'
            );
        }
        file_put_contents(self::$logFile, $entry, FILE_APPEND | LOCK_EX);
    }

    private static function sendEmail($error) {
        $subject = 'Критическая ошибка на сайте';
        $body = print_r($error, true);
        mail(self::$email, $subject, $body);
    }
}

// Регистрация обработчиков
ErrorHandler::register();

// Тестовая фатальная ошибка
echo $undefinedVar;
?>

Результат (содержимое лог-файла):

[2023-05-15 12:34:56] Тип: 8 | Undefined variable: undefinedVar в /var/www/index.php:44
[2023-05-15 12:34:56] Тип: 1 | Call to undefined function foo() в /var/www/index.php:47
Стек вызовов (Xdebug):
  {main}() at /var/www/index.php:47
Пример
<?php
// Использование Xdebug для трассировки в файл (без программирования)
// Настройки в php.ini:
xdebug.auto_trace = On
xdebug.trace_output_dir = /var/log/xdebug_traces
xdebug.collect_params = 4
xdebug.collect_return = On
// После этого при каждом запросе будет создаваться файл трассировки
// При фатальной ошибке стек будет в этом файле.
?>

Результат: файл трассировки, например trace.1234567890.xt содержит полный стек вызовов, включая параметры и возвращаемые значения.

TRACE START [2023-05-15 12:35:00]
    0.0001    123456    -> {main}() /var/www/index.php:0
    0.0002    123456    -> test() /var/www/index.php:5
    0.0003    123456    -> strpos() /var/www/index.php:6
    0.0004    123456    -> trigger_error() /var/www/index.php:7
    0.0005    123456    -> exit() /var/www/index.php:7
TRACE END   [2023-05-15 12:35:00]
Пример
<?php
ini_set('log_errors', '1');
ini_set('error_log', 'syslog');

register_shutdown_function(function() {
    $error = error_get_last();
    if ($error && in_array($error['type'], [E_ERROR, E_PARSE])) {
        syslog(LOG_CRIT, "Фатальная ошибка: {$error['message']} в {$error['file']}:{$error['line']}");
    }
});

// Тест
include 'nonexistent.php';
?>

Результат: запись в системный лог (например, /var/log/syslog или /var/log/messages).

May 15 12:36:00 server php[12345]: Фатальная ошибка: require(nonexistent.php): failed to open stream: No such file or directory в /var/www/index.php:10

Трассировка фатальной ошибки PHP7 - comments

En
Php 7 trace fatal error (php)