roqmeu/symfony-span-bundle
A Symfony bundle that integrates the Span library into Symfony applications, providing framework-native configuration, dependency injection wiring, and bundle setup so you can add span-based functionality with minimal boilerplate.
Install and confirm what actually ships. composer require roqmeu/symfony-span-bundle — but read the repo root before writing code. It is a Symfony bundle, so the first question is whether the package exposes a framework-agnostic core (plain PHP classes for spans/exporters) or whether all wiring lives in DependencyInjection/. If the latter, you will be hand-registering services in a Laravel service provider rather than relying on config/bundles.php.
Where to look first, in order:
README.md — the only reliable source for config keys and the intended integration point.src/DependencyInjection/Configuration.php (or equivalent) — this is the authoritative list of options; the README usually drifts.composer.json — check the symfony/* constraints against your PHP version. A bundle written against Symfony 6/7 contracts will pull in symfony/http-kernel even though Laravel never boots it.src/ for a Span, SpanInterface, or exporter class you can instantiate directly.First use case. Don't start with the bundle's auto-wiring. Start with the smallest possible span around a single route so you can see output end-to-end:
Route::get('/orders/{order}', function (Order $order, SpanFactory $spans) {
$span = $spans->start('orders.show', ['order.id' => $order->id]);
try {
return OrderResource::make($order->load('lines'));
} finally {
$span->finish();
}
});
If that produces a timed span in your log/exporter, the core works and the remaining work is wiring (middleware, decorators).
Version note: v1.0.1 is a patch release. The changelog is a bare compare link with no commit summary, so treat it as "no documented behaviour change" rather than "verified safe." Diff v1.0.0...v1.0.1 yourself before upgrading — a one-commit patch on a v1.0.0 package is often a tagging or CI fix, but it can equally be a signature change that nobody wrote up.
HTTP: middleware, not controllers. Wrap the request lifecycle once so you never touch individual controllers:
final class SpanMiddleware
{
public function __construct(private SpanFactory $spans) {}
public function handle($request, Closure $next)
{
$span = $this->spans->start('http.request', [
'http.method' => $request->method(),
'http.route' => optional($request->route())->getName(),
]);
try {
$response = $next($request);
$span->setAttribute('http.status_code', $response->getStatusCode());
return $response;
} catch (\Throwable $e) {
$span->recordException($e);
throw $e;
} finally {
$span->finish();
}
}
}
Register it in bootstrap/app.php (->withMiddleware(...)) on Laravel 11+, or Kernel::$middleware on 10 and below. Keep it in the global stack only if you want spans for every request including health checks — otherwise put it in web/api and exclude what you don't care about.
Nesting via a scoped context manager. Spans are only useful if they nest. If the bundle ships an active-span/context concept, use try/finally with a scope object rather than manual setParent calls; hand-rolled parenting leaks the wrong parent as soon as an exception unwinds.
Queues: job middleware. Illuminate\Queue\Middleware is the clean insertion point — a span that starts when the job is popped and finishes when it's released/deleted. Propagate a trace/span identifier in the job payload so the queued span links back to the HTTP span that dispatched it; otherwise your queue work is a disconnected island.
Decorators over if statements. Rather than sprinkling span calls through services, decorate the ones you care about in a service provider:
$this->app->extend(PricingClient::class, function ($inner, $app) {
return new TracedPricingClient($inner, $app->make(SpanFactory::class));
});
This keeps tracing out of domain code and makes it trivially removable in tests.
Console and scheduler. Wrap Artisan::starting / a base command, and tag scheduled tasks with the command name as the span name. This is where tracing pays for itself fastest — slow overnight jobs are exactly what nobody instruments.
Naming conventions. Pick a fixed scheme (http.request, queue.{JobClass}, db.query, http.client.{host}) on day one. Ad-hoc names produce traces you can't aggregate across.
Bundle, Extension, and CompilerPass classes are inert here. Anything loaded through DependencyInjection will not run. Budget time for a manual service provider — and expect the bundle's own README examples to be Symfony YAML that doesn't translate.finally. A single early return that skips finish() will grow memory until the worker recycles. If the bundle uses a static/singleton context, assert count() === 0 after each request in a test.config:cache and env(). If the bundle's config file calls env() outside Laravel's config layer, values vanish after php artisan config:cache. Move them into config/span.php and publish/override there.local.authorization, cookie, and any ?token=-shaped query parameter inside the middleware before the attribute is set — retroactively cleaning an exporter is much harder.travel()), and a NullSpan implementation for tests. If NullSpan is absent, wrap span creation behind your own factory so tests don't assert on timing.php artisan route:list -v shows middleware for a route), then confirm the exporter is bound — a common failure is a silently-null logger receiving spans that go nowhere.^1.0 constraint gives you very little information when something breaks. Pin to 1.0.1 and upgrade deliberately.How can I help you explore Laravel packages today?