Skip to content

Commit a0dfbe3

Browse files
committed
wip: capture spans for controllers automatically
1 parent c22df17 commit a0dfbe3

File tree

1 file changed

+157
-0
lines changed

1 file changed

+157
-0
lines changed

src/Instrumentation/Symfony/src/SymfonyInstrumentation.php

+157
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;
@@ -141,5 +143,160 @@ public static function register(): void
141143
$span->end();
142144
}
143145
);
146+
147+
/**
148+
* Extract symfony event dispatcher known events and trace the controller
149+
*
150+
* Adapted from https://github.com/DataDog/dd-trace-php/blob/master/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php
151+
*/
152+
hook(
153+
EventDispatcher::class,
154+
'dispatch',
155+
pre: static function (
156+
EventDispatcher $dispatcher,
157+
array $params,
158+
string $class,
159+
string $function,
160+
?string $filename,
161+
?int $lineno,
162+
) use ($instrumentation): array {
163+
if (!isset($args[0])) {
164+
return $params;
165+
}
166+
167+
if (\is_object($args[0])) {
168+
// dispatch($event, string $eventName = null)
169+
$event = $args[0];
170+
$eventName = isset($args[1]) && \is_string($args[1]) ? $args[1] : \get_class($event);
171+
} elseif (\is_string($args[0])) {
172+
// dispatch($eventName, Event $event = null)
173+
$eventName = $args[0];
174+
$event = isset($args[1]) && \is_object($args[1]) ? $args[1] : null;
175+
} else {
176+
return $params;
177+
}
178+
179+
if ($eventName === 'kernel.controller' && \method_exists($event, 'getController')) {
180+
$controller = $event->getController();
181+
if (!($controller instanceof \Closure)) {
182+
if (\is_callable($controller, false, $controllerName) && $controllerName !== null) {
183+
if (\strpos($controllerName, '::') > 0) {
184+
list($class, $method) = \explode('::', $controllerName);
185+
if (isset($class, $method)) {
186+
hook(
187+
$class,
188+
$method,
189+
pre: static function (
190+
object $controller,
191+
array $params,
192+
string $class,
193+
string $function,
194+
?string $filename,
195+
?int $lineno,
196+
) use ($instrumentation) {
197+
$parent = Context::getCurrent();
198+
$builder = $instrumentation
199+
->tracer()
200+
->spanBuilder(sprintf('%s::%s', $class, $function))
201+
->setParent($parent);
202+
$span = $builder->startSpan();
203+
$parent = Context::getCurrent();
204+
Context::storage()->attach($span->storeInContext($parent));
205+
},
206+
post: static function (
207+
object $controller,
208+
array $params,
209+
$result,
210+
?\Throwable $exception
211+
) {
212+
$scope = Context::storage()->scope();
213+
if (null === $scope) {
214+
return;
215+
}
216+
217+
$scope->detach();
218+
$span = Span::fromContext($scope->context());
219+
220+
if (null !== $exception) {
221+
$span->recordException($exception, [
222+
TraceAttributes::EXCEPTION_ESCAPED => true,
223+
]);
224+
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
225+
}
226+
227+
$span->end();
228+
}
229+
);
230+
}
231+
}
232+
}
233+
}
234+
}
235+
236+
$parent = Context::getCurrent();
237+
$span = $instrumentation->tracer()
238+
->spanBuilder('symfony.' . $eventName)
239+
->setParent($parent)
240+
->startSpan();
241+
242+
Context::storage()->attach($span->storeInContext($parent));
243+
244+
if ($event === null) {
245+
return $params;
246+
}
247+
248+
self::setControllerNameAsSpanName($event, $eventName);
249+
250+
return $params;
251+
},
252+
post: static function (
253+
EventDispatcher $dispatcher,
254+
array $params,
255+
$result,
256+
?\Throwable $exception
257+
) use ($instrumentation): void {
258+
$scope = Context::storage()->scope();
259+
if (null === $scope) {
260+
return;
261+
}
262+
263+
$scope->detach();
264+
$span = Span::fromContext($scope->context());
265+
266+
if (null !== $exception) {
267+
$span->recordException($exception, [
268+
TraceAttributes::EXCEPTION_ESCAPED => true,
269+
]);
270+
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
271+
}
272+
273+
$span->end();
274+
}
275+
);
276+
}
277+
278+
private static function setControllerNameAsSpanName($event, $eventName): void
279+
{
280+
if (
281+
!\defined("\Symfony\Component\HttpKernel\KernelEvents::CONTROLLER")
282+
|| $eventName !== KernelEvents::CONTROLLER
283+
|| !method_exists($event, 'getController')
284+
) {
285+
return;
286+
}
287+
288+
/** @var callable $controllerAndAction */
289+
$controllerAndAction = $event->getController();
290+
291+
if (
292+
!is_array($controllerAndAction)
293+
|| count($controllerAndAction) !== 2
294+
|| !is_object($controllerAndAction[0])
295+
) {
296+
return;
297+
}
298+
299+
$action = get_class($controllerAndAction[0]) . '::' . $controllerAndAction[1];
300+
Span::getCurrent()->updateName($action);
144301
}
145302
}

0 commit comments

Comments
 (0)