跳到內容

契約 / 可觀測性

可觀測性領域涵蓋一組對外揭露引擎執行期狀態的契約: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 的事件速率,而非引擎本身。performance_budget 中的 1500 ms wall 與 64 MB 尖峰,是由這些契約所觀測的底層工作決定,而非契約本身。其可重現性設定檔為 structural。事件串流與例外情境都會包含時間戳。兩次執行會在那些欄位上有所不同,但結構會維持完全一致。

若結構化例外情境帶有機密資料,它就是資料外洩的攻擊面。這份契約將情境限制為基本型別,可降低物件意外外洩的風險。在情境抵達日誌接收端之前,部署端仍必須清除其中的敏感值。這就是專案日誌政策中的安全遙測義務。加速 sidecar 是透過傳輸層連線的獨立行程。請求方法會攜帶用於授權的範圍宣告(scope claim)。部署端必須把 sidecar 邊界視為信任邊界。設為 Permissive 的降級政策,可能會掩蓋與安全相關的處理能力喪失。在以輸出正確性作為控制措施之處,請使用 Strict。請把例外情境、工作事件與 sidecar 回應都視為可能會被記錄的資料,並據此清除。

本頁未主張任何直接的規範性宣告。可觀測性契約只負責揭露引擎狀態,並未實作任何引擎必須援引其條款的標準化協定。上文提及的安全遙測與日誌清除義務,源自專案內部的日誌政策,而非任何外部標準。當被觀測的操作本身就是標準化操作(例如簽章或 PDF/A 文件)時,其符合性會記載於簽署或擷取頁面。