open-telemetry/opentelemetry-auto-symfony
OpenTelemetry auto-instrumentation for Symfony adds zero-code tracing to your app, capturing HTTP requests, console commands, and framework activity as spans so you can send telemetry to any OpenTelemetry backend.
Install the package and an exporter. The bundle only wires instrumentation; it does not ship a backend. You will usually need the SDK and OTLP exporter too:
composer require open-telemetry/opentelemetry-auto-symfony \
open-telemetry/sdk \
open-telemetry/exporter-otlp
Register the bundle in config/bundles.php (verify the exact class name in vendor/open-telemetry/opentelemetry-auto-symfony/src for your pinned version):
return [
// ...
OpenTelemetry\Contrib\Symfony\OtelBundle::class => ['all' => true],
];
Configure the SDK through environment variables, which is the least surprising path for Symfony deployments:
OTEL_SERVICE_NAME=my-symfony-app
OTEL_TRACES_EXPORTER=otlp
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=dev,service.version=1.0.0
If your release uses the OpenTelemetry PHP extension hooks rather than a pure bundle, also install/enable the opentelemetry extension and set OTEL_PHP_AUTOLOAD_ENABLED=true.
Clear the cache and make a request.
bin/console cache:clear
bin/console about
Hit a normal route and confirm in your backend that you see an HTTP server span for the route with attributes such as http.request.method, url.path, and the response status code.
Where to look first: the package's README.md, src/OtelBundle.php (or _register.php), and its DependencyInjection configuration. This tells you exactly which Symfony components are auto-instrumented in your version. Then check your OpenTelemetry backend for a single end-to-end trace before adding manual spans.
First real use case: trace one business transaction end to end — browser request → Symfony controller → service layer → Doctrine query → outbound HTTP call. Use that trace to find the slow or missing segment, then add manual spans only where the auto-instrumentation is blind.
Enrich the auto-created request span. Use a kernel event listener to attach domain attributes without creating a second server span:
use OpenTelemetry\API\Trace\Span;
use Symfony\Component\HttpKernel\Event\RequestEvent;
public function onKernelRequest(RequestEvent $event): void
{
$span = Span::getCurrent();
if ($span->isRecording()) {
$span->setAttribute('enduser.id', $event->getRequest()->getUser()?->getUserIdentifier() ?? 'anonymous');
$span->setAttribute('app.tenant', $event->getRequest()->headers->get('X-Tenant') ?? 'unknown');
}
}
Instrument the service layer manually. Inject the tracer provider (add an alias in services.yaml if the bundle does not autowire it) and wrap meaningful operations:
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\API\Trace\TracerProviderInterface;
final class OrderService
{
public function __construct(private TracerProviderInterface $tracerProvider) {}
public function place(Order $order): void
{
$span = $this->tracerProvider
->getTracer('app.order')
->spanBuilder('order.place')
->setAttribute('order.id', $order->id)
->startSpan();
$scope = $span->activate();
try {
// business logic
} catch (\Throwable $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
} finally {
$scope->detach();
$span->end();
}
}
}
Propagate context across boundaries. For outbound HTTP calls, inject the W3C traceparent header so the downstream service joins the same trace:
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
$carrier = [];
TraceContextPropagator::getInstance()->inject($carrier);
$client->request('GET', $url, ['headers' => $carrier]);
Messenger and long-running consumers. Create one span per handled message, and put the trace context into a StampInterface when dispatching. On the consumer side, extract it and activate the scope before handling the message. Detach and end the span in finally so long-running workers do not leak context between messages.
Console commands. Auto-instrumentation may cover command execution, but the useful span is usually the command's actual work. Add a child span named after the command with console.command and key arguments as attributes. Avoid putting free-form user input directly into span names.
Sampling and environments. Keep 100% sampling locally. In production start with OTEL_TRACES_SAMPLER=parentbased_traceidratio and OTEL_TRACES_SAMPLER_ARG=0.1, then tune. Use resource attributes for environment and version so traces can be filtered.
Testing. Set OTEL_SDK_DISABLED=true in test or use an in-memory exporter. Assert on span names and attributes rather than hitting a real collector.
OTEL_SDK_DISABLED, exporter endpoint/protocol, OTEL_PHP_AUTOLOAD_ENABLED if extension-based, and whether the bundle is registered for the current environment. bin/console debug:container is useful for confirming services are present.kernel.terminate; in CLI and workers, call $tracerProvider->shutdown() or register a shutdown function. A fatal error or exit can bypass the batch processor entirely. Use the simple processor in development if you need immediate visibility.detach() scopes and clear any per-request context. Otherwise spans from one message become children of the previous message's trace.SpanProcessor, Sampler, and ResourceDetector implementations, or decorating TracerProviderInterface, are cleaner than modifying the bundle. This keeps you upgradeable.TracerProvider, do not let the bundle silently build a second one. Confirm whether the bundle consumes OpenTelemetry\API\Globals or builds its own provider, and align the rest of the app with that choice.How can I help you explore Laravel packages today?