Skip to content

Commit 72cfdf7

Browse files
authored
perf(observability): tag APM spans with matched Express route (#635)
* perf(observability): tag APM spans with Express route (LFXV2-1637) Add an applyCustomAttributesOnSpan hook on HttpInstrumentation that sets http.route to req.baseUrl + req.route.path and updates the span name to METHOD ROUTE. Datadog uses these to compute resource_name, so spans now bucket by endpoint (e.g. GET /api/meetings/:id) instead of bare HTTP method. For SSR catch-all routes where req.route isn't set, fall back to bucketing by first URL segment (e.g. /meetings/*) so SSR latency is no longer lumped into a single GET bucket. Signed-off-by: Asitha de Silva <asithade@gmail.com> * fix(observability): bucket root SSR requests under route '/' Address PR #635 review feedback from coderabbitai, copilot[bot]: - otel.mjs: handle root path in SSR fallback so http.route is set to '/' when URL has no segments. Previously, requests to '/' left http.route unset and landed in the bare GET resource bucket — the exact problem this PR set out to fix. The root dashboard route (app.routes.ts path: '') is real SSR traffic, so this matters. Resolves 2 review threads. Signed-off-by: Asitha de Silva <asithade@gmail.com> * fix(review): address PR #635 review feedback Address review comments from copilot-pull-request-reviewer: - otel.mjs: skip pure wildcard route paths ('**', '*', '/*', '/**') so static-asset fallthrough does not collapse SSR spans into GET ** (per copilot[bot]) - otel.mjs: strip trailing slash from concatenated routes so router.get('/') mounts (e.g. /api/meetings/) bucket as their canonical path (per copilot[bot]) - otel.mjs: bucket single-segment fallback URLs as exact paths (e.g. /login) instead of /login/*, since they are concrete endpoints not prefixes (per copilot[bot]) Resolves 4 review threads. Signed-off-by: Asitha de Silva <asithade@gmail.com> --------- Signed-off-by: Asitha de Silva <asithade@gmail.com>
1 parent b2dce1f commit 72cfdf7

1 file changed

Lines changed: 35 additions & 0 deletions

File tree

apps/lfx-one/otel.mjs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,41 @@ if (!otlpEndpoint) {
107107
const url = req.url || '';
108108
return url === '/health' || url === '/api/health' || url.startsWith('/.well-known');
109109
},
110+
applyCustomAttributesOnSpan: (span, request, response) => {
111+
const req = 'req' in response ? response.req : undefined;
112+
if (!req) return;
113+
114+
const routePath = req.route?.path;
115+
// Skip pure wildcard route paths ('**', '*', '/*', '/**') leaked from
116+
// static-asset fallthrough — they collapse SSR traffic into a single bucket.
117+
const isWildcardOnly = typeof routePath === 'string' && /^\/?\*+$/.test(routePath);
118+
119+
if (routePath && !isWildcardOnly) {
120+
const baseUrl = req.baseUrl || '';
121+
let fullRoute = `${baseUrl}${routePath}`;
122+
// Strip trailing slash from mounted router root (e.g. `/api/meetings/` → `/api/meetings`).
123+
if (fullRoute.length > 1 && fullRoute.endsWith('/')) {
124+
fullRoute = fullRoute.slice(0, -1);
125+
}
126+
span.setAttribute('http.route', fullRoute);
127+
span.updateName(`${request.method} ${fullRoute}`);
128+
return;
129+
}
130+
131+
const url = (req.originalUrl || req.url || '').split('?')[0];
132+
const segments = url.split('/').filter(Boolean);
133+
let bucket;
134+
if (segments.length === 0) {
135+
bucket = '/';
136+
} else if (segments.length === 1) {
137+
// Single-segment URLs (e.g. `/login`) are concrete endpoints, not prefixes.
138+
bucket = `/${segments[0]}`;
139+
} else {
140+
bucket = `/${segments[0]}/*`;
141+
}
142+
span.setAttribute('http.route', bucket);
143+
span.updateName(`${request.method} ${bucket}`);
144+
},
110145
}),
111146
new ExpressInstrumentation(),
112147
new UndiciInstrumentation({

0 commit comments

Comments
 (0)