Skip to content

Commit 9f0c80f

Browse files
committed
wip: capture spans for controllers automatically
1 parent 56e4005 commit 9f0c80f

File tree

1 file changed

+157
-0
lines changed

1 file changed

+157
-0
lines changed

src/Instrumentation/Symfony/src/SymfonyInstrumentation.php

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
use OpenTelemetry\API\Trace\SpanKind;
1212
use OpenTelemetry\API\Trace\StatusCode;
1313
use OpenTelemetry\Context\Context;
14+
use Symfony\Component\HttpKernel\KernelEvents;
1415
use function OpenTelemetry\Instrumentation\hook;
1516
use OpenTelemetry\SemConv\TraceAttributes;
17+
use Symfony\Component\EventDispatcher\EventDispatcher;
1618
use Symfony\Component\HttpFoundation\Request;
1719
use Symfony\Component\HttpFoundation\Response;
1820
use Symfony\Component\HttpKernel\HttpKernel;
@@ -169,5 +171,160 @@ public static function register(): void
169171
return $params;
170172
},
171173
);
174+
175+
/**
176+
* Extract symfony event dispatcher known events and trace the controller
177+
*
178+
* Adapted from https://github.com/DataDog/dd-trace-php/blob/master/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php
179+
*/
180+
hook(
181+
EventDispatcher::class,
182+
'dispatch',
183+
pre: static function (
184+
EventDispatcher $dispatcher,
185+
array $params,
186+
string $class,
187+
string $function,
188+
?string $filename,
189+
?int $lineno,
190+
) use ($instrumentation): array {
191+
if (!isset($args[0])) {
192+
return $params;
193+
}
194+
195+
if (\is_object($args[0])) {
196+
// dispatch($event, string $eventName = null)
197+
$event = $args[0];
198+
$eventName = isset($args[1]) && \is_string($args[1]) ? $args[1] : \get_class($event);
199+
} elseif (\is_string($args[0])) {
200+
// dispatch($eventName, Event $event = null)
201+
$eventName = $args[0];
202+
$event = isset($args[1]) && \is_object($args[1]) ? $args[1] : null;
203+
} else {
204+
return $params;
205+
}
206+
207+
if ($eventName === 'kernel.controller' && \method_exists($event, 'getController')) {
208+
$controller = $event->getController();
209+
if (!($controller instanceof \Closure)) {
210+
if (\is_callable($controller, false, $controllerName) && $controllerName !== null) {
211+
if (\strpos($controllerName, '::') > 0) {
212+
list($class, $method) = \explode('::', $controllerName);
213+
if (isset($class, $method)) {
214+
hook(
215+
$class,
216+
$method,
217+
pre: static function (
218+
object $controller,
219+
array $params,
220+
string $class,
221+
string $function,
222+
?string $filename,
223+
?int $lineno,
224+
) use ($instrumentation) {
225+
$parent = Context::getCurrent();
226+
$builder = $instrumentation
227+
->tracer()
228+
->spanBuilder(sprintf('%s::%s', $class, $function))
229+
->setParent($parent);
230+
$span = $builder->startSpan();
231+
$parent = Context::getCurrent();
232+
Context::storage()->attach($span->storeInContext($parent));
233+
},
234+
post: static function (
235+
object $controller,
236+
array $params,
237+
$result,
238+
?\Throwable $exception
239+
) {
240+
$scope = Context::storage()->scope();
241+
if (null === $scope) {
242+
return;
243+
}
244+
245+
$scope->detach();
246+
$span = Span::fromContext($scope->context());
247+
248+
if (null !== $exception) {
249+
$span->recordException($exception, [
250+
TraceAttributes::EXCEPTION_ESCAPED => true,
251+
]);
252+
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
253+
}
254+
255+
$span->end();
256+
}
257+
);
258+
}
259+
}
260+
}
261+
}
262+
}
263+
264+
$parent = Context::getCurrent();
265+
$span = $instrumentation->tracer()
266+
->spanBuilder('symfony.' . $eventName)
267+
->setParent($parent)
268+
->startSpan();
269+
270+
Context::storage()->attach($span->storeInContext($parent));
271+
272+
if ($event === null) {
273+
return $params;
274+
}
275+
276+
self::setControllerNameAsSpanName($event, $eventName);
277+
278+
return $params;
279+
},
280+
post: static function (
281+
EventDispatcher $dispatcher,
282+
array $params,
283+
$result,
284+
?\Throwable $exception
285+
) use ($instrumentation): void {
286+
$scope = Context::storage()->scope();
287+
if (null === $scope) {
288+
return;
289+
}
290+
291+
$scope->detach();
292+
$span = Span::fromContext($scope->context());
293+
294+
if (null !== $exception) {
295+
$span->recordException($exception, [
296+
TraceAttributes::EXCEPTION_ESCAPED => true,
297+
]);
298+
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
299+
}
300+
301+
$span->end();
302+
}
303+
);
304+
}
305+
306+
private static function setControllerNameAsSpanName($event, $eventName): void
307+
{
308+
if (
309+
!\defined("\Symfony\Component\HttpKernel\KernelEvents::CONTROLLER")
310+
|| $eventName !== KernelEvents::CONTROLLER
311+
|| !method_exists($event, 'getController')
312+
) {
313+
return;
314+
}
315+
316+
/** @var callable $controllerAndAction */
317+
$controllerAndAction = $event->getController();
318+
319+
if (
320+
!is_array($controllerAndAction)
321+
|| count($controllerAndAction) !== 2
322+
|| !is_object($controllerAndAction[0])
323+
) {
324+
return;
325+
}
326+
327+
$action = get_class($controllerAndAction[0]) . '::' . $controllerAndAction[1];
328+
Span::getCurrent()->updateName($action);
172329
}
173330
}

0 commit comments

Comments
 (0)