|
11 | 11 | use OpenTelemetry\API\Trace\SpanKind;
|
12 | 12 | use OpenTelemetry\API\Trace\StatusCode;
|
13 | 13 | use OpenTelemetry\Context\Context;
|
| 14 | +use Symfony\Component\HttpKernel\KernelEvents; |
14 | 15 | use function OpenTelemetry\Instrumentation\hook;
|
15 | 16 | use OpenTelemetry\SemConv\TraceAttributes;
|
| 17 | +use Symfony\Component\EventDispatcher\EventDispatcher; |
16 | 18 | use Symfony\Component\HttpFoundation\Request;
|
17 | 19 | use Symfony\Component\HttpFoundation\Response;
|
18 | 20 | use Symfony\Component\HttpKernel\HttpKernel;
|
@@ -169,5 +171,160 @@ public static function register(): void
|
169 | 171 | return $params;
|
170 | 172 | },
|
171 | 173 | );
|
| 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); |
172 | 329 | }
|
173 | 330 | }
|
0 commit comments