-
Notifications
You must be signed in to change notification settings - Fork 221
Description
Filing here as
opentelemetry-php-contribhas issues disabled. This concernsopen-telemetry/opentelemetry-auto-symfony1.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.