OpenTelemetry tracing for Spiral Framework
This package plugs the OpenTelemetry PHP SDK into the Spiral telemetry component as the otel tracer driver, so the spans of your application are exported to any OpenTelemetry-compatible backend and the trace context is propagated between services with W3C traceparent headers.
composer require spiral/otel-bridgeInstall an exporter as well, for example OTLP:
composer require open-telemetry/exporter-otlpThen add the bootloader to your application kernel:
public function defineBootloaders(): array
{
return [
// ...
\Spiral\OpenTelemetry\Bootloader\OpenTelemetryBootloader::class,
// ...
];
}Select the otel driver and configure the SDK with the standard OTEL_* environment variables:
# Telemetry driver [log, null, otel]
TELEMETRY_DRIVER=otel
OTEL_SERVICE_NAME=php # Your application name
OTEL_TRACES_EXPORTER=otlp
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318The exporter is created lazily, on the first export. The full list of variables is in the OpenTelemetry SDK configuration.
The trace context is read from and written to headers with the propagators of OTEL_PROPAGATORS, by default W3C Trace Context and Baggage. Other propagators need their package, for example b3 needs open-telemetry/extension-propagator-b3: a name the SDK does not know is skipped with a warning, and with none left the propagator is a no-op one.
getContext() depends on where it is called:
- before the tracer has started any trace, it returns the incoming headers the tracer was made with, baggage included, limited to the fields of the propagator;
- while a trace runs, the context of the span running there, with the baggage current there, or the incoming baggage when no baggage is current;
- once the traces of the tracer have ended, as when
Spiral\Http\Httpadds the context to the response, the trace context of the last finished outermost trace, without baggage.
The incoming baggage is current inside a scoped trace that takes the incoming parent, so code reads it with Baggage::getCurrent() and can extend it.
With OTEL_PHP_TRACES_PROCESSOR=batch (the SDK default) spans are queued and flushed after every request or job,
and on application shutdown, through the Spiral finalizer. Spans still queued are lost when the worker is killed hard,
e.g. by SIGKILL or the RoadRunner stop timeout.
The names, attributes and status of the spans of Spiral components, like the SERVER span of a request and its HTTP attributes, are set by those components, spiral/http among them; the bridge exports them as they are.
A status set on a Spiral span is exported as the OpenTelemetry span status:
- the strings
okanderror, in any case, giveOkandError; any other string leaves the statusUnset; - the integers
0,1and2are the OTLP values ofUnset,OkandError; - any other integer is taken for an HTTP status code: 1xx–3xx leave the status
Unset, 4xx isErrorexcept on aSERVERspan, 5xx isError, and a number out of the HTTP range isError.
When the traced callback throws, the span keeps its name and attributes and gets status Error with the exception message, unless the callback set Ok or Error.
The bridge works with and without the opentelemetry extension. With it, auto-instrumentation packages (open-telemetry/opentelemetry-auto-*) add spans for code Spiral does not trace: database drivers, HTTP clients, caches.
The spans of those hooks and the Spiral spans form one trace and go through one SDK pipeline:
- When the app boots, the bridge registers its tracer provider and propagator as the global ones that auto-instrumentation takes from
OpenTelemetry\API\Globals. The provider is still built on first use. Spans of hooks are flushed together with the Spiral spans at the end of each request or job, so under RoadRunner they are not held until the worker exits. - With
OTEL_PHP_AUTOLOAD_ENABLED=truethe SDK autoloader configures the global provider and propagator, from the env or from the file ofOTEL_CONFIG_FILE(OTEL_EXPERIMENTAL_CONFIG_FILEbefore SDK 1.14), which needsopen-telemetry/sdk-configuration, and the bridge takes them fromGlobals. The bridge flushes that provider after every request or job, while shutting it down is left to the SDK at process exit. The autoloader decides when Composer loads, so the variable has to be in the process environment orphp.ini: set only in.env, it is reported and the bridge builds its own provider. - Spans that hooks start while the exporter sends a batch, like the PSR-18 or curl request of the OTLP exporter itself, are not recorded, so that every flush does not send a new trace of its own.
The HTTP stack of Spiral is PSR-15, so open-telemetry/opentelemetry-auto-psr15 hooks into it. With the provider built by the bridge, the spans in which it doubles Spiral are suppressed, and the tree of a request is the same as without the extension:
- its
SERVERspan ofSpiral\Http\Http::handle()is suppressed, and theSERVERspan of Spiral stays the root of the request, under the incoming parent; - its
INTERNALspans of every request handler and middleware are suppressed, as Spiral traces its middleware pipeline itself.
Disabling that instrumentation gives the same tree:
OTEL_PHP_DISABLED_INSTRUMENTATIONS=psr15Nothing else is suppressed: a request handled inside a request or a job run inside a job keeps both spans. Suppression by span kind or by semantic conventions is opt-in, with OTEL_EXPERIMENTAL_SPAN_SUPPRESSION_STRATEGY=spankind or semconv, as the SDK reads it.
With OTEL_PHP_AUTOLOAD_ENABLED=true the provider comes from the SDK, so the bridge cannot suppress anything: disable psr15 or configure suppression in the SDK.
A span left active when a request or job ends would become the parent of every later span of the worker and hide the incoming trace context; the bridge writes a warning to the SDK log when it sees one.
Spans of hooks nest under the active span. A Spiral trace that is not scoped, like Routing or Job push, is never active, so database or HTTP client spans made inside it hang on the enclosing scoped span.
- The instrumentation scope of the Spiral spans is
spiral/otel-bridgewith the package version, notOTEL_SERVICE_NAME. The service name stays a resource attribute. The version comes fromresources/version.json, the release manifest, which moved there from.github/. - The propagator is built from
OTEL_PROPAGATORS, by defaulttracecontext,baggage, and is a singleton. - The tracer provider and propagator are registered in
Globalsfrom boot until the app terminates; withOTEL_PHP_AUTOLOAD_ENABLED=truein the process environment they are taken from there instead. - Status codes: integers and unknown strings no longer give
Ok, see Span status. - A callback that throws makes the span
Errorwith the exception message and keeps the name and attributes set in it. getContext()returns the context of the span running where it is called, with the baggage; after all traces have ended, the trace context of the last finished outermost one, without baggage.- With
opentelemetry-auto-psr15, its spans that double Spiral ones are suppressed. OpenTelemetryBootloader::initTraceProvider()takes the span processor from the container instead of a parameter.- Dependencies: Spiral packages
^3.18,open-telemetry/sdk>=1.9 <1.16,open-telemetry/api^1.7,open-telemetry/context^1.4;open-telemetry/opentelemetry-auto-psr15conflicts outside>=1.0.6 <1.4,open-telemetry/sem-convbelow1.22.