Skip to content

Commit 842d167

Browse files
nicohrubecclaude
andauthored
ref(node): Vendor connect instrumentation (#20955)
Vendors `@opentelemetry/instrumentation-connect` into the SDK with no logic changes. Types from `@types/connect` are inlined to avoid requiring the package as a dependency. Closes #20148 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d114415 commit 842d167

8 files changed

Lines changed: 357 additions & 14 deletions

File tree

.oxlintrc.base.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@
146146
"**/integrations/tracing/genericPool/vendored/**/*.ts",
147147
"**/integrations/fs/vendored/**/*.ts",
148148
"**/integrations/tracing/knex/vendored/**/*.ts",
149-
"**/integrations/tracing/mongo/vendored/**/*.ts"
149+
"**/integrations/tracing/mongo/vendored/**/*.ts",
150+
"**/integrations/tracing/connect/vendored/**/*.ts"
150151
],
151152
"rules": {
152153
"typescript/no-explicit-any": "off"

packages/node/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@
6969
"@opentelemetry/core": "^2.6.1",
7070
"@opentelemetry/instrumentation": "^0.214.0",
7171
"@opentelemetry/instrumentation-amqplib": "0.61.0",
72-
"@opentelemetry/instrumentation-connect": "0.57.0",
7372
"@opentelemetry/instrumentation-graphql": "0.62.0",
7473
"@opentelemetry/instrumentation-hapi": "0.60.0",
7574
"@opentelemetry/instrumentation-http": "0.214.0",

packages/node/src/integrations/tracing/connect.ts renamed to packages/node/src/integrations/tracing/connect/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ConnectInstrumentation } from '@opentelemetry/instrumentation-connect';
1+
import { ConnectInstrumentation } from './vendored/instrumentation';
22
import type { IntegrationFn, Span } from '@sentry/core';
33
import {
44
captureException,
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
* NOTICE from the Sentry authors:
17+
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-connect
18+
* - Upstream version: @opentelemetry/instrumentation-connect@0.61.0
19+
*/
20+
/* eslint-disable */
21+
22+
export enum AttributeNames {
23+
CONNECT_TYPE = 'connect.type',
24+
CONNECT_NAME = 'connect.name',
25+
}
26+
27+
export enum ConnectTypes {
28+
MIDDLEWARE = 'middleware',
29+
REQUEST_HANDLER = 'request_handler',
30+
}
31+
32+
export enum ConnectNames {
33+
MIDDLEWARE = 'middleware',
34+
REQUEST_HANDLER = 'request handler',
35+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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+
* NOTICE from the Sentry authors:
17+
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-connect
18+
* - Upstream version: @opentelemetry/instrumentation-connect@0.61.0
19+
* - Minor TypeScript strictness adjustments for this repository's compiler settings
20+
*/
21+
/* eslint-disable */
22+
23+
import { context, Span, SpanOptions } from '@opentelemetry/api';
24+
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
25+
import type { ServerResponse } from 'http';
26+
import { AttributeNames, ConnectNames, ConnectTypes } from './enums/AttributeNames';
27+
import { HandleFunction, NextFunction, Server, PatchedRequest, Use, UseArgs, UseArgs2 } from './internal-types';
28+
import { SDK_VERSION } from '@sentry/core';
29+
import {
30+
InstrumentationBase,
31+
InstrumentationConfig,
32+
InstrumentationNodeModuleDefinition,
33+
isWrapped,
34+
} from '@opentelemetry/instrumentation';
35+
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
36+
import { replaceCurrentStackRoute, addNewStackLayer, generateRoute } from './utils';
37+
38+
const PACKAGE_NAME = '@sentry/instrumentation-connect';
39+
40+
export const ANONYMOUS_NAME = 'anonymous';
41+
42+
/** Connect instrumentation for OpenTelemetry */
43+
export class ConnectInstrumentation extends InstrumentationBase {
44+
constructor(config: InstrumentationConfig = {}) {
45+
super(PACKAGE_NAME, SDK_VERSION, config);
46+
}
47+
48+
init() {
49+
return [
50+
new InstrumentationNodeModuleDefinition('connect', ['>=3.0.0 <4'], moduleExports => {
51+
return this._patchConstructor(moduleExports);
52+
}),
53+
];
54+
}
55+
56+
private _patchApp(patchedApp: Server) {
57+
if (!isWrapped(patchedApp.use)) {
58+
this._wrap(patchedApp, 'use', this._patchUse.bind(this));
59+
}
60+
if (!isWrapped(patchedApp.handle)) {
61+
this._wrap(patchedApp, 'handle', this._patchHandle.bind(this));
62+
}
63+
}
64+
65+
private _patchConstructor(original: () => Server): () => Server {
66+
const instrumentation = this;
67+
return function (this: Server, ...args: any[]) {
68+
const app = original.apply(this, args) as Server;
69+
instrumentation._patchApp(app);
70+
return app;
71+
};
72+
}
73+
74+
public _patchNext(next: NextFunction, finishSpan: () => void): NextFunction {
75+
return function nextFunction(this: NextFunction, err?: any): void {
76+
const result = next.apply(this, [err]);
77+
finishSpan();
78+
return result;
79+
};
80+
}
81+
82+
public _startSpan(routeName: string, middleWare: HandleFunction): Span {
83+
let connectType: ConnectTypes;
84+
let connectName: string;
85+
let connectTypeName: string;
86+
if (routeName) {
87+
connectType = ConnectTypes.REQUEST_HANDLER;
88+
connectTypeName = ConnectNames.REQUEST_HANDLER;
89+
connectName = routeName;
90+
} else {
91+
connectType = ConnectTypes.MIDDLEWARE;
92+
connectTypeName = ConnectNames.MIDDLEWARE;
93+
connectName = middleWare.name || ANONYMOUS_NAME;
94+
}
95+
const spanName = `${connectTypeName} - ${connectName}`;
96+
const options: SpanOptions = {
97+
attributes: {
98+
[ATTR_HTTP_ROUTE]: routeName.length > 0 ? routeName : '/',
99+
[AttributeNames.CONNECT_TYPE]: connectType,
100+
[AttributeNames.CONNECT_NAME]: connectName,
101+
},
102+
};
103+
104+
return this.tracer.startSpan(spanName, options);
105+
}
106+
107+
public _patchMiddleware(routeName: string, middleWare: HandleFunction): HandleFunction {
108+
const instrumentation = this;
109+
const isErrorMiddleware = middleWare.length === 4;
110+
111+
function patchedMiddleware(this: Use): void {
112+
if (!instrumentation.isEnabled()) {
113+
return (middleWare as any).apply(this, arguments);
114+
}
115+
const [reqArgIdx, resArgIdx, nextArgIdx] = isErrorMiddleware ? [1, 2, 3] : [0, 1, 2];
116+
const req = arguments[reqArgIdx] as PatchedRequest;
117+
const res = arguments[resArgIdx] as ServerResponse;
118+
const next = arguments[nextArgIdx] as NextFunction;
119+
120+
replaceCurrentStackRoute(req, routeName);
121+
122+
const rpcMetadata = getRPCMetadata(context.active());
123+
if (routeName && rpcMetadata?.type === RPCType.HTTP) {
124+
rpcMetadata.route = generateRoute(req);
125+
}
126+
127+
let spanName = '';
128+
if (routeName) {
129+
spanName = `request handler - ${routeName}`;
130+
} else {
131+
spanName = `middleware - ${middleWare.name || ANONYMOUS_NAME}`;
132+
}
133+
const span = instrumentation._startSpan(routeName, middleWare);
134+
instrumentation._diag.debug('start span', spanName);
135+
let spanFinished = false;
136+
137+
function finishSpan() {
138+
if (!spanFinished) {
139+
spanFinished = true;
140+
instrumentation._diag.debug(`finishing span ${(span as any).name}`);
141+
span.end();
142+
} else {
143+
instrumentation._diag.debug(`span ${(span as any).name} - already finished`);
144+
}
145+
res.removeListener('close', finishSpan);
146+
}
147+
148+
res.addListener('close', finishSpan);
149+
arguments[nextArgIdx] = instrumentation._patchNext(next, finishSpan);
150+
151+
return (middleWare as any).apply(this, arguments);
152+
}
153+
154+
Object.defineProperty(patchedMiddleware, 'length', {
155+
value: middleWare.length,
156+
writable: false,
157+
configurable: true,
158+
});
159+
160+
return patchedMiddleware;
161+
}
162+
163+
public _patchUse(original: Server['use']): Use {
164+
const instrumentation = this;
165+
return function (this: Server, ...args: UseArgs): Server {
166+
const middleWare = args[args.length - 1] as HandleFunction;
167+
const routeName = (args[args.length - 2] || '') as string;
168+
169+
args[args.length - 1] = instrumentation._patchMiddleware(routeName, middleWare);
170+
171+
return original.apply(this, args as UseArgs2);
172+
};
173+
}
174+
175+
public _patchHandle(original: Server['handle']): Server['handle'] {
176+
const instrumentation = this;
177+
return function (this: Server): ReturnType<Server['handle']> {
178+
const [reqIdx, outIdx] = [0, 2];
179+
const req = arguments[reqIdx] as PatchedRequest;
180+
const out = arguments[outIdx];
181+
const completeStack = addNewStackLayer(req);
182+
183+
if (typeof out === 'function') {
184+
arguments[outIdx] = instrumentation._patchOut(out as NextFunction, completeStack);
185+
}
186+
187+
return (original as any).apply(this, arguments);
188+
};
189+
}
190+
191+
public _patchOut(out: NextFunction, completeStack: () => void): NextFunction {
192+
return function nextFunction(this: NextFunction, ...args: any[]): void {
193+
completeStack();
194+
return Reflect.apply(out, this, args);
195+
};
196+
}
197+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
* NOTICE from the Sentry authors:
17+
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-connect
18+
* - Upstream version: @opentelemetry/instrumentation-connect@0.61.0
19+
* - Some types vendored from @types/connect
20+
*/
21+
/* eslint-disable */
22+
23+
import type * as http from 'http';
24+
25+
export type IncomingMessage = http.IncomingMessage & {
26+
originalUrl?: http.IncomingMessage['url'] | undefined;
27+
};
28+
29+
export type NextFunction = (err?: any) => void;
30+
31+
export type SimpleHandleFunction = (req: IncomingMessage, res: http.ServerResponse) => void;
32+
export type NextHandleFunction = (req: IncomingMessage, res: http.ServerResponse, next: NextFunction) => void;
33+
export type ErrorHandleFunction = (
34+
err: any,
35+
req: IncomingMessage,
36+
res: http.ServerResponse,
37+
next: NextFunction,
38+
) => void;
39+
export type HandleFunction = SimpleHandleFunction | NextHandleFunction | ErrorHandleFunction;
40+
41+
export interface Server extends NodeJS.EventEmitter {
42+
(req: http.IncomingMessage, res: http.ServerResponse, next?: Function): void;
43+
44+
route: string;
45+
stack: Array<{ route: string; handle: HandleFunction | http.Server }>;
46+
47+
use(fn: NextHandleFunction): Server;
48+
use(fn: HandleFunction): Server;
49+
use(route: string, fn: NextHandleFunction): Server;
50+
use(route: string, fn: HandleFunction): Server;
51+
52+
handle(req: http.IncomingMessage, res: http.ServerResponse, next: Function): void;
53+
}
54+
55+
export const _LAYERS_STORE_PROPERTY: unique symbol = Symbol(
56+
'opentelemetry.instrumentation-connect.request-route-stack',
57+
);
58+
59+
export type UseArgs1 = [HandleFunction];
60+
export type UseArgs2 = [string, HandleFunction];
61+
export type UseArgs = UseArgs1 | UseArgs2;
62+
export type Use = (...args: UseArgs) => Server;
63+
export type PatchedRequest = {
64+
[_LAYERS_STORE_PROPERTY]: string[];
65+
} & IncomingMessage;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
* NOTICE from the Sentry authors:
17+
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-connect
18+
* - Upstream version: @opentelemetry/instrumentation-connect@0.61.0
19+
*/
20+
/* eslint-disable */
21+
22+
import { diag } from '@opentelemetry/api';
23+
import { _LAYERS_STORE_PROPERTY, PatchedRequest } from './internal-types';
24+
25+
export const addNewStackLayer = (request: PatchedRequest) => {
26+
if (Array.isArray(request[_LAYERS_STORE_PROPERTY]) === false) {
27+
Object.defineProperty(request, _LAYERS_STORE_PROPERTY, {
28+
enumerable: false,
29+
value: [],
30+
});
31+
}
32+
request[_LAYERS_STORE_PROPERTY].push('/');
33+
34+
const stackLength = request[_LAYERS_STORE_PROPERTY].length;
35+
36+
return () => {
37+
if (stackLength === request[_LAYERS_STORE_PROPERTY].length) {
38+
request[_LAYERS_STORE_PROPERTY].pop();
39+
} else {
40+
diag.warn('Connect: Trying to pop the stack multiple time');
41+
}
42+
};
43+
};
44+
45+
export const replaceCurrentStackRoute = (request: PatchedRequest, newRoute?: string) => {
46+
if (newRoute) {
47+
request[_LAYERS_STORE_PROPERTY].splice(-1, 1, newRoute);
48+
}
49+
};
50+
51+
// generate route from existing stack on request object.
52+
// splash between stack layer will be deduped
53+
// ["/first/", "/second", "/third/"] => /first/second/third/
54+
export const generateRoute = (request: PatchedRequest) => {
55+
return request[_LAYERS_STORE_PROPERTY].reduce((acc, sub) => acc.replace(/\/+$/, '') + sub);
56+
};

0 commit comments

Comments
 (0)