跳转到内容

Telemetry:OpenTelemetry 桥接与无操作回退

Telemetry 模块是引擎的可选 OpenTelemetry 桥接层。安装 OpenTelemetry SDK 后,它会发出带有已清理属性的 span 与 metric。若未安装,完整的一组无操作 tracer 与 meter 对象会让检测调用保持有效且零成本。因此,检测代码始终可以安全地留在执行路径中。

Terminal window
composer require nextpdf/core:^3

设计目标是实现「不存在时零成本」的可观测性。引擎的热路径会无条件调用 tracer 与 meter。这些调用是否实际执行工作,取决于运行时,而不是每个调用点上的条件判断。

OpenTelemetryInterceptor 就是这个桥接层。isAvailable() 会报告 OTel SDK 是否存在。startSpan(string $name, array $attributes = []) / endSpan(?object $span) 会包裹一次受追踪的操作,而 recordMetric() 会记录计数器或量规值。当 OTel 不存在时,拦截器会报告不可用,这些调用则保持惰性(不执行任何动作)。TelemetryBridge 会把拦截器接入引擎的观测点。

AttributeSanitizer 是安全防护层。sanitize(array $attributes) 会在属性映射离开进程之前先完成清洗。遥测属性是常见的意外 PII 泄漏通道,因此清理是契约的一部分,而不是附加选项。它与拦截器、桥接层一样,都是 @since 2.3.0

NullTracerNullSpanBuilderNullSpanNullMeterNullCounterNullHistogram 就是无操作回退。它们实现了与 OTel SDK 暴露的相同 API 形状——spanBuilder()setAttribute()(可链式调用)、startSpan()end()createHistogram()createUpDownCounter()add()record()——但本身什么也不做。由于回退是完整的,受检测的代码无需根据可用性做分支判断;它可以直接调用 tracer,由无操作回退吸收这次调用。

主要成员角色
OpenTelemetryInterceptorisAvailable(), startSpan(), endSpan(), recordMetric()OTel span/metric 桥接(@since 2.3.0
TelemetryBridge引擎接线将拦截器连接到观测点(@since 2.3.0
AttributeSanitizersanitize(array $attributes): arrayPII 安全的属性清洗器(@since 2.3.0
NullTracerspanBuilder(string $name): NullSpanBuilder无操作 tracer
NullSpanBuildersetAttribute(), startSpan(): NullSpan无操作 span builder(可链式调用)
NullSpanend()无操作 span
NullMetercreateHistogram(), createUpDownCounter()无操作 meter
NullCounter / NullHistogramadd(), record()无操作仪器

运行 composer docs:generate-api-php -- --module=Telemetry 可获取完整的 PHPDoc 表格。

来源:examples/33-opentelemetry-observability.php

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Telemetry\OpenTelemetryInterceptor;
$otel = new OpenTelemetryInterceptor(/* optional OTel tracer/meter */);
$span = $otel->startSpan('pdf.render', ['doc.pages' => 12]);
// ... render work ...
$otel->endSpan($span);
$otel->recordMetric('pdf.render.bytes', 482_113, ['profile' => 'pdfa4']);

当 OTel SDK 不存在时,上面的每个调用都是无操作——代码保持不变,成本为零。

使用已清理的属性包裹一次渲染,防止调用方提供的元数据泄漏到 span 中。

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Telemetry\AttributeSanitizer;
use NextPDF\Telemetry\OpenTelemetryInterceptor;
final readonly class InstrumentedRenderer
{
public function __construct(
private OpenTelemetryInterceptor $otel,
private AttributeSanitizer $sanitizer,
) {}
/**
* @param callable():string $render Returns the rendered PDF bytes.
* @param array<string, mixed> $attributes Caller-supplied span attributes.
*/
public function render(callable $render, array $attributes): string
{
$span = $this->otel->startSpan('pdf.render', $this->sanitizer->sanitize($attributes));
try {
return $render();
} finally {
$this->otel->endSpan($span);
}
}
}
  • 无操作回退按设计就是完整的。不要为了「省下工作」而用 isAvailable() 防护检测代码。无操作本来就没有成本,这层防护反而会新增一个分支;设计目标正是消除这个分支。
  • 务必先让调用方提供的属性经过 AttributeSanitizer,再进入 span 或 metric。遥测属性是意外 PII 泄漏的通道。
  • endSpan(null) 是有效的——null span 表示无操作情形。每一次 startSpan() 都应搭配一次位于 finally 中的 endSpan()
  • NullSpanBuilder::setAttribute() 会返回 static 以支持链式调用;在无操作下,这条调用链是惰性的,这是有意设计,并非缺陷。
  • 可重现性配置为 structural:span 会带有时间戳与 trace id,因此两次运行在这些字段上的值会不同。

当 OTel 不存在时,成本只是进入一次无操作方法调用——实际等同于零。当它存在时,成本来自 OTel SDK,而非本模块;桥接层只额外增加属性清理,其成本与属性数量成线性关系。1500 ms 墙钟时间/64 MB 峰值的 performance_budget 是引擎参考值,而不是遥测 SLA。

遥测是一个数据出口面。AttributeSanitizer 是阻止机密与 PII 进入 span 和 metric 的控制机制。对于任何受调用方影响的属性,都应将清理视为必需;这是本项目的安全遥测义务。OTel 导出器会把数据发送到外部后端,而该后端边界就是一道信任边界。请从密钥管理器配置其端点与凭据,不要使用已提交(committed)的配置文件。应假设 span 与 metric 数据会抵达某个日志接收端,并据此清洗。请参阅 /modules/core/security/ 中的引擎威胁模型。

本模块不会作出任何关于 PDF 规范的规范性声明。它桥接的是 OpenTelemetry 数据模型;这是外部可观测性规范,而不是 PDF 条款。无操作回退会镜像 OTel API 接口,使受检测代码具备可移植性;这是一种 API 兼容性特性,而不是 PDF 符合性声明。引擎符合性由 /modules/core/conformance/ 中所述的 oracle 与 golden 测试套件验证。