Skip to content

Commit 704f76f

Browse files
chigia001pichlermarcFlarnahaddasbronfman
authored
fix(connect): fix wrong rpcMetada.route value not handle nested route (#1555)
* fix(connect): fix wrong rpcMetada.route value not handle nested route * fix: update base on PR feedback * fix: lint issue * Fix PR's feedback * Fix lint issue * Fix typo --------- Co-authored-by: Marc Pichler <[email protected]> Co-authored-by: Gerhard Stöbich <[email protected]> Co-authored-by: Haddas Bronfman <[email protected]>
1 parent 8802eae commit 704f76f

File tree

5 files changed

+285
-4
lines changed

5 files changed

+285
-4
lines changed

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

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
ConnectNames,
2424
ConnectTypes,
2525
} from './enums/AttributeNames';
26-
import { Use, UseArgs, UseArgs2 } from './internal-types';
26+
import { PatchedRequest, Use, UseArgs, UseArgs2 } from './internal-types';
2727
import { VERSION } from './version';
2828
import {
2929
InstrumentationBase,
@@ -32,6 +32,11 @@ import {
3232
isWrapped,
3333
} from '@opentelemetry/instrumentation';
3434
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
35+
import {
36+
replaceCurrentStackRoute,
37+
addNewStackLayer,
38+
generateRoute,
39+
} from './utils';
3540

3641
export const ANONYMOUS_NAME = 'anonymous';
3742

@@ -65,6 +70,9 @@ export class ConnectInstrumentation extends InstrumentationBase<Server> {
6570
if (!isWrapped(patchedApp.use)) {
6671
this._wrap(patchedApp, 'use', this._patchUse.bind(this));
6772
}
73+
if (!isWrapped(patchedApp.handle)) {
74+
this._wrap(patchedApp, 'handle', this._patchHandle.bind(this));
75+
}
6876
}
6977

7078
private _patchConstructor(original: () => Server): () => Server {
@@ -120,14 +128,20 @@ export class ConnectInstrumentation extends InstrumentationBase<Server> {
120128
if (!instrumentation.isEnabled()) {
121129
return (middleWare as any).apply(this, arguments);
122130
}
123-
const [resArgIdx, nextArgIdx] = isErrorMiddleware ? [2, 3] : [1, 2];
131+
const [reqArgIdx, resArgIdx, nextArgIdx] = isErrorMiddleware
132+
? [1, 2, 3]
133+
: [0, 1, 2];
134+
const req = arguments[reqArgIdx] as PatchedRequest;
124135
const res = arguments[resArgIdx] as ServerResponse;
125136
const next = arguments[nextArgIdx] as NextFunction;
126137

138+
replaceCurrentStackRoute(req, routeName);
139+
127140
const rpcMetadata = getRPCMetadata(context.active());
128141
if (routeName && rpcMetadata?.type === RPCType.HTTP) {
129-
rpcMetadata.route = routeName;
142+
rpcMetadata.route = generateRoute(req);
130143
}
144+
131145
let spanName = '';
132146
if (routeName) {
133147
spanName = `request handler - ${routeName}`;
@@ -180,4 +194,30 @@ export class ConnectInstrumentation extends InstrumentationBase<Server> {
180194
return original.apply(this, args as UseArgs2);
181195
};
182196
}
197+
198+
public _patchHandle(original: Server['handle']): Server['handle'] {
199+
const instrumentation = this;
200+
return function (this: Server): ReturnType<Server['handle']> {
201+
const [reqIdx, outIdx] = [0, 2];
202+
const req = arguments[reqIdx] as PatchedRequest;
203+
const out = arguments[outIdx];
204+
const completeStack = addNewStackLayer(req);
205+
206+
if (typeof out === 'function') {
207+
arguments[outIdx] = instrumentation._patchOut(
208+
out as NextFunction,
209+
completeStack
210+
);
211+
}
212+
213+
return (original as any).apply(this, arguments);
214+
};
215+
}
216+
217+
public _patchOut(out: NextFunction, completeStack: () => void): NextFunction {
218+
return function nextFunction(this: NextFunction, ...args: any[]): void {
219+
completeStack();
220+
return Reflect.apply(out, this, args);
221+
};
222+
}
183223
}

plugins/node/opentelemetry-instrumentation-connect/src/internal-types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,16 @@
1414
* limitations under the License.
1515
*/
1616

17-
import type { HandleFunction, Server } from 'connect';
17+
import type { HandleFunction, IncomingMessage, Server } from 'connect';
18+
19+
export const _LAYERS_STORE_PROPERTY: unique symbol = Symbol(
20+
'opentelemetry.instrumentation-connect.request-route-stack'
21+
);
1822

1923
export type UseArgs1 = [HandleFunction];
2024
export type UseArgs2 = [string, HandleFunction];
2125
export type UseArgs = UseArgs1 | UseArgs2;
2226
export type Use = (...args: UseArgs) => Server;
27+
export type PatchedRequest = {
28+
[_LAYERS_STORE_PROPERTY]: string[];
29+
} & IncomingMessage;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { diag } from '@opentelemetry/api';
17+
import { _LAYERS_STORE_PROPERTY, PatchedRequest } from './internal-types';
18+
19+
export const addNewStackLayer = (request: PatchedRequest) => {
20+
if (Array.isArray(request[_LAYERS_STORE_PROPERTY]) === false) {
21+
Object.defineProperty(request, _LAYERS_STORE_PROPERTY, {
22+
enumerable: false,
23+
value: [],
24+
});
25+
}
26+
request[_LAYERS_STORE_PROPERTY].push('/');
27+
28+
const stackLength = request[_LAYERS_STORE_PROPERTY].length;
29+
30+
return () => {
31+
if (stackLength === request[_LAYERS_STORE_PROPERTY].length) {
32+
request[_LAYERS_STORE_PROPERTY].pop();
33+
} else {
34+
diag.warn('Connect: Trying to pop the stack multiple time');
35+
}
36+
};
37+
};
38+
39+
export const replaceCurrentStackRoute = (
40+
request: PatchedRequest,
41+
newRoute?: string
42+
) => {
43+
if (newRoute) {
44+
request[_LAYERS_STORE_PROPERTY].splice(-1, 1, newRoute);
45+
}
46+
};
47+
48+
// generage route from existing stack on request object.
49+
// splash between stack layer will be dedup
50+
// ["/first/", "/second", "/third/"] => /first/second/thrid/
51+
export const generateRoute = (request: PatchedRequest) => {
52+
return request[_LAYERS_STORE_PROPERTY].reduce(
53+
(acc, sub) => acc.replace(/\/+$/, '') + sub
54+
);
55+
};

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,5 +243,88 @@ describe('connect', () => {
243243
changedRootSpan.spanContext().spanId
244244
);
245245
});
246+
247+
it('should append nested route in RpcMetadata', async () => {
248+
const rootSpan = tracer.startSpan('root span');
249+
const rpcMetadata: RPCMetadata = { type: RPCType.HTTP, span: rootSpan };
250+
app.use((req, res, next) => {
251+
return context.with(
252+
setRPCMetadata(
253+
trace.setSpan(context.active(), rootSpan),
254+
rpcMetadata
255+
),
256+
next
257+
);
258+
});
259+
260+
const nestedApp = connect();
261+
262+
app.use('/foo/', nestedApp);
263+
nestedApp.use('/bar/', (req, res, next) => {
264+
next();
265+
});
266+
267+
await httpRequest.get(`http://localhost:${PORT}/foo/bar`);
268+
rootSpan.end();
269+
270+
assert.strictEqual(rpcMetadata.route, '/foo/bar/');
271+
});
272+
273+
it('should use latest match route when multiple route is match', async () => {
274+
const rootSpan = tracer.startSpan('root span');
275+
const rpcMetadata: RPCMetadata = { type: RPCType.HTTP, span: rootSpan };
276+
app.use((req, res, next) => {
277+
return context.with(
278+
setRPCMetadata(
279+
trace.setSpan(context.active(), rootSpan),
280+
rpcMetadata
281+
),
282+
next
283+
);
284+
});
285+
286+
app.use('/foo', (req, res, next) => {
287+
next();
288+
});
289+
290+
app.use('/foo/bar', (req, res, next) => {
291+
next();
292+
});
293+
294+
await httpRequest.get(`http://localhost:${PORT}/foo/bar`);
295+
rootSpan.end();
296+
297+
assert.strictEqual(rpcMetadata.route, '/foo/bar');
298+
});
299+
300+
it('should use latest match route when multiple route is match (with nested app)', async () => {
301+
const rootSpan = tracer.startSpan('root span');
302+
const rpcMetadata: RPCMetadata = { type: RPCType.HTTP, span: rootSpan };
303+
app.use((req, res, next) => {
304+
return context.with(
305+
setRPCMetadata(
306+
trace.setSpan(context.active(), rootSpan),
307+
rpcMetadata
308+
),
309+
next
310+
);
311+
});
312+
313+
const nestedApp = connect();
314+
315+
app.use('/foo/', nestedApp);
316+
nestedApp.use('/bar/', (req, res, next) => {
317+
next();
318+
});
319+
320+
app.use('/foo/bar/test', (req, res, next) => {
321+
next();
322+
});
323+
324+
await httpRequest.get(`http://localhost:${PORT}/foo/bar/test`);
325+
rootSpan.end();
326+
327+
assert.strictEqual(rpcMetadata.route, '/foo/bar/test');
328+
});
246329
});
247330
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import * as assert from 'assert';
17+
18+
import { PatchedRequest, _LAYERS_STORE_PROPERTY } from '../src/internal-types';
19+
import {
20+
addNewStackLayer,
21+
generateRoute,
22+
replaceCurrentStackRoute,
23+
} from '../src/utils';
24+
25+
describe('utils', () => {
26+
describe('addNewStackLayer', () => {
27+
it('should inject new array to symbol property if not exist', () => {
28+
const fakeRequest = {} as PatchedRequest;
29+
30+
addNewStackLayer(fakeRequest);
31+
32+
assert.strictEqual(fakeRequest[_LAYERS_STORE_PROPERTY].length, 1);
33+
});
34+
35+
it('should append new stack item if private symbol already exists', () => {
36+
const stack = ['/first'];
37+
const fakeRequest = {
38+
[_LAYERS_STORE_PROPERTY]: stack,
39+
} as PatchedRequest;
40+
41+
addNewStackLayer(fakeRequest);
42+
43+
assert.equal(fakeRequest[_LAYERS_STORE_PROPERTY], stack);
44+
assert.strictEqual(fakeRequest[_LAYERS_STORE_PROPERTY].length, 2);
45+
});
46+
47+
it('should return pop method to remove newly add stack', () => {
48+
const fakeRequest = {} as PatchedRequest;
49+
50+
const pop = addNewStackLayer(fakeRequest);
51+
52+
assert.notStrictEqual(pop, undefined);
53+
54+
pop();
55+
56+
assert.strictEqual(fakeRequest[_LAYERS_STORE_PROPERTY].length, 0);
57+
});
58+
59+
it('should prevent pop the same stack item multiple time', () => {
60+
const fakeRequest = {} as PatchedRequest;
61+
62+
addNewStackLayer(fakeRequest); // add first stack item
63+
const pop = addNewStackLayer(fakeRequest); // add second stack item
64+
65+
pop();
66+
pop();
67+
68+
assert.strictEqual(fakeRequest[_LAYERS_STORE_PROPERTY].length, 1);
69+
});
70+
});
71+
72+
describe('replaceCurrentStackRoute', () => {
73+
it('should replace the last stack item with new value', () => {
74+
const fakeRequest = {
75+
[_LAYERS_STORE_PROPERTY]: ['/first', '/second'],
76+
} as PatchedRequest;
77+
78+
replaceCurrentStackRoute(fakeRequest, '/new_route');
79+
80+
assert.strictEqual(fakeRequest[_LAYERS_STORE_PROPERTY].length, 2);
81+
assert.strictEqual(fakeRequest[_LAYERS_STORE_PROPERTY][1], '/new_route');
82+
});
83+
});
84+
85+
describe('generateRoute', () => {
86+
it('should combine the stack and striped any slash between layer', () => {
87+
const fakeRequest = {
88+
[_LAYERS_STORE_PROPERTY]: ['/first/', '/second', '/third/'],
89+
} as PatchedRequest;
90+
91+
const route = generateRoute(fakeRequest);
92+
93+
assert.strictEqual(route, '/first/second/third/');
94+
});
95+
});
96+
});

0 commit comments

Comments
 (0)