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

FriendsOfOpenTelemetry / opentelemetry-bundle / 8191090493

07 Mar 2024 03:58PM UTC coverage: 85.969% (-0.1%) from 86.11%
8191090493

push

github

gaelreyrol
feat(instrumentation): add debug log when there is no active scope

12 of 17 new or added lines in 13 files covered. (70.59%)

3 existing lines in 3 files now uncovered.

1973 of 2295 relevant lines covered (85.97%)

6.56 hits per line

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

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

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

5
use FriendsOfOpenTelemetry\OpenTelemetryBundle\OpenTelemetry\Context\Attribute\HttpKernelTraceAttributeEnum;
6
use OpenTelemetry\API\Trace\SpanInterface;
7
use OpenTelemetry\API\Trace\SpanKind;
8
use OpenTelemetry\API\Trace\StatusCode;
9
use OpenTelemetry\API\Trace\TracerInterface;
10
use OpenTelemetry\Context\Context;
11
use OpenTelemetry\Context\Propagation\PropagationGetterInterface;
12
use OpenTelemetry\Context\Propagation\TextMapPropagatorInterface;
13
use OpenTelemetry\Context\ScopeInterface;
14
use OpenTelemetry\SemConv\TraceAttributes;
15
use Psr\Log\LoggerInterface;
16
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
17
use Symfony\Component\HttpFoundation\HeaderBag;
18
use Symfony\Component\HttpFoundation\Request;
19
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
20
use Symfony\Component\HttpKernel\Event\ControllerEvent;
21
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
22
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
23
use Symfony\Component\HttpKernel\Event\RequestEvent;
24
use Symfony\Component\HttpKernel\Event\ResponseEvent;
25
use Symfony\Component\HttpKernel\Event\TerminateEvent;
26
use Symfony\Component\HttpKernel\Event\ViewEvent;
27
use Symfony\Component\HttpKernel\KernelEvents;
28

29
final class TraceableHttpKernelEventSubscriber implements EventSubscriberInterface
30
{
31
    private const REQUEST_ATTRIBUTE_SPAN = '__opentelemetry_symfony_internal_span';
32
    private const REQUEST_ATTRIBUTE_SCOPE = '__opentelemetry_symfony_internal_scope';
33
    private const REQUEST_ATTRIBUTE_EXCEPTION = '__opentelemetry_symfony_internal_exception';
34

35
    /**
36
     * @var array<string, ?string>
37
     */
38
    private array $requestHeaderAttributes;
39

40
    /**
41
     * @var array<string, ?string>
42
     */
43
    private array $responseHeaderAttributes;
44

45
    /**
46
     * @param iterable<string> $requestHeaders
47
     * @param iterable<string> $responseHeaders
48
     */
49
    public function __construct(
50
        private readonly TracerInterface $tracer,
51
        private readonly TextMapPropagatorInterface $propagator,
52
        private readonly PropagationGetterInterface $propagationGetter,
53
        private readonly ?LoggerInterface $logger = null,
54
        iterable $requestHeaders = [],
55
        iterable $responseHeaders = [],
56
    ) {
57
        $this->requestHeaderAttributes = $this->createHeaderAttributeMapping('request', $requestHeaders);
4✔
58
        $this->responseHeaderAttributes = $this->createHeaderAttributeMapping('response', $responseHeaders);
4✔
59
    }
60

61
    public static function getSubscribedEvents(): array
62
    {
63
        return [
×
64
            KernelEvents::REQUEST => [
×
65
                ['startRequest', 10000],
×
66
                ['recordRoute', 31], // after RouterListener
×
67
            ],
×
68
            KernelEvents::CONTROLLER => [
×
69
                ['recordController'],
×
70
            ],
×
71
            KernelEvents::CONTROLLER_ARGUMENTS => [
×
72
                ['recordControllerArguments'],
×
73
            ],
×
74
            KernelEvents::VIEW => [
×
75
                ['recordView'],
×
76
            ],
×
77
            KernelEvents::RESPONSE => [
×
78
                ['recordResponse', -10000],
×
79
            ],
×
80
            KernelEvents::FINISH_REQUEST => [
×
81
                ['endScope', -10000],
×
82
                ['endRequest', -10000],
×
83
            ],
×
84
            KernelEvents::TERMINATE => [
×
85
                ['terminateRequest', 10000],
×
86
            ],
×
87
            KernelEvents::EXCEPTION => [
×
88
                ['recordException'],
×
89
            ],
×
90
        ];
×
91
    }
92

93
    public function startRequest(RequestEvent $event): void
94
    {
95
        $request = $event->getRequest();
4✔
96

97
        $spanBuilder = $this->tracer
4✔
98
            ->spanBuilder(sprintf('HTTP %s', $request->getMethod()))
4✔
99
            ->setSpanKind(SpanKind::KIND_INTERNAL)
4✔
100
            ->setAttributes($this->requestAttributes($request))
4✔
101
            ->setAttributes($this->headerAttributes($request->headers, $this->requestHeaderAttributes))
4✔
102
        ;
4✔
103

104
        $parent = Context::getCurrent();
4✔
105

106
        if ($event->isMainRequest()) {
4✔
107
            $spanBuilder->setSpanKind(SpanKind::KIND_SERVER);
4✔
108
            $parent = $this->propagator->extract(
4✔
109
                $request,
4✔
110
                $this->propagationGetter,
4✔
111
                $parent,
4✔
112
            );
4✔
113

114
            $requestTime = $request->server->get('REQUEST_TIME_FLOAT');
4✔
115
            if (null !== $requestTime) {
4✔
116
                $spanBuilder->setStartTimestamp($requestTime * 1_000_000_000);
4✔
117
            }
118
        }
119

120
        $span = $spanBuilder->setParent($parent)->startSpan();
4✔
121

122
        $this->logger?->debug(sprintf('Starting span "%s"', $span->getContext()->getSpanId()));
4✔
123

124
        $scope = $span->storeInContext($parent)->activate();
4✔
125

126
        $request->attributes->set(self::REQUEST_ATTRIBUTE_SPAN, $span);
4✔
127
        $request->attributes->set(self::REQUEST_ATTRIBUTE_SCOPE, $scope);
4✔
128
    }
129

130
    public function recordRoute(RequestEvent $event): void
131
    {
132
        $span = $this->fetchRequestSpan($event->getRequest());
4✔
133
        if (null === $span) {
4✔
134
            return;
×
135
        }
136

137
        $routeName = $event->getRequest()->attributes->get('_route', '');
4✔
138
        if ('' === $routeName) {
4✔
139
            return;
2✔
140
        }
141

142
        $span->updateName($routeName);
4✔
143
        $span->setAttribute(TraceAttributes::HTTP_ROUTE, $routeName);
4✔
144
    }
145

146
    public function recordController(ControllerEvent $event): void
147
    {
148
        $span = $this->fetchRequestSpan($event->getRequest());
4✔
149
        if (null === $span) {
4✔
150
            return;
×
151
        }
152
    }
153

154
    public function recordControllerArguments(ControllerArgumentsEvent $event): void
155
    {
156
        $span = $this->fetchRequestSpan($event->getRequest());
4✔
157
        if (null === $span) {
4✔
158
            return;
×
159
        }
160
    }
161

162
    public function recordView(ViewEvent $event): void
163
    {
164
        $span = $this->fetchRequestSpan($event->getRequest());
×
165
        if (null === $span) {
×
166
            return;
×
167
        }
168
    }
169

170
    public function recordException(ExceptionEvent $event): void
171
    {
172
        $span = $this->fetchRequestSpan($event->getRequest());
1✔
173
        if (null === $span) {
1✔
174
            return;
×
175
        }
176

177
        $span->recordException($event->getThrowable());
1✔
178
        $event->getRequest()->attributes->set(self::REQUEST_ATTRIBUTE_EXCEPTION, $event->getThrowable());
1✔
179
    }
180

181
    public function recordResponse(ResponseEvent $event): void
182
    {
183
        $span = $this->fetchRequestSpan($event->getRequest());
4✔
184
        if (null === $span) {
4✔
185
            return;
×
186
        }
187

188
        $event->getRequest()->attributes->remove(self::REQUEST_ATTRIBUTE_EXCEPTION);
4✔
189

190
        if (!$span->isRecording()) {
4✔
191
            return;
×
192
        }
193

194
        $response = $event->getResponse();
4✔
195
        $span->setAttribute(TraceAttributes::HTTP_RESPONSE_BODY_SIZE, $response->headers->get('Content-Length'));
4✔
196
        $span->setAttribute(TraceAttributes::NETWORK_PROTOCOL_VERSION, $response->getProtocolVersion());
4✔
197
        $span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $response->getStatusCode());
4✔
198
        if ($response->getStatusCode() >= 500 && $response->getStatusCode() < 600) {
4✔
199
            $span->setStatus(StatusCode::STATUS_ERROR);
2✔
200
        } else {
201
            $span->setStatus(StatusCode::STATUS_OK);
2✔
202
        }
203

204
        $span->setAttributes($this->headerAttributes($response->headers, $this->responseHeaderAttributes));
4✔
205
    }
206

207
    public function endScope(FinishRequestEvent $event): void
208
    {
209
        $scope = $this->fetchRequestScope($event->getRequest());
4✔
210
        if (null === $scope) {
4✔
NEW
211
            $this->logger?->debug('No active scope');
×
212

UNCOV
213
            return;
×
214
        }
215
        $this->logger?->debug(sprintf('Detaching scope "%s"', spl_object_id($scope)));
4✔
216
        $scope->detach();
4✔
217
    }
218

219
    public function endRequest(FinishRequestEvent $event): void
220
    {
221
        $span = $this->fetchRequestSpan($event->getRequest());
4✔
222
        if (null === $span) {
4✔
223
            return;
×
224
        }
225

226
        $exception = $this->fetchRequestException($event->getRequest());
4✔
227
        if (null !== $exception) {
4✔
228
            $span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
×
229
        } elseif ($event->isMainRequest()) {
4✔
230
            // End span on ::terminateRequest() instead
231
            return;
4✔
232
        }
233

234
        $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId()));
2✔
235
        $span->end();
2✔
236
    }
237

238
    public function terminateRequest(TerminateEvent $event): void
239
    {
240
        $span = $this->fetchRequestSpan($event->getRequest());
4✔
241
        if (null === $span) {
4✔
242
            return;
×
243
        }
244

245
        $this->logger?->debug(sprintf('Ending span "%s"', $span->getContext()->getSpanId()));
4✔
246
        $span->end();
4✔
247
    }
248

249
    private function fetchRequestSpan(Request $request): ?SpanInterface
250
    {
251
        return $this->fetchRequestAttribute($request, self::REQUEST_ATTRIBUTE_SPAN, SpanInterface::class);
4✔
252
    }
253

254
    private function fetchRequestScope(Request $request): ?ScopeInterface
255
    {
256
        return $this->fetchRequestAttribute($request, self::REQUEST_ATTRIBUTE_SCOPE, ScopeInterface::class);
4✔
257
    }
258

259
    private function fetchRequestException(Request $request): ?\Throwable
260
    {
261
        return $this->fetchRequestAttribute($request, self::REQUEST_ATTRIBUTE_EXCEPTION, \Throwable::class);
4✔
262
    }
263

264
    /**
265
     * @template T of object
266
     *
267
     * @param class-string<T> $type
268
     *
269
     * @phpstan-return T|null
270
     */
271
    private function fetchRequestAttribute(Request $request, string $key, string $type): ?object
272
    {
273
        return ($object = $request->attributes->get($key)) instanceof $type ? $object : null;
4✔
274
    }
275

276
    /**
277
     * @return iterable<string, string>
278
     */
279
    private function requestAttributes(Request $request): iterable
280
    {
281
        return [
4✔
282
            TraceAttributes::URL_FULL => $request->getUri(),
4✔
283
            TraceAttributes::HTTP_REQUEST_METHOD => $request->getMethod(),
4✔
284
            TraceAttributes::URL_PATH => $request->getPathInfo(),
4✔
285
            HttpKernelTraceAttributeEnum::HttpHost->toString() => $request->getHttpHost(),
4✔
286
            TraceAttributes::URL_SCHEME => $request->getScheme(),
4✔
287
            TraceAttributes::NETWORK_PROTOCOL_VERSION => ($protocolVersion = $request->getProtocolVersion()) !== null
4✔
288
                ? strtr($protocolVersion, ['HTTP/' => ''])
4✔
289
                : null,
290
            TraceAttributes::USER_AGENT_ORIGINAL => $request->headers->get('User-Agent'),
4✔
291
            TraceAttributes::HTTP_REQUEST_BODY_SIZE => $request->headers->get('Content-Length'),
4✔
292
            TraceAttributes::NETWORK_PEER_ADDRESS => $request->getClientIp(),
4✔
293

294
            HttpKernelTraceAttributeEnum::NetPeerIp->toString() => $request->server->get('REMOTE_ADDR'),
4✔
295
            TraceAttributes::CLIENT_ADDRESS => $request->server->get('REMOTE_HOST'),
4✔
296
            TraceAttributes::CLIENT_PORT => $request->server->get('REMOTE_PORT'),
4✔
297
            HttpKernelTraceAttributeEnum::NetHostIp->toString() => $request->server->get('SERVER_ADDR'),
4✔
298
            TraceAttributes::SERVER_ADDRESS => $request->server->get('SERVER_NAME'),
4✔
299
            TraceAttributes::SERVER_PORT => $request->server->get('SERVER_PORT'),
4✔
300
        ];
4✔
301
    }
302

303
    /**
304
     * @param array<string> $headers
305
     *
306
     * @return array<string, mixed>
307
     */
308
    private function headerAttributes(HeaderBag $headerBag, array $headers): iterable
309
    {
310
        foreach ($headers as $header => $attribute) {
4✔
311
            if ($headerBag->has($header)) {
×
312
                yield $attribute => $headerBag->all($header);
×
313
            }
314
        }
315
    }
316

317
    /**
318
     * @param iterable<string> $headers
319
     *
320
     * @return array<string, string>
321
     */
322
    private function createHeaderAttributeMapping(string $type, iterable $headers): array
323
    {
324
        $headerAttributes = [];
4✔
325
        foreach ($headers as $header) {
4✔
326
            $lcHeader = strtolower($header);
×
327
            $headerAttributes[$lcHeader] = sprintf('http.%s.header.%s', $type, strtr($lcHeader, ['-' => '_']));
×
328
        }
329

330
        return $headerAttributes;
4✔
331
    }
332
}
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

© 2025 Coveralls, Inc