Skip to content

Commit 125fe11

Browse files
committed
ref(node): Streamline knex instrumentation
Refactors the vendored knex instrumentation off the OpenTelemetry tracing APIs onto Sentry's span APIs, mirroring the mongoose (#21481) and mysql2 (#21509) streamlines. - Replace `tracer.startSpan` + manual `context.with`/`.then`/`.catch` with `startSpan`. `Runner.query` returns a real, already-executing Promise, so `startSpan` can safely await it and auto-end the span while keeping it active so the underlying `pg`/`mysql2` driver spans nest correctly. - Preserve the build-time `contextSymbol` parent + require-parent-span behavior (only instrument queries that run within an existing trace). - Bake the `auto.db.otel.knex` origin into the span attributes and drop the `spanStart` hook from `index.ts`. - Drop the env-gated `OTEL_SEMCONV_STABILITY_OPT_IN` dual-emission; only the OLD semantic conventions (`db.system`, `db.statement`, ...) were ever emitted. Behavior change: the unsupported stable-semconv opt-in is no longer honored. - Hardcode `requireParentSpan`/`maxQueryLength` (the integration only ever used the defaults), delete the now-dead `types.ts`/`constants.ts` and `otelExceptionFromKnexError`, drop OTel `recordException`, and remove the blanket `eslint-disable`. - Extend the `pg` integration suite with a failing query to cover the error path (`status: internal_error`).
1 parent 51f8b40 commit 125fe11

8 files changed

Lines changed: 103 additions & 222 deletions

File tree

dev-packages/node-integration-tests/suites/tracing/knex/pg/scenario.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ async function run() {
2929

3030
await pgClient('User').insert({ name: 'bob', email: 'bob@domain.com' });
3131
await pgClient('User').select('*');
32+
33+
// Trigger a failing query to capture the error span (table does not exist).
34+
await pgClient('DoesNotExist')
35+
.select('*')
36+
.catch(() => {});
3237
} finally {
3338
await pgClient.destroy();
3439
}

dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ describe('knex auto instrumentation', () => {
5757
description: 'select * from "User"',
5858
origin: 'auto.db.otel.knex',
5959
}),
60+
61+
expect.objectContaining({
62+
data: expect.objectContaining({
63+
'knex.version': KNEX_VERSION,
64+
'db.operation': 'select',
65+
'db.sql.table': 'DoesNotExist',
66+
'db.system': 'postgresql',
67+
'db.name': 'tests',
68+
'db.statement': 'select * from "DoesNotExist"',
69+
'sentry.origin': 'auto.db.otel.knex',
70+
'sentry.op': 'db',
71+
}),
72+
status: 'internal_error',
73+
description: 'select * from "DoesNotExist"',
74+
origin: 'auto.db.otel.knex',
75+
}),
6076
]),
6177
};
6278

packages/node/src/integrations/tracing/knex/index.ts

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,17 @@
11
import { KnexInstrumentation } from './vendored/instrumentation';
22
import type { IntegrationFn } from '@sentry/core';
3-
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core';
4-
import { generateInstrumentOnce, instrumentWhenWrapped } from '@sentry/node-core';
3+
import { defineIntegration } from '@sentry/core';
4+
import { generateInstrumentOnce } from '@sentry/node-core';
55

66
const INTEGRATION_NAME = 'Knex';
77

8-
export const instrumentKnex = generateInstrumentOnce(
9-
INTEGRATION_NAME,
10-
() => new KnexInstrumentation({ requireParentSpan: true }),
11-
);
8+
export const instrumentKnex = generateInstrumentOnce(INTEGRATION_NAME, () => new KnexInstrumentation());
129

1310
const _knexIntegration = (() => {
14-
let instrumentationWrappedCallback: undefined | ((callback: () => void) => void);
15-
1611
return {
1712
name: INTEGRATION_NAME,
1813
setupOnce() {
19-
const instrumentation = instrumentKnex();
20-
instrumentationWrappedCallback = instrumentWhenWrapped(instrumentation);
21-
},
22-
23-
setup(client) {
24-
instrumentationWrappedCallback?.(() =>
25-
client.on('spanStart', span => {
26-
const { data } = spanToJSON(span);
27-
// knex.version is always set in the span data
28-
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/0309caeafc44ac9cb13a3345b790b01b76d0497d/plugins/node/opentelemetry-instrumentation-knex/src/instrumentation.ts#L138
29-
if ('knex.version' in data) {
30-
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.knex');
31-
}
32-
}),
33-
);
14+
instrumentKnex();
3415
},
3516
};
3617
}) satisfies IntegrationFn;

packages/node/src/integrations/tracing/knex/vendored/constants.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

packages/node/src/integrations/tracing/knex/vendored/instrumentation.ts

Lines changed: 76 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,15 @@
1717
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-knex
1818
* - Upstream version: @opentelemetry/instrumentation-knex@0.62.0
1919
* - Minor TypeScript strictness adjustments for this repository's compiler settings
20+
* - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs
2021
*/
21-
/* eslint-disable */
2222

2323
import * as api from '@opentelemetry/api';
24-
import { SDK_VERSION } from '@sentry/core';
25-
import * as constants from './constants';
26-
import {
27-
InstrumentationBase,
28-
InstrumentationNodeModuleDefinition,
29-
isWrapped,
30-
SemconvStability,
31-
semconvStabilityFromStr,
32-
} from '@opentelemetry/instrumentation';
24+
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
25+
import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation';
26+
import type { SpanAttributes } from '@sentry/core';
27+
import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startSpan } from '@sentry/core';
3328
import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile';
34-
import * as utils from './utils';
35-
import { KnexInstrumentationConfig } from './types';
36-
import {
37-
ATTR_DB_COLLECTION_NAME,
38-
ATTR_DB_NAMESPACE,
39-
ATTR_DB_OPERATION_NAME,
40-
ATTR_DB_QUERY_TEXT,
41-
ATTR_DB_SYSTEM_NAME,
42-
ATTR_SERVER_ADDRESS,
43-
ATTR_SERVER_PORT,
44-
} from '@opentelemetry/semantic-conventions';
4529
import {
4630
ATTR_DB_NAME,
4731
ATTR_DB_OPERATION,
@@ -53,65 +37,69 @@ import {
5337
ATTR_NET_PEER_PORT,
5438
ATTR_NET_TRANSPORT,
5539
} from './semconv';
40+
import * as utils from './utils';
5641

5742
const PACKAGE_NAME = '@sentry/instrumentation-knex';
43+
const ORIGIN = 'auto.db.otel.knex';
44+
45+
const MODULE_NAME = 'knex';
46+
const SUPPORTED_VERSIONS = [
47+
// use "lib/execution" for runner.js, "lib" for client.js as basepath, latest tested 0.95.6
48+
'>=0.22.0 <4',
49+
// use "lib" as basepath
50+
'>=0.10.0 <0.18.0',
51+
'>=0.19.0 <0.22.0',
52+
// use "src" as basepath
53+
'>=0.18.0 <0.19.0',
54+
];
55+
56+
// Max length of the query text captured in the `db.statement` attribute; ".." is appended when truncated.
57+
const MAX_QUERY_LENGTH = 1022;
5858

5959
const contextSymbol = Symbol('opentelemetry.instrumentation-knex.context');
60-
const DEFAULT_CONFIG: KnexInstrumentationConfig = {
61-
maxQueryLength: 1022,
62-
requireParentSpan: false,
63-
};
64-
65-
export class KnexInstrumentation extends InstrumentationBase<KnexInstrumentationConfig> {
66-
private _semconvStability: SemconvStability;
67-
68-
constructor(config: KnexInstrumentationConfig = {}) {
69-
super(PACKAGE_NAME, SDK_VERSION, { ...DEFAULT_CONFIG, ...config });
7060

71-
this._semconvStability = semconvStabilityFromStr('database', process.env.OTEL_SEMCONV_STABILITY_OPT_IN);
61+
export class KnexInstrumentation extends InstrumentationBase<InstrumentationConfig> {
62+
public constructor(config: InstrumentationConfig = {}) {
63+
super(PACKAGE_NAME, SDK_VERSION, config);
7264
}
7365

74-
override setConfig(config: KnexInstrumentationConfig = {}) {
75-
super.setConfig({ ...DEFAULT_CONFIG, ...config });
76-
}
77-
78-
init() {
79-
const module = new InstrumentationNodeModuleDefinition(constants.MODULE_NAME, constants.SUPPORTED_VERSIONS);
66+
public init(): InstrumentationNodeModuleDefinition {
67+
const module = new InstrumentationNodeModuleDefinition(MODULE_NAME, SUPPORTED_VERSIONS);
8068

8169
module.files.push(
82-
this.getClientNodeModuleFileInstrumentation('src'),
83-
this.getClientNodeModuleFileInstrumentation('lib'),
84-
this.getRunnerNodeModuleFileInstrumentation('src'),
85-
this.getRunnerNodeModuleFileInstrumentation('lib'),
86-
this.getRunnerNodeModuleFileInstrumentation('lib/execution'),
70+
this._getClientNodeModuleFileInstrumentation('src'),
71+
this._getClientNodeModuleFileInstrumentation('lib'),
72+
this._getRunnerNodeModuleFileInstrumentation('src'),
73+
this._getRunnerNodeModuleFileInstrumentation('lib'),
74+
this._getRunnerNodeModuleFileInstrumentation('lib/execution'),
8775
);
8876

8977
return module;
9078
}
9179

92-
private getRunnerNodeModuleFileInstrumentation(basePath: string) {
80+
private _getRunnerNodeModuleFileInstrumentation(basePath: string): InstrumentationNodeModuleFile {
9381
return new InstrumentationNodeModuleFile(
9482
`knex/${basePath}/runner.js`,
95-
constants.SUPPORTED_VERSIONS,
83+
SUPPORTED_VERSIONS,
9684
(Runner: any, moduleVersion?: string) => {
97-
this.ensureWrapped(Runner.prototype, 'query', this.createQueryWrapper(moduleVersion));
85+
this._ensureWrapped(Runner.prototype, 'query', this._createQueryWrapper(moduleVersion));
9886
return Runner;
9987
},
100-
(Runner: any, _moduleVersion?: string) => {
88+
(Runner: any) => {
10189
this._unwrap(Runner.prototype, 'query');
10290
return Runner;
10391
},
10492
);
10593
}
10694

107-
private getClientNodeModuleFileInstrumentation(basePath: string) {
95+
private _getClientNodeModuleFileInstrumentation(basePath: string): InstrumentationNodeModuleFile {
10896
return new InstrumentationNodeModuleFile(
10997
`knex/${basePath}/client.js`,
110-
constants.SUPPORTED_VERSIONS,
98+
SUPPORTED_VERSIONS,
11199
(Client: any) => {
112-
this.ensureWrapped(Client.prototype, 'queryBuilder', this.storeContext.bind(this));
113-
this.ensureWrapped(Client.prototype, 'schemaBuilder', this.storeContext.bind(this));
114-
this.ensureWrapped(Client.prototype, 'raw', this.storeContext.bind(this));
100+
this._ensureWrapped(Client.prototype, 'queryBuilder', this._storeContext.bind(this));
101+
this._ensureWrapped(Client.prototype, 'schemaBuilder', this._storeContext.bind(this));
102+
this._ensureWrapped(Client.prototype, 'raw', this._storeContext.bind(this));
115103
return Client;
116104
},
117105
(Client: any) => {
@@ -123,9 +111,7 @@ export class KnexInstrumentation extends InstrumentationBase<KnexInstrumentation
123111
);
124112
}
125113

126-
private createQueryWrapper(moduleVersion?: string) {
127-
const instrumentation = this;
128-
114+
private _createQueryWrapper(moduleVersion?: string) {
129115
return function wrapQuery(original: (...args: any[]) => any) {
130116
return function wrapped_logging_method(this: any, query: any) {
131117
const config = this.client.config;
@@ -137,83 +123,55 @@ export class KnexInstrumentation extends InstrumentationBase<KnexInstrumentation
137123
config?.connection?.filename ||
138124
config?.connection?.database ||
139125
utils.extractDatabaseFromConnectionString(connectionString);
140-
const { maxQueryLength } = instrumentation.getConfig();
141126

142-
const attributes: api.Attributes = {
127+
const attributes: SpanAttributes = {
128+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,
143129
'knex.version': moduleVersion,
130+
[ATTR_DB_SYSTEM]: utils.mapSystem(this.client.driverName),
131+
[ATTR_DB_SQL_TABLE]: table,
132+
[ATTR_DB_OPERATION]: operation,
133+
[ATTR_DB_USER]: config?.connection?.user,
134+
[ATTR_DB_NAME]: name,
135+
[ATTR_NET_PEER_NAME]: config?.connection?.host ?? utils.extractHostFromConnectionString(connectionString),
136+
[ATTR_NET_PEER_PORT]: config?.connection?.port ?? utils.extractPortFromConnectionString(connectionString),
137+
[ATTR_NET_TRANSPORT]: config?.connection?.filename === ':memory:' ? 'inproc' : undefined,
138+
[ATTR_DB_STATEMENT]: utils.limitLength(query?.sql, MAX_QUERY_LENGTH),
144139
};
145-
const transport = config?.connection?.filename === ':memory:' ? 'inproc' : undefined;
146-
147-
if (instrumentation._semconvStability & SemconvStability.OLD) {
148-
Object.assign(attributes, {
149-
[ATTR_DB_SYSTEM]: utils.mapSystem(this.client.driverName),
150-
[ATTR_DB_SQL_TABLE]: table,
151-
[ATTR_DB_OPERATION]: operation,
152-
[ATTR_DB_USER]: config?.connection?.user,
153-
[ATTR_DB_NAME]: name,
154-
[ATTR_NET_PEER_NAME]: config?.connection?.host ?? utils.extractHostFromConnectionString(connectionString),
155-
[ATTR_NET_PEER_PORT]: config?.connection?.port ?? utils.extractPortFromConnectionString(connectionString),
156-
[ATTR_NET_TRANSPORT]: transport,
157-
});
158-
}
159-
if (instrumentation._semconvStability & SemconvStability.STABLE) {
160-
Object.assign(attributes, {
161-
[ATTR_DB_SYSTEM_NAME]: utils.mapSystem(this.client.driverName),
162-
[ATTR_DB_COLLECTION_NAME]: table,
163-
[ATTR_DB_OPERATION_NAME]: operation,
164-
[ATTR_DB_NAMESPACE]: name,
165-
[ATTR_SERVER_ADDRESS]: config?.connection?.host ?? utils.extractHostFromConnectionString(connectionString),
166-
[ATTR_SERVER_PORT]: config?.connection?.port ?? utils.extractPortFromConnectionString(connectionString),
167-
});
168-
}
169-
if (maxQueryLength) {
170-
const queryText = utils.limitLength(query?.sql, maxQueryLength);
171-
if (instrumentation._semconvStability & SemconvStability.STABLE) {
172-
attributes[ATTR_DB_QUERY_TEXT] = queryText;
173-
}
174-
if (instrumentation._semconvStability & SemconvStability.OLD) {
175-
attributes[ATTR_DB_STATEMENT] = queryText;
176-
}
177-
}
178140

141+
// The query builder captures the context active when it was created (see `_storeContext`).
142+
// We only instrument queries that run as part of an existing trace.
179143
const parentContext = this.builder[contextSymbol] || api.context.active();
180144
const parentSpan = api.trace.getSpan(parentContext);
181145
const hasActiveParent = parentSpan && api.trace.isSpanContextValid(parentSpan.spanContext());
182-
if (instrumentation._config.requireParentSpan && !hasActiveParent) {
146+
if (!hasActiveParent) {
183147
return original.bind(this)(...arguments);
184148
}
185149

186-
const span = instrumentation.tracer.startSpan(
187-
utils.getName(name, operation, table),
188-
{
189-
kind: api.SpanKind.CLIENT,
190-
attributes,
191-
},
192-
parentContext,
150+
const args = arguments;
151+
return api.context.with(parentContext, () =>
152+
startSpan(
153+
{
154+
name: utils.getName(name, operation, table),
155+
kind: api.SpanKind.CLIENT,
156+
attributes,
157+
},
158+
span =>
159+
// `Runner.query` returns a real, already-executing Promise, so it is safe to let
160+
// `startSpan` await it and auto-end the span.
161+
original.apply(this, args).catch((err: any) => {
162+
const formatter = utils.getFormatter(this);
163+
const fullQuery = formatter(query.sql, query.bindings || []);
164+
const message = err.message.replace(`${fullQuery} - `, '');
165+
span.setStatus({ code: SPAN_STATUS_ERROR, message });
166+
throw err;
167+
}),
168+
),
193169
);
194-
const spanContext = api.trace.setSpan(api.context.active(), span);
195-
196-
return api.context
197-
.with(spanContext, original, this, ...arguments)
198-
.then((result: unknown) => {
199-
span.end();
200-
return result;
201-
})
202-
.catch((err: any) => {
203-
const formatter = utils.getFormatter(this);
204-
const fullQuery = formatter(query.sql, query.bindings || []);
205-
const message = err.message.replace(fullQuery + ' - ', '');
206-
const exc = utils.otelExceptionFromKnexError(err, message);
207-
span.recordException(exc);
208-
span.setStatus({ code: api.SpanStatusCode.ERROR, message });
209-
span.end();
210-
throw err;
211-
});
212170
};
213171
};
214172
}
215173

216-
private storeContext(original: Function) {
174+
private _storeContext(original: (...args: any[]) => any) {
217175
return function wrapped_logging_method(this: any) {
218176
const builder = original.apply(this, arguments);
219177
Object.defineProperty(builder, contextSymbol, {
@@ -223,7 +181,7 @@ export class KnexInstrumentation extends InstrumentationBase<KnexInstrumentation
223181
};
224182
}
225183

226-
ensureWrapped(obj: any, methodName: string, wrapper: (original: any) => any) {
184+
private _ensureWrapped(obj: any, methodName: string, wrapper: (original: any) => any): void {
227185
if (isWrapped(obj[methodName])) {
228186
this._unwrap(obj, methodName);
229187
}

packages/node/src/integrations/tracing/knex/vendored/semconv.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-knex
1818
* - Upstream version: @opentelemetry/instrumentation-knex@0.62.0
1919
*/
20-
/* eslint-disable */
2120

2221
/**
2322
* @deprecated Replaced by `db.namespace`.

0 commit comments

Comments
 (0)