Skip to content

Commit d3cc76b

Browse files
committed
feat(instrumentations-tedious): support net.* and database semconv migration
This adds support for using `OTEL_SEMCONV_STABILITY_OPT_IN` for controlled migration to stable `net.*` and `db.*` semconv. The `net.*` attributes are controlled by the `http[/dup]` token in `OTEL_SEMCONV_STABILITY_OPT_IN` (as [discussed here](open-telemetry/opentelemetry-js#5663 (comment))) and `db.*` with the `database[/dup]` token. Refs: open-telemetry/opentelemetry-js#5663 Refs: #2953
1 parent c28f4ed commit d3cc76b

File tree

5 files changed

+209
-66
lines changed

5 files changed

+209
-66
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/instrumentation-tedious/README.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,32 @@ registerInstrumentations({
4242

4343
## Semantic Conventions
4444

45-
This package uses `@opentelemetry/semantic-conventions` version `1.22+`, which implements Semantic Convention [Version 1.7.0](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/semantic_conventions/README.md)
45+
This instrumentation implements Semantic Conventions (semconv) v1.7.0. Since then, networking (in semconv v1.23.1) and database (in semconv v1.33.0) semantic conventions were stabilized. As of `@opentelemetry/[email protected]` support has been added for migrating to the stable semantic conventions using the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable as follows:
46+
47+
1. Upgrade to the latest version of this instrumentation package.
48+
2. Set `OTEL_SEMCONV_STABILITY_OPT_IN=http/dup,database/dup` to emit both old and stable semantic conventions. (The `http` token is used to control the `net.*` attributes, the `database` token to control to `db.*` attributes.)
49+
3. Modify alerts, dashboards, metrics, and other processes in your Observability system to use the stable semantic conventions.
50+
4. Set `OTEL_SEMCONV_STABILITY_OPT_IN=http,database` to emit only the stable semantic conventions.
51+
52+
By default, if `OTEL_SEMCONV_STABILITY_OPT_IN` includes neither of the above tokens, the old v1.7.0 semconv is used.
53+
The intent is to provide an approximate 6 month time window for users of this instrumentation to migrate to the new database and networking semconv, after which a new minor version will use the new semconv by default and drop support for the old semconv.
54+
See [the HTTP migration guide](https://opentelemetry.io/docs/specs/semconv/non-normative/http-migration/) and the [database migration guide](https://opentelemetry.io/docs/specs/semconv/non-normative/db-migration/) for details.
4655

4756
Attributes collected:
4857

49-
| Attribute | Short Description |
50-
| ----------------------- | ------------------------------------------------------------------------------ |
51-
| `db.name` | This attribute is used to report the name of the database being accessed. |
52-
| `db.sql.table` | The name of the primary table that the operation is acting upon. |
53-
| `db.statement` | The database statement being executed. |
54-
| `db.system` | An identifier for the database management system (DBMS) product being used. |
55-
| `db.user` | Username for accessing the database. |
56-
| `net.peer.name` | Remote hostname or similar. |
57-
| `net.peer.port` | Remote port number. |
58+
| Old semconv | Stable semconv | Description |
59+
| ---------------------- | ---------------- | ---------------------------------- |
60+
| `db.system` | `db.system.name` | 'mssql' (old), 'microsoft.sql_server' (stable) |
61+
| `db.statement` | `db.query.text` | The database query being executed. |
62+
| `db.user` | Removed | Username for accessing the database. |
63+
| `db.name` | Removed | Integrated into new `db.namespace`. |
64+
| (not included) | `db.namespace` | The database associated with the connection, qualified by the instance name. |
65+
| `db.sql.table` | `db.collection.name` | The name of a collection (table, container) within the database. |
66+
| `net.peer.name` | `server.address` | Remote hostname or similar. |
67+
| `net.peer.port` | `server.port` | Remote port number. |
68+
69+
XXX Get review from owner on db.namespace and lengthy rec from https://opentelemetry.io/docs/specs/semconv/database/sql-server/ note `[1]`.
70+
5871

5972
### Trace Context Propagation
6073

packages/instrumentation-tedious/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
},
6161
"dependencies": {
6262
"@opentelemetry/instrumentation": "^0.208.0",
63+
"@opentelemetry/semantic-conventions": "^1.33.0",
6364
"@types/tedious": "^4.0.14"
6465
},
6566
"homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-tedious#readme"

packages/instrumentation-tedious/src/instrumentation.ts

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,18 @@ import {
2020
InstrumentationBase,
2121
InstrumentationNodeModuleDefinition,
2222
isWrapped,
23+
SemconvStability,
24+
semconvStabilityFromStr,
2325
} from '@opentelemetry/instrumentation';
26+
import {
27+
ATTR_DB_COLLECTION_NAME,
28+
ATTR_DB_NAMESPACE,
29+
ATTR_DB_QUERY_TEXT,
30+
ATTR_DB_SYSTEM_NAME,
31+
ATTR_SERVER_ADDRESS,
32+
ATTR_SERVER_PORT,
33+
DB_SYSTEM_NAME_VALUE_MICROSOFT_SQL_SERVER,
34+
} from '@opentelemetry/semantic-conventions';
2435
import {
2536
DB_SYSTEM_VALUE_MSSQL,
2637
ATTR_DB_NAME,
@@ -75,9 +86,24 @@ function setDatabase(this: ApproxConnection, databaseName: string) {
7586

7687
export class TediousInstrumentation extends InstrumentationBase<TediousInstrumentationConfig> {
7788
static readonly COMPONENT = 'tedious';
89+
private _netSemconvStability!: SemconvStability;
90+
private _dbSemconvStability!: SemconvStability;
7891

7992
constructor(config: TediousInstrumentationConfig = {}) {
8093
super(PACKAGE_NAME, PACKAGE_VERSION, config);
94+
this._setSemconvStabilityFromEnv();
95+
}
96+
97+
// Used for testing.
98+
private _setSemconvStabilityFromEnv() {
99+
this._netSemconvStability = semconvStabilityFromStr(
100+
'http',
101+
process.env.OTEL_SEMCONV_STABILITY_OPT_IN
102+
);
103+
this._dbSemconvStability = semconvStabilityFromStr(
104+
'database',
105+
process.env.OTEL_SEMCONV_STABILITY_OPT_IN
106+
);
81107
}
82108

83109
protected init() {
@@ -209,22 +235,41 @@ export class TediousInstrumentation extends InstrumentationBase<TediousInstrumen
209235
return request.sqlTextOrProcedure;
210236
})(request);
211237

238+
const attributes: api.Attributes = {};
239+
if (thisPlugin._dbSemconvStability & SemconvStability.OLD) {
240+
attributes[ATTR_DB_SYSTEM] = DB_SYSTEM_VALUE_MSSQL;
241+
attributes[ATTR_DB_NAME] = databaseName;
242+
// >=4 uses `authentication` object; older versions just userName and password pair
243+
attributes[ATTR_DB_USER] =
244+
this.config?.userName ??
245+
this.config?.authentication?.options?.userName;
246+
attributes[ATTR_DB_STATEMENT] = sql;
247+
attributes[ATTR_DB_SQL_TABLE] = request.table;
248+
}
249+
if (thisPlugin._dbSemconvStability & SemconvStability.STABLE) {
250+
attributes[ATTR_DB_NAMESPACE] = databaseName;
251+
attributes[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_MICROSOFT_SQL_SERVER;
252+
attributes[ATTR_DB_QUERY_TEXT] = sql;
253+
attributes[ATTR_DB_COLLECTION_NAME] = request.table;
254+
// See https://opentelemetry.io/docs/specs/semconv/database/sql-server/#spans
255+
// TODO: can `db.response.status_code` be added?
256+
// TODO: is `operation` correct for `db.operation.name`
257+
// TODO: can `db.query.summary` reliably be calculated?
258+
// TODO: `db.stored_procedure.name`
259+
}
260+
if (thisPlugin._netSemconvStability & SemconvStability.OLD) {
261+
attributes[ATTR_NET_PEER_NAME] = this.config?.server;
262+
attributes[ATTR_NET_PEER_PORT] = this.config?.options?.port;
263+
}
264+
if (thisPlugin._netSemconvStability & SemconvStability.STABLE) {
265+
attributes[ATTR_SERVER_ADDRESS] = this.config?.server;
266+
attributes[ATTR_SERVER_PORT] = this.config?.options?.port;
267+
}
212268
const span = thisPlugin.tracer.startSpan(
213269
getSpanName(operation, databaseName, sql, request.table),
214270
{
215271
kind: api.SpanKind.CLIENT,
216-
attributes: {
217-
[ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_MSSQL,
218-
[ATTR_DB_NAME]: databaseName,
219-
[ATTR_NET_PEER_PORT]: this.config?.options?.port,
220-
[ATTR_NET_PEER_NAME]: this.config?.server,
221-
// >=4 uses `authentication` object, older versions just userName and password pair
222-
[ATTR_DB_USER]:
223-
this.config?.userName ??
224-
this.config?.authentication?.options?.userName,
225-
[ATTR_DB_STATEMENT]: sql,
226-
[ATTR_DB_SQL_TABLE]: request.table,
227-
},
272+
attributes,
228273
}
229274
);
230275

@@ -242,6 +287,7 @@ export class TediousInstrumentation extends InstrumentationBase<TediousInstrumen
242287
code: api.SpanStatusCode.ERROR,
243288
message: err.message,
244289
});
290+
// TODO: set `error.type` attribute?
245291
}
246292
span.end();
247293
});
@@ -279,7 +325,7 @@ export class TediousInstrumentation extends InstrumentationBase<TediousInstrumen
279325
if (!shouldInject) return runUserRequest();
280326

281327
const traceparent = thisPlugin._buildTraceparent(span);
282-
328+
283329
void thisPlugin
284330
._injectContextInfo(this, tediousModule, traceparent)
285331
.finally(runUserRequest);

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

Lines changed: 125 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { context, trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
17+
import { context, trace, SpanStatusCode, SpanKind, type Attributes } from '@opentelemetry/api';
1818
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
1919
import {
2020
DB_SYSTEM_VALUE_MSSQL,
@@ -26,7 +26,7 @@ import {
2626
ATTR_NET_PEER_NAME,
2727
ATTR_NET_PEER_PORT,
2828
} from '../src/semconv';
29-
import * as util from 'util';
29+
import { SemconvStability } from '@opentelemetry/instrumentation';
3030
import * as testUtils from '@opentelemetry/contrib-test-utils';
3131
import {
3232
BasicTracerProvider,
@@ -39,6 +39,13 @@ import { TediousInstrumentation } from '../src';
3939
import makeApi from './api';
4040
import type { Connection } from 'tedious';
4141
import * as semver from 'semver';
42+
// XXX has prettier config stopped working in linting?
43+
import { ATTR_DB_COLLECTION_NAME, ATTR_DB_NAMESPACE, ATTR_DB_QUERY_TEXT, ATTR_DB_SYSTEM_NAME, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, DB_SYSTEM_NAME_VALUE_MICROSOFT_SQL_SERVER } from '@opentelemetry/semantic-conventions';
44+
45+
// By default tests run with both old and stable semconv. Some test cases
46+
// specifically test the various values of OTEL_SEMCONV_STABILITY_OPT_IN.
47+
process.env.OTEL_SEMCONV_STABILITY_OPT_IN = 'http/dup,database/dup';
48+
const DEFAULT_NET_SEMCONV_STABILITY = SemconvStability.DUPLICATE;
4249

4350
const port = Number(process.env.MSSQL_PORT) || 1433;
4451
const database = process.env.MSSQL_DATABASE || 'master';
@@ -307,6 +314,65 @@ describe('tedious', () => {
307314
});
308315
});
309316

317+
describe('various values of OTEL_SEMCONV_STABILITY_OPT_IN', () => {
318+
const _origOptInEnv = process.env.OTEL_SEMCONV_STABILITY_OPT_IN;
319+
after(() => {
320+
process.env.OTEL_SEMCONV_STABILITY_OPT_IN = _origOptInEnv;
321+
(instrumentation as any)._setSemconvStabilityFromEnv();
322+
});
323+
324+
it('OTEL_SEMCONV_STABILITY_OPT_IN=(empty)', async () => {
325+
process.env.OTEL_SEMCONV_STABILITY_OPT_IN = '';
326+
(instrumentation as any)._setSemconvStabilityFromEnv();
327+
memoryExporter.reset();
328+
329+
const queryString = "SELECT 42, 'hello world'";
330+
const PARENT_NAME = 'parentSpan';
331+
const parentSpan = provider.getTracer('default').startSpan(PARENT_NAME);
332+
assert.deepStrictEqual(
333+
await context.with(trace.setSpan(context.active(), parentSpan), () =>
334+
tedious.query(connection, queryString)
335+
),
336+
[42, 'hello world']
337+
);
338+
parentSpan.end();
339+
const spans = memoryExporter.getFinishedSpans();
340+
assert.strictEqual(spans.length, 2, 'Received incorrect number of spans');
341+
assertSpan(spans[0], {
342+
name: 'execSql master',
343+
sql: queryString,
344+
parentSpan,
345+
}, SemconvStability.OLD);
346+
assert.strictEqual(spans[1].name, PARENT_NAME);
347+
});
348+
349+
it('OTEL_SEMCONV_STABILITY_OPT_IN=http,database', async () => {
350+
process.env.OTEL_SEMCONV_STABILITY_OPT_IN = 'http,database';
351+
(instrumentation as any)._setSemconvStabilityFromEnv();
352+
memoryExporter.reset();
353+
354+
const queryString = "SELECT 42, 'hello world'";
355+
const PARENT_NAME = 'parentSpan';
356+
const parentSpan = provider.getTracer('default').startSpan(PARENT_NAME);
357+
assert.deepStrictEqual(
358+
await context.with(trace.setSpan(context.active(), parentSpan), () =>
359+
tedious.query(connection, queryString)
360+
),
361+
[42, 'hello world']
362+
);
363+
parentSpan.end();
364+
const spans = memoryExporter.getFinishedSpans();
365+
assert.strictEqual(spans.length, 2, 'Received incorrect number of spans');
366+
assertSpan(spans[0], {
367+
name: 'execSql master',
368+
sql: queryString,
369+
parentSpan,
370+
}, SemconvStability.STABLE);
371+
assert.strictEqual(spans[1].name, PARENT_NAME);
372+
});
373+
});
374+
375+
310376
describe('trace context propagation via CONTEXT_INFO', () => {
311377
function traceparentFromSpan(span: ReadableSpan) {
312378
const sc = span.spanContext();
@@ -370,13 +436,6 @@ describe('tedious', () => {
370436
});
371437
});
372438

373-
const assertMatch = (actual: string | undefined, expected: RegExp) => {
374-
assert(
375-
actual && expected.test(actual),
376-
`Expected ${util.inspect(actual)} to match ${expected}`
377-
);
378-
};
379-
380439
const assertRejects = (
381440
asyncFn: () => Promise<unknown>,
382441
expectedMessageRegexp: RegExp | undefined
@@ -392,52 +451,75 @@ const assertRejects = (
392451
throw error;
393452
}
394453
if (expectedMessageRegexp) {
395-
assertMatch(err?.message || err, expectedMessageRegexp);
454+
assert.match(err?.message || err, expectedMessageRegexp);
396455
}
397456
});
398457
};
399458

400-
function assertSpan(span: ReadableSpan, expected: any) {
401-
assert(span);
459+
function assertSpan(
460+
span: ReadableSpan, expected: any, semconvStability: SemconvStability = DEFAULT_NET_SEMCONV_STABILITY)
461+
{
462+
assert.ok(span);
402463
assert.strictEqual(span.name, expected.name);
403464
assert.strictEqual(span.kind, SpanKind.CLIENT);
404-
assert.strictEqual(span.attributes[ATTR_DB_SYSTEM], DB_SYSTEM_VALUE_MSSQL);
405-
assert.strictEqual(
406-
span.attributes[ATTR_DB_NAME],
407-
expected.database ?? database
408-
);
409-
assert.strictEqual(span.attributes[ATTR_NET_PEER_PORT], port);
410-
assert.strictEqual(span.attributes[ATTR_NET_PEER_NAME], host);
411-
assert.strictEqual(span.attributes[ATTR_DB_USER], user);
412-
assert.strictEqual(
413-
span.attributes['tedious.procedure_count'],
414-
expected.procCount ?? 1,
415-
'Invalid procedure_count'
416-
);
417-
assert.strictEqual(
418-
span.attributes['tedious.statement_count'],
419-
expected.statementCount ?? 1,
420-
'Invalid statement_count'
421-
);
465+
466+
// Attributes
467+
const actualAttrs = {...span.attributes};
468+
const expectedAttrs: Attributes = {
469+
'tedious.procedure_count': expected.procCount ?? 1,
470+
'tedious.statement_count': expected.statementCount ?? 1,
471+
};
472+
if (semconvStability & SemconvStability.OLD) {
473+
expectedAttrs[ATTR_DB_SYSTEM] = DB_SYSTEM_VALUE_MSSQL;
474+
expectedAttrs[ATTR_DB_NAME] = expected.database ?? database;
475+
expectedAttrs[ATTR_DB_USER] = user;
476+
expectedAttrs[ATTR_NET_PEER_NAME] = host;
477+
expectedAttrs[ATTR_NET_PEER_PORT] = port;
478+
if (expected.table) {
479+
expectedAttrs[ATTR_DB_SQL_TABLE] = expected.table;
480+
}
481+
// "db.statement"
482+
if (expected.sql) {
483+
if (expected.sql instanceof RegExp) {
484+
assert.match(span.attributes[ATTR_DB_STATEMENT] as string, expected.sql);
485+
} else {
486+
assert.strictEqual(span.attributes[ATTR_DB_STATEMENT], expected.sql, ATTR_DB_STATEMENT);
487+
}
488+
} else {
489+
assert.strictEqual(actualAttrs[ATTR_DB_STATEMENT], undefined);
490+
}
491+
delete actualAttrs[ATTR_DB_STATEMENT];
492+
}
493+
if (semconvStability & SemconvStability.STABLE) {
494+
expectedAttrs[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_MICROSOFT_SQL_SERVER;
495+
expectedAttrs[ATTR_DB_NAMESPACE] = expected.database ?? database;
496+
expectedAttrs[ATTR_SERVER_ADDRESS] = host;
497+
expectedAttrs[ATTR_SERVER_PORT] = port;
498+
if (expected.table) {
499+
expectedAttrs[ATTR_DB_COLLECTION_NAME] = expected.table;
500+
}
501+
// "db.statement"
502+
if (expected.sql) {
503+
if (expected.sql instanceof RegExp) {
504+
assert.match(span.attributes[ATTR_DB_QUERY_TEXT] as string, expected.sql);
505+
} else {
506+
assert.strictEqual(span.attributes[ATTR_DB_QUERY_TEXT], expected.sql, ATTR_DB_QUERY_TEXT);
507+
}
508+
} else {
509+
assert.strictEqual(actualAttrs[ATTR_DB_QUERY_TEXT], undefined);
510+
}
511+
delete actualAttrs[ATTR_DB_QUERY_TEXT];
512+
}
513+
assert.deepEqual(actualAttrs, expectedAttrs);
514+
515+
422516
if (expected.parentSpan) {
423517
assert.strictEqual(
424518
span.parentSpanContext?.spanId,
425519
expected.parentSpan.spanContext().spanId
426520
);
427521
}
428-
assert.strictEqual(span.attributes[ATTR_DB_SQL_TABLE], expected.table);
429-
if (expected.sql) {
430-
if (expected.sql instanceof RegExp) {
431-
assertMatch(
432-
span.attributes[ATTR_DB_STATEMENT] as string | undefined,
433-
expected.sql
434-
);
435-
} else {
436-
assert.strictEqual(span.attributes[ATTR_DB_STATEMENT], expected.sql);
437-
}
438-
} else {
439-
assert.strictEqual(span.attributes[ATTR_DB_STATEMENT], undefined);
440-
}
522+
441523
if (expected.error) {
442524
assert(
443525
expected.error.test(span.status.message),

0 commit comments

Comments
 (0)