Стек вызовов по ID в PHP: отладка и логирование
Основные подходы к трассировке стека по ID
Как связать стек вызовов с уникальным идентификатором запроса или ошибки?
Эффективное решение: создание собственного логгера, который при каждом вызове сохраняет результат debug_backtrace() вместе с переданным ID. Такой подход позволяет в любой момент восстановить последовательность вызовов для конкретного события.
Пример базового класса:
class TraceLogger {
private static $storage = [];
public static function log($id) {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
self::$storage[$id] = $trace;
}
public static function get($id) {
return self::$storage[$id] ?? null;
}
}
Stack php id (стек по id в php)
Использование:
$requestId = uniqid('req_', true);
TraceLogger::log($requestId);
// ... выполнение кода ...
$trace = TraceLogger::get($requestId);
Типичная проблема: массив debug_backtrace() может содержать слишком много вложенных вызовов, что замедляет работу. Решение: использовать флаг DEBUG_BACKTRACE_IGNORE_ARGS для исключения аргументов функций. Также можно ограничивать глубину трейса параметром limit (доступно с PHP 5.4).
Как получить стек без сохранения в переменную?
Функция debug_print_backtrace() выводит стек напрямую в стандартный поток вывода. Удобна для быстрой отладки, но не подходит для логирования с ID.
function test() {
debug_print_backtrace();
}
test();
Недостаток: нельзя перехватить вывод для записи в файл. Для перенаправления используйте буферизацию (ob_start() и ob_get_clean()), но это неэффективно.
Как получить стек в виде строки, не выбрасывая исключение?
Создание объекта Exception без throw и вызов getTraceAsString().
$e = new Exception();
$stackString = $e->getTraceAsString();
Результат - форматированный многострочный стек. Можно сохранить с ID.
Ошибка: если включен zend.assertions, создание исключения может быть медленным. Используйте только при отладке.
Как использовать Xdebug для получения стека по ID?
Xdebug предоставляет функцию xdebug_get_function_stack(), которая возвращает массив с подробной информацией (номера строк, аргументы).
$stack = xdebug_get_function_stack();
file_put_contents('/tmp/stack_' . $id . '.log', print_r($stack, true));
Xdebug должен быть включен. В production среде его отключают из-за снижения производительности. Используйте только для разработки.
Как добавить ID в глобальный обработчик ошибок?
С помощью set_error_handler и set_exception_handler можно перехватывать ошибки и записывать стек вместе с уникальным ID, переданным через контекст (например, через замыкание).
$requestId = bin2hex(random_bytes(8));
set_error_handler(function($severity, $message, $file, $line) use ($requestId) {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
error_log("[$requestId] $message in $file:$line\n" . print_r($trace, true));
});
Проблема: обработчик ошибок не срабатывает для фатальных ошибок. Используйте register_shutdown_function для их захвата.
Расширенные примеры трассировки стека с идентификаторами
Пример 1. Логирование стека в файл с микро-ID и временем
function logStackWithId($id = null) {
$id = $id ?? uniqid('st_');
$trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 10);
$logData = [
'id' => $id,
'time' => microtime(true),
'trace' => $trace
];
file_put_contents(
'/var/log/stack_traces.log',
json_encode($logData) . PHP_EOL,
FILE_APPEND | LOCK_EX
);
}
// Использование
logStackWithId('order_123');
Пример записи в лог:
{"id":"order_123","time":1712345678.9012,"trace":[{"file":"/var/www/index.php","line":10,"function":"logStackWithId","args":["order_123"]},{"file":"/var/www/index.php","line":15,"function":"{main}"}]}
Пример 2. Интеграция с Monolog и добавление контекста trace_id
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$logger = new Logger('app');
$logger->pushHandler(new StreamHandler('/var/log/app.log', Logger::DEBUG));
$traceId = hash('crc32b', microtime());
$logger->debug('Processing order', [
'trace_id' => $traceId,
'stack' => (new Exception())->getTraceAsString()
]);
Результат в логе:
[2025-04-02 10:15:30] app.DEBUG: Processing order {"trace_id":"a1b2c3d4","stack":"#0 /var/www/index.php(45): ..."}
Пример 3. Получение полного стека с аргументами через Xdebug (только dev)
// Предварительная настройка php.ini:
// xdebug.mode = develop,debug
// xdebug.show_exception_trace = 1
function deepFunction($arg) {
if ($arg === 0) {
$stack = xdebug_get_function_stack();
$id = 'deep_call_' . $arg;
file_put_contents(
"/tmp/xdebug_$id.log",
var_export($stack, true)
);
} else {
deepFunction($arg - 1);
}
}
deepFunction(3);
Содержимое файла xdebug_deep_call_0.log:
array (
0 =>
array (
'function' => 'deepFunction',
'file' => '/var/www/xdebug_test.php',
'line' => 5,
'params' => '0',
),
1 =>
array (
'function' => 'deepFunction',
'file' => '/var/www/xdebug_test.php',
'line' => 11,
'params' => '1',
),
2 =>
array (
'function' => 'deepFunction',
'file' => '/var/www/xdebug_test.php',
'line' => 11,
'params' => '2',
),
3 =>
array (
'function' => 'deepFunction',
'file' => '/var/www/xdebug_test.php',
'line' => 11,
'params' => '3',
),
4 =>
array (
'function' => '{main}',
'file' => '/var/www/xdebug_test.php',
'line' => 14,
'params' => '',
),
)
Пример 4. Использование register_shutdown_function для захвата фатальных ошибок с ID
$errorId = 'fatal_' . date('Ymd_His');
register_shutdown_function(function() use ($errorId) {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
file_put_contents(
'/var/log/fatal_errors.log',
"[$errorId] {$error['message']} in {$error['file']}:{$error['line']}\n" .
print_r($trace, true) . "\n---\n",
FILE_APPEND
);
}
});
// Вызов несуществующей функции для теста
undefinedFunction();
Содержимое fatal_errors.log:
[fatal_20250402_101530] Call to undefined function undefinedFunction() in /var/www/test.php:18
Array
(
[0] => Array
(
[file] => /var/www/test.php
[line] => 20
[function] => {main}
)
)
---
Пример 5. Кастомный трейсер с группировкой по ID в оперативной памяти (Redis)
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$traceId = $_SERVER['HTTP_X_TRACE_ID'] ?? bin2hex(random_bytes(8));
$redis->lPush("trace:$traceId", json_encode(debug_backtrace(0)));
// Ограничение длины очереди (последние 100 вызовов)
$redis->lTrim("trace:$traceId", 0, 99);
// По завершении запроса
$fullTrace = $redis->lRange("trace:$traceId", 0, -1);
$redis->expire("trace:$traceId", 3600);
Полный стек в Redis можно получить через команду LRANGE trace:abc123 0 -1. Каждый элемент - JSON с одним фреймом.