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

Open
#1,905 0 comments 0 reactions 0 assignees View on GitHub

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-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:

  1. Make a request that returns a non-200 response (e.g. hit a route that returns a 404)
  2. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.