From df4dde88f90907c33fb70fe8d384dceff8c8f579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Andr=C3=A9=20dos=20Santos=20Lopes?= Date: Wed, 25 Feb 2026 14:15:51 +0100 Subject: [PATCH 1/2] Optimize Symfony http.route caching with path map approach Replace per-route caching with a single cached map of all route paths. This significantly improves performance and reduces memory usage. Key improvements: - Cache entire route map under single key '_datadog.symfony.route_paths' - Store simple array: ['route_name' => '/path', ...] - Smart invalidation based on compiled routes file mtime - Cache indefinitely until Symfony recompiles routes or cache is invalidated - Gracefully disable if cache.app unavailable Cache invalidation logic: - Checks {cache_dir}/url_generating_routes.php modification time - Compares against cached timestamp (created at cache build time) - Rebuilds cache only when compiled routes file is newer --- .../Symfony/SymfonyIntegration.php | 100 +++++++++++++++--- 1 file changed, 83 insertions(+), 17 deletions(-) diff --git a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php index 7fe3095201..20f2462d23 100644 --- a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php +++ b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php @@ -422,43 +422,109 @@ static function() { ); if (\dd_trace_env_config('DD_TRACE_SYMFONY_HTTP_ROUTE')) { + /** + * Resolves the http.route tag for a given route name by looking up + * the route path in a cached map of all routes. + * + * Caching strategy: + * - Caches the entire route path map under a single key: '_datadog.symfony.route_paths' + * - Stores: ['mtime' => timestamp, 'paths' => ['route_name' => '/path', ...]] + * - Invalidates cache when Symfony's compiled routes file is newer than cached mtime + * - Falls back gracefully if cache.app is unavailable (no http.route tag) + */ $handle_http_route = static function($route_name, $request, $rootSpan) { if (self::$kernel === null) { return; } + /** @var ContainerInterface $container */ $container = self::$kernel->getContainer(); + try { $cache = $container->get('cache.app'); } catch (\Exception $e) { return; } - /** @var \Symfony\Bundle\FrameworkBundle\Routing\Router $router */ - $router = $container->get('router'); if (!\method_exists($cache, 'getItem')) { return; } - $itemName = "_datadog.route.path.$route_name"; - $locale = $request->get('_locale'); - if ($locale !== null) { - $itemName .= ".$locale"; + + /** @var \Symfony\Bundle\FrameworkBundle\Routing\Router $router */ + try { + $router = $container->get('router'); + } catch (\Exception $e) { + return; } - $item = $cache->getItem($itemName); - if ($item->isHit()) { - $route = $item->get(); - } else { + + // Get the compiled routes file mtime for cache invalidation + $compiledRoutesMtime = null; + $cacheDir = \method_exists($router, 'getOption') ? $router->getOption('cache_dir') : null; + if ($cacheDir !== null) { + $compiledRoutesFile = $cacheDir . '/url_generating_routes.php'; + if (\file_exists($compiledRoutesFile)) { + $compiledRoutesMtime = @\filemtime($compiledRoutesFile); + } + } + + $cacheKey = '_datadog.symfony.route_paths'; + /** @var ItemInterface $item */ + $item = $cache->getItem($cacheKey); + $cachedData = $item->isHit() ? $item->get() : null; + + $routePathMap = null; + $needsRebuild = true; + + if (\is_array($cachedData) && isset($cachedData['paths']) && \is_array($cachedData['paths'])) { + // Check if cache is still valid + if ($compiledRoutesMtime === null) { + // No compiled file to check against - cache is valid + $needsRebuild = false; + $routePathMap = $cachedData['paths']; + } elseif (isset($cachedData['mtime']) && $cachedData['mtime'] >= $compiledRoutesMtime) { + // Cached data is newer than or equal to compiled routes - cache is valid + $needsRebuild = false; + $routePathMap = $cachedData['paths']; + } + // Otherwise: compiled routes file is newer, rebuild cache + } + + if ($needsRebuild) { + $startTime = \function_exists('hrtime') ? \hrtime(true) : null; + + $routePathMap = []; $routeCollection = $router->getRouteCollection(); - $route = $routeCollection->get($route_name); - if ($route == null && $locale !== null) { - $route = $routeCollection->get($route_name . '.' . $locale); + foreach ($routeCollection->all() as $name => $route) { + $routePathMap[$name] = $route->getPath(); + } + + if ($startTime !== null) { + $durationNanoseconds = \hrtime(true) - $startTime; + $durationMicroseconds = (int)($durationNanoseconds / 1000); + $rootSpan->metrics['_dd.symfony.route.map_build_duration_us'] = $durationMicroseconds; } - $item->set($route); - $item->expiresAfter(3600); + + $item->set([ + 'mtime' => \time(), + 'paths' => $routePathMap, + ]); $cache->save($item); } - if (isset($route)) { - $rootSpan->meta[Tag::HTTP_ROUTE] = $route->getPath(); + + // Look up the route path + $path = null; + if (isset($routePathMap[$route_name])) { + $path = $routePathMap[$route_name]; + } else { + // Try with locale suffix (Symfony i18n routing convention) + $locale = $request->get('_locale'); + if ($locale !== null && isset($routePathMap[$route_name . '.' . $locale])) { + $path = $routePathMap[$route_name . '.' . $locale]; + } + } + + if ($path !== null) { + $rootSpan->meta[Tag::HTTP_ROUTE] = $path; } }; From 9700e8532ebaec2367ffe3053709da5ae4e77254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Andr=C3=A9=20dos=20Santos=20Lopes?= Date: Thu, 5 Mar 2026 15:21:12 +0000 Subject: [PATCH 2/2] Fix DD_TRACE_SYMFONY_HTTP_ROUTE=false DD_TRACE_SYMFONY_HTTP_ROUTE=false incorrectly fully disabled Symfony request handling instrumentation. Properly scoped it to its original intent: the extraction of the Symfony route path for meta tag http.route. --- .../php/integration/Symfony62Tests.groovy | 9 ++- .../Symfony/SymfonyIntegration.php | 74 ++++++++++--------- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy index eea08a5773..89bcf65f19 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Symfony62Tests.groovy @@ -125,14 +125,17 @@ class Symfony62Tests { service apache2 restart''') assert res.exitCode == 0 + // path params are always pushed to AppSec regardless of DD_TRACE_SYMFONY_HTTP_ROUTE, + // so the WAF still blocks based on the path param key 'param01' HttpRequest req = container.buildReq('/dynamic-path/someValue').GET().build() def trace = container.traceFromRequest(req, ofString()) { HttpResponse re -> - assert re.statusCode() == 200 - assert re.body().contains('Hi someValue!') + assert re.statusCode() == 403 } Span span = trace.first() - assert span.meta."http.route" != '/dynamic-path/{param01}' + assert span.meta."http.route" == null + assert span.meta."symfony.route.name" != null + assert span.resource == 'app_home_dynamic' } finally { def res = CONTAINER.execInContainer( 'bash', '-c', diff --git a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php index 20f2462d23..d660b2deb5 100644 --- a/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php +++ b/src/DDTrace/Integrations/Symfony/SymfonyIntegration.php @@ -527,50 +527,52 @@ static function() { $rootSpan->meta[Tag::HTTP_ROUTE] = $path; } }; + } else { + $handle_http_route = static function() { /* noop */ }; + } - \DDTrace\trace_method( - 'Symfony\Component\HttpKernel\HttpKernel', - 'handle', - static function(SpanData $span, $args, $response) use ($handle_http_route) { - /** @var Request $request */ - list($request) = $args; + \DDTrace\trace_method( + 'Symfony\Component\HttpKernel\HttpKernel', + 'handle', + static function(SpanData $span, $args, $response) use ($handle_http_route) { + /** @var Request $request */ + list($request) = $args; - $span->name = 'symfony.kernel.handle'; - $span->service = \ddtrace_config_app_name(self::$frameworkPrefix); - $span->type = Type::WEB_SERVLET; - $span->meta[Tag::COMPONENT] = self::NAME; + $span->name = 'symfony.kernel.handle'; + $span->service = \ddtrace_config_app_name(self::$frameworkPrefix); + $span->type = Type::WEB_SERVLET; + $span->meta[Tag::COMPONENT] = self::NAME; - $rootSpan = \DDTrace\root_span(); - $rootSpan->meta[Tag::HTTP_METHOD] = $request->getMethod(); - $rootSpan->meta[Tag::COMPONENT] = self::$frameworkPrefix; - $rootSpan->meta[Tag::SPAN_KIND] = 'server'; - self::addTraceAnalyticsIfEnabled($rootSpan); + $rootSpan = \DDTrace\root_span(); + $rootSpan->meta[Tag::HTTP_METHOD] = $request->getMethod(); + $rootSpan->meta[Tag::COMPONENT] = self::$frameworkPrefix; + $rootSpan->meta[Tag::SPAN_KIND] = 'server'; + self::addTraceAnalyticsIfEnabled($rootSpan); - if (!array_key_exists(Tag::HTTP_URL, $rootSpan->meta)) { - $rootSpan->meta[Tag::HTTP_URL] = Normalizer::urlSanitize($request->getUri()); - } - if (isset($response)) { - $rootSpan->meta[Tag::HTTP_STATUS_CODE] = $response->getStatusCode(); - } + if (!array_key_exists(Tag::HTTP_URL, $rootSpan->meta)) { + $rootSpan->meta[Tag::HTTP_URL] = Normalizer::urlSanitize($request->getUri()); + } + if (isset($response)) { + $rootSpan->meta[Tag::HTTP_STATUS_CODE] = $response->getStatusCode(); + } - $route_name = $request->get('_route'); - if ($route_name !== null) { - if (dd_trace_env_config("DD_HTTP_SERVER_ROUTE_BASED_NAMING")) { - $rootSpan->resource = $route_name; - } - $rootSpan->meta['symfony.route.name'] = $route_name; - $handle_http_route($route_name, $request, $rootSpan); + $route_name = $request->get('_route'); + if ($route_name !== null) { + if (dd_trace_env_config("DD_HTTP_SERVER_ROUTE_BASED_NAMING")) { + $rootSpan->resource = $route_name; } + $rootSpan->meta['symfony.route.name'] = $route_name; + $handle_http_route($route_name, $request, $rootSpan); + } - $parameters = $request->get('_route_params'); - if (!empty($parameters) && - is_array($parameters) && - function_exists('datadog\appsec\push_addresses')) { - \datadog\appsec\push_addresses(["server.request.path_params" => $parameters]); - } + $parameters = $request->get('_route_params'); + if (!empty($parameters) && + is_array($parameters) && + function_exists('datadog\appsec\push_addresses')) { + \datadog\appsec\push_addresses(["server.request.path_params" => $parameters]); } - ); - } + } + ); /* * EventDispatcher v4.3 introduced an arg hack that mutates the arguments.