Skip to content

Commit 51ccb3c

Browse files
authored
feat(tedious): optional context propagation to SQL Server using SET CONTEXT_INFO (#3141)
1 parent 711a8b8 commit 51ccb3c

File tree

5 files changed

+164
-16
lines changed

5 files changed

+164
-16
lines changed

packages/instrumentation-tedious/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ Attributes collected:
5656
| `net.peer.name` | Remote hostname or similar. |
5757
| `net.peer.port` | Remote port number. |
5858

59+
### Trace Context Propagation
60+
61+
Database trace context propagation can be enabled by setting `enableTraceContextPropagation`to `true`.
62+
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)
63+
command to set [traceparent](https://www.w3.org/TR/trace-context/#traceparent-header)information
64+
for the current connection, which results in **an additional round-trip to the database**.
65+
5966
## Useful links
6067

6168
- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>

packages/instrumentation-tedious/src/instrumentation.ts

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ import { PACKAGE_NAME, PACKAGE_VERSION } from './version';
4040
const CURRENT_DATABASE = Symbol(
4141
'opentelemetry.instrumentation-tedious.current-database'
4242
);
43+
44+
export const INJECTED_CTX = Symbol(
45+
'opentelemetry.instrumentation-tedious.context-info-injected'
46+
);
47+
4348
const PATCHED_METHODS = [
4449
'callProcedure',
4550
'execSql',
@@ -89,7 +94,7 @@ export class TediousInstrumentation extends InstrumentationBase<TediousInstrumen
8994
this._wrap(
9095
ConnectionPrototype,
9196
method,
92-
this._patchQuery(method) as any
97+
this._patchQuery(method, moduleExports) as any
9398
);
9499
}
95100

@@ -127,11 +132,61 @@ export class TediousInstrumentation extends InstrumentationBase<TediousInstrumen
127132
};
128133
}
129134

130-
private _patchQuery(operation: string) {
135+
private _buildTraceparent(span: api.Span): string {
136+
const sc = span.spanContext();
137+
return `00-${sc.traceId}-${sc.spanId}-0${Number(sc.traceFlags || api.TraceFlags.NONE).toString(16)}`;
138+
}
139+
140+
/**
141+
* Fire a one-off `SET CONTEXT_INFO @opentelemetry_traceparent` on the same
142+
* connection. Marks the request with INJECTED_CTX so our patch skips it.
143+
*/
144+
private _injectContextInfo(
145+
connection: any,
146+
tediousModule: typeof tedious,
147+
traceparent: string
148+
): Promise<void> {
149+
return new Promise(resolve => {
150+
try {
151+
const sql = 'set context_info @opentelemetry_traceparent';
152+
const req = new tediousModule.Request(sql, (_err: any) => {
153+
resolve();
154+
});
155+
Object.defineProperty(req, INJECTED_CTX, { value: true });
156+
const buf = Buffer.from(traceparent, 'utf8');
157+
req.addParameter(
158+
'opentelemetry_traceparent',
159+
(tediousModule as any).TYPES.VarBinary,
160+
buf,
161+
{ length: buf.length }
162+
);
163+
164+
connection.execSql(req);
165+
} catch {
166+
resolve();
167+
}
168+
});
169+
}
170+
171+
private _shouldInjectFor(operation: string): boolean {
172+
return (
173+
operation === 'execSql' ||
174+
operation === 'execSqlBatch' ||
175+
operation === 'callProcedure' ||
176+
operation === 'execute'
177+
);
178+
}
179+
180+
private _patchQuery(operation: string, tediousModule: typeof tedious) {
131181
return (originalMethod: UnknownFunction): UnknownFunction => {
132182
const thisPlugin = this;
133183

134184
function patchedMethod(this: ApproxConnection, request: ApproxRequest) {
185+
// Skip our own injected request
186+
if ((request as any)?.[INJECTED_CTX]) {
187+
return originalMethod.apply(this, arguments as unknown as any[]);
188+
}
189+
135190
if (!(request instanceof EventEmitter)) {
136191
thisPlugin._diag.warn(
137192
`Unexpected invocation of patched ${operation} method. Span not recorded`
@@ -207,12 +262,27 @@ export class TediousInstrumentation extends InstrumentationBase<TediousInstrumen
207262
thisPlugin._diag.error('Expected request.callback to be a function');
208263
}
209264

210-
return api.context.with(
211-
api.trace.setSpan(api.context.active(), span),
212-
originalMethod,
213-
this,
214-
...arguments
215-
);
265+
const runUserRequest = () => {
266+
return api.context.with(
267+
api.trace.setSpan(api.context.active(), span),
268+
originalMethod,
269+
this,
270+
...arguments
271+
);
272+
};
273+
274+
const cfg = thisPlugin.getConfig();
275+
const shouldInject =
276+
cfg.enableTraceContextPropagation &&
277+
thisPlugin._shouldInjectFor(operation);
278+
279+
if (!shouldInject) return runUserRequest();
280+
281+
const traceparent = thisPlugin._buildTraceparent(span);
282+
283+
void thisPlugin
284+
._injectContextInfo(this, tediousModule, traceparent)
285+
.finally(runUserRequest);
216286
}
217287

218288
Object.defineProperty(patchedMethod, 'length', {

packages/instrumentation-tedious/src/types.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,11 @@
1515
*/
1616

1717
import { InstrumentationConfig } from '@opentelemetry/instrumentation';
18-
19-
export type TediousInstrumentationConfig = InstrumentationConfig;
18+
export interface TediousInstrumentationConfig extends InstrumentationConfig {
19+
/**
20+
* If true, injects the current DB span's W3C traceparent into SQL Server
21+
* session state via `SET CONTEXT_INFO @opentelemetry_traceparent` (varbinary).
22+
* Off by default to avoid the extra round-trip per request.
23+
*/
24+
enableTraceContextPropagation?: boolean;
25+
}

packages/instrumentation-tedious/test/api.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import * as assert from 'assert';
1818
import { promisify } from 'util';
1919
import type { Connection, Request, TYPES } from 'tedious';
20-
20+
import { INJECTED_CTX } from '../src/instrumentation';
2121
type Method = keyof Connection & ('execSql' | 'execSqlBatch' | 'prepare');
2222
export type tedious = {
2323
Connection: typeof Connection;
@@ -67,7 +67,8 @@ export const makeApi = (tedious: tedious) => {
6767
const query = (
6868
connection: Connection,
6969
params: string,
70-
method: Method = 'execSql'
70+
method: Method = 'execSql',
71+
noTracking?: boolean
7172
): Promise<any[]> => {
7273
return new Promise((resolve, reject) => {
7374
const result: any[] = [];
@@ -78,7 +79,9 @@ export const makeApi = (tedious: tedious) => {
7879
resolve(result);
7980
}
8081
});
81-
82+
if (noTracking) {
83+
Object.defineProperty(request, INJECTED_CTX, { value: true });
84+
}
8285
// request.on('returnValue', console.log.bind(console, /*request.sqlTextOrProcedure,*/ 'returnValue:'));
8386
// request.on('error', console.log.bind(console, /*request.sqlTextOrProcedure,*/ 'error:'));
8487
// request.on('row', console.log.bind(console, /*request.sqlTextOrProcedure,*/ 'row:'));
@@ -314,7 +317,9 @@ export const makeApi = (tedious: tedious) => {
314317
if exists(SELECT * FROM sysobjects WHERE name='test_transact' AND xtype='U') DROP TABLE ${transaction.tableName};
315318
if exists(SELECT * FROM sysobjects WHERE name='test_proced' AND xtype='U') DROP PROCEDURE ${storedProcedure.procedureName};
316319
if exists(SELECT * FROM sys.databases WHERE name = 'temp_otel_db') DROP DATABASE temp_otel_db;
317-
`.trim()
320+
`.trim(),
321+
'execSql',
322+
true
318323
);
319324
};
320325

packages/instrumentation-tedious/test/instrumentation.test.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ const user = process.env.MSSQL_USER || 'sa';
4747
const password = process.env.MSSQL_PASSWORD || 'mssql_passw0rd';
4848

4949
const instrumentation = new TediousInstrumentation();
50-
instrumentation.enable();
51-
instrumentation.disable();
5250

5351
const config: any = {
5452
userName: user,
@@ -308,6 +306,68 @@ describe('tedious', () => {
308306
table: 'test_bulk',
309307
});
310308
});
309+
310+
describe('trace context propagation via CONTEXT_INFO', () => {
311+
function traceparentFromSpan(span: ReadableSpan) {
312+
const sc = span.spanContext();
313+
const flags = sc.traceFlags & 0x01 ? '01' : '00';
314+
return `00-${sc.traceId}-${sc.spanId}-${flags}`;
315+
}
316+
317+
beforeEach(() => {
318+
instrumentation.setConfig({
319+
enableTraceContextPropagation: true,
320+
});
321+
});
322+
323+
after(() => {
324+
instrumentation.setConfig({ enableTraceContextPropagation: false });
325+
});
326+
327+
it('injects DB-span traceparent for execSql', async function () {
328+
const sql =
329+
"SELECT REPLACE(CONVERT(varchar(128), CONTEXT_INFO()), CHAR(0), '') AS traceparent";
330+
const result = await tedious.query(connection, sql);
331+
332+
const spans = memoryExporter.getFinishedSpans();
333+
assert.strictEqual(spans.length, 1);
334+
const expectedTp = traceparentFromSpan(spans[0]);
335+
assert.strictEqual(
336+
result[0],
337+
expectedTp,
338+
'CONTEXT_INFO traceparent should match DB span'
339+
);
340+
});
341+
342+
it('injects for execSqlBatch', async function () {
343+
const batch = `
344+
SELECT REPLACE(CONVERT(varchar(128), CONTEXT_INFO()), CHAR(0), '') AS tp;
345+
SELECT 42;
346+
`;
347+
const result = await tedious.query(connection, batch, 'execSqlBatch');
348+
349+
assert.deepStrictEqual(result, [result[0], 42]);
350+
351+
const spans = memoryExporter.getFinishedSpans();
352+
assert.strictEqual(spans.length, 1);
353+
const expectedTp = traceparentFromSpan(spans[0]);
354+
assert.strictEqual(result[0], expectedTp);
355+
});
356+
357+
it('when disabled, CONTEXT_INFO stays empty', async function () {
358+
instrumentation.setConfig({
359+
enableTraceContextPropagation: false,
360+
});
361+
362+
const [val] = await tedious.query(
363+
connection,
364+
"SELECT REPLACE(CONVERT(varchar(128), CONTEXT_INFO()), CHAR(0), '')"
365+
);
366+
assert.strictEqual(val, null);
367+
const spans = memoryExporter.getFinishedSpans();
368+
assert.strictEqual(spans.length, 1);
369+
});
370+
});
311371
});
312372

313373
const assertMatch = (actual: string | undefined, expected: RegExp) => {

0 commit comments

Comments
 (0)