open-telemetry / open-telemetry/opentelemetry-php
[contrib-auto-symfony] SUB_REQUEST scope not cleaned up in handle post-hook, causing root HTTP spans to never be exported
Nobody has claimed this yet.
- Dominant language
- PHP
- Stars
- 912
- Forks
- 232
- Avg merge
- 7d 16h
- Merged PRs (30d)
- 4
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the Symfony auto-instrumentation handle post-hook and terminate() entry point described in the issue, then reproduce with a 404 or another ExceptionController response. Verify that the SUB_REQUEST scope is cleaned up immediately and that terminate() ends and exports the root SERVER span.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php, symfony
- Domain
- observability
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100