jcchavezs/zipkin-symfony
Zipkin integration for Symfony apps that adds distributed tracing to HTTP requests and other components, helping you collect and report spans to a Zipkin server for latency analysis and service dependency visibility.
This bundle wires the openzipkin/zipkin PHP library into Symfony's DI container so you get a configured Zipkin\Tracing service out of the box, without hand-rolling reporters and samplers.
As of 3.1.0 (current release) the package supports PHP 7.4, 8.0 and 8.1, and pins the underlying openzipkin/zipkin library at 3.x. PHP 8.2/8.3 and Symfony 6/7 are not covered by the CI matrix — verify before adopting on a modern stack.
Minimal setup:
composer require jcchavezs/zipkin-symfony (check the exact composer name in the release tarball — the repo is zipkin-instrumentation-symfony; the vendor/package string has historically differed from the repo slug).ZipkinBundle in config/bundles.php (needed on Symfony < Flex auto-registration; verify against your Symfony version).config/packages/zipkin.yaml:zipkin:
service_name: my-service
endpoint: '%env(ZIPKIN_ENDPOINT)%' # e.g. http://localhost:9411/api/v2/spans
sampler:
type: always # bound + rate for production
docker run -d -p 9411:9411 openzipkin/zipkin
Zipkin\Tracing somewhere and create your first span:public function indexAction(Tracing $tracing)
{
$span = $tracing->getTracer()->nextSpan()->withName('homepage.render');
$span->start();
$span->tag('user.id', (string) $user->getId());
// ... work ...
$span->finish();
}
Where to look first: ZipkinBundle.php and DependencyInjection/Configuration.php in the package source — that tells you exactly which services are registered and which config keys are actually supported. The openzipkin/zipkin 3.0 docs cover the underlying Tracing/Tracer/Span API, which is where the real work happens.
First realistic use case: wrap a slow controller action or an outbound HTTP call, tag it with a couple of low-cardinality attributes, and confirm it appears in the Zipkin UI. Once you see one trace end-to-end, the API model clicks.
Manual spans around a unit of work. This is the only instrumentation you can rely on; the bundle does not magically trace everything.
$span = $tracing->getTracer()->nextSpan()->withName('report.export');
$span->start();
try {
$rows = $repository->export($params);
$span->tag('rows', (string) count($rows));
return $rows;
} catch (\Throwable $e) {
$span->tag('error', 'true');
$span->tag('error.message', $e->getMessage());
throw $e;
} finally {
$span->finish();
}
Outbound HTTP calls. Wrap Guzzle/PSR-18/Symfony HttpClient in one place (a shared decorator or middleware) so every external call becomes a client span with the target host as the span name and B3 headers injected:
$span = $tracing->getTracer()->nextSpan();
$span->setKind(Kind\CLIENT);
$child = $tracing->getTracer()->getCurrentSpan(); // parent = currently active span
Keep B3 (X-B3-TraceId, X-B3-SpanId, X-B3-ParentSpanId, X-B3-Sampled) consistent — single-header vs multi-header must match on both sides, or the downstream service starts a new trace instead of joining yours.
Inbound request context. Without a listener that reads the incoming B3 headers and calls SpanContext::createFromRemoteParent(...), incoming traces will not link to the sender. If you need true distributed tracing, add a small kernel request listener yourself — the bundle does not ship cross-service context propagation for free.
DBAL / Doctrine. Instantiate a middleware that opens a span per SQL statement with tags db.statement (bounded length), db.instance, and db.type. Register it in doctrine.dbal config. Keep the statement tag low-cardinality-safe by hashing or truncating if queries are highly variable.
Sampling strategy by environment. Always-on in dev, bounded in prod:
# config/packages/prod/zipkin.yaml
zipkin:
sampler:
type: bound
rate: 0.05
Binary always/never for staging, ratio for production — this is the single biggest lever on reporter load and collector cost.
Console commands and Symfony Messenger. Wrap command handling in a span; for Messenger, capture the current trace context in the message envelope (a stamp with trace id/span id), then reconstruct the remote parent in the handler. This gives you async trace continuity across queues — a custom middleware on your side, not built into the bundle.
openzipkin/zipkin 3.0. No new config keys, no new instrumentation, no deprecations announced.openzipkin/zipkin directly, or you wrote a custom reporter/sampler against the 2.x API (constructor signatures, Http reporter options, SpanContext factories), reconcile your code with 3.0 before upgrading. Composer conflicts here surface as an unresolvable dependency graph rather than a runtime error, so read the composer why-not output.kernel.terminate flush, you silently lose data. Wire a kernel.terminate listener or a shutdown function to call $reporter->flush().endpoint is the collector, not the UI. It is http://zipkin:9411/api/v2/spans. Pointing at :9411/ silently fails.local_service_name is required and is what shows up as the service in the UI. Make it match your deployable (orders-api, not backend).start() / finish() in try/finally; tag() after finish() is a silent no-op in most implementations. Use Tracer::getCurrentSpan() to nest rather than passing spans through every function signature.zipkin.reporter or zipkin.sampler service definitions in services.yaml to swap in your own reporter (e.g., a log reporter for local debugging, or an OTLP bridge). Confirm aliases in ZipkinBundle/Resources/config/services.xml.ZIPKIN_ENDPOINT reachability with curl; then confirm the sampler is always in dev; then confirm finish() is being called; then check the reporter flush. After the 3.1.0 upgrade, also composer show openzipkin/zipkin to confirm you actually resolved to 3.x and not a stale pinned 2.x.How can I help you explore Laravel packages today?