Skip to content

Contracts / Observability

The observability domain defines the contracts that expose the engine’s runtime state: ContextAwareExceptionInterface for structured error context, SpectrumInterface for the optional acceleration sidecar, JobNotificationInterface for streamed job progress, and the DegradationPolicy enum for capability-loss behavior.

Terminal window
composer require nextpdf/core:^3

ContextAwareExceptionInterface is the diagnostic contract. Every NextPDF domain exception implements it. You can cast any caught NextPDF exception to this contract and retrieve structured context for an Application Performance Monitoring (APM) tool, a logging pipeline, or an error reporter. The context is an associative array with snake_case keys and primitive values only. It contains no nested objects. As a result, it serializes predictably to a JavaScript Object Notation (JSON) payload or an APM payload. You do not need to parse an exception message to recover diagnostic data. It is stable since 3.1.0.

SpectrumInterface is the contract for the optional acceleration sidecar. Spectrum runs work in parallel on the central processing unit (CPU), off-loading hardware detection, Portable Document Format (PDF) parsing, and image compression to a local sidecar process. The contract reports availability behind a circuit breaker, so frequent health checks do not amplify a failure when the sidecar is down. It probes hardware capabilities and caches the result. It exposes the active resource budget. It also provides a general request transport for higher-level modules. The engine works without the sidecar. The contract makes acceleration an injectable option, not a hard dependency. JobNotificationInterface streams typed job events from the sidecar’s server-sent-events endpoint as a generator. The generator stops when a terminal event arrives or the stream closes.

DegradationPolicy is the behavior enum for capability loss. When a capability degrades, the policy decides whether to throw, warn, or collect silently, based on impact. Strict throws when the impact is a compliance risk, a semantic loss, or blocking. Use it in a regulated environment where output correctness is mandatory. Balanced, the default, emits structured warnings and continues for bounded degradation, and throws only on blocking impact. Use it for most production deployments. Permissive collects every event silently and never throws. Use it for preview or draft mode where best-effort output is acceptable. The SpectrumInterface, JobNotificationInterface, and DegradationPolicy types are experimental. Their compatibility promise is weaker than ContextAwareExceptionInterface.

TypeKindKey membersStabilitySince
ContextAwareExceptionInterfaceinterfacegetContext(): array<string, mixed>stable3.1.0
SpectrumInterfaceinterfaceisAvailable(), probe(), getBudget(), request()experimental2.1.0
JobNotificationInterfaceinterfacestreamEvents(string): Generator<int, JobEvent>experimental2.2.0
DegradationPolicyenum (string)Strict, Balanced, Permissiveexperimental2.3.0

getContext() returns only primitives or lists of primitives. streamEvents() yields JobEvent objects until a terminal event. SpectrumInterface::request() returns the raw response body as a string. probe() returns a HardwareReport, and getBudget() returns a 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());
}

Structured context passes into the log record without message parsing.

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

The nullable SpectrumInterface makes acceleration optional. The contract exposes one transport method, request(), which returns the raw response body as a string. A higher-level parser turns that body into a NextPDF\Accelerator\BatchResult. The concrete SpectrumClient adds typed helpers such as parseBatch() that wrap request() and return BatchResult directly. Those helpers are not part of the frozen contract. The degradation policy decides whether a missing sidecar is fatal.

  • Not every \Throwable is a NextPDF exception. Guard with instanceof ContextAwareExceptionInterface before you call getContext().
  • getContext() returns primitives only by contract. If you expect nested objects, the assumption is wrong; the contract guarantees JSON-safe values.
  • SpectrumInterface::isAvailable() runs behind a circuit breaker and is safe to call often, but a true result is a point-in-time check. Handle a sidecar that drops between the check and the call.
  • JobNotificationInterface::streamEvents() is a generator. Iterating it twice does not replay events. Consume it once.
  • DegradationPolicy::Permissive never throws. In that mode, a compliance-affecting degradation passes silently. Do not use it for regulated output.

The observability contracts add negligible cost. getContext() returns a pre-built array. isAvailable() is a cached, circuit-broken health probe. The contract requires implementations to cache the probe result for at least 30 seconds, so a hot path does not call the sidecar repeatedly. streamEvents() is bounded by the sidecar’s event rate, not by the engine. The performance_budget of 1500 ms wall and 64 MB peak is set by the underlying work the contracts observe, not by the contracts themselves. The reproducibility profile is structural. Event streams and exception contexts include timestamps. Two runs differ in those fields while the structure stays identical.

Structured exception context is a data-exfiltration surface if it carries secrets. The contract restricts context to primitives, which limits accidental object leakage. You must still scrub sensitive values before the context reaches a log sink. This is the safe-telemetry obligation in the project’s logging policy. The acceleration sidecar is a separate process reached over a transport. The request method carries scope claims for authorization. You must treat the sidecar boundary as a trust boundary. A degradation policy set to Permissive can mask a security-relevant capability loss. Use Strict where output correctness is a control. Treat exception context, job events, and sidecar responses as data that may be logged, and scrub accordingly.

This page asserts no direct normative claim. The observability contracts expose engine state and do not implement a standardized protocol whose clauses the engine must cite. The safe-telemetry and log-scrubbing obligations referenced above derive from the project’s internal logging policy, not from an external standard. When an observed operation is itself standardized — a signature, a PDF/A document — its conformance is documented on the signing or extraction page.