Skip to content

Commit 6d00aba

Browse files
nicohrubecclaude
andauthored
ref(node): Vendor tedious instrumentation (#21010)
Vendors @opentelemetry/instrumentation-tedious into the SDK with no logic changes. Types from tedious are inlined as simplified interfaces to avoid requiring the package as a dependency. Closes #20163 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 11839bd commit 6d00aba

9 files changed

Lines changed: 544 additions & 19 deletions

File tree

.oxlintrc.base.json

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

packages/node/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@
7878
"@opentelemetry/instrumentation-mysql": "0.60.0",
7979
"@opentelemetry/instrumentation-mysql2": "0.60.0",
8080
"@opentelemetry/instrumentation-pg": "0.66.0",
81-
"@opentelemetry/instrumentation-tedious": "0.33.0",
8281
"@opentelemetry/sdk-trace-base": "^2.6.1",
8382
"@opentelemetry/semantic-conventions": "^1.40.0",
8483
"@prisma/instrumentation": "7.6.0",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { TediousInstrumentation } from '@opentelemetry/instrumentation-tedious';
1+
import { TediousInstrumentation } from './vendored/instrumentation';
22
import type { IntegrationFn } from '@sentry/core';
33
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core';
44
import { generateInstrumentOnce, instrumentWhenWrapped } from '@sentry/node-core';
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
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-tedious
18+
* - Upstream version: @opentelemetry/instrumentation-tedious@0.37.0
19+
* - Minor TypeScript strictness adjustments
20+
*/
21+
/* eslint-disable */
22+
23+
import * as api from '@opentelemetry/api';
24+
import { EventEmitter } from 'events';
25+
import {
26+
InstrumentationBase,
27+
InstrumentationNodeModuleDefinition,
28+
isWrapped,
29+
SemconvStability,
30+
semconvStabilityFromStr,
31+
} from '@opentelemetry/instrumentation';
32+
import {
33+
ATTR_DB_COLLECTION_NAME,
34+
ATTR_DB_NAMESPACE,
35+
ATTR_DB_QUERY_TEXT,
36+
ATTR_DB_SYSTEM_NAME,
37+
ATTR_SERVER_ADDRESS,
38+
ATTR_SERVER_PORT,
39+
DB_SYSTEM_NAME_VALUE_MICROSOFT_SQL_SERVER,
40+
} from '@opentelemetry/semantic-conventions';
41+
import {
42+
DB_SYSTEM_VALUE_MSSQL,
43+
ATTR_DB_NAME,
44+
ATTR_DB_SQL_TABLE,
45+
ATTR_DB_STATEMENT,
46+
ATTR_DB_SYSTEM,
47+
ATTR_DB_USER,
48+
ATTR_NET_PEER_NAME,
49+
ATTR_NET_PEER_PORT,
50+
} from './semconv';
51+
import type * as tedious from './tedious-types';
52+
import { TediousInstrumentationConfig } from './types';
53+
import { getSpanName, once } from './utils';
54+
import { SDK_VERSION } from '@sentry/core';
55+
56+
const PACKAGE_NAME = '@sentry/instrumentation-tedious';
57+
58+
const CURRENT_DATABASE = Symbol('opentelemetry.instrumentation-tedious.current-database');
59+
60+
export const INJECTED_CTX = Symbol('opentelemetry.instrumentation-tedious.context-info-injected');
61+
62+
const PATCHED_METHODS = ['callProcedure', 'execSql', 'execSqlBatch', 'execBulkLoad', 'prepare', 'execute'];
63+
64+
type UnknownFunction = (...args: any[]) => any;
65+
type ApproxConnection = EventEmitter & {
66+
[CURRENT_DATABASE]: string;
67+
config: any;
68+
};
69+
type ApproxRequest = EventEmitter & {
70+
sqlTextOrProcedure: string | undefined;
71+
callback: any;
72+
table: string | undefined;
73+
parametersByName: any;
74+
};
75+
76+
function setDatabase(this: ApproxConnection, databaseName: string) {
77+
Object.defineProperty(this, CURRENT_DATABASE, {
78+
value: databaseName,
79+
writable: true,
80+
});
81+
}
82+
83+
export class TediousInstrumentation extends InstrumentationBase<TediousInstrumentationConfig> {
84+
static readonly COMPONENT = 'tedious';
85+
private _netSemconvStability!: SemconvStability;
86+
private _dbSemconvStability!: SemconvStability;
87+
88+
constructor(config: TediousInstrumentationConfig = {}) {
89+
super(PACKAGE_NAME, SDK_VERSION, config);
90+
this._setSemconvStabilityFromEnv();
91+
}
92+
93+
// Used for testing.
94+
private _setSemconvStabilityFromEnv() {
95+
this._netSemconvStability = semconvStabilityFromStr('http', process.env.OTEL_SEMCONV_STABILITY_OPT_IN);
96+
this._dbSemconvStability = semconvStabilityFromStr('database', process.env.OTEL_SEMCONV_STABILITY_OPT_IN);
97+
}
98+
99+
protected init() {
100+
return [
101+
new InstrumentationNodeModuleDefinition(
102+
TediousInstrumentation.COMPONENT,
103+
['>=1.11.0 <20'],
104+
(moduleExports: typeof tedious) => {
105+
const ConnectionPrototype: any = moduleExports.Connection.prototype;
106+
for (const method of PATCHED_METHODS) {
107+
if (isWrapped(ConnectionPrototype[method])) {
108+
this._unwrap(ConnectionPrototype, method);
109+
}
110+
this._wrap(ConnectionPrototype, method, this._patchQuery(method, moduleExports) as any);
111+
}
112+
113+
if (isWrapped(ConnectionPrototype.connect)) {
114+
this._unwrap(ConnectionPrototype, 'connect');
115+
}
116+
this._wrap(ConnectionPrototype, 'connect', this._patchConnect);
117+
118+
return moduleExports;
119+
},
120+
(moduleExports: typeof tedious) => {
121+
if (moduleExports === undefined) return;
122+
const ConnectionPrototype: any = moduleExports.Connection.prototype;
123+
for (const method of PATCHED_METHODS) {
124+
this._unwrap(ConnectionPrototype, method);
125+
}
126+
this._unwrap(ConnectionPrototype, 'connect');
127+
},
128+
),
129+
];
130+
}
131+
132+
private _patchConnect(original: UnknownFunction): UnknownFunction {
133+
return function patchedConnect(this: ApproxConnection) {
134+
setDatabase.call(this, this.config?.options?.database);
135+
136+
// remove the listener first in case it's already added
137+
this.removeListener('databaseChange', setDatabase);
138+
this.on('databaseChange', setDatabase);
139+
140+
this.once('end', () => {
141+
this.removeListener('databaseChange', setDatabase);
142+
});
143+
return original.apply(this, arguments as unknown as any[]);
144+
};
145+
}
146+
147+
private _buildTraceparent(span: api.Span): string {
148+
const sc = span.spanContext();
149+
return `00-${sc.traceId}-${sc.spanId}-0${Number(sc.traceFlags || api.TraceFlags.NONE).toString(16)}`;
150+
}
151+
152+
/**
153+
* Fire a one-off `SET CONTEXT_INFO @opentelemetry_traceparent` on the same
154+
* connection. Marks the request with INJECTED_CTX so our patch skips it.
155+
*/
156+
private _injectContextInfo(connection: any, tediousModule: typeof tedious, traceparent: string): Promise<void> {
157+
return new Promise(resolve => {
158+
try {
159+
const sql = 'set context_info @opentelemetry_traceparent';
160+
const req = new tediousModule.Request(sql, (_err: any) => {
161+
resolve();
162+
});
163+
Object.defineProperty(req, INJECTED_CTX, { value: true });
164+
const buf = Buffer.from(traceparent, 'utf8');
165+
req.addParameter('opentelemetry_traceparent', (tediousModule as any).TYPES.VarBinary, buf, {
166+
length: buf.length,
167+
});
168+
169+
connection.execSql(req);
170+
} catch {
171+
resolve();
172+
}
173+
});
174+
}
175+
176+
private _shouldInjectFor(operation: string): boolean {
177+
return (
178+
operation === 'execSql' ||
179+
operation === 'execSqlBatch' ||
180+
operation === 'callProcedure' ||
181+
operation === 'execute'
182+
);
183+
}
184+
185+
private _patchQuery(operation: string, tediousModule: typeof tedious) {
186+
return (originalMethod: UnknownFunction): UnknownFunction => {
187+
const thisPlugin = this;
188+
189+
function patchedMethod(this: ApproxConnection, request: ApproxRequest) {
190+
// Skip our own injected request
191+
if ((request as any)?.[INJECTED_CTX]) {
192+
return originalMethod.apply(this, arguments as unknown as any[]);
193+
}
194+
195+
if (!(request instanceof EventEmitter)) {
196+
thisPlugin._diag.warn(`Unexpected invocation of patched ${operation} method. Span not recorded`);
197+
return originalMethod.apply(this, arguments as unknown as any[]);
198+
}
199+
let procCount = 0;
200+
let statementCount = 0;
201+
const incrementStatementCount = () => statementCount++;
202+
const incrementProcCount = () => procCount++;
203+
const databaseName = this[CURRENT_DATABASE];
204+
const sql = (request => {
205+
// Required for <11.0.9
206+
if (request.sqlTextOrProcedure === 'sp_prepare' && request.parametersByName?.stmt?.value) {
207+
return request.parametersByName.stmt.value;
208+
}
209+
return request.sqlTextOrProcedure;
210+
})(request);
211+
212+
const attributes: api.Attributes = {};
213+
if (thisPlugin._dbSemconvStability & SemconvStability.OLD) {
214+
attributes[ATTR_DB_SYSTEM] = DB_SYSTEM_VALUE_MSSQL;
215+
attributes[ATTR_DB_NAME] = databaseName;
216+
// >=4 uses `authentication` object; older versions just userName and password pair
217+
attributes[ATTR_DB_USER] = this.config?.userName ?? this.config?.authentication?.options?.userName;
218+
attributes[ATTR_DB_STATEMENT] = sql;
219+
attributes[ATTR_DB_SQL_TABLE] = request.table;
220+
}
221+
if (thisPlugin._dbSemconvStability & SemconvStability.STABLE) {
222+
// The OTel spec for "db.namespace" discusses handling for connection
223+
// to MSSQL "named instances". This isn't currently supported.
224+
// https://opentelemetry.io/docs/specs/semconv/database/sql-server/#:~:text=%5B1%5D%20db%2Enamespace
225+
attributes[ATTR_DB_NAMESPACE] = databaseName;
226+
attributes[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_MICROSOFT_SQL_SERVER;
227+
attributes[ATTR_DB_QUERY_TEXT] = sql;
228+
attributes[ATTR_DB_COLLECTION_NAME] = request.table;
229+
// See https://opentelemetry.io/docs/specs/semconv/database/sql-server/#spans
230+
// TODO(3290): can `db.response.status_code` be added?
231+
// TODO(3290): is `operation` correct for `db.operation.name`
232+
// TODO(3290): can `db.query.summary` reliably be calculated?
233+
// TODO(3290): `db.stored_procedure.name`
234+
}
235+
if (thisPlugin._netSemconvStability & SemconvStability.OLD) {
236+
attributes[ATTR_NET_PEER_NAME] = this.config?.server;
237+
attributes[ATTR_NET_PEER_PORT] = this.config?.options?.port;
238+
}
239+
if (thisPlugin._netSemconvStability & SemconvStability.STABLE) {
240+
attributes[ATTR_SERVER_ADDRESS] = this.config?.server;
241+
attributes[ATTR_SERVER_PORT] = this.config?.options?.port;
242+
}
243+
const span = thisPlugin.tracer.startSpan(getSpanName(operation, databaseName, sql, request.table), {
244+
kind: api.SpanKind.CLIENT,
245+
attributes,
246+
});
247+
248+
const endSpan = once((err?: any) => {
249+
request.removeListener('done', incrementStatementCount);
250+
request.removeListener('doneInProc', incrementStatementCount);
251+
request.removeListener('doneProc', incrementProcCount);
252+
request.removeListener('error', endSpan);
253+
this.removeListener('end', endSpan);
254+
255+
span.setAttribute('tedious.procedure_count', procCount);
256+
span.setAttribute('tedious.statement_count', statementCount);
257+
if (err) {
258+
span.setStatus({
259+
code: api.SpanStatusCode.ERROR,
260+
message: err.message,
261+
});
262+
// TODO(3290): set `error.type` attribute?
263+
}
264+
span.end();
265+
});
266+
267+
request.on('done', incrementStatementCount);
268+
request.on('doneInProc', incrementStatementCount);
269+
request.on('doneProc', incrementProcCount);
270+
request.once('error', endSpan);
271+
this.on('end', endSpan);
272+
273+
if (typeof request.callback === 'function') {
274+
thisPlugin._wrap(request, 'callback', thisPlugin._patchCallbackQuery(endSpan));
275+
} else {
276+
thisPlugin._diag.error('Expected request.callback to be a function');
277+
}
278+
279+
const runUserRequest = () => {
280+
return api.context.with(api.trace.setSpan(api.context.active(), span), originalMethod, this, ...arguments);
281+
};
282+
283+
const cfg = thisPlugin.getConfig();
284+
const shouldInject = cfg.enableTraceContextPropagation && thisPlugin._shouldInjectFor(operation);
285+
286+
if (!shouldInject) return runUserRequest();
287+
288+
const traceparent = thisPlugin._buildTraceparent(span);
289+
290+
void thisPlugin._injectContextInfo(this, tediousModule, traceparent).finally(runUserRequest);
291+
}
292+
293+
Object.defineProperty(patchedMethod, 'length', {
294+
value: originalMethod.length,
295+
writable: false,
296+
});
297+
298+
return patchedMethod;
299+
};
300+
}
301+
302+
private _patchCallbackQuery(endSpan: Function) {
303+
return (originalCallback: Function) => {
304+
return function (this: any, err: Error | undefined | null, rowCount?: number, rows?: any) {
305+
endSpan(err);
306+
return originalCallback.apply(this, arguments);
307+
};
308+
};
309+
}
310+
}

0 commit comments

Comments
 (0)