Filing here as opentelemetry-php-contrib has issues disabled. This concerns open-telemetry/opentelemetry-auto-symfony 1.1.1.
Summary
When Symfony handles a request that results in a non-200 response, HttpKernel::handle() is called twice — once for the main request and once internally as a SUB_REQUEST to render the error page via ExceptionController. The handle post-hook returns early for both calls (because $exception is null for successful sub-requests), leaving two scopes on the OTEL context stack. When terminate() fires it pops only the topmost scope (the sub-request), ending that span. The main request scope (the root SERVER span) is never popped or ended — and therefore never exported.
Environment
open-telemetry/opentelemetry-auto-symfony: 1.1.1
- Symfony:
6.4
- PHP:
8.2
- OTEL exporter: OTLP/gRPC → Tempo
Steps to Reproduce
Any request that triggers Symfony's error handling internally (e.g. a 404, 403, or any response that goes through ExceptionController) will reproduce this:
- Make a request that returns a non-200 response (e.g. hit a route that returns a 404)
- Check your trace backend (Jaeger, Tempo, etc.)
Result: Child spans (e.g. Doctrine queries) appear with rootServiceName: <root span not yet received>. The root HTTP span is never received.
Root Cause
After both handle() calls complete, the OTEL context stack looks like this:
[top] scope 1 → sub-request span (KIND_INTERNAL, child of scope 2)
[bottom] scope 2 → main request span (KIND_SERVER, root, no parent)
The handle post-hook:
post: static function (...) {
$scope = Context::storage()->scope();
if (null === $scope || null === $exception) {
return; // ← returns early for all successful requests, including sub-requests
}
// ...
}
Both the SUB_REQUEST post-hook (no exception) and the MAIN_REQUEST post-hook (no exception when error page renders successfully) return early. Both scopes remain on the stack.
terminate() then pops one scope (scope 1, the sub-request), ends it, and returns. Scope 2 (the main HTTP span) stays on the stack forever — never ended, never exported.
Note: this is distinct from the exception case fixed in PR #317. The bug occurs on normal request handling whenever Symfony renders an error response internally.
Expected Behavior
The root HTTP span (KIND_SERVER) should be ended and exported for every request, including those that produce error responses.
Proposed Fix
In the handle post-hook, detect SUB_REQUEST and end those spans immediately, since sub-requests never receive a terminate() call:
post: static function (
HttpKernel $kernel,
array $params,
?Response $response,
?\Throwable $exception
): void {
$type = $params[1] ?? HttpKernelInterface::MAIN_REQUEST;
$scope = Context::storage()->scope();
if (null === $scope) {
return;
}
// Main request with no exception: terminate() handles span ending
if ($type === HttpKernelInterface::MAIN_REQUEST && null === $exception) {
return;
}
$span = Span::fromContext($scope->context());
$scope->detach();
if (null !== $exception) {
$span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]);
if (null !== $response && $response->getStatusCode() >= Response::HTTP_INTERNAL_SERVER_ERROR) {
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
}
}
// Sub-requests don't get a terminate() call — end the span here
if ($type === HttpKernelInterface::SUB_REQUEST) {
$span->end();
}
},
This ensures the sub-request scope is cleaned up immediately, so when terminate() fires only the main request scope remains on the stack and is properly ended and exported.
We are currently working around this with cweagans/composer-patches but would love to see this fixed upstream. Happy to open a PR if this direction looks good.
Summary
When Symfony handles a request that results in a non-200 response,
HttpKernel::handle()is called twice — once for the main request and once internally as aSUB_REQUESTto render the error page viaExceptionController. Thehandlepost-hook returns early for both calls (because$exceptionisnullfor successful sub-requests), leaving two scopes on the OTEL context stack. Whenterminate()fires it pops only the topmost scope (the sub-request), ending that span. The main request scope (the rootSERVERspan) is never popped or ended — and therefore never exported.Environment
open-telemetry/opentelemetry-auto-symfony:1.1.16.48.2Steps to Reproduce
Any request that triggers Symfony's error handling internally (e.g. a 404, 403, or any response that goes through
ExceptionController) will reproduce this:Result: Child spans (e.g. Doctrine queries) appear with
rootServiceName: <root span not yet received>. The root HTTP span is never received.Root Cause
After both
handle()calls complete, the OTEL context stack looks like this:The
handlepost-hook:Both the SUB_REQUEST post-hook (no exception) and the MAIN_REQUEST post-hook (no exception when error page renders successfully) return early. Both scopes remain on the stack.
terminate()then pops one scope (scope 1, the sub-request), ends it, and returns. Scope 2 (the main HTTP span) stays on the stack forever — never ended, never exported.Note: this is distinct from the exception case fixed in PR #317. The bug occurs on normal request handling whenever Symfony renders an error response internally.
Expected Behavior
The root HTTP span (
KIND_SERVER) should be ended and exported for every request, including those that produce error responses.Proposed Fix
In the
handlepost-hook, detectSUB_REQUESTand end those spans immediately, since sub-requests never receive aterminate()call:This ensures the sub-request scope is cleaned up immediately, so when
terminate()fires only the main request scope remains on the stack and is properly ended and exported.We are currently working around this with
cweagans/composer-patchesbut would love to see this fixed upstream. Happy to open a PR if this direction looks good.