Распределённая трассировка и 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. Значения записываются как есть — в отличие от db.statement, они не обфусцируются, поэтому в трассировках могут оказаться персональные данные (email-адреса, токены и т. п.). Включайте только там, где это приемлемо. Булево значение — см. Булевы значения
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.

Автоматический захват исключений на корневом спане

Когда запрос завершается необработанным исключением или фатальной ошибкой и возвращает 5xx, OxPHP автоматически прикрепляет событие exception к корневому спану запроса — не нужны ни атрибут #[OxPHP\Apm\Trace], ни вызов oxphp_apm_error(). Ошибка 500 становится самоописывающейся в трассировке, а бэкенды, группирующие ошибки по событию исключения (например, New Relic Errors Inbox), начинают работать без какого-либо кода приложения.

Событие несёт стандартные exception.type, exception.message и exception.stacktrace, а также два расширения OxPHP — exception.file и exception.line, указывающие на место выброса (или на место фатальной ошибки). Для бесклассового фаталаtrigger_error(…, E_USER_ERROR), аварийного завершения из-за нехватки памяти или таймаута выполнения — exception.type содержит синтетическое имя (константу ошибки PHP, например E_USER_ERROR), а трассировка стека отсутствует. Вызов неопределённой функции — не бесклассовый фатал в PHP 8: он выбрасывает обычный Error, несущий полную трассировку стека, как любое другое исключение. Сообщение и трассировка стека подчиняются тем же ограничениям OTEL_APM_MESSAGE_MAX_BYTES / OTEL_APM_STACKTRACE_MAX_BYTES, что и остальные события исключений.

Это работает для чистого PHP, приложений без собственного обработчика исключений и обработчиков режима воркеров.

Граница: фреймворки, поглощающие исключения (традиционный путь запроса). В режимах Traditional / Framework / SPA приложение, устанавливающее set_exception_handler() и отображающее собственную страницу ошибки (Laravel, Symfony, WordPress, …), с точки зрения движка исключение обработало. Оно никогда не распространяется необработанным, поэтому OxPHP видит только статус 500 и не может восстановить Throwable. Автоматический захват для таких запросов не срабатывает; записывайте исключение явно из механизма отчётов об ошибках вашего фреймворка через oxphp_apm_error($e).

Режим воркеров не проходит через set_exception_handler(). Рантайм воркера ловит исключение, покинувшее ваше замыкание oxphp_worker(), напрямую, не вызывая пользовательский обработчик исключений движка. Поэтому для обработчиков воркеров автоматический захват срабатывает всякий раз, когда исключение покидает замыкание, даже если код зарегистрировал собственный set_exception_handler() — тот обработчик тогда применяется только к исключениям, которые само замыкание ловит, а не к покидающим его.

Потоковые ответы. Для потокового ответа (SSE или любого цикла с oxphp_stream_flush()) HTTP-статус фиксируется, как только заголовки ушли в сеть, и запрос с этого момента считается завершённым. Фатал, выброшенный после фиксации статуса — на потоковом ответе или на ответе, вызвавшем finish_request(), — только логируется; он не добавляется к корневому спану (трассировка по-прежнему показывает спан, но без события exception). Это задокументированная граница, и действует она одинаково для зафиксированного 5xx и зафиксированного 2xx.

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

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

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

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

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

Категория Функции
PDO PDO::__construct, PDO::query, PDO::exec, PDO::prepare, PDOStatement::execute
mysqli mysqli::__construct, mysqli::query, mysqli::prepare, mysqli_stmt::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 молча пропускаются.

Атрибуты спанов базы данных

Перехватчики баз данных (PDO, mysqli) снабжают свои спаны атрибутами семантических соглашений OpenTelemetry:

Атрибут Источник Пример
db.statement Текст запроса с литеральными значениями, заменёнными на ?, чтобы персональные данные (email-адреса, токены) никогда не попадали в бэкенд трассировки SELECT * FROM users WHERE email = ?
db.operation Ведущее ключевое слово SQL SELECT
db.system Извлекается из DSN PDO / конструктора mysqli mysql, postgresql, sqlite
server.address, server.port Хост/порт соединения (опускаются для unix-сокета или файла SQLite) db.internal, 5432
db.name Имя базы данных или путь к файлу для SQLite shop
oxphp.db.slow true, когда wall-time вызова достигает или превышает OTEL_APM_SLOW_QUERY_MS true
db.params Привязанные параметры, записываемые как есть (без обфускации) — поэтому они могут содержать персональные данные; только при OTEL_APM_DB_CAPTURE_PARAMS_ENABLED=true [1, active]

db.statement читается из аргументов самого вызова query / exec / prepare, поэтому появляется на его спане. На спане PDOStatement::execute он тоже присутствует — прочитанный из собственного свойства queryString объекта выражения, поэтому никогда не может оказаться SQL другого выражения. У спана mysqli_stmt::execute нет db.statement (mysqli не даёт такого свойства), но SQL есть на предшествующем спане mysqli::prepare. Каждый спан execute несёт тайминг, а с ним и флаг медленного запроса, а для PDO — ещё и db.params. Перехватчики кэшей (Redis, Memcached), HTTP-клиента (cURL) и файлового ввода-вывода выдают спан с одним лишь таймингом.

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

Установка перехватчиков использует двухфазную схему для потокобезопасности в 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.11.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.

См. также

Нашли ошибку? Сообщите →