Skip to content

Commit 706e4d2

Browse files
committed
handle route naming
1 parent 4f3ccec commit 706e4d2

File tree

4 files changed

+366
-17
lines changed

4 files changed

+366
-17
lines changed

plugins/node/opentelemetry-instrumentation-express/src/instrumentation.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import {
3232
getLayerPath,
3333
isLayerIgnored,
3434
storeLayerPath,
35+
getActualMatchedRoute,
36+
getConstructedRoute,
3537
} from './utils';
3638
/** @knipignore */
3739
import { PACKAGE_NAME, PACKAGE_VERSION } from './version';
@@ -188,25 +190,21 @@ export class ExpressInstrumentation extends InstrumentationBase<ExpressInstrumen
188190
res: express.Response
189191
) {
190192
const { isLayerPathStored } = storeLayerPath(req, layerPath);
191-
const route = (req[_LAYERS_STORE_PROPERTY] as string[])
192-
.filter(path => path !== '/' && path !== '/*')
193-
.join('')
194-
// remove duplicate slashes to normalize route
195-
.replace(/\/{2,}/g, '/');
196193

197-
const actualRoute = route.length > 0 ? route : undefined;
194+
const constructedRoute = getConstructedRoute(req);
195+
const actualMatchedRoute = getActualMatchedRoute(req);
198196

199197
const attributes: Attributes = {
200-
[SEMATTRS_HTTP_ROUTE]: actualRoute,
198+
[SEMATTRS_HTTP_ROUTE]: actualMatchedRoute,
201199
};
202-
const metadata = getLayerMetadata(route, layer, layerPath);
200+
const metadata = getLayerMetadata(constructedRoute, layer, layerPath);
203201
const type = metadata.attributes[
204202
AttributeNames.EXPRESS_TYPE
205203
] as ExpressLayerType;
206204

207205
const rpcMetadata = getRPCMetadata(context.active());
208206
if (rpcMetadata?.type === RPCType.HTTP) {
209-
rpcMetadata.route = actualRoute;
207+
rpcMetadata.route = actualMatchedRoute;
210208
}
211209

212210
// verify against the config if the layer should be ignored
@@ -225,7 +223,7 @@ export class ExpressInstrumentation extends InstrumentationBase<ExpressInstrumen
225223
{
226224
request: req,
227225
layerType: type,
228-
route,
226+
route: constructedRoute,
229227
},
230228
metadata.name
231229
);
@@ -243,7 +241,7 @@ export class ExpressInstrumentation extends InstrumentationBase<ExpressInstrumen
243241
requestHook(span, {
244242
request: req,
245243
layerType: type,
246-
route,
244+
route: constructedRoute,
247245
}),
248246
e => {
249247
if (e) {

plugins/node/opentelemetry-instrumentation-express/src/utils.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,81 @@ const extractLayerPathSegment = (arg: LayerPathSegment) => {
213213

214214
return;
215215
};
216+
217+
export function getConstructedRoute(req: {
218+
originalUrl: PatchedRequest['originalUrl'];
219+
[_LAYERS_STORE_PROPERTY]?: string[];
220+
}) {
221+
const layersStore: string[] = Array.isArray(req[_LAYERS_STORE_PROPERTY])
222+
? (req[_LAYERS_STORE_PROPERTY] as string[])
223+
: [];
224+
225+
const meaningfulPaths = layersStore.filter(
226+
path => path !== '/' && path !== '/*'
227+
);
228+
229+
// Handle standalone wildcard case
230+
if (meaningfulPaths.length === 1 && meaningfulPaths[0] === '*') {
231+
return '*';
232+
}
233+
234+
// Construct the route by joining paths and normalizing
235+
return meaningfulPaths.join('').replace(/\/{2,}/g, '/'); // Remove duplicate slashes
236+
}
237+
238+
/**
239+
* Extracts the actual matched route from Express request for OpenTelemetry instrumentation.
240+
* Returns the route that should be used as the http.route attribute.
241+
*
242+
* @param req - The Express request object with layers store
243+
* @param layersStoreProperty - The property name where layer paths are stored
244+
* @returns The matched route string or undefined if no valid route is found
245+
*/
246+
export function getActualMatchedRoute(req: {
247+
originalUrl: PatchedRequest['originalUrl'];
248+
[_LAYERS_STORE_PROPERTY]?: string[];
249+
}): string | undefined {
250+
const layersStore: string[] = Array.isArray(req[_LAYERS_STORE_PROPERTY])
251+
? (req[_LAYERS_STORE_PROPERTY] as string[])
252+
: [];
253+
254+
// If no layers are stored, no route can be determined
255+
if (layersStore.length === 0) {
256+
return undefined;
257+
}
258+
259+
// Handle root path case - if all paths are root, only return root if originalUrl is also root
260+
if (layersStore.every(path => path === '/')) {
261+
return req.originalUrl === '/' ? '/' : undefined;
262+
}
263+
264+
const constructedRoute = getConstructedRoute(req);
265+
if (constructedRoute === '*') {
266+
return constructedRoute;
267+
}
268+
269+
// Ensure route starts with '/' if it doesn't already
270+
const normalizedRoute = constructedRoute.startsWith('/')
271+
? constructedRoute
272+
: `/${constructedRoute}`;
273+
274+
// Validate that this appears to be a matched route
275+
// A route is considered matched if:
276+
// 1. We have a constructed route
277+
// 2. The original URL matches or starts with our route pattern
278+
const isValidRoute =
279+
normalizedRoute.length > 0 &&
280+
(req.originalUrl === normalizedRoute ||
281+
req.originalUrl.startsWith(normalizedRoute) ||
282+
isRoutePattern(normalizedRoute));
283+
284+
return isValidRoute ? normalizedRoute : undefined;
285+
}
286+
287+
/**
288+
* Checks if a route contains parameter patterns (e.g., :id, :userId)
289+
* which are valid even if they don't exactly match the original URL
290+
*/
291+
function isRoutePattern(route: string): boolean {
292+
return route.includes(':') || route.includes('*');
293+
}

plugins/node/opentelemetry-instrumentation-express/test/express.test.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,17 @@ describe('ExpressInstrumentation', () => {
8989
const spans = memoryExporter.getFinishedSpans();
9090

9191
// Should have middleware spans but no request handler span
92-
const middlewareSpans = spans.filter(span =>
93-
span.name.includes('middleware') ||
94-
span.name.includes('expressInit') ||
95-
span.name.includes('jsonParser')
92+
const middlewareSpans = spans.filter(
93+
span =>
94+
span.name.includes('middleware') ||
95+
span.name.includes('expressInit') ||
96+
span.name.includes('jsonParser')
9697
);
9798

98-
assert.ok(middlewareSpans.length > 0, 'Middleware spans should be created');
99+
assert.ok(
100+
middlewareSpans.length > 0,
101+
'Middleware spans should be created'
102+
);
99103

100104
for (const span of spans) {
101105
assert.strictEqual(

0 commit comments

Comments
 (0)