Техники трассировки фатальных сбоев в PHP7: от перехвата до логирования
Трассировка фатальных ошибок в 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)
Пояснение шагов:
- Регистрируется shutdown-функция, которая будет выполнена после завершения любого скрипта.
- Внутри функции
error_get_last()возвращает массив с информацией о последней произошедшей ошибке. - Проверяется тип ошибки - если это фатальная (E_ERROR и т.д.), выполняется логирование.
- Важно:
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