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 289b3c62c4..a62f1b86e0 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,28 @@ export class TediousInstrumentation extends InstrumentationBase {
+ return api.context.with(
+ api.trace.setSpan(api.context.active(), span),
+ originalMethod,
+ this,
+ ...arguments
+ );
+ };
+
+ const cfg = thisPlugin.getConfig();
+ const shouldInject =
+ cfg.enableTraceContextPropagation &&
+ thisPlugin._shouldInjectFor(operation);
+
+ if (!shouldInject) return runUserRequest();
+
+ const traceparent = thisPlugin._buildTraceparent(span);
+ thisPlugin
+ ._injectContextInfo(this, tediousModule, traceparent)
+ .finally(runUserRequest);
+
+ return;
}
Object.defineProperty(patchedMethod, 'length', {
diff --git a/packages/instrumentation-tedious/src/types.ts b/packages/instrumentation-tedious/src/types.ts
index 3342997061..f752553c87 100644
--- a/packages/instrumentation-tedious/src/types.ts
+++ b/packages/instrumentation-tedious/src/types.ts
@@ -15,5 +15,11 @@
*/
import { InstrumentationConfig } from '@opentelemetry/instrumentation';
-
-export type TediousInstrumentationConfig = InstrumentationConfig;
+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..3031ade69f 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 96b45c54ae..00b32ab46e 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,68 @@ 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}`;
+ }
+
+ 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 expectedTp = traceparentFromSpan(spans[0]);
+ 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 expectedTp = traceparentFromSpan(spans[0]);
+ 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) => {