分布式追踪与 APM

OxPHP 支持 W3C Trace Context 传播、OpenTelemetry(OTel)导出,以及内置的应用性能监控(APM)。传入的 traceparent 头会被解析并延续,trace ID 可在 PHP 中通过 $_SERVER 获取,访问日志包含追踪字段,span 可导出到 Jaeger、Grafana Tempo、Zipkin 或任何兼容 OTLP 的后端。

APM 插件在 OTel 基础之上增加了三层追踪能力:

  • 自动埋点 —— 在引擎层挂钩 PHP 内部函数(PDO、mysqli、cURL、Redis、Memcached、文件 I/O);每次调用都会生成一个 span,无需改动任何代码
  • 基于注解的追踪 —— 用 #[OxPHP\Apm\Trace] 注解任意 PHP 函数或方法即可自动创建 span
  • PHP SDK —— 提供 10 个 oxphp_apm_*() 函数,用于手动创建 span、设置属性、记录事件与错误

工作原理

  1. 传入请求 —— OxPHP 依据 W3C Trace Context 规范读取 traceparent 和 tracestate 头
  2. 新 span —— 为本跳生成一个新的 span ID。传入的 span ID 成为其父 span
  3. 传播到 PHP —— trace ID 被注入到 $_SERVER['OXPHP_TRACE_ID']、$_SERVER['OXPHP_SPAN_ID'] 和 $_SERVER['OXPHP_PARENT_SPAN_ID']
  4. 访问日志 —— 结构化 JSON 日志包含 trace_id 和 span_id 字段,用于日志关联
  5. 响应头 —— 更新后的 traceparent 头(带有 OxPHP 的 span ID)会被加入响应,以便下游服务延续该追踪
  6. OTel 导出(可选)—— 当启用 OTel 插件时,每个请求都会成为一个 span,通过 OTLP 携带 HTTP 语义约定属性导出

如果不存在 traceparent 头,OxPHP 会生成新的 trace ID 和 span ID,并开启一条全新的追踪。

配置

W3C Trace Context(内置)

变量 默认值 说明
TRACE_CONTEXT false 启用 W3C Trace Context 传播。设为 true 或 1

OpenTelemetry 插件

OTel 插件是一个编译期特性(plugin-otel)。启用后,它会自动开启 trace-context 传播(效果等同于设置 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 collector 端点。使用 http/protobuf 时,这是 collector 的基础 URL,span 会按照 OTLP 规范的要求发送到其下的 /v1/traces:http://collector:4318 会发送到 http://collector:4318/v1/traces,因此值中不要带上 /v1/traces。https:// URL 会在两种传输方式上通过 TLS 导出,并针对系统信任库进行校验(运行时镜像必须自带 CA 证书包,例如 ca-certificates —— 官方镜像已安装它);尚不支持自定义 CA 证书包和 mTLS
OTEL_EXPORTER_OTLP_TIMEOUT 10000 导出超时,单位毫秒。在 gRPC 上,它同时限定连接 collector 的时间(包括 TLS 握手),因此要让它大于一次连接所需的时间;需要先建立连接的一次尝试最长可能耗时该超时的两倍。在 Linux 上使用 gRPC 时,它还决定一个不再接收数据的连接多快被关闭 —— 参见日志中的 span 导出错误
OTEL_EXPORTER_OTLP_HEADERS (未设置) 认证头:key=value,key2=value2
OTEL_SERVICE_NAME oxphp 导出 span 中的服务名
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 慢查询阈值,单位毫秒。超过此值的查询会在其 span 上被标记 oxphp.db.slow=true
OTEL_APM_DB_CAPTURE_PARAMS_ENABLED false 在 db.params span 属性中记录绑定参数。**值按原样记录——与 db.statement 不同,它们不会被脱敏,因此可能把 PII(邮箱、令牌等)放进你的追踪数据。只在可以接受这一点的场合启用。**布尔值 —— 参见 布尔值
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

当 TRACE_CONTEXT=true 时,你的 PHP 脚本中可以使用三个 $_SERVER 变量:

变量 说明 示例
OXPHP_TRACE_ID W3C trace ID(32 个十六进制字符) 4bf92f3577b34da6a3ce929d0e0e4736
OXPHP_SPAN_ID 本次请求的 OxPHP span ID(16 个十六进制字符) 00f067aa0ba902b7
OXPHP_PARENT_SPAN_ID 传入的父 span ID(16 个十六进制字符,若为新追踪则为空) a3ce929d0e0e4736

利用它们把 trace context 传播到下游服务:

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", ], ]);

访问日志关联

启用 trace context 后,结构化 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", "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0", "message": "request completed" } }

随后你就可以在日志聚合系统(Loki、Elasticsearch、Splunk、CloudWatch)中按 trace ID 搜索日志,找到某条分布式追踪对应的每一条日志记录。

响应头

OxPHP 会为每个响应加上 traceparent 头,携带 OxPHP 自己的 span ID:

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

如果传入请求包含 tracestate 头,它也会一并转发到响应中。

OpenTelemetry 集成

启用 OTel 插件后,每个 HTTP 请求都会成为一个 span,通过 OTLP 导出到你的追踪后端。

Span 属性

导出的 span 包含标准的 HTTP 语义约定属性:

属性 说明
http.request.method HTTP 方法(GET、POST 等)
url.path 请求路径
http.response.status_code 响应状态码
client.address 客户端 IP 地址
user_agent.original 请求的 User-Agent 头(如果发送了多个,取第一个),超过 512 字节时截断到 512 字节(包括一个 …(truncated) 标记)。不是有效 UTF-8 的字节会被替换为 U+FFFD。请求未发送该头时不存在
server.address 服务器监听地址
oxphp.request_id OxPHP 请求 ID
http.request.body.size 请求体大小,单位字节(若非零)
http.response.body.size 响应体大小,单位字节(若非零)
oxphp.shed_reason 准入拒绝该请求的原因,仅出现在被拒绝的请求上:529 时为 queue_full、wait_timeout、waiting_full 或 waiting_bytes,503 时为 shutting_down,500 时为 pool_unavailable —— 即 oxphp_admission_refused_total 上 reason 标签的取值。其他情况下不存在,包括应用自己返回的 529

5xx 响应会被标记为错误 span。

Span 事件

子 span 还会携带 span 事件 —— 带时间戳的注解,作为 OpenTelemetry 事件导出,并被 Jaeger、Grafana Tempo 及其他 OTLP 后端原生渲染。每个事件上的 oxphp.event.kind 属性用于标识其类型:

oxphp.event.kind 来源 事件属性
exception 抛出异常的 #[OxPHP\Apm\Trace] 函数、oxphp_apm_error(),或落在请求根 span 上的未处理异常/致命错误(参见根 span 异常自动捕获) 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

对于由 APM 埋点生成的事件,oxphp.event.kind 属性还可能额外携带 sql、http 或 alloc。

根 span 异常自动捕获

当请求因未处理的异常或致命错误失败并返回 5xx 时,OxPHP 会自动把一个 exception 事件附加到该请求的根 span 上——不需要 #[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,像任何其他异常一样携带完整调用栈。消息和调用栈遵循与其他 exception 事件相同的 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 状态在响应头上线的那一刻就固定了,请求也在那一刻被视为已完成。在状态提交之后抛出的致命错误——在流式响应上,或在调用过 oxphp_finish_request() 的响应上——只会被记录到日志;它不会被加到根 span 上(追踪里仍有该 span,只是没有 exception 事件)。这是一条成文的边界,对已提交的 5xx 和已提交的 2xx 同样成立。

配合 OTel 的请求 ID

当 OTel 插件处于激活状态时,请求 ID 会从 trace context 派生:取 trace ID 的前 16 个字符与 span ID 的前 8 个字符,用短横线分隔。它会出现在日志、X-Request-ID 响应头以及 PHP 的 oxphp_request_id() 中。

APM:自动埋点

启用 APM 插件后,OxPHP 会在引擎层自动挂钩 34 个 PHP 内部函数。每次调用被挂钩的函数都会在当前请求的根 span 下创建一个子 span —— 无需改动任何代码。

被挂钩的函数

类别 函数
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
文件 I/O fopen、fread、fwrite、file_get_contents、file_put_contents

只有对实际已加载的扩展才会安装挂钩。如果你的构建不包含 Redis 扩展,Redis 挂钩会被静默跳过。

数据库 span 属性

数据库挂钩(PDO、mysqli)会用 OpenTelemetry 语义约定属性装饰其 span:

属性 来源 示例
db.statement 查询文本,字面值被替换为 ?,因此 PII(邮箱、令牌)永远不会到达追踪后端 SELECT * FROM users WHERE email = ?
db.operation 开头的 SQL 关键字 SELECT
db.system 从 PDO DSN / mysqli 构造函数解析 mysql、postgresql、sqlite
server.address、server.port 连接的主机/端口(unix socket 或 SQLite 文件时省略) db.internal、5432
db.name 数据库名,SQLite 则为文件路径 shop
oxphp.db.slow 当调用的墙钟时间达到或超过 OTEL_APM_SLOW_QUERY_MS 时为 true true
db.params 绑定参数,按**原样(不脱敏)**记录——因此可能包含 PII;仅当 OTEL_APM_DB_CAPTURE_PARAMS_ENABLED=true 时才记录 [1, active]

db.statement 从每次 query / exec / prepare 调用自己的参数中读取,因此出现在对应的 span 上。在 PDOStatement::execute 的 span 上它也存在,读取的是语句对象自身的 queryString 属性,因此绝不可能是另一条语句的 SQL。mysqli_stmt::execute 的 span 没有 db.statement(mysqli 没有暴露这样的属性),但 SQL 在它之前的 mysqli::prepare span 上。每个 execute span 都携带计时——连同慢查询标记——对 PDO 还携带 db.params。缓存(Redis、Memcached)、HTTP 客户端(cURL)和文件 I/O 挂钩只产生一个纯计时 span。

挂钩安装

挂钩安装采用两阶段设计,以保证 PHP ZTS 下的线程安全:

  1. 阶段 1(MINIT) —— 在模块初始化期间,OxPHP 会针对已加载的扩展校验每个目标函数,并将每个目标在那一时刻的处理器指针捕获到一个只读的已批准列表中
  2. 阶段 2(RINIT) —— 在每个工作线程处理首个请求时,已批准的挂钩会被安装到该线程的函数表中

这确保了每个 ZTS 工作线程都拥有一致的函数表修改和线程本地状态。

span 可能包含等待连接的时间

阶段 1 在 OxPHP 自己替换处理器之后运行。因此,当 RUNTIME_HOOKS 启用了 streams 时,对于该类别同样会挂钩的 PDO、mysqli 或 Redis 方法,捕获到的处理器就是 OxPHP 带守卫的那个:对查询或命令,它会为发起调用的纤程占用连接;对 PDO::__construct,它会把指向同一数据源的持久连接构造串行化(参见运行时钩子)。span 包裹着这个守卫。在工作进程模式的请求纤程和异步任务纤程内部(守卫在那里可能需要等待),span 的耗时(对查询而言还有慢查询标记)包含了等待另一个请求或任务正在使用的连接所花的时间。

APM:基于注解的追踪

#[OxPHP\Apm\Trace] 注解会自动围绕被装饰的函数和方法创建 span。与自动埋点挂钩(针对内部 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 插件会在初始化期间自动注册该装饰器。

如果被装饰的函数抛出异常,span 的状态会被设为错误,并记录一个 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_*() 函数,用于手动管理 span。无论 APM 是否启用,这些函数都会被注册,因此你的代码无需修改即可在任何环境中运行。APM 被禁用时,span 函数变为空操作,并返回哨兵值(ID 为 0,字符串为 "")。

trace 辅助函数始终执行其回调

空操作规则的唯一例外是 oxphp_apm_trace():APM 被禁用时,它仍会执行传入的回调,并照常返回回调的返回值。因此,追踪关闭时回调中的业务逻辑依然会运行。

创建 span

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); }

传播 trace context

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) mixed 在一个 span 内执行回调并返回其返回值。回调接收该 span 的本地 ID;若回调抛出异常,span 会被标记为错误并附带一个 exception 事件(包含类型和消息,不含堆栈跟踪),异常随后继续向上抛出
oxphp_apm_start(name, ?attributes) int 开启一个 span 并返回其本地 ID。APM 被禁用时返回 0
oxphp_apm_end(span_id) void 关闭给定本地 ID 的 span
oxphp_apm_attribute(key, value, ?span_id) void 在当前或指定的 span 上设置一个属性
oxphp_apm_event(name, ?attributes, ?span_id) void 在当前或指定的 span 上记录一个带时间戳的事件
oxphp_apm_error(exception, ?span_id) void 将当前或指定的 span 标记为错误并记录一个 exception 事件。Throwable 对象会贡献 exception.type、exception.message 和 exception.stacktrace;而单纯的字符串参数会被记录为 exception.message,并归入通用的 exception.type(值为 Error),以便在按类型分组的后端中该事件仍然可见
oxphp_apm_status(code, ?description, ?span_id) void 设置 span 状态:0 = Unset,1 = Ok,2 = Error
oxphp_apm_trace_id() string 当前 trace ID(32 个十六进制字符)。APM 被禁用时为空
oxphp_apm_span_id() string 当前 span ID(16 个十六进制字符)。没有活动 span 时为空
oxphp_apm_header() string 当前 span 上下文的 W3C traceparent 头值

完整的函数签名参考,请参见 PHP 函数。

Docker 示例

开箱即用的 compose.yaml 变体:

在没有外部后端的情况下启用 W3C trace 传播:

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

必须在构建时启用 plugin-apm Cargo 特性。官方 OxPHP 镜像默认包含它。

可观测性栈

OxPHP 提供三大协同工作的可观测性支柱:

支柱 特性 关联方式
指标(Metrics) 位于 /metrics 的 Prometheus 计数器和直方图 聚合的性能数据
日志(Logging) 通过 ACCESS_LOG 输出的结构化 JSON 访问日志 每请求级别的细节,可按 trace_id 搜索
追踪(Tracing) W3C Trace Context + OTLP 导出 端到端的分布式请求流

三者共享同一个 trace_id 和 request_id,因此你可以从 Grafana 仪表盘的告警一路下钻到 Tempo 追踪,再到某个请求对应的 Loki 日志行。

排查故障

响应中没有出现追踪头

TRACE_CONTEXT 未启用。

修复: 设置 TRACE_CONTEXT=true,或用 OTEL_ENABLED=true 启用 OTel 插件(它会自动启用 trace context)。

$_SERVER 追踪变量为空

trace context 被禁用,或者这些变量是在 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 中没有出现 span

检查: 确认从 OxPHP 容器可以访问 OTLP 端点:

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 指向正确的 collector 地址 —— 使用 http/protobuf 时,填写它的基础 URL,不带 /v1/traces。如果日志中显示导出失败,日志中的 span 导出错误一节说明了每种失败的含义。

生产环境采样量过高

在高流量下导出每一个 span 代价高昂。

修复: 降低采样比率:

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

基于父级的采样意味着:如果传入请求携带的是已采样的追踪,那么无论比率如何它都会被采样。在 OxPHP 处开启的新追踪则按配置的比率采样。如果 OTEL_TRACES_SAMPLER 被设为无法识别的值,OxPHP 会记录一条警告并回退到 parentbased_traceidratio。

日志中的 span 导出错误

OxPHP 以批次的方式把 span 发送给 collector,并把每次失败的导出单独写成日志行,其 plugin 为 otel:

  • WARN OTLP span export failed, retrying once —— 第一次尝试快速失败,没有得到 collector 的应答,正在重新发送。字段:spans(批次大小)、error。
  • ERROR OTLP span export failed, the batch is dropped —— 导出最终失败,其中的 span 不会再次发送。字段:spans、retried、error。

一条 WARN 行之后如果没有跟着 ERROR 行,说明重试已送达该批次。collector 自己应答的错误不会重试:它们只表现为一条 ERROR 行,且 retried 为 false。

每条 ERROR 行都伴随着同一次导出的另一条日志,由 OpenTelemetry 库写出。它的 message 为空,原因在 error 中:

json
{"level":"ERROR","fields":{"message":"","name":"BatchSpanProcessor.ExportError","error":"Operation failed: code: 'Unknown error', message: \"transport error\", source: ..."}}

error 字段指向的问题:

error 中的内容 发生了什么
code: 'The request does not have valid authentication credentials'(gRPC)、kind: Status(401, None)(http/protobuf) collector 拒绝了 API 密钥
ConnectionReset(两种协议都有)、connection closed(gRPC)、IncompleteMessage(http/protobuf) OxPHP 与 collector 之间的某个环节(例如负载均衡器或 NAT)在连接空闲时把它丢弃了
kind: TimedOut, error: Elapsed(gRPC) 导出需要先建立连接,而连接过程 —— 名称解析、TCP,以及 https:// 端点的 TLS 握手 —— 没有在 OTEL_EXPORTER_OTLP_TIMEOUT 内完成。当名称解析出多个地址时,超时会在它们之间分摊,因此这也可能意味着某一个地址没有在分给它的那份时间内应答

如果连接的对端已经消失却没有发出重置(例如被 NAT 或负载均衡器静默丢弃的连接),经由它进行的导出会像任何 collector 未能及时应答的导出一样,撞上 OTEL_EXPORTER_OTLP_TIMEOUT。在 Linux 上使用 gRPC 时,一旦该连接上已发送的数据在这么长的时间内都未得到确认,连接就会被关闭,之后的导出会重新建立连接。gRPC 连接还带有 TCP keepalive —— 无流量 30 秒后发出第一个探测,之后每 10 秒一个 —— 它让连接在两次导出之间保留在 NAT 和负载均衡器的表项中,并且在默认超时下,会在大约一分钟内关闭一个已失效的空闲连接。

OpenTelemetry 导出指标统计失败的导出、其中的 span 以及重试,并说明导出在什么情况下会被重试。

给你的智能体的提示词

这些提示词假定你的智能体已连接 OxPHP 文档 MCP 服务器——需要哪些页面它会自己去读。

prompt
把 OxPHP 的追踪数据导出到我的追踪后端。通过 OxPHP 文档 MCP 服务器读取 `features/distributed-tracing` 页面。 从我的 Compose 或 Kubernetes 文件里判断 collector 在哪里,给我环境变量配置块和一个我的流量负担得起的采样比率,并指出这个仓库里哪些出站调用必须把 trace context 传播下去。
prompt
我的 OxPHP span 没有到达追踪后端。通过 OxPHP 文档 MCP 服务器读取 `features/distributed-tracing` 页面。 检查我的部署所设置的插件开关、OTLP 端点与协议,以及采样器,告诉我追踪数据是在哪一跳丢掉的。

另请参阅

  • PHP 函数 —— oxphp_apm_*() 函数参考
  • 装饰器 —— 基于注解的函数拦截,包括 #[Trace]
  • 访问日志 —— 带追踪字段的结构化 JSON 日志
  • 请求 ID —— 请求 ID 如何与 trace context 交互
  • 指标 —— Prometheus 指标参考
  • 健康检查 —— 显示 trace context 状态的 /config 端点
  • 配置参考 —— 所有环境变量
发现错误?报告问题 →