Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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',
Expand Down
174 changes: 121 additions & 53 deletions src/DDTrace/Integrations/Symfony/SymfonyIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -422,89 +422,157 @@ 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;
}

// 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);
}
}
$item = $cache->getItem($itemName);
if ($item->isHit()) {
$route = $item->get();
} else {

$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();
}
$item->set($route);
$item->expiresAfter(3600);

if ($startTime !== null) {
$durationNanoseconds = \hrtime(true) - $startTime;
$durationMicroseconds = (int)($durationNanoseconds / 1000);
$rootSpan->metrics['_dd.symfony.route.map_build_duration_us'] = $durationMicroseconds;
}

$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;
}
};
} 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.
Expand Down
Loading