Telemetry: OpenTelemetry bridge and no-op fallback
At a glance
Section titled “At a glance”The Telemetry module is the engine’s optional bridge to the OpenTelemetry (OTel) software development kit (SDK). When the SDK is installed, it emits spans and metrics with sanitized attributes. When the SDK is absent, a complete set of no-op tracer and meter objects keeps instrumentation calls valid and effectively free. You can safely leave instrumentation in the code path.
Install
Section titled “Install”composer require nextpdf/core:^3Conceptual overview
Section titled “Conceptual overview”The design goal is observability with zero cost when the SDK is absent. The engine’s hot paths call a tracer and a meter without checking first. Whether those calls do work depends on the runtime, not on a conditional at each call site.
OpenTelemetryInterceptor is the bridge. isAvailable() reports whether the
OTel SDK is present. startSpan(string $name, array $attributes = []) /
endSpan(?object $span) bracket a traced operation, and recordMetric()
records a counter or gauge value. When OTel is absent, the interceptor reports
unavailable, and the calls are inert. TelemetryBridge wires the interceptor
into the engine’s observation points.
AttributeSanitizer is the safety layer. sanitize(array $attributes) scrubs
the attribute map before it leaves the process. Telemetry attributes are a
common accidental personally identifiable information (PII) channel, so
sanitization is part of the contract, not an add-on. The sanitizer,
interceptor, and bridge are @since 2.3.0.
NullTracer, NullSpanBuilder, NullSpan, NullMeter, NullCounter, and
NullHistogram are the no-op fallback. They match the call shapes the OTel SDK
exposes: spanBuilder(), setAttribute() (chainable), startSpan(), end(),
createHistogram(), createUpDownCounter(), add(), and record(). They do
nothing. Because the fallback is complete, instrumented code does not branch on
availability; it calls the tracer, and the no-op absorbs the call.
API surface
Section titled “API surface”| Class | Key members | Role |
|---|---|---|
OpenTelemetryInterceptor | isAvailable(), startSpan(), endSpan(), recordMetric() | OTel span and metric bridge (@since 2.3.0) |
TelemetryBridge | engine wiring | Connects the interceptor to engine observation points (@since 2.3.0) |
AttributeSanitizer | sanitize(array $attributes): array | Scrubs attributes for PII safety (@since 2.3.0) |
NullTracer | spanBuilder(string $name): NullSpanBuilder | No-op tracer |
NullSpanBuilder | setAttribute(), startSpan(): NullSpan | No-op span builder (chainable) |
NullSpan | end() | No-op span |
NullMeter | createHistogram(), createUpDownCounter() | No-op meter |
NullCounter / NullHistogram | add(), record() | No-op instruments |
Run composer docs:generate-api-php -- --module=Telemetry to generate the full
PHPDoc table.
Code sample — Quick start
Section titled “Code sample — Quick start”Source: 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']);When the OTel SDK is absent, every call above is a no-op. The code stays identical, and the cost is zero.
Code sample — Production
Section titled “Code sample — Production”Wrap a render operation with sanitized attributes so caller-supplied metadata cannot leak into a 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); } }}Edge cases & gotchas
Section titled “Edge cases & gotchas”- The no-op fallback is complete by design. Do not guard instrumentation with
isAvailable()“to save work”. The no-op already costs nothing, and the guard adds the branch this design removes. - Always run caller-supplied attributes through
AttributeSanitizerbefore you attach them to a span or metric. Telemetry attributes are an accidental PII channel. endSpan(null)is valid: a null span is the no-op case. Pair everystartSpan()with anendSpan()in afinally.NullSpanBuilder::setAttribute()returnsstaticfor chaining. Under the no-op, the chain is inert by design.- The reproducibility profile is
structural: spans carry timestamps and trace identifiers, so two runs differ in those fields.
Performance
Section titled “Performance”When OTel is absent, the cost is a method call into a no-op, effectively free.
When OTel is present, the cost comes from the OTel SDK; the bridge adds
attribute sanitization, which is linear in the attribute count. The
performance_budget of 1500 ms wall / 64 MB peak is the engine reference
budget, not a telemetry service-level agreement (SLA).
Security notes
Section titled “Security notes”Telemetry is a data-egress surface. AttributeSanitizer keeps secrets and PII
out of spans and metrics. Treat sanitization as mandatory for any
caller-influenced attribute; that is the project’s safe-telemetry obligation.
The OTel exporter sends data to an external backend, and that backend is a trust
boundary. Configure its endpoint and credentials from a secret manager, not
committed config. Assume span and metric data reaches a log sink, and scrub it
accordingly. See the engine threat model in /modules/core/security/.
Conformance
Section titled “Conformance”This module makes no Portable Document Format (PDF) specification normative
claim. It bridges to the OpenTelemetry data model, an external observability
specification, not a PDF clause. The no-op fallback mirrors the OpenTelemetry
application programming interface (API) surface so instrumented code stays
portable. That is an API-compatibility property, not a PDF conformance
statement. Engine conformance is validated by the oracle and golden suites
described in /modules/core/conformance/.
See also
Section titled “See also”- Observability module — broader runtime-state surface.
- Performance module — memory diagnostics that pair with telemetry.
- Contracts / Observability — structured-exception and degradation contracts.
- Engine security model