Стек вызовов по ID в PHP: отладка и логирование

Раздел: Отладка 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 с одним фреймом.

Стек по ID в PHP - comments

En
Stack php id (php)