• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FriendsOfOpenTelemetry / opentelemetry-bundle / 25035624412

28 Apr 2026 05:27AM UTC coverage: 92.409% (+0.7%) from 91.751%
25035624412

push

github

web-flow
feat(Instrumentation/Messenger): implement worker and middleware instrumentation (#173)

* chore(messenger): add trace parent in dispatched messages (AMQP)

* chore(messenger): use Messenger events to start/end span for instrumentation

* remove strict type, merge start/end span into 1 subscriber, close span on error and on message handled

* propagation not related to AMQP + read incoming trace (async context)

* rename event subscriber

* chore(messenger): use Messenger events to start/end span for instrumentation

* chore(messenger): add trace parent in dispatched messages (AMQP)

* remove strict type, merge start/end span into 1 subscriber, close span on error and on message handled

* propagation not related to AMQP + read incoming trace (async context)

* rename event subscriber

* fix(messenger): clean up worker subscriber and add functional tests

Fix several issues in the WorkerMessageEventSubscriber introduced by PR #173:
- Replace SDK Span import with API Span to respect API/SDK separation
- Implement InstrumentationTypeInterface for consistency with other subscribers
- Add event priorities (10000/-10000) to wrap all other processing
- Add messaging semantic convention attributes (operation.type, destination.name)
- Include message class name in span name for better trace readability
- Remove stale imports and duplicate propagation middleware service definition
- Clean up propagation middleware when messenger tracing is disabled
- Add PHPStan baseline entries for untyped $carrier interface params
- Add functional tests for worker message handled, failed, and attribute mode
- Reorganize messenger tests into Messenger/ subdirectory
- Disable retry on test transport to isolate worker span assertions

* test(messenger): add tests for transport tracing and propagation middleware

Cover TraceableMessengerTransport (get/ack/reject spans + TransportException
error recording) and AddStampForPropagationMiddleware (stamp skip, no-scope
passthrough, ac... (continued)

187 of 191 new or added lines in 14 files covered. (97.91%)

12 existing lines in 10 files now uncovered.

2459 of 2661 relevant lines covered (92.41%)

15.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.41
/src/Instrumentation/Symfony/HttpKernel/TraceableHttpKernelEventSubscriber.php
1
<?php
2

3
namespace FriendsOfOpenTelemetry\OpenTelemetryBundle\Instrumentation\Symfony\HttpKernel;
4

5
use FriendsOfOpenTelemetry\OpenTelemetryBundle\Instrumentation\InstrumentationTypeEnum;
6
use FriendsOfOpenTelemetry\OpenTelemetryBundle\Instrumentation\InstrumentationTypeInterface;
7
use FriendsOfOpenTelemetry\OpenTelemetryBundle\Instrumentation\Symfony\Framework\Routing\TraceableRouteLoader;
8
use FriendsOfOpenTelemetry\OpenTelemetryBundle\OpenTelemetry\Context\Attribute\HttpKernelTraceAttributeEnum;
9
use OpenTelemetry\API\Trace\SpanInterface;
10
use OpenTelemetry\API\Trace\SpanKind;
11
use OpenTelemetry\API\Trace\StatusCode;
12
use OpenTelemetry\API\Trace\TracerInterface;
13
use OpenTelemetry\Context\Context;
14
use OpenTelemetry\Context\Propagation\PropagationGetterInterface;
15
use OpenTelemetry\Context\Propagation\TextMapPropagatorInterface;
16
use OpenTelemetry\Context\ScopeInterface;
17
use OpenTelemetry\SemConv\Attributes\ClientAttributes;
18
use OpenTelemetry\SemConv\Attributes\HttpAttributes;
19
use OpenTelemetry\SemConv\Attributes\NetworkAttributes;
20
use OpenTelemetry\SemConv\Attributes\ServerAttributes;
21
use OpenTelemetry\SemConv\Attributes\UrlAttributes;
22
use OpenTelemetry\SemConv\Attributes\UserAgentAttributes;
23
use OpenTelemetry\SemConv\Incubating\Attributes\HttpIncubatingAttributes;
24
use Psr\Log\LoggerInterface;
25
use Symfony\Component\DependencyInjection\ServiceLocator;
26
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
27
use Symfony\Component\HttpFoundation\HeaderBag;
28
use Symfony\Component\HttpFoundation\Request;
29
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
30
use Symfony\Component\HttpKernel\Event\ControllerEvent;
31
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
32
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
33
use Symfony\Component\HttpKernel\Event\RequestEvent;
34
use Symfony\Component\HttpKernel\Event\ResponseEvent;
35
use Symfony\Component\HttpKernel\Event\TerminateEvent;
36
use Symfony\Component\HttpKernel\Event\ViewEvent;
37
use Symfony\Component\HttpKernel\KernelEvents;
38
use Symfony\Contracts\Service\ServiceSubscriberInterface;
39

40
final class TraceableHttpKernelEventSubscriber implements EventSubscriberInterface, ServiceSubscriberInterface, InstrumentationTypeInterface
41
{
42
    private const REQUEST_ATTRIBUTE_SPAN = '__opentelemetry_symfony_internal_span';
43
    private const REQUEST_ATTRIBUTE_SCOPE = '__opentelemetry_symfony_internal_scope';
44
    private const REQUEST_ATTRIBUTE_EXCEPTION = '__opentelemetry_symfony_internal_exception';
45

46
    /**
47
     * @var array<string, ?string>
48
     */
49
    private array $requestHeaderAttributes;
50

51
    /**
52
     * @var array<string, ?string>
53
     */
54
    private array $responseHeaderAttributes;
55

56
    private InstrumentationTypeEnum $instrumentationType = InstrumentationTypeEnum::Auto;
57

58
    /**
59
     * @var string[]
60
     */
61
    private array $excludePaths = [];
62

63
    /**
64
     * @param iterable<string> $requestHeaders
65
     * @param iterable<string> $responseHeaders
66
     */
67
    public function __construct(
68
        private readonly TracerInterface $tracer,
69
        private readonly TextMapPropagatorInterface $propagator,
70
        private readonly PropagationGetterInterface $propagationGetter,
71
        /** @var ServiceLocator<TracerInterface> */
72
        private readonly ServiceLocator $tracerLocator,
73
        private readonly ?LoggerInterface $logger = null,
74
        iterable $requestHeaders = [],
75
        iterable $responseHeaders = [],
76
    ) {
77
        $this->requestHeaderAttributes = $this->createHeaderAttributeMapping('request', $requestHeaders);
20✔
78
        $this->responseHeaderAttributes = $this->createHeaderAttributeMapping('response', $responseHeaders);
20✔
79
    }
80

81
    public static function getSubscribedEvents(): array
82
    {
83
        return [
7✔
84
            KernelEvents::REQUEST => [
7✔
85
                ['startRequest', 10000],
7✔
86
                ['recordRoute', 31], // after \Symfony\Component\HttpKernel\EventListener\RouterListener
7✔
87
            ],
7✔
88
            KernelEvents::CONTROLLER => [
7✔
89
                ['recordController'],
7✔
90
            ],
7✔
91
            KernelEvents::CONTROLLER_ARGUMENTS => [
7✔
92
                ['recordControllerArguments'],
7✔
93
            ],
7✔
94
            KernelEvents::VIEW => [
7✔
95
                ['recordView'],
7✔
96
            ],
7✔
97
            KernelEvents::RESPONSE => [
7✔
98
                ['recordResponse', -10000],
7✔
99
            ],
7✔
100
            KernelEvents::FINISH_REQUEST => [
7✔
101
                ['endScope', -10000],
7✔
102
                ['endRequest', -10000],
7✔
103
            ],
7✔
104
            KernelEvents::TERMINATE => [
7✔
105
                ['terminateRequest', 10000],
7✔
106
            ],
7✔
107
            KernelEvents::EXCEPTION => [
7✔
108
                ['recordException'],
7✔
109
            ],
7✔
110
        ];
7✔
111
    }
112

113
    public static function getSubscribedServices(): array
114
    {
115
        return [TracerInterface::class];
7✔
116
    }
117

118
    public function startRequest(RequestEvent $event): void
119
    {
120
        $request = $event->getRequest();
20✔
121
        if (true === $event->isMainRequest() && false === $this->isAutoTraceable($request)) {
20✔
122
            return;
17✔
123
        }
124

125
        $span = $this->startSpan($event);
5✔
126
    }
127

128
    public function recordRoute(RequestEvent $event): void
129
    {
130
        $request = $event->getRequest();
20✔
131
        $span = $this->fetchRequestSpan($request);
20✔
132

133
        if (null === $span && true === $this->isAttributeTraceable($request)) {
20✔
134
            $span = $this->startSpan($event);
13✔
135
        }
136

137
        if (null === $span) {
20✔
138
            return;
4✔
139
        }
140

141
        $routeName = $request->attributes->get('_route', '');
16✔
142
        if ('' === $routeName) {
16✔
143
            return;
2✔
144
        }
145

146
        $span->updateName($routeName);
16✔
147
        $span->setAttribute(HttpAttributes::HTTP_ROUTE, $routeName);
16✔
148
    }
149

150
    private function startSpan(RequestEvent $event): SpanInterface
151
    {
152
        $request = $event->getRequest();
16✔
153
        $tracer = $this->getTracer($request);
16✔
154

155
        $spanBuilder = $tracer
16✔
156
            ->spanBuilder(sprintf('HTTP %s', $request->getMethod()))
16✔
157
            ->setSpanKind(SpanKind::KIND_INTERNAL)
16✔
158
            ->setAttributes($this->requestAttributes($request))
16✔
159
            ->setAttributes($this->headerAttributes($request->headers, $this->requestHeaderAttributes))
16✔
160
        ;
16✔
161

162
        $parent = Context::getCurrent();
16✔
163

164
        if ($event->isMainRequest()) {
16✔
165
            $spanBuilder->setSpanKind(SpanKind::KIND_SERVER);
16✔
166
            $parent = $this->propagator->extract(
16✔
167
                $request,
16✔
168
                $this->propagationGetter,
16✔
169
                $parent,
16✔
170
            );
16✔
171

172
            $requestTime = $request->server->get('REQUEST_TIME_FLOAT');
16✔
173
            if (null !== $requestTime) {
16✔
174
                $spanBuilder->setStartTimestamp($requestTime * 1_000_000_000);
16✔
175
            }
176
        }
177

178
        $span = $spanBuilder->setParent($parent)->startSpan();
16✔
179

180
        $this->logger?->debug(sprintf('Starting span "%s"', $span->getContext()->getSpanId()));
16✔
181

182
        $scope = $span->storeInContext($parent)->activate();
16✔
183

184
        $request->attributes->set(self::REQUEST_ATTRIBUTE_SPAN, $span);
16✔
185
        $request->attributes->set(self::REQUEST_ATTRIBUTE_SCOPE, $scope);
16✔
186

187
        return $span;
16✔
188
    }
189

190
    public function recordController(ControllerEvent $event): void
191
    {
192
        $span = $this->fetchRequestSpan($event->getRequest());
20✔
193
        if (null === $span) {
20✔
194
            return;
4✔
195
        }
196
    }
197

198
    public function recordControllerArguments(ControllerArgumentsEvent $event): void
199
    {
200
        $span = $this->fetchRequestSpan($event->getRequest());
20✔
201
        if (null === $span) {
20✔
202
            return;
4✔
203
        }
204
    }
205

206
    public function recordView(ViewEvent $event): void
207
    {
208
        $span = $this->fetchRequestSpan($event->getRequest());
×
209
        if (null === $span) {
×
210
            return;
×
211
        }
212
    }
213

214
    public function recordException(ExceptionEvent $event): void
215
    {
216
        $span = $this->fetchRequestSpan($event->getRequest());
1✔
217
        if (null === $span) {
1✔
218
            return;
×
219
        }
220

221
        $span->recordException($event->getThrowable());
1✔
222
        $event->getRequest()->attributes->set(self::REQUEST_ATTRIBUTE_EXCEPTION, $event->getThrowable());
1✔
223
    }
224

225
    public function recordResponse(ResponseEvent $event): void
226
    {
227
        $span = $this->fetchRequestSpan($event->getRequest());
20✔
228
        if (null === $span) {
20✔
229
            return;
4✔
230
        }
231

232
        $event->getRequest()->attributes->remove(self::REQUEST_ATTRIBUTE_EXCEPTION);
16✔
233

234
        if (!$span->isRecording()) {
16✔
235
            return;
×
236
        }
237

238
        $response = $event->getResponse();
16✔
239
        $span->setAttribute(HttpIncubatingAttributes::HTTP_RESPONSE_BODY_SIZE, $response->headers->get('Content-Length'));
16✔
240
        $span->setAttribute(NetworkAttributes::NETWORK_PROTOCOL_VERSION, $response->getProtocolVersion());
16✔
241
        $span->setAttribute(HttpAttributes::HTTP_RESPONSE_STATUS_CODE, $response->getStatusCode());
16✔
242
        if ($response->getStatusCode() >= 500 && $response->getStatusCode() < 600) {
16✔
243
            $span->setStatus(StatusCode::STATUS_ERROR);
2✔
244
        } else {
245
            $span->setStatus(StatusCode::STATUS_OK);
14✔
246
        }
247

248
        $span->setAttributes($this->headerAttributes($response->headers, $this->responseHeaderAttributes));
16✔
249
    }
250

251
    public function endScope(FinishRequestEvent $event): void
252
    {
253
        $scope = $this->fetchRequestScope($event->getRequest());
20✔
254
        if (null === $scope) {
20✔
255
            $this->logger?->debug('No active scope');
4✔
256

257
            return;
4✔
258
        }
259
        $this->logger?->debug(sprintf('Detaching scope "%s"', spl_object_id($scope)));
16✔
260
        $scope->detach();
16✔
261
    }
262

263
    public function endRequest(FinishRequestEvent $event): void
264
    {
265
        $span = $this->fetchRequestSpan($event->getRequest());
20✔
266
        if (null === $span) {
20✔
267
            return;
4✔
268
        }
269

270
        $exception = $this->fetchRequestException($event->getRequest());
16✔
271
        if (null !== $exception) {
16✔
272
            $span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
×
273
        } elseif ($event->isMainRequest()) {
16✔
274
            // End span on ::terminateRequest() instead
275
            return;
16✔
276
        }
277

278
        $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId()));
2✔
279
        $span->end();
2✔
280
    }
281

282
    public function terminateRequest(TerminateEvent $event): void
283
    {
284
        $span = $this->fetchRequestSpan($event->getRequest());
20✔
285
        if (null === $span) {
20✔
286
            return;
4✔
287
        }
288

289
        $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId()));
16✔
290
        $span->end();
16✔
291
    }
292

293
    private function isAutoTraceable(Request $request): bool
294
    {
295
        if (InstrumentationTypeEnum::Auto !== $this->instrumentationType) {
20✔
296
            return false;
16✔
297
        }
298

299
        if (0 === count($this->excludePaths)) {
4✔
300
            return true;
1✔
301
        }
302

303
        $combinedExcludePaths = implode('|', $this->excludePaths);
3✔
304
        if (preg_match("#{$combinedExcludePaths}#", $request->getPathInfo())) {
3✔
305
            return false;
1✔
306
        }
307

308
        return true;
2✔
309
    }
310

311
    private function isAttributeTraceable(Request $request): bool
312
    {
313
        return InstrumentationTypeEnum::Attribute === $this->instrumentationType
17✔
314
            && true === $request->attributes->get(TraceableRouteLoader::DEFAULT_KEY, false);
17✔
315
    }
316

317
    private function getTracer(Request $request): TracerInterface
318
    {
319
        $tracer = $request->attributes->get(TraceableRouteLoader::TRACER_KEY);
16✔
320
        if (null !== $tracer) {
16✔
321
            return $this->tracerLocator->get($tracer);
1✔
322
        }
323

324
        return $this->tracer;
15✔
325
    }
326

327
    private function fetchRequestSpan(Request $request): ?SpanInterface
328
    {
329
        return $this->fetchRequestAttribute($request, self::REQUEST_ATTRIBUTE_SPAN, SpanInterface::class);
20✔
330
    }
331

332
    private function fetchRequestScope(Request $request): ?ScopeInterface
333
    {
334
        return $this->fetchRequestAttribute($request, self::REQUEST_ATTRIBUTE_SCOPE, ScopeInterface::class);
20✔
335
    }
336

337
    private function fetchRequestException(Request $request): ?\Throwable
338
    {
339
        return $this->fetchRequestAttribute($request, self::REQUEST_ATTRIBUTE_EXCEPTION, \Throwable::class);
16✔
340
    }
341

342
    /**
343
     * @template T of object
344
     *
345
     * @param class-string<T> $type
346
     *
347
     * @phpstan-return T|null
348
     */
349
    private function fetchRequestAttribute(Request $request, string $key, string $type): ?object
350
    {
351
        return ($object = $request->attributes->get($key)) instanceof $type ? $object : null;
20✔
352
    }
353

354
    /**
355
     * @return array<string, mixed>
356
     */
357
    private function requestAttributes(Request $request): array
358
    {
359
        return [
16✔
360
            UrlAttributes::URL_FULL => $request->getUri(),
16✔
361
            HttpAttributes::HTTP_REQUEST_METHOD => $request->getMethod(),
16✔
362
            UrlAttributes::URL_PATH => $request->getPathInfo(),
16✔
363
            HttpKernelTraceAttributeEnum::HttpHost->toString() => $request->getHttpHost(),
16✔
364
            UrlAttributes::URL_SCHEME => $request->getScheme(),
16✔
365
            NetworkAttributes::NETWORK_PROTOCOL_VERSION => ($protocolVersion = $request->getProtocolVersion()) !== null
16✔
366
                ? strtr($protocolVersion, ['HTTP/' => ''])
16✔
UNCOV
367
                : null,
×
368
            UserAgentAttributes::USER_AGENT_ORIGINAL => $request->headers->get('User-Agent'),
16✔
369
            HttpIncubatingAttributes::HTTP_REQUEST_BODY_SIZE => $request->headers->get('Content-Length'),
16✔
370
            NetworkAttributes::NETWORK_PEER_ADDRESS => $request->getClientIp(),
16✔
371

372
            HttpKernelTraceAttributeEnum::NetPeerIp->toString() => $request->server->get('REMOTE_ADDR'),
16✔
373
            ClientAttributes::CLIENT_ADDRESS => $request->server->get('REMOTE_HOST'),
16✔
374
            ClientAttributes::CLIENT_PORT => $request->server->get('REMOTE_PORT'),
16✔
375
            HttpKernelTraceAttributeEnum::NetHostIp->toString() => $request->server->get('SERVER_ADDR'),
16✔
376
            ServerAttributes::SERVER_ADDRESS => $request->server->get('SERVER_NAME'),
16✔
377
            ServerAttributes::SERVER_PORT => $request->server->get('SERVER_PORT'),
16✔
378
        ];
16✔
379
    }
380

381
    /**
382
     * @param array<string> $headers
383
     *
384
     * @return \Generator<string, mixed>
385
     */
386
    private function headerAttributes(HeaderBag $headerBag, array $headers): \Generator
387
    {
388
        foreach ($headers as $header => $attribute) {
16✔
389
            if ($headerBag->has($header)) {
×
390
                yield $attribute => $headerBag->all($header);
×
391
            }
392
        }
393
    }
394

395
    /**
396
     * @param iterable<string> $headers
397
     *
398
     * @return array<string, string>
399
     */
400
    private function createHeaderAttributeMapping(string $type, iterable $headers): array
401
    {
402
        $headerAttributes = [];
20✔
403
        foreach ($headers as $header) {
20✔
404
            $lcHeader = strtolower($header);
×
405
            $headerAttributes[$lcHeader] = sprintf('http.%s.header.%s', $type, strtr($lcHeader, ['-' => '_']));
×
406
        }
407

408
        return $headerAttributes;
20✔
409
    }
410

411
    public function setInstrumentationType(InstrumentationTypeEnum $type): void
412
    {
413
        $this->instrumentationType = $type;
20✔
414
    }
415

416
    /**
417
     * @param string[] $excludePaths
418
     */
419
    public function setExcludePaths(array $excludePaths): void
420
    {
421
        $this->excludePaths = $excludePaths;
3✔
422
    }
423
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc