open-telemetry / open-telemetry/opentelemetry-php

Calling `exit` before the request lifecycle ends causes `Scope: missing call to Scope::detach() for scope`

Open
#1,899 1 comment 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

Hello, maybe you'd be able to guide us, if it is possible to handle it correctly, but:

  • we are using Laravel application with the service provider which registers the tracing like this
$resource = ResourceInfo::create(
                Attributes::create([
                    'service.name' => config('otel.service_name'),
                    'deployment.environment' => config('app.deployed_env'),
                ])
            );

            $transport = (new OtlpHttpTransportFactory)->create(
                config('otel.endpoint'),
                Protocols::contentType(config('otel.protocol')),
                ['X-SF-Token' => config('otel.sf_token')],
            );

            $tracerProvider = TracerProvider::builder()
                ->setResource($resource)
                ->addSpanProcessor(new BatchSpanProcessor(new SpanExporter($transport), SystemClock::create()))
                ->build();

            Sdk::builder()
                ->setTracerProvider($tracerProvider)
                ->setAutoShutdown(true)
                ->buildAndRegisterGlobal();

            $this->app->singleton(\OpenTelemetry\API\Trace\TracerInterface::class, fn () => $tracerProvider->getTracer(config('app.name')));

then we have the middleware which is like this:

{
    public function handle(Request $request, Closure $next): Response
    {
        if (isFeatureFlagEnabled('backoffice.disable-otel-splunk-telemetry')) {
            return $next($request);
        }

        $propagator = TraceContextPropagator::getInstance();

        $parentContext = $propagator->extract(
            $request->headers->all(),
            \OpenTelemetry\Context\Propagation\ArrayAccessGetterSetter::getInstance(),
        );

        $tracer = Globals::tracerProvider()->getTracer(config('app.name'));

        $route = $request->route()?->uri() ?? $request->path();

        $span = $tracer
            ->spanBuilder($request->method().' '.$route)
            ->setParent($parentContext)
            ->setSpanKind(SpanKind::KIND_SERVER)
            ->startSpan();

        $scope = $span->activate();

        // ====== SEMANTIC CONVENTIONS (HTTP SERVER) ======
        $span->setAttribute('http.method', $request->method());
        $span->setAttribute('http.route', $route);
        $span->setAttribute('http.target', $request->getRequestUri());
        $span->setAttribute('http.scheme', $request->getScheme());
        $span->setAttribute('http.host', $request->getHost());
        $span->setAttribute('http.user_agent', $request->userAgent() ?? '');
        $span->setAttribute('net.peer.ip', $request->ip());

        try {
            $response = $next($request);

            $statusCode = $response->getStatusCode();

            $span->setAttribute('http.status_code', $statusCode);

            if ($statusCode >= 500) {
                $span->setStatus(StatusCode::STATUS_ERROR);
            }

            // ====== Inject trace headers do response ======
            $propagationSetter = new class implements \OpenTelemetry\Context\Propagation\PropagationSetterInterface
            {
                public function set(&$carrier, string $key, string $value): void
                {
                    $carrier->headers->set($key, $value, false);
                }
            };

            $propagator->inject($response, $propagationSetter, OtelContext::getCurrent());

            return $response;
        } catch (\Throwable $e) {
            $span->recordException($e);
            $span->setStatus(StatusCode::STATUS_ERROR);

            throw $e;
        } finally {
            $span->end();
            $scope->detach();
        }
    }
}

Everything seem to work correctly when the full request lifecycle ends, because the finally clause will be called and detach the scope. But if during the lifecycle I'll call something like dd() which is an function which prints something and calls exit(1) then the finally seem to run after the DebugScope destructor will be called and thus, since the scope has not been detached the error Scope: missing call to Scope::detach() for scope # will be returned along with the echoed/dumped data.

 "message": "Scope: missing call to Scope::detach() for scope #2051, created \n\tat OpenTelemetry.Context.Context.activate(Context.php:82)\n\tat OpenTelemetry.API.Trace.Span.activate(Span.php:56)\n\tat App.Http.Middleware.OtelPropagator.handle(OtelPropagator.php:39)\n\tat Illuminate.Pipeline.Pipeline.{closure:{closure:Illuminate.Pipeline.Pipeline::carry():194}:195}(Pipeline.php:219)\n\tat Illuminate.Http.Middleware.HandleCors.handle(HandleCors.php:61)\n\tat Illuminate.Pipeline.Pipeline.{closure:{closure:Illuminate.Pipeline.Pipeline::carry():194}:195}(Pipeline.php:219)\n\tat Illuminate.Http.Middleware.TrustProxies.handle(TrustProxies.php:58)\n\tat Illuminate.Pipeline.Pipeline.{closure:{closure:Illuminate.Pipeline.Pipeline::carry():194}:195}(Pipeline.php:219)\n\tat Illuminate.Pipeline.Pipeline.then(Pipeline.php:137)\n\tat Illuminate.Foundation.Http.Kernel.sendRequestThroughRouter(Kernel.php:175)\n\tat Illuminate.Foundation.Http.Kernel.handle(Kernel.php:144)\n\tat require(index.php:52)\n\tat {main}(server.php:139)\n",
    "exception": "ErrorException",
    "file": "D:\\vhosts\\fifa.gop.backoffice-backend\\vendor\\open-telemetry\\context\\DebugScope.php",

It is not a breaker, but I was wondering what could be the issue here. It looks like the setAutoShutdown should handle it, but it looks like it is not, I mean the destructor is called even before the callback in register_shutdown_function is called so it does not pass the

if (self::$finalShutdownPhase && $this->fiberId !== self::currentFiberId()) {
                return;
            }
``` condition.

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 with Context.php and DebugScope.php, then inspect Span.php and the shutdown behavior described around setAutoShutdown and register_shutdown_function. Reproduce the Laravel middleware case with dd() or exit(1), and determine the expected scope-cleanup behavior when destructors run before the shutdown callback; done should include a confirmed explanation and an agreed fix or documentation path.

Written by the indexing model from the issue text.

Assessment

Tech stack
laravel, php
Domain
backend, observability
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
32/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.