跳转到内容

契约 / 可观测性

可观测性领域涵盖一组向外部暴露引擎运行时状态的契约:ContextAwareExceptionInterface 提供结构化错误上下文,SpectrumInterface 对应可选的加速 sidecar,JobNotificationInterface 流式返回工作进度,DegradationPolicy 枚举用于处理能力丧失时的行为。

Terminal window
composer require nextpdf/core:^3

ContextAwareExceptionInterface 是用于诊断的契约。每个 NextPDF 领域异常都实现了它。任何捕获到的 NextPDF 异常都可以转换为它,以取得结构化上下文,供应用程序性能监控(APM)工具、日志管道或错误报告器使用。这份上下文是一个关联数组,键为 snake_case,值只会是基本类型。它不包含任何嵌套对象,因此可以顺利序列化为 JSON 或 APM 载荷,不会出现意外。这样一来,就无需再解析异常消息来还原诊断数据。自 3.1.0 起,它即为 stable(稳定)。

SpectrumInterface 是可选加速 sidecar 的契约。Spectrum 是一套 CPU 并行运算引擎,会把硬件检测、PDF 解析与图像压缩卸载到本机 sidecar 进程处理。这份契约通过断路器(circuit breaker)报告可用性,因此即使频繁进行健康检查,在 sidecar 停摆时也不会引发级联故障。它会探测硬件能力,并缓存结果。它会向外暴露当前生效的资源预算。它会为上层模块提供一个通用的请求传输(transport)接口。没有 sidecar,引擎仍然可以运行。这份契约的目的,是让加速成为可注入的选项,而不是硬依赖。JobNotificationInterface 会以生成器(generator)的形式,从 sidecar 的 server-sent-events 端点(endpoint)流式返回带类型的工作事件。当终止事件到达,或流(stream)关闭时,生成器即停止。

DegradationPolicy 是处理能力丧失时的行为枚举。当某项能力降级时,这套政策会决定是抛出异常、发出警告,还是静默收集,并且这个决策会考虑实际影响。当影响属于合规风险、语义损失或阻断性时,Strict 会抛出异常。在输出正确性属于强制要求的受监管环境中,这是你应选用的政策。默认的 Balanced 会针对有边界的降级发出结构化警告并继续执行,只有在影响属于阻断性时才抛出异常。对大多数生产环境部署而言,这是你应选用的政策。Permissive 会静默收集每一个事件,且永不抛出异常。在可接受尽力而为输出的预览或草稿模式中,这是你应选用的政策。SpectrumInterfaceJobNotificationInterfaceDegradationPolicy 等类型属于 experimental(实验性)。它们的兼容性承诺比 ContextAwareExceptionInterface 更弱。

类型种类主要成员稳定度起始版本
ContextAwareExceptionInterface接口(interface)getContext(): array<string, mixed>稳定3.1.0
SpectrumInterface接口(interface)isAvailable()probe()getBudget()request()实验性2.1.0
JobNotificationInterface接口(interface)streamEvents(string): Generator<int, JobEvent>实验性2.2.0
DegradationPolicy枚举(enum,字符串)StrictBalancedPermissive实验性2.3.0

getContext() 只会返回基本类型,或由基本类型组成的列表。streamEvents() 会持续产出 JobEvent 对象,直到出现终止事件为止。SpectrumInterface::request() 会以 string 形式返回原始响应正文。probe() 会返回一个 HardwareReport,而 getBudget() 会返回一个 SpectrumBudget

examples/contracts/observability-quickstart.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\ContextAwareExceptionInterface;
use Psr\Log\LoggerInterface;
/**
* Log a NextPDF exception with its structured context.
*
* @param \Throwable $e A caught exception.
* @param LoggerInterface $logger A PSR-3 logger.
*/
function logWithContext(\Throwable $e, LoggerInterface $logger): void
{
if ($e instanceof ContextAwareExceptionInterface) {
$logger->error($e->getMessage(), $e->getContext());
return;
}
$logger->error($e->getMessage());
}

结构化上下文会直接传入日志记录,无需解析消息。

examples/contracts/observability-production.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\DegradationPolicy;
use NextPDF\Contracts\SpectrumInterface;
use Psr\Log\LoggerInterface;
final readonly class AcceleratedParseService
{
public function __construct(
private ?SpectrumInterface $spectrum,
private DegradationPolicy $policy,
private LoggerInterface $logger,
) {}
/**
* Send a parse batch to the sidecar when healthy, otherwise fall back.
*
* @param list<array{id: string, data: string}> $documents PDF binaries with caller IDs.
*
* @return string Raw sidecar response body; decode with a batch-result parser.
*/
public function parse(array $documents): string
{
if ($this->spectrum?->isAvailable() === true) {
return $this->spectrum->request(
'POST',
'/v1/parse',
json: ['documents' => $documents],
scope: ['parse'],
);
}
if ($this->policy === DegradationPolicy::Strict) {
throw new \RuntimeException('Accelerator required under strict policy.');
}
$this->logger->info('Accelerator unavailable; using PHP fallback.');
return $this->phpFallback($documents);
}
/** @param list<array{id: string, data: string}> $documents @return string */
private function phpFallback(array $documents): string
{
// Pure-PHP parse path omitted for brevity.
return '';
}
}

可为 null 的 SpectrumInterface 让加速成为可选功能。这份契约向外暴露单一传输方法 request(),它会以 string 形式返回原始响应正文。上层解析器会把那份正文转换成 NextPDF\Accelerator\BatchResult。具体的 SpectrumClient 会加上带类型的辅助方法,例如 parseBatch():它会封装 request() 并直接返回 BatchResult。那些辅助方法并不属于已冻结的契约。降级政策会决定缺少 sidecar 是否属于致命情况。

  • 并非每一个 \Throwable 都是 NextPDF 异常。务必先用 instanceof ContextAwareExceptionInterface 做防护,再调用 getContext()
  • getContext() 依契约只会返回基本类型。若有消费端预期会拿到嵌套对象,那是错误的假设;这份契约保证返回的是 JSON 安全的值。
  • SpectrumInterface::isAvailable() 受断路器保护,可以放心频繁调用,但 true 结果只是一个当下时间点的检查。请处理 sidecar 在检查与调用之间突然失联的情况。
  • JobNotificationInterface::streamEvents() 是一个生成器。反复遍历它两次并不会重放事件。只能消费它一次。
  • DegradationPolicy::Permissive 永不抛出异常。在该模式下,会影响合规的降级将会静默通过。请勿将它用于受监管的输出。

可观测性契约增加的成本微乎其微。getContext() 返回的是预先构造好的数组。isAvailable() 是一个带缓存、受断路器保护的健康探测。这份契约要求实现方至少将探测结果缓存 30 秒,这样热路径就不会反复调用 sidecar。streamEvents() 的吞吐受限于 sidecar 的事件速率,而非引擎本身。1500 ms wall 与 64 MB 峰值的 performance_budget,是由这些契约所观测的底层工作决定,而非契约本身。其可重现性配置文件为 structural。事件流与异常上下文都会包含时间戳。两次执行会在那些字段上有所不同,但结构保持完全一致。

若结构化异常上下文带有机密数据,它就是一个数据泄露攻击面。这份契约把上下文限制为基本类型,可降低对象意外外泄的风险。在上下文抵达日志接收端之前,部署端仍必须清除其中的敏感值。这就是项目日志政策中的安全遥测义务。加速 sidecar 是一个通过传输层连接的独立进程。请求方法会携带用于授权的范围声明(scope claim)。部署端必须把 sidecar 边界视为信任边界。设为 Permissive 的降级政策,可能会掩盖与安全相关的处理能力丧失。在以输出正确性作为控制措施的场景中,请使用 Strict。请把异常上下文、工作事件与 sidecar 响应都视为可能会被记录的数据,并据此加以清除。

本页未主张任何直接的规范性声明。可观测性契约只负责暴露引擎状态,并未实现任何引擎必须援引其条款的标准化协议。上文提及的安全遥测与日志清除义务,源自项目内部的日志政策,而非任何外部标准。当被观测的操作本身就是标准化的(例如签章或 PDF/A 文件),其符合性会记载于签署或提取页面上。