Распределённая трассировка и 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_*()для ручного создания спанов, атрибутов, событий и записи ошибок
Как это работает
- Входящий запрос — OxPHP читает заголовки
traceparentиtracestateв соответствии со спецификацией W3C Trace Context - Новый спан — для этого перехода генерируется новый идентификатор спана. Входящий идентификатор спана становится родительским
- Передача в PHP — идентификаторы трассировки помещаются в
$_SERVER['OXPHP_TRACE_ID'],$_SERVER['OXPHP_SPAN_ID']и$_SERVER['OXPHP_PARENT_SPAN_ID'] - Журнал доступа — структурированные JSON-логи содержат поля
trace_idиspan_idдля корреляции логов - Заголовки ответа — обновлённый заголовок
traceparent(с идентификатором спана OxPHP) добавляется в ответ, чтобы нижестоящие сервисы могли продолжить трассировку - Экспорт в 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) для сэмплеров на основе коэффициента |
Недопустимые или выходящие за границы значения 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
$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
$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:
{
"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 в каждый ответ со своим собственным идентификатором спана:
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 (MINIT) — во время инициализации модуля OxPHP проверяет каждую целевую функцию по загруженным расширениям и сохраняет указатели на оригинальные обработчики в доступный только для чтения список разрешённых
- Фаза 2 (RINIT) — при первом запросе в каждом потоке воркера разрешённые перехватчики устанавливаются в таблицы функций этого потока
Это гарантирует, что каждый поток воркера ZTS имеет согласованные изменения таблицы функций и потоколокальное состояние.
APM: трассировка на основе атрибутов
Атрибут #[OxPHP\Apm\Trace] автоматически создаёт спаны вокруг декорированных функций и методов. В отличие от перехватчиков автоматического инструментирования (которые нацелены на внутренние функции на C), это работает с пользовательским кодом 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
// 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
$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
$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
// 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 без внешнего бэкенда:
services:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "80:80"
environment:
- TRACE_CONTEXT=true
- INTERNAL_ADDR=0.0.0.0:9090Полный стек наблюдаемости с Jaeger в качестве бэкенда трассировки:
services:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "80:80"
environment:
- OTEL_ENABLED=true
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
- OTEL_SERVICE_NAME=my-app
- OTEL_SERVICE_VERSION=1.0.0
- OTEL_RESOURCE_ATTRIBUTES=env=production
- INTERNAL_ADDR=0.0.0.0:9090
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPCservices:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "80:80"
environment:
- OTEL_ENABLED=true
- OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4317
- OTEL_SERVICE_NAME=my-app
tempo:
image: grafana/tempo:latest
ports:
- "4317:4317"
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"Полная наблюдаемость с автоматическим инструментированием запросов к базе данных, HTTP-вызовов, операций с кэшем и файлового ввода-вывода:
services:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "80:80"
environment:
- OTEL_ENABLED=true
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
- OTEL_SERVICE_NAME=my-app
- OTEL_APM_ENABLED=true
- OTEL_APM_SLOW_QUERY_MS=50
- INTERNAL_ADDR=0.0.0.0:9090
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC
environment:
- COLLECTOR_OTLP_ENABLED=trueФункция 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
echo $_SERVER['OXPHP_TRACE_ID'] ?? 'trace context not enabled';Спаны не появляются в Jaeger/Tempo
Проверка: Убедитесь, что эндпоинт OTLP доступен из контейнера OxPHP:
docker compose exec app curl -v http://jaeger:4317Проверка: Убедитесь, что плагин включён:
curl -s http://localhost:9090/config | jq '.plugins'Решение: Убедитесь, что OTEL_ENABLED=true и OTEL_EXPORTER_OTLP_ENDPOINT указывает на правильный адрес коллектора.
Большой объём сэмплирования в продакшене
Экспорт каждого спана дорого обходится при высоких объёмах трафика.
Решение: Уменьшите коэффициент сэмплирования:
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1 # Sample 10% of tracesСэмплирование на основе родителя означает, что если входящий запрос несёт сэмплированную трассировку, она всегда будет сэмплироваться независимо от коэффициента. Новые трассировки, начатые в OxPHP, сэмплируются с настроенной частотой. Если OTEL_TRACES_SAMPLER установлен в неизвестное значение, OxPHP записывает предупреждение в лог и откатывается к parentbased_traceidratio.
См. также
- Функции PHP — справочник функций
oxphp_apm_*() - Декораторы — перехват функций на основе атрибутов, включая
#[Trace] - Логирование доступа — структурированные JSON-логи с полями трассировки
- Идентификаторы запросов — как идентификаторы запросов взаимодействуют с контекстом трассировки
- Метрики — справочник метрик Prometheus
- Проверки работоспособности — эндпоинт
/config, показывающий статус контекста трассировки - Справочник конфигурации — все переменные окружения