From c93bd3af909e2d61c944aee538c10b2cd717c8f7 Mon Sep 17 00:00:00 2001 From: Marten Hennoch Date: Wed, 24 Sep 2025 14:12:25 +0300 Subject: [PATCH 1/3] Add traceparent to CONTEXT_INFO --- .../src/instrumentation.ts | 97 +++++++++++++++++-- packages/instrumentation-tedious/src/types.ts | 11 ++- packages/instrumentation-tedious/test/api.ts | 13 ++- .../test/instrumentation.test.ts | 73 +++++++++++++- 4 files changed, 178 insertions(+), 16 deletions(-) diff --git a/packages/instrumentation-tedious/src/instrumentation.ts b/packages/instrumentation-tedious/src/instrumentation.ts index adc328b8ed..5ba82a8624 100644 --- a/packages/instrumentation-tedious/src/instrumentation.ts +++ b/packages/instrumentation-tedious/src/instrumentation.ts @@ -40,6 +40,11 @@ import { PACKAGE_NAME, PACKAGE_VERSION } from './version'; const CURRENT_DATABASE = Symbol( 'opentelemetry.instrumentation-tedious.current-database' ); + +export const INJECTED_CTX = Symbol( + 'opentelemetry.instrumentation-tedious.context-info-injected' +); + const PATCHED_METHODS = [ 'callProcedure', 'execSql', @@ -89,7 +94,7 @@ export class TediousInstrumentation extends InstrumentationBase { + return new Promise((resolve) => { + try { + const sql = 'set context_info @opentelemetry_traceparent'; + const req = new tediousModule.Request(sql, (_err: any) => { + resolve(); + }); + Object.defineProperty(req, INJECTED_CTX, { value: true }); + const buf = Buffer.from(traceparent, 'utf8'); + req.addParameter( + 'opentelemetry_traceparent', + (tediousModule as any).TYPES.VarBinary, + buf, + { length: buf.length } + ); + + connection.execSql(req); + } catch { + resolve(); + } + }); + } + + private _shouldInjectFor(operation: string): boolean { + return ( + operation === 'execSql' || + operation === 'execSqlBatch' || + operation === 'callProcedure' || + operation === 'execute' + ); + } + + private _patchQuery(operation: string, tediousModule: typeof tedious) { return (originalMethod: UnknownFunction): UnknownFunction => { const thisPlugin = this; function patchedMethod(this: ApproxConnection, request: ApproxRequest) { + // Skip our own injected request + if ((request as any)?.[INJECTED_CTX]) { + return originalMethod.apply(this, arguments as unknown as any[]); + } + if (!(request instanceof EventEmitter)) { thisPlugin._diag.warn( `Unexpected invocation of patched ${operation} method. Span not recorded` @@ -207,12 +266,34 @@ export class TediousInstrumentation extends InstrumentationBase { + return api.context.with( + api.trace.setSpan(api.context.active(), span), + originalMethod, + this, + ...arguments + ); + }; + + const cfg = thisPlugin.getConfig?.() as TediousInstrumentationConfig | undefined; + const shouldInject = + cfg?.enableTraceContextPropagation && thisPlugin._shouldInjectFor(operation); + + if (shouldInject) { + try { + const traceparent = thisPlugin._buildTraceparent(span); + // Include overhead in the span by injecting first, then running the user request. + thisPlugin + ._injectContextInfo(this, tediousModule, traceparent) + .then(runUserRequest) + .catch(() => runUserRequest()); + return; + } catch (e: any) { + return runUserRequest(); + } + } + + return runUserRequest(); } Object.defineProperty(patchedMethod, 'length', { diff --git a/packages/instrumentation-tedious/src/types.ts b/packages/instrumentation-tedious/src/types.ts index 3342997061..ee50f0fb97 100644 --- a/packages/instrumentation-tedious/src/types.ts +++ b/packages/instrumentation-tedious/src/types.ts @@ -14,6 +14,13 @@ * limitations under the License. */ -import { InstrumentationConfig } from '@opentelemetry/instrumentation'; -export type TediousInstrumentationConfig = InstrumentationConfig; +import { InstrumentationConfig } from '@opentelemetry/instrumentation'; +export interface TediousInstrumentationConfig extends InstrumentationConfig { + /** + * If true, injects the current DB span's W3C traceparent into SQL Server + * session state via `SET CONTEXT_INFO @opentelemetry_traceparent` (varbinary). + * Off by default to avoid the extra round-trip per request. + */ + enableTraceContextPropagation?: boolean; +} diff --git a/packages/instrumentation-tedious/test/api.ts b/packages/instrumentation-tedious/test/api.ts index f4e356783f..0c4ae8ebed 100644 --- a/packages/instrumentation-tedious/test/api.ts +++ b/packages/instrumentation-tedious/test/api.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import { promisify } from 'util'; import type { Connection, Request, TYPES } from 'tedious'; - +import { INJECTED_CTX } from '../src/instrumentation'; type Method = keyof Connection & ('execSql' | 'execSqlBatch' | 'prepare'); export type tedious = { Connection: typeof Connection; @@ -67,7 +67,8 @@ export const makeApi = (tedious: tedious) => { const query = ( connection: Connection, params: string, - method: Method = 'execSql' + method: Method = 'execSql', + noTracking?: boolean, ): Promise => { return new Promise((resolve, reject) => { const result: any[] = []; @@ -78,7 +79,9 @@ export const makeApi = (tedious: tedious) => { resolve(result); } }); - + if (noTracking) { + Object.defineProperty(request, INJECTED_CTX, { value: true }); + } // request.on('returnValue', console.log.bind(console, /*request.sqlTextOrProcedure,*/ 'returnValue:')); // request.on('error', console.log.bind(console, /*request.sqlTextOrProcedure,*/ 'error:')); // request.on('row', console.log.bind(console, /*request.sqlTextOrProcedure,*/ 'row:')); @@ -314,7 +317,9 @@ export const makeApi = (tedious: tedious) => { if exists(SELECT * FROM sysobjects WHERE name='test_transact' AND xtype='U') DROP TABLE ${transaction.tableName}; if exists(SELECT * FROM sysobjects WHERE name='test_proced' AND xtype='U') DROP PROCEDURE ${storedProcedure.procedureName}; if exists(SELECT * FROM sys.databases WHERE name = 'temp_otel_db') DROP DATABASE temp_otel_db; - `.trim() + `.trim(), + 'execSql', + true ); }; diff --git a/packages/instrumentation-tedious/test/instrumentation.test.ts b/packages/instrumentation-tedious/test/instrumentation.test.ts index 46eed76563..78871df035 100644 --- a/packages/instrumentation-tedious/test/instrumentation.test.ts +++ b/packages/instrumentation-tedious/test/instrumentation.test.ts @@ -47,8 +47,6 @@ const user = process.env.MSSQL_USER || 'sa'; const password = process.env.MSSQL_PASSWORD || 'mssql_passw0rd'; const instrumentation = new TediousInstrumentation(); -instrumentation.enable(); -instrumentation.disable(); const config: any = { userName: user, @@ -308,6 +306,77 @@ describe('tedious', () => { table: 'test_bulk', }); }); + + describe('trace context propagation via CONTEXT_INFO', () => { + + function traceparentFromSpan(span: ReadableSpan) { + const sc = span.spanContext(); + const flags = (sc.traceFlags & 0x01) ? '01' : '00'; + return `00-${sc.traceId}-${sc.spanId}-${flags}`; + } + + function findDbSpan(spans: ReadableSpan[], startsWith: string) { + return spans.find( + s => + s.kind === SpanKind.CLIENT && + s.attributes[SEMATTRS_DB_SYSTEM] === DBSYSTEMVALUES_MSSQL && + s.name.startsWith(startsWith) + )!; + } + + beforeEach(() => { + instrumentation.setConfig({ + enableTraceContextPropagation: true, + }); + }); + + after(() => { + instrumentation.setConfig({enableTraceContextPropagation: false}); + }); + + it('injects DB-span traceparent for execSql', async function () { + const sql = "SELECT REPLACE(CONVERT(varchar(128), CONTEXT_INFO()), CHAR(0), '') AS traceparent"; + const result = await tedious.query(connection, sql) + + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + const dbSpan = findDbSpan(spans, 'execSql'); + const expectedTp = traceparentFromSpan(dbSpan); + assert.strictEqual(result[0], expectedTp, 'CONTEXT_INFO traceparent should match DB span'); + }); + + it('injects for execSqlBatch', async function () { + const batch = ` + SELECT REPLACE(CONVERT(varchar(128), CONTEXT_INFO()), CHAR(0), '') AS tp; + SELECT 42; + `; + const result = await tedious.query(connection, batch, 'execSqlBatch'); + + assert.deepStrictEqual(result, [result[0], 42]); + + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + const dbSpan = findDbSpan(spans, 'execSqlBatch'); + const expectedTp = traceparentFromSpan(dbSpan); + + assert.strictEqual(result[0], expectedTp); + }); + + it('when disabled, CONTEXT_INFO stays empty', async function () { + instrumentation.setConfig({ + enableTraceContextPropagation: false, + }); + + const [val] = await tedious.query( + connection, + "SELECT REPLACE(CONVERT(varchar(128), CONTEXT_INFO()), CHAR(0), '')" + ); + assert.strictEqual(val, null); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + }); + + }); }); const assertMatch = (actual: string | undefined, expected: RegExp) => { From b4f5f13309c9bbad949fbace73a48ce2988778ec Mon Sep 17 00:00:00 2001 From: Marten Hennoch Date: Thu, 2 Oct 2025 17:40:21 +0300 Subject: [PATCH 2/3] Don't return a promise --- packages/instrumentation-tedious/README.md | 7 +++++ .../src/instrumentation.ts | 30 ++++++++----------- packages/instrumentation-tedious/src/types.ts | 1 - packages/instrumentation-tedious/test/api.ts | 2 +- .../test/instrumentation.test.ts | 17 ++++++----- 5 files changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/instrumentation-tedious/README.md b/packages/instrumentation-tedious/README.md index 740b4da52f..03e12a7791 100644 --- a/packages/instrumentation-tedious/README.md +++ b/packages/instrumentation-tedious/README.md @@ -56,6 +56,13 @@ Attributes collected: | `net.peer.name` | Remote hostname or similar. | | `net.peer.port` | Remote port number. | +### Trace Context Propagation + +Database trace context propagation can be enabled by setting `enableTraceContextPropagation`to `true`. +This uses the [SET CONTEXT_INFO](https://learn.microsoft.com/en-us/sql/t-sql/statements/set-context-info-transact-sql?view=sql-server-ver16) +command to set [traceparent](https://www.w3.org/TR/trace-context/#traceparent-header)information +for the current connection, which results in **an additional round-trip to the database**. + ## Useful links - For more information on OpenTelemetry, visit: diff --git a/packages/instrumentation-tedious/src/instrumentation.ts b/packages/instrumentation-tedious/src/instrumentation.ts index 5ba82a8624..8955a9a8cc 100644 --- a/packages/instrumentation-tedious/src/instrumentation.ts +++ b/packages/instrumentation-tedious/src/instrumentation.ts @@ -150,7 +150,7 @@ export class TediousInstrumentation extends InstrumentationBase { - return new Promise((resolve) => { + return new Promise(resolve => { try { const sql = 'set context_info @opentelemetry_traceparent'; const req = new tediousModule.Request(sql, (_err: any) => { @@ -275,25 +275,19 @@ export class TediousInstrumentation extends InstrumentationBase runUserRequest()); - return; - } catch (e: any) { - return runUserRequest(); - } - } + cfg.enableTraceContextPropagation && + thisPlugin._shouldInjectFor(operation); + + if (!shouldInject) return runUserRequest(); + + const traceparent = thisPlugin._buildTraceparent(span); + thisPlugin + ._injectContextInfo(this, tediousModule, traceparent) + .finally(runUserRequest); - return runUserRequest(); + return; } Object.defineProperty(patchedMethod, 'length', { diff --git a/packages/instrumentation-tedious/src/types.ts b/packages/instrumentation-tedious/src/types.ts index ee50f0fb97..f752553c87 100644 --- a/packages/instrumentation-tedious/src/types.ts +++ b/packages/instrumentation-tedious/src/types.ts @@ -14,7 +14,6 @@ * limitations under the License. */ - import { InstrumentationConfig } from '@opentelemetry/instrumentation'; export interface TediousInstrumentationConfig extends InstrumentationConfig { /** diff --git a/packages/instrumentation-tedious/test/api.ts b/packages/instrumentation-tedious/test/api.ts index 0c4ae8ebed..3031ade69f 100644 --- a/packages/instrumentation-tedious/test/api.ts +++ b/packages/instrumentation-tedious/test/api.ts @@ -68,7 +68,7 @@ export const makeApi = (tedious: tedious) => { connection: Connection, params: string, method: Method = 'execSql', - noTracking?: boolean, + noTracking?: boolean ): Promise => { return new Promise((resolve, reject) => { const result: any[] = []; diff --git a/packages/instrumentation-tedious/test/instrumentation.test.ts b/packages/instrumentation-tedious/test/instrumentation.test.ts index 78871df035..005920e986 100644 --- a/packages/instrumentation-tedious/test/instrumentation.test.ts +++ b/packages/instrumentation-tedious/test/instrumentation.test.ts @@ -308,10 +308,9 @@ describe('tedious', () => { }); describe('trace context propagation via CONTEXT_INFO', () => { - function traceparentFromSpan(span: ReadableSpan) { const sc = span.spanContext(); - const flags = (sc.traceFlags & 0x01) ? '01' : '00'; + const flags = sc.traceFlags & 0x01 ? '01' : '00'; return `00-${sc.traceId}-${sc.spanId}-${flags}`; } @@ -331,18 +330,23 @@ describe('tedious', () => { }); after(() => { - instrumentation.setConfig({enableTraceContextPropagation: false}); + instrumentation.setConfig({ enableTraceContextPropagation: false }); }); it('injects DB-span traceparent for execSql', async function () { - const sql = "SELECT REPLACE(CONVERT(varchar(128), CONTEXT_INFO()), CHAR(0), '') AS traceparent"; - const result = await tedious.query(connection, sql) + const sql = + "SELECT REPLACE(CONVERT(varchar(128), CONTEXT_INFO()), CHAR(0), '') AS traceparent"; + const result = await tedious.query(connection, sql); const spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 1); const dbSpan = findDbSpan(spans, 'execSql'); const expectedTp = traceparentFromSpan(dbSpan); - assert.strictEqual(result[0], expectedTp, 'CONTEXT_INFO traceparent should match DB span'); + assert.strictEqual( + result[0], + expectedTp, + 'CONTEXT_INFO traceparent should match DB span' + ); }); it('injects for execSqlBatch', async function () { @@ -375,7 +379,6 @@ describe('tedious', () => { const spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 1); }); - }); }); From ad9881eb6d5c0307997fb3998909ab98ea8ee403 Mon Sep 17 00:00:00 2001 From: Marten Hennoch Date: Mon, 6 Oct 2025 11:33:23 +0300 Subject: [PATCH 3/3] fix semconv --- .../test/instrumentation.test.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/instrumentation-tedious/test/instrumentation.test.ts b/packages/instrumentation-tedious/test/instrumentation.test.ts index a04b205b7e..00b32ab46e 100644 --- a/packages/instrumentation-tedious/test/instrumentation.test.ts +++ b/packages/instrumentation-tedious/test/instrumentation.test.ts @@ -314,15 +314,6 @@ describe('tedious', () => { return `00-${sc.traceId}-${sc.spanId}-${flags}`; } - function findDbSpan(spans: ReadableSpan[], startsWith: string) { - return spans.find( - s => - s.kind === SpanKind.CLIENT && - s.attributes[SEMATTRS_DB_SYSTEM] === DBSYSTEMVALUES_MSSQL && - s.name.startsWith(startsWith) - )!; - } - beforeEach(() => { instrumentation.setConfig({ enableTraceContextPropagation: true, @@ -340,8 +331,7 @@ describe('tedious', () => { const spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 1); - const dbSpan = findDbSpan(spans, 'execSql'); - const expectedTp = traceparentFromSpan(dbSpan); + const expectedTp = traceparentFromSpan(spans[0]); assert.strictEqual( result[0], expectedTp, @@ -360,9 +350,7 @@ describe('tedious', () => { const spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 1); - const dbSpan = findDbSpan(spans, 'execSqlBatch'); - const expectedTp = traceparentFromSpan(dbSpan); - + const expectedTp = traceparentFromSpan(spans[0]); assert.strictEqual(result[0], expectedTp); });