open-telemetry/extension-propagator-b3
Provides B3 (Zipkin-style) context propagation for OpenTelemetry PHP, letting you read and inject single- or multi-header B3 trace headers alongside or instead of the default W3C Trace Context. Useful for services interoperating with Zipkin, Brave, or other B3-based tracing systems.
This package gives OpenTelemetry PHP a B3 propagator — the Zipkin-native header format — so your Laravel app can join traces with services that speak b3 or X-B3-* rather than W3C traceparent.
Minimal setup:
Install the package and its native counterpart, then confirm it is loaded:
composer require open-telemetry/extension-propagator-b3
php -m | grep -i otel
php --ri opentelemetry
These OTel "extension-propagator-*" packages are bridges over the native opentelemetry PHP extension. If php --ri opentelemetry fails, the propagator classes will not be available regardless of Composer.
Tell the SDK to use it via environment (read at boot):
OTEL_PROPAGATORS=b3
# or: b3multi
# or combine: tracecontext,baggage,b3multi
First use case — continue an inbound trace from a Zipkin-instrumented upstream:
use OpenTelemetry\API\Globals;
use OpenTelemetry\Extension\Propagator\B3\B3Propagator;
$propagator = B3Propagator::getInstance(); // single `b3` header
// $propagator = B3MultiPropagator::getInstance(); // X-B3-* headers
$headers = array_map(fn ($v) => $v[0] ?? '', getallheaders());
$parent = $propagator->extract($headers);
$span = Globals::tracerProvider()
->getTracer('my-service')
->spanBuilder('handle-request')
->setParent($parent)
->startSpan();
Look first at the package README, its composer.json (to see the exact ext-* requirement and version constraints), and the unit tests — the tests encode header fixtures and are the best source of truth for edge-case behaviour.
Environment-driven wiring (preferred). Set OTEL_PROPAGATORS before the SDK/autoloader bootstraps. The SDK builds a composite propagator from the registered names, so your application code never touches the propagator directly. This is the pattern to standardise on; hand-constructing propagators in every controller drifts quickly.
Laravel middleware for extract/activate.
public function handle($request, Closure $next)
{
$carrier = array_map(fn ($v) => $v[0] ?? '', $request->headers->all());
$parent = B3Propagator::getInstance()->extract($carrier);
$span = Globals::tracerProvider()->getTracer('app')
->spanBuilder($request->path())->setParent($parent)->startSpan();
$scope = $span->activate();
try {
return $next($request);
} finally {
$scope->detach();
$span->end();
}
}
Inject for outbound HTTP. For Guzzle/PSR-18 clients not already covered by OTel middleware:
$carrier = [];
B3Propagator::getInstance()->inject($carrier);
$client->request('GET', $url, ['headers' => $carrier]);
Prefer the official Guzzle/PSR-18 instrumentation middleware when available; use manual injection for bespoke clients (SOAP, raw curl, queue payloads).
Combine propagators for mixed estates. OTEL_PROPAGATORS=tracecontext,baggage,b3multi lets the same service accept W3C from internal services and B3 from Zipkin-side partners. Extraction merges contexts; injection emits both header sets, which is usually fine but see the gotchas.
Match your exporter. If spans go to Zipkin (Zipkin exporter or OTLP into a Zipkin-compatible backend), B3 propagation keeps trace IDs consistent with what Zipkin shows in its UI and dependency graph.
Testing pattern. Assert both directions: a request with a known b3 header must produce a child span sharing traceId/spanId; an outgoing call must contain the expected header names. Unit-test with fixed header arrays — much faster than spinning up a Zipkin instance.
b3 vs b3multi is a compatibility decision, not a preference. Single-header (b3: {trace}-{span}-{sampled}-{parent}) is compact; multi-header (X-B3-TraceId, X-B3-SpanId, X-B3-Sampled, X-B3-ParentSpanId) is what older Zipkin clients emit. Ask the peer; guessing wrong produces two disconnected traces, not an error.OTEL_PROPAGATORS=b3 is set but the extension is missing or version-mismatched, registration can silently fall back — verify by dumping headers at ingress/egress, not by trusting config.inject() takes the carrier by reference; passing a copy or an immutable PSR-7 request will lose headers. Build a plain array, then attach it to the request.HeaderBag::all() returns arrays of values. The propagator expects flat strings — flatten before extraction or lookups silently miss.Sampled (and the d debug flag). Pair the propagator with a ParentBased sampler; otherwise the parent's sampling decision is ignored and traces fragment at service boundaries.d as "always sample"; the PHP propagator primarily moves context. Don't build sampling logic on it without verifying against your actual installed version.b3 and b3multi injects duplicate header sets; strict peers may reject or double-count. Combine b3 with tracecontext/baggage, not with b3multi..so fails to load, and the failure mode is often "no spans" rather than a clear error.extract() returns a context containing a remote span context, confirm your span's traceId matches the inbound one. If step two fails, the problem is carrier shape or extension loading — not the exporter.TextMapPropagatorInterface yourself and register it explicitly on the SDK builder rather than patching this package.How can I help you explore Laravel packages today?