Распределённая трассировка и APM

OxPHP поддерживает распространение W3C Trace Context, экспорт в OpenTelemetry (OTel) и встроенный мониторинг производительности приложений (APM). Входящие заголовки traceparent разбираются и продолжаются, идентификаторы трассировки доступны в PHP через $_SERVER, журналы доступа включают поля трассировки, а спаны можно экспортировать в Jaeger, Grafana Tempo, Zipkin или любой OTLP-совместимый бэкенд.

Плагин APM добавляет три уровня трассировки поверх основы OTel:

  • Автоматическое инструментирование — внутренние функции PHP (PDO, mysqli, cURL, Redis, Memcached, файловый ввод-вывод) перехватываются на уровне движка; каждый вызов становится спаном без единого изменения в коде
  • Трассировка на основе атрибутов — пометьте любую функцию или метод PHP атрибутом #[OxPHP\Apm\Trace], чтобы спаны создавались автоматически
  • PHP SDK — 10 функций oxphp_apm_*() для ручного создания спанов, атрибутов, событий и записи ошибок

Как это работает

  1. Входящий запрос — OxPHP читает заголовки traceparent и tracestate в соответствии со спецификацией W3C Trace Context
  2. Новый спан — для этого перехода генерируется новый идентификатор спана. Входящий идентификатор спана становится родительским
  3. Передача в PHP — идентификаторы трассировки помещаются в $_SERVER['OXPHP_TRACE_ID'], $_SERVER['OXPHP_SPAN_ID'] и $_SERVER['OXPHP_PARENT_SPAN_ID']
  4. Журнал доступа — структурированные JSON-логи содержат поля trace_id и span_id для корреляции логов
  5. Заголовки ответа — обновлённый заголовок traceparent (с идентификатором спана OxPHP) добавляется в ответ, чтобы нижестоящие сервисы могли продолжить трассировку
  6. Экспорт в OTel (необязательно) — когда плагин OTel включён, каждый запрос становится спаном, экспортируемым через OTLP с атрибутами семантических соглашений HTTP

Если заголовок traceparent отсутствует, OxPHP генерирует новый идентификатор трассировки и идентификатор спана и начинает новую трассировку.

Конфигурация

W3C Trace Context (встроено)

Переменная По умолчанию Описание
TRACE_CONTEXT false Включить распространение W3C Trace Context. Установите true или 1

Плагин OpenTelemetry

Плагин OTel — это функция времени компиляции (plugin-otel). При включении он автоматически активирует распространение контекста трассировки (тот же эффект, что и установка TRACE_CONTEXT=true).

Переменная По умолчанию Описание
OTEL_ENABLED false Включить плагин OpenTelemetry. Булево значение — см. Булевы значения
OTEL_EXPORTER_OTLP_PROTOCOL grpc Протокол экспорта: grpc или http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT http://localhost:4317 (gRPC) или http://localhost:4318 (HTTP) Эндпоинт коллектора OTLP. URL с https:// экспортируется по TLS на обоих транспортах, с проверкой по системному хранилищу доверенных сертификатов (образ среды выполнения должен содержать CA-бандл, например ca-certificates — официальный образ его устанавливает); пользовательские CA-бандлы и mTLS пока не поддерживаются
OTEL_EXPORTER_OTLP_TIMEOUT 10000 Таймаут экспорта в миллисекундах
OTEL_EXPORTER_OTLP_HEADERS (не задано) Заголовки аутентификации: key=value,key2=value2
OTEL_SERVICE_NAME oxphp Имя сервиса в экспортируемых спанах
OTEL_SERVICE_VERSION (не задано) Атрибут версии сервиса
OTEL_RESOURCE_ATTRIBUTES (не задано) Дополнительные атрибуты ресурса: env=prod,region=us-east-1
OTEL_TRACES_SAMPLER parentbased_traceidratio Стратегия сэмплирования: always_on, always_off, traceidratio, parentbased_always_on, parentbased_always_off, parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG 1.0 Коэффициент сэмплирования (0.0–1.0) для сэмплеров на основе коэффициента
Note

Недопустимые или выходящие за границы значения OTEL_TRACES_SAMPLER_ARG ограничиваются диапазоном [0.0, 1.0] и записываются в лог на уровне warn. Неизвестные значения OTEL_TRACES_SAMPLER откатываются к parentbased_traceidratio и записываются в лог.

Плагин APM

Плагин APM — это функция времени компиляции (plugin-apm), зависящая от плагина OTel. Он добавляет автоматическое инструментирование, декоратор #[OxPHP\Apm\Trace] и PHP-SDK для трассировки.

Переменная По умолчанию Описание
OTEL_APM_ENABLED false Включить APM: автоматическое инструментирование, захват ошибок, PHP SDK. Требует OTEL_ENABLED=true. Булево значение — см. Булевы значения
OTEL_APM_SLOW_QUERY_MS 100 Порог медленного запроса в миллисекундах. Запросы, превышающие его, получают oxphp.db.slow=true на своих спанах
OTEL_APM_DB_CAPTURE_PARAMS_ENABLED false Записывать привязанные параметры в атрибут спана db.params. Булево значение — см. Булевы значения
OTEL_APM_STACKTRACE_MAX_BYTES 8192 Максимальный размер атрибута exception.stacktrace в байтах. При превышении лимита трассировка стека усекается с конца (корневой кадр сохраняется) с маркером …(truncated). 0 отключает усечение
OTEL_APM_MESSAGE_MAX_BYTES 4096 Максимальный размер атрибута exception.message в байтах (значение по умолчанию совпадает с лимитом New Relic на значение одного атрибута). При превышении лимита сообщение усекается с конца с маркером …(truncated). 0 отключает усечение

Контекст трассировки в PHP

Когда TRACE_CONTEXT=true, в ваших PHP-скриптах доступны три переменные $_SERVER:

Переменная Описание Пример
OXPHP_TRACE_ID Идентификатор трассировки W3C (32 hex-символа) 4bf92f3577b34da6a3ce929d0e0e4736
OXPHP_SPAN_ID Идентификатор спана OxPHP для этого запроса (16 hex-символов) 00f067aa0ba902b7
OXPHP_PARENT_SPAN_ID Входящий родительский идентификатор спана (16 hex-символов, пусто для новой трассировки) a3ce929d0e0e4736

Используйте их для передачи контекста трассировки нижестоящим сервисам:

php
<?php $traceId = $_SERVER['OXPHP_TRACE_ID'] ?? ''; $spanId = $_SERVER['OXPHP_SPAN_ID'] ?? ''; if ($traceId) { // Build a traceparent header for downstream calls $traceparent = "00-{$traceId}-{$spanId}-01"; $response = file_get_contents('https://api.example.com/data', false, stream_context_create([ 'http' => [ 'header' => "traceparent: {$traceparent}\r\n", ], ]) ); }

С Guzzle

php
<?php $traceId = $_SERVER['OXPHP_TRACE_ID'] ?? ''; $spanId = $_SERVER['OXPHP_SPAN_ID'] ?? ''; $client = new \GuzzleHttp\Client(); $response = $client->get('https://api.example.com/users', [ 'headers' => [ 'traceparent' => "00-{$traceId}-{$spanId}-01", ], ]);

Корреляция журнала доступа

Когда контекст трассировки включён, структурированные JSON-логи доступа содержат поля trace_id и span_id:

json
{ "timestamp": "2026-03-23T10:15:30.123Z", "level": "INFO", "fields": { "request_id": "4bf92f3577b34da6-00f067aa", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "span_id": "00f067aa0ba902b7", "method": "GET", "path": "/api/users", "status": 200, "duration_us": 1523, "remote_ip": "10.0.0.1", "message": "request completed" } }

После этого вы можете искать логи по идентификатору трассировки в системах агрегации логов (Loki, Elasticsearch, Splunk, CloudWatch), чтобы найти все записи логов для распределённой трассировки.

Заголовки ответа

OxPHP добавляет заголовок traceparent в каждый ответ со своим собственным идентификатором спана:

http
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

Если входящий запрос содержал заголовок tracestate, он также передаётся в ответе.

Интеграция с OpenTelemetry

Когда плагин OTel включён, каждый HTTP-запрос становится спаном, экспортируемым в ваш бэкенд трассировки через OTLP.

Атрибуты спана

Экспортируемые спаны включают стандартные атрибуты семантических соглашений HTTP:

Атрибут Описание
http.request.method HTTP-метод (GET, POST и т. д.)
url.path Путь запроса
http.response.status_code Код статуса ответа
client.address IP-адрес клиента
server.address Адрес прослушивания сервера
oxphp.request_id Идентификатор запроса OxPHP
http.request.body.size Размер тела запроса в байтах (если ненулевой)
http.response.body.size Размер тела ответа в байтах (если ненулевой)

Ответы 5xx помечаются как спаны с ошибкой.

События спана

Дочерние спаны также несут события спана — аннотации с временными метками, экспортируемые как события OpenTelemetry и отображаемые нативно в Jaeger, Grafana Tempo и других OTLP-бэкендах. Атрибут oxphp.event.kind у каждого события определяет его тип:

oxphp.event.kind Источник Атрибуты события
exception Функция #[OxPHP\Apm\Trace], выбросившая исключение, или oxphp_apm_error() exception.type, exception.message, exception.stacktrace
custom oxphp_apm_event() заданные пользователем
mark аннотация #[Mark] профилировщика заданные пользователем
slow превышение порога #[SlowThreshold] профилировщика threshold_ms, elapsed_ms
memory_spike превышение порога #[MemoryThreshold] профилировщика threshold_kb, delta_bytes

Атрибут oxphp.event.kind может дополнительно принимать значения sql, http или alloc на событиях, генерируемых инструментированием APM.

Идентификатор запроса с OTel

Когда плагин OTel активен, идентификаторы запросов формируются из контекста трассировки: первые 16 символов идентификатора трассировки и первые 8 символов идентификатора спана, разделённые дефисом. Это отображается в логах, в заголовке ответа X-Request-ID и в oxphp_request_id() в PHP.

APM: автоматическое инструментирование

Когда плагин APM включён, OxPHP автоматически перехватывает 33 внутренние функции PHP на уровне движка. Каждый вызов перехваченной функции создаёт дочерний спан под корневым спаном текущего запроса — без каких-либо изменений в коде.

Перехватываемые функции

Категория Функции
PDO PDO::__construct, PDO::query, PDO::exec, PDO::prepare, PDOStatement::execute
mysqli mysqli::__construct, mysqli::query, mysqli::prepare, mysqli_stmt::execute
cURL curl_init, curl_setopt, curl_exec, curl_multi_exec
Redis Redis::connect, Redis::get, Redis::set, Redis::del, Redis::mget, Redis::mset, Redis::hget, Redis::hset, Redis::lpush, Redis::rpush
Memcached Memcached::get, Memcached::set, Memcached::delete, Memcached::getMulti, Memcached::setMulti
Файловый ввод-вывод fopen, fread, fwrite, file_get_contents, file_put_contents

Перехватчики устанавливаются только для расширений, которые действительно загружены. Если ваша сборка не включает расширение Redis, перехватчики Redis молча пропускаются.

Установка перехватчиков

Установка перехватчиков использует двухфазную схему для потокобезопасности в PHP ZTS:

  1. Фаза 1 (MINIT) — во время инициализации модуля OxPHP проверяет каждую целевую функцию по загруженным расширениям и сохраняет указатели на оригинальные обработчики в доступный только для чтения список разрешённых
  2. Фаза 2 (RINIT) — при первом запросе в каждом потоке воркера разрешённые перехватчики устанавливаются в таблицы функций этого потока

Это гарантирует, что каждый поток воркера ZTS имеет согласованные изменения таблицы функций и потоколокальное состояние.

APM: трассировка на основе атрибутов

Атрибут #[OxPHP\Apm\Trace] автоматически создаёт спаны вокруг декорированных функций и методов. В отличие от перехватчиков автоматического инструментирования (которые нацелены на внутренние функции на C), это работает с пользовательским кодом PHP.

php
<?php use OxPHP\Apm\Trace; #[Trace] function processOrder(int $orderId): void { // A span named "processOrder" is created on entry and closed on exit. // If an exception is thrown, the span is marked as error and an // "exception" span event records exception.type, exception.message // and exception.stacktrace. } class PaymentService { #[Trace] public function charge(float $amount): bool { // Span named "PaymentService::charge" return true; } }

Атрибут #[Trace] применим как к функциям, так и к методам. Вызов регистрации не требуется — плагин APM регистрирует декоратор автоматически во время инициализации.

Если декорированная функция выбрасывает исключение, статус спана устанавливается в error, и записывается событие exception с полными данными семантических соглашений OpenTelemetry: exception.type (класс), exception.message (сообщение) и exception.stacktrace (стек вызовов из getTraceAsString()). Сообщение усекается до OTEL_APM_MESSAGE_MAX_BYTES байт (по умолчанию 4096), а трассировка стека — до OTEL_APM_STACKTRACE_MAX_BYTES байт (по умолчанию 8192); 0 отключает любой из лимитов. Захват аргументов внутри кадров подчиняется собственной настройке PHP zend.exception_ignore_args.

APM: PHP-SDK для трассировки

Плагин APM регистрирует 10 функций oxphp_apm_*() для ручного управления спанами. Все функции безопасно выполняются вхолостую, когда APM отключён, поэтому ваш код работает без изменений в любом окружении.

Создание спанов

php
<?php // Start a span and get its local ID $spanId = oxphp_apm_start('cache.warm', ['cache.size' => '1024']); // ... do work ... // Close the span oxphp_apm_end($spanId);

Добавление атрибутов и событий

php
<?php $spanId = oxphp_apm_start('order.process'); // Add attributes to the current span (or a specific one) oxphp_apm_attribute('order.id', $orderId); oxphp_apm_attribute('order.total', $total, $spanId); // Record an event on the span oxphp_apm_event('payment.authorized', [ 'provider' => 'stripe', 'amount' => (string) $amount, ]); oxphp_apm_end($spanId);

Запись ошибок

php
<?php $spanId = oxphp_apm_start('external.api'); try { $result = callExternalApi(); } catch (\Throwable $e) { // Mark the span as error oxphp_apm_error($e, $spanId); throw $e; } finally { oxphp_apm_end($spanId); }

Передача контекста трассировки

php
<?php // Get the current trace ID and span ID $traceId = oxphp_apm_trace_id(); $currentSpanId = oxphp_apm_span_id(); // Or get a ready-to-use traceparent header value $traceparent = oxphp_apm_header(); // "00-{trace_id}-{span_id}-01" // Propagate to downstream services $response = file_get_contents('https://api.example.com/data', false, stream_context_create([ 'http' => [ 'header' => "traceparent: {$traceparent}\r\n", ], ]) );

Справочник функций

Функция Возвращает Описание
oxphp_apm_trace(name, callback, ?attributes) void Выполнить callback внутри спана (зарезервировано для будущего использования)
oxphp_apm_start(name, ?attributes) int Открыть спан и вернуть его локальный ID. 0, когда APM отключён
oxphp_apm_end(span_id) void Закрыть спан с указанным локальным ID
oxphp_apm_attribute(key, value, ?span_id) void Установить атрибут на текущем или указанном спане
oxphp_apm_event(name, ?attributes, ?span_id) void Записать событие с временной меткой на текущем или указанном спане
oxphp_apm_error(exception, ?span_id) void Пометить текущий или указанный спан как error и записать событие exception. Объект Throwable даёт exception.type, exception.message и exception.stacktrace; строковый аргумент записывается как exception.message с обобщённым exception.type, равным Error (чтобы событие оставалось видимым в бэкендах, группирующих по типу)
oxphp_apm_status(code, ?description, ?span_id) void Установить статус спана: 0 = Unset, 1 = Ok, 2 = Error
oxphp_apm_trace_id() string Текущий идентификатор трассировки (32 hex-символа). Пусто, когда APM отключён
oxphp_apm_span_id() string Текущий идентификатор спана (16 hex-символов). Пусто, когда нет активного спана
oxphp_apm_header() string Значение заголовка W3C traceparent для текущего контекста спана

Полный справочник сигнатур функций см. в разделе Функции PHP.

Пример Docker

Готовые к запуску варианты compose.yaml:

Включить распространение трассировки W3C без внешнего бэкенда:

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.10.0 ports: - "80:80" environment: - TRACE_CONTEXT=true - INTERNAL_ADDR=0.0.0.0:9090
Note

Функция Cargo plugin-apm должна быть включена во время сборки. Официальный образ OxPHP включает её по умолчанию.

Стек наблюдаемости

OxPHP предоставляет три столпа наблюдаемости, работающие вместе:

Столп Возможность Корреляция
Метрики Счётчики и гистограммы Prometheus на /metrics Агрегированные данные о производительности
Логирование Структурированные JSON-логи доступа с ACCESS_LOG Детализация по каждому запросу, с поиском по trace_id
Трассировка W3C Trace Context + экспорт OTLP Сквозной распределённый поток запросов

Все три используют один и тот же trace_id и request_id, так что вы можете спуститься от алерта на дашборде Grafana к трассировке в Tempo и к строкам логов в Loki для одного запроса.

Устранение неполадок

Заголовки трассировки не появляются в ответах

TRACE_CONTEXT не включён.

Решение: Установите TRACE_CONTEXT=true или включите плагин OTel через OTEL_ENABLED=true (что автоматически включает контекст трассировки).

Переменные трассировки в $_SERVER пусты

Контекст трассировки отключён, или переменные проверяются вне OxPHP.

Проверка: Переменные OXPHP_TRACE_ID, OXPHP_SPAN_ID и OXPHP_PARENT_SPAN_ID существуют только когда TRACE_CONTEXT=true и запрос обслуживается OxPHP. Проверьте с помощью:

php
<?php echo $_SERVER['OXPHP_TRACE_ID'] ?? 'trace context not enabled';
Спаны не появляются в Jaeger/Tempo

Проверка: Убедитесь, что эндпоинт OTLP доступен из контейнера OxPHP:

bash
docker compose exec app curl -v http://jaeger:4317

Проверка: Убедитесь, что плагин включён:

bash
curl -s http://localhost:9090/config | jq '.plugins'

Решение: Убедитесь, что OTEL_ENABLED=true и OTEL_EXPORTER_OTLP_ENDPOINT указывает на правильный адрес коллектора.

Большой объём сэмплирования в продакшене

Экспорт каждого спана дорого обходится при высоких объёмах трафика.

Решение: Уменьшите коэффициент сэмплирования:

bash
OTEL_TRACES_SAMPLER=parentbased_traceidratio OTEL_TRACES_SAMPLER_ARG=0.1 # Sample 10% of traces

Сэмплирование на основе родителя означает, что если входящий запрос несёт сэмплированную трассировку, она всегда будет сэмплироваться независимо от коэффициента. Новые трассировки, начатые в OxPHP, сэмплируются с настроенной частотой. Если OTEL_TRACES_SAMPLER установлен в неизвестное значение, OxPHP записывает предупреждение в лог и откатывается к parentbased_traceidratio.

См. также