Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Exporter Zipkin Laravel Package

open-telemetry/exporter-zipkin

Zipkin exporter for OpenTelemetry PHP, sending traces to Zipkin for visualization and analysis. Configure via the OpenTelemetry PHP SDK; read-only subtree split of the main monorepo.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a plug-in exporter only — it converts finished OpenTelemetry spans into Zipkin v2 JSON and POSTs them to a collector. It does not instrument anything, so on day one you are really wiring up three things: the OpenTelemetry SDK (tracer provider + processor), this exporter, and a Zipkin endpoint to look at.

Minimum viable setup (≈10 minutes):

  1. Run a collector locally so you have somewhere to look:
    docker run -d -p 9411:9411 openzipkin/zipkin
    
  2. Install the SDK, this exporter, and a PSR-18 client (the exporter needs one — Laravel usually already has Guzzle):
    composer require open-telemetry/sdk open-telemetry/exporter-zipkin guzzlehttp/guzzle
    
  3. Create config/telemetry.php with endpoint (must include the path — http://zipkin:9411/api/v2/spans), service_name, and sample_ratio.
  4. Register a TracerProvider singleton in a service provider and expose it as the global provider:
    use OpenTelemetry\Contrib\Zipkin\Exporter;
    use OpenTelemetry\SDK\Trace\{TracerProvider, SpanProcessor\BatchSpanProcessor};
    use OpenTelemetry\SDK\Trace\Sampler\{ParentBased, TraceIdRatioBasedSampler};
    
    $this->app->singleton(TracerProvider::class, function () {
        $exporter = new Exporter(
            config('telemetry.endpoint'),      // full URL, incl. /api/v2/spans
            config('telemetry.service_name'),  // becomes Zipkin localEndpoint.serviceName
        );
    
        return new TracerProvider(
            new BatchSpanProcessor($exporter),
            new ParentBased(new TraceIdRatioBasedSampler(config('telemetry.sample_ratio'))),
        );
    });
    
    // publish to OpenTelemetry\API\Globals::tracerProvider()
    Sdk::builder()
        ->setTracerProvider($this->app->make(TracerProvider::class))
        ->buildAndRegisterGlobal();
    
    Constructor signature changed at 1.0 — verify against your installed version; pre-1.0 betas used fromConnectionString().
  5. Flush at the end of the request (non-negotiable in PHP-FPM):
    $this->app->terminating(fn () => $this->app->make(TracerProvider::class)->shutdown());
    

First use case: a single root span around one endpoint. Add a middleware that starts a span with SpanKind::KIND_SERVER, ends it in a finally, hit a slow route, and confirm it appears in the Zipkin UI at localhost:9411 filtered by your service name. Do not attempt DB/queue/HTTP-client instrumentation until this one span round-trips — the majority of first-day failures are transport/config, not instrumentation.

Where to read: the exporter example at examples/traces/exporters/zipkin.php in the monorepo (the standalone README is a stub), plus the OpenTelemetry PHP exporters doc page linked from it.

Implementation Patterns

Root span via middleware, children via ambient context. Once a span is activated, everything downstream that asks Globals::tracerProvider()->getTracer(...) becomes a child automatically. Keep the root middleware dumb; push domain knowledge into small helpers.

$span = $tracer->spanBuilder($request->method().' '.$request->route()?->uri())
    ->setSpanKind(SpanKind::KIND_SERVER)
    ->setAttribute('http.request.method', $request->method())
    ->startSpan();
$scope = $span->activate();
try {
    $response = $next($request);
    $span->setAttribute('http.response.status_code', $response->getStatusCode());
    return $response;
} catch (\Throwable $e) {
    $span->recordException($e);
    $span->setStatus(Status::STATUS_ERROR);
    throw $e;
} finally {
    $scope->detach();
    $span->end();
}

Instrument at Laravel's seams, not inside your code. These cover ~90% of real trace value with almost no invasiveness:

  • DB — DB::listen(fn ($q) => $span->setAttribute('db.statement', $q->sql)), or wrap DB::transaction(); better still, add a query count/duration attribute and leave the raw SQL out (see Gotchas).
  • Outbound HTTP — Http::globalMiddleware() (Laravel 11+) or a Guzzle Middleware::mapRequest() that injects the propagation header and a Middleware::mapResponse() that closes the span. Set server.address/server.port so Zipkin can draw the dependency graph.
  • Queues — Queue::before / Queue::after events, or a job middleware: start a CONSUMER span in handle(), end it after $next($job). Set messaging.system/messaging.destination.name.
  • Console — wrap Artisan::starting() or your commands' handle().

Batch processor in production, simple processor in tests. BatchSpanProcessor keeps the request path hot but defers I/O; swap in SimpleSpanProcessor (or the in-memory/console exporter) when you want deterministic assertions in Pest/PHPUnit instead of sleeping and curling Zipkin.

Sampling is the provider's job, not the exporter's. Use ParentBased(TraceIdRatioBasedSampler(0.1)) in production and 1.0 locally. The exporter has no sampling logic — an unsampled span never reaches it.

Propagation: the SDK defaults to W3C traceparent. Zipkin-native ecosystems (Spring Sleuth, older services) speak B3 — install open-telemetry/extension-propagator-b3 and configure the propagator so cross-service traces stitch together.

Name spans for humans searching the UI. Zipkin's primary index is serviceName + span name. Use the route pattern (GET /orders/{order}), not the resolved path; put the status code and controller in tags. Reserve zipkin.kind conventions for internal spans — SpanKind::KIND_INTERNAL lands as a tag rather than Zipkin's kind field, so it won't appear in the client/server call graph.

Correlate traces with logs. Add a Monolog processor that reads Span::getCurrent()->getContext()->getTraceId() and stamps trace_id on every log line. This is the single highest-value follow-up to shipping the exporter — you can jump from a Zipkin trace to the exact Laravel log lines.

If you already run an OTel Collector, prefer open-telemetry/exporter-otlp from the app and let the Collector fan out to Zipkin (and anything else). It decouples your app from the backend and gives you retry/buffering you won't get from a PHP exporter.

Gotchas and Tips

  • Spans silently vanish without a shutdown/flush. PHP has no long-lived tracer provider in FPM mode, so a BatchSpanProcessor that is never flushed drops everything queued at request end. $provider->shutdown() in app()->terminating() is mandatory. Under Octane/Queue workers the provider persists — flush on WorkerStopping/Looping, not per request.
  • The endpoint must be the full spans path. http://zipkin:9411 fails silently-ish; you need /api/v2/spans. Test with a raw curl -X POST before blaming the exporter.
  • The second constructor argument is your localEndpoint.serviceName, and it does not automatically follow OTEL_SERVICE_NAME. Mismatch here makes every trace appear under a phantom service in the UI.
  • Everything becomes a string tag. 1.1.1 fixed casting of numeric/bool attributes; nested arrays and objects are not part of Zipkin's model. json_encode() them yourself or you'll get dropped data or warnings.
  • Tags are not free. Zipkin stores them verbatim and the UI chokes on multi-KB values. Never attach raw SQL with bindings, request bodies, or full stack traces as attributes; use recordException() (bounded) and length limits (OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT). Set OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT deliberately.
  • A missing PSR-18 client fails at container time, not at export time: you'll get "no PSR-18 clients found" from php-http/discovery. Install guzzlehttp/guzzle (plus nyholm/psr7 if there's no PSR-17 factory) or symfony/http-client.
  • Set aggressive timeouts and isolate failures. A hung collector should never add latency to a request. Configure the HTTP client with a 1–2s timeout (timeout support landed in 1.1.0), and consider a thin decorator exporter that wraps export() in try/catch (\Throwable) and returns ExportResult::failed(...) — export exceptions in some versions propagate to the span processor.
  • 128-bit trace IDs. OTel PHP emits 32-hex trace IDs; Zipkin accepts them, but older tooling that assumes 64-bit will truncate or mis-correlate. Worth checking before you mix in a legacy Zipkin client.
  • Version/maturity reality check. The standalone repo is a read-only subtree split of the monorepo; the last split release is 1.2.0 (Sept 2023) while main now requires PHP 8.2+ and 1.2.0 targets 8.1+. Pin the SDK and exporter together, expect the mirror to lag, and file issues/PRs against open-telemetry/opentelemetry-php, never the subtree.
  • Debug loop: if spans don't appear, first swap the batch processor for SimpleSpanProcessor (removes queueing from the equation), then point the exporter at a local nc -l / request-capturing proxy, then check OTEL_LOG_LEVEL=debug output. Only after the HTTP call is confirmed should you question the instrumentation.
  • Don't chase Zipkin for metrics or logs. It is traces-only; metrics/log correlation belongs to the OpenTelemetry metrics SDK or your log stack.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core