Skip to content

Commit 1f56ffa

Browse files
feat(node): Add knex integration (#13526)
Implement Knex OTEL instrumentation in `packages/node`. This integration is not enabled by default. Signed-off-by: Kaung Zin Hein <[email protected]> Co-authored-by: Abhijeet Prasad <[email protected]>
1 parent 68781c5 commit 1f56ffa

File tree

16 files changed

+384
-8
lines changed

16 files changed

+384
-8
lines changed

dev-packages/node-integration-tests/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"graphql": "^16.3.0",
5151
"http-terminator": "^3.2.0",
5252
"ioredis": "^5.4.1",
53+
"knex": "^2.5.1",
5354
"kafkajs": "2.2.4",
5455
"lru-memoizer": "2.3.0",
5556
"mongodb": "^3.7.3",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
version: '3.9'
2+
3+
services:
4+
db_postgres:
5+
image: postgres:13
6+
restart: always
7+
container_name: integration-tests-knex-postgres
8+
ports:
9+
- '5445:5432'
10+
environment:
11+
POSTGRES_USER: test
12+
POSTGRES_PASSWORD: test
13+
POSTGRES_DB: tests
14+
15+
db_mysql2:
16+
image: mysql:8
17+
restart: always
18+
container_name: integration-tests-knex-mysql2
19+
ports:
20+
- '3307:3306'
21+
environment:
22+
MYSQL_ROOT_PASSWORD: docker
23+
MYSQL_DATABASE: tests
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
integrations: [Sentry.knexIntegration()],
10+
});
11+
12+
// Stop the process from exiting before the transaction is sent
13+
setInterval(() => {}, 1000);
14+
15+
const knex = require('knex').default;
16+
17+
const mysql2Client = knex({
18+
client: 'mysql2',
19+
connection: {
20+
host: 'localhost',
21+
port: 3307,
22+
user: 'root',
23+
password: 'docker',
24+
database: 'tests',
25+
},
26+
});
27+
28+
async function run() {
29+
await Sentry.startSpan(
30+
{
31+
name: 'Test Transaction',
32+
op: 'transaction',
33+
},
34+
async () => {
35+
try {
36+
await mysql2Client.schema.createTable('User', table => {
37+
table.increments('id').notNullable().primary({ constraintName: 'User_pkey' });
38+
table.timestamp('createdAt', { precision: 3 }).notNullable().defaultTo(mysql2Client.fn.now(3));
39+
table.text('email').notNullable();
40+
table.text('name').notNullable();
41+
});
42+
43+
await mysql2Client('User').insert({ name: 'jane', email: '[email protected]' });
44+
await mysql2Client('User').select('*');
45+
} finally {
46+
await mysql2Client.destroy();
47+
}
48+
},
49+
);
50+
}
51+
52+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
53+
run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
integrations: [Sentry.knexIntegration()],
10+
});
11+
12+
// Stop the process from exiting before the transaction is sent
13+
setInterval(() => {}, 1000);
14+
15+
const knex = require('knex').default;
16+
17+
const pgClient = knex({
18+
client: 'pg',
19+
connection: {
20+
host: 'localhost',
21+
port: 5445,
22+
user: 'test',
23+
password: 'test',
24+
database: 'tests',
25+
},
26+
});
27+
28+
async function run() {
29+
await Sentry.startSpan(
30+
{
31+
name: 'Test Transaction',
32+
op: 'transaction',
33+
},
34+
async () => {
35+
try {
36+
await pgClient.schema.createTable('User', table => {
37+
table.increments('id').notNullable().primary({ constraintName: 'User_pkey' });
38+
table.timestamp('createdAt', { precision: 3 }).notNullable().defaultTo(pgClient.fn.now(3));
39+
table.text('email').notNullable();
40+
table.text('name').notNullable();
41+
});
42+
43+
await pgClient('User').insert({ name: 'bob', email: '[email protected]' });
44+
await pgClient('User').select('*');
45+
} finally {
46+
await pgClient.destroy();
47+
}
48+
},
49+
);
50+
}
51+
52+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
53+
run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { createRunner } from '../../../utils/runner';
2+
3+
// When running docker compose, we need a larger timeout, as this takes some time...
4+
jest.setTimeout(90000);
5+
6+
describe('knex auto instrumentation', () => {
7+
// Update this if another knex version is installed
8+
const KNEX_VERSION = '2.5.1';
9+
10+
test('should auto-instrument `knex` package when using `pg` client', done => {
11+
const EXPECTED_TRANSACTION = {
12+
transaction: 'Test Transaction',
13+
spans: expect.arrayContaining([
14+
expect.objectContaining({
15+
data: expect.objectContaining({
16+
'knex.version': KNEX_VERSION,
17+
'db.system': 'postgresql',
18+
'db.name': 'tests',
19+
'sentry.origin': 'auto.db.otel.knex',
20+
'sentry.op': 'db',
21+
'net.peer.name': 'localhost',
22+
'net.peer.port': 5445,
23+
}),
24+
status: 'ok',
25+
description:
26+
'create table "User" ("id" serial primary key, "createdAt" timestamptz(3) not null default CURRENT_TIMESTAMP(3), "email" text not null, "name" text not null)',
27+
origin: 'auto.db.otel.knex',
28+
}),
29+
expect.objectContaining({
30+
data: expect.objectContaining({
31+
'knex.version': KNEX_VERSION,
32+
'db.system': 'postgresql',
33+
'db.name': 'tests',
34+
'sentry.origin': 'auto.db.otel.knex',
35+
'sentry.op': 'db',
36+
'net.peer.name': 'localhost',
37+
'net.peer.port': 5445,
38+
}),
39+
status: 'ok',
40+
// In the knex-otel spans, the placeholders (e.g., `$1`) are replaced by a `?`.
41+
description: 'insert into "User" ("email", "name") values (?, ?)',
42+
origin: 'auto.db.otel.knex',
43+
}),
44+
45+
expect.objectContaining({
46+
data: expect.objectContaining({
47+
'knex.version': KNEX_VERSION,
48+
'db.operation': 'select',
49+
'db.sql.table': 'User',
50+
'db.system': 'postgresql',
51+
'db.name': 'tests',
52+
'db.statement': 'select * from "User"',
53+
'sentry.origin': 'auto.db.otel.knex',
54+
'sentry.op': 'db',
55+
}),
56+
status: 'ok',
57+
description: 'select * from "User"',
58+
origin: 'auto.db.otel.knex',
59+
}),
60+
]),
61+
};
62+
63+
createRunner(__dirname, 'scenario-withPostgres.js')
64+
.withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] })
65+
.expect({ transaction: EXPECTED_TRANSACTION })
66+
.start(done);
67+
});
68+
69+
test('should auto-instrument `knex` package when using `mysql2` client', done => {
70+
const EXPECTED_TRANSACTION = {
71+
transaction: 'Test Transaction',
72+
spans: expect.arrayContaining([
73+
expect.objectContaining({
74+
data: expect.objectContaining({
75+
'knex.version': KNEX_VERSION,
76+
'db.system': 'mysql2',
77+
'db.name': 'tests',
78+
'db.user': 'root',
79+
'sentry.origin': 'auto.db.otel.knex',
80+
'sentry.op': 'db',
81+
'net.peer.name': 'localhost',
82+
'net.peer.port': 3307,
83+
}),
84+
status: 'ok',
85+
description:
86+
'create table `User` (`id` int unsigned not null auto_increment primary key, `createdAt` timestamp(3) not null default CURRENT_TIMESTAMP(3), `email` text not null, `name` text not null)',
87+
origin: 'auto.db.otel.knex',
88+
}),
89+
expect.objectContaining({
90+
data: expect.objectContaining({
91+
'knex.version': KNEX_VERSION,
92+
'db.system': 'mysql2',
93+
'db.name': 'tests',
94+
'db.user': 'root',
95+
'sentry.origin': 'auto.db.otel.knex',
96+
'sentry.op': 'db',
97+
'net.peer.name': 'localhost',
98+
'net.peer.port': 3307,
99+
}),
100+
status: 'ok',
101+
description: 'insert into `User` (`email`, `name`) values (?, ?)',
102+
origin: 'auto.db.otel.knex',
103+
}),
104+
105+
expect.objectContaining({
106+
data: expect.objectContaining({
107+
'knex.version': KNEX_VERSION,
108+
'db.operation': 'select',
109+
'db.sql.table': 'User',
110+
'db.system': 'mysql2',
111+
'db.name': 'tests',
112+
'db.statement': 'select * from `User`',
113+
'db.user': 'root',
114+
'sentry.origin': 'auto.db.otel.knex',
115+
'sentry.op': 'db',
116+
}),
117+
status: 'ok',
118+
description: 'select * from `User`',
119+
origin: 'auto.db.otel.knex',
120+
}),
121+
]),
122+
};
123+
124+
createRunner(__dirname, 'scenario-withMysql2.js')
125+
.withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port: 3306'] })
126+
.expect({ transaction: EXPECTED_TRANSACTION })
127+
.start(done);
128+
});
129+
});

packages/astro/src/index.server.ts

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export {
6969
isInitialized,
7070
kafkaIntegration,
7171
koaIntegration,
72+
knexIntegration,
7273
lastEventId,
7374
linkedErrorsIntegration,
7475
localVariablesIntegration,

packages/aws-serverless/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export {
9292
fsIntegration,
9393
genericPoolIntegration,
9494
graphqlIntegration,
95+
knexIntegration,
9596
kafkaIntegration,
9697
lruMemoizerIntegration,
9798
mongoIntegration,

packages/bun/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export {
113113
setupConnectErrorHandler,
114114
genericPoolIntegration,
115115
graphqlIntegration,
116+
knexIntegration,
116117
kafkaIntegration,
117118
lruMemoizerIntegration,
118119
mongoIntegration,

packages/google-cloud-serverless/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export {
9292
fastifyIntegration,
9393
genericPoolIntegration,
9494
graphqlIntegration,
95+
knexIntegration,
9596
kafkaIntegration,
9697
lruMemoizerIntegration,
9798
mongoIntegration,

packages/node/package.json

+3-6
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99
"engines": {
1010
"node": ">=14.18"
1111
},
12-
"files": [
13-
"/build"
14-
],
12+
"files": ["/build"],
1513
"main": "build/cjs/index.js",
1614
"module": "build/esm/index.js",
1715
"types": "build/types/index.d.ts",
@@ -56,9 +54,7 @@
5654
},
5755
"typesVersions": {
5856
"<4.9": {
59-
"build/types/index.d.ts": [
60-
"build/types-ts3.8/index.d.ts"
61-
]
57+
"build/types/index.d.ts": ["build/types-ts3.8/index.d.ts"]
6258
}
6359
},
6460
"publishConfig": {
@@ -81,6 +77,7 @@
8177
"@opentelemetry/instrumentation-http": "0.53.0",
8278
"@opentelemetry/instrumentation-ioredis": "0.43.0",
8379
"@opentelemetry/instrumentation-kafkajs": "0.4.0",
80+
"@opentelemetry/instrumentation-knex": "0.41.0",
8481
"@opentelemetry/instrumentation-koa": "0.43.0",
8582
"@opentelemetry/instrumentation-lru-memoizer": "0.40.0",
8683
"@opentelemetry/instrumentation-mongodb": "0.48.0",

packages/node/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/h
2828
export { koaIntegration, setupKoaErrorHandler } from './integrations/tracing/koa';
2929
export { connectIntegration, setupConnectErrorHandler } from './integrations/tracing/connect';
3030
export { spotlightIntegration } from './integrations/spotlight';
31+
export { knexIntegration } from './integrations/tracing/knex';
3132
export { tediousIntegration } from './integrations/tracing/tedious';
3233
export { genericPoolIntegration } from './integrations/tracing/genericPool';
3334
export { dataloaderIntegration } from './integrations/tracing/dataloader';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { KnexInstrumentation } from '@opentelemetry/instrumentation-knex';
2+
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, spanToJSON } from '@sentry/core';
3+
import type { IntegrationFn } from '@sentry/types';
4+
import { generateInstrumentOnce } from '../../otel/instrument';
5+
6+
const INTEGRATION_NAME = 'Knex';
7+
8+
export const instrumentKnex = generateInstrumentOnce(
9+
INTEGRATION_NAME,
10+
() => new KnexInstrumentation({ requireParentSpan: true }),
11+
);
12+
13+
const _knexIntegration = (() => {
14+
return {
15+
name: INTEGRATION_NAME,
16+
setupOnce() {
17+
instrumentKnex();
18+
},
19+
20+
setup(client) {
21+
client.on('spanStart', span => {
22+
const { data } = spanToJSON(span);
23+
// knex.version is always set in the span data
24+
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/0309caeafc44ac9cb13a3345b790b01b76d0497d/plugins/node/opentelemetry-instrumentation-knex/src/instrumentation.ts#L138
25+
if (data && 'knex.version' in data) {
26+
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.knex');
27+
}
28+
});
29+
},
30+
};
31+
}) satisfies IntegrationFn;
32+
33+
/**
34+
* Knex integration
35+
*
36+
* Capture tracing data for [Knex](https://knexjs.org/).
37+
*
38+
* @example
39+
* ```javascript
40+
* import * as Sentry from '@sentry/node';
41+
*
42+
* Sentry.init({
43+
* integrations: [Sentry.knexIntegration()],
44+
* });
45+
* ```
46+
*/
47+
export const knexIntegration = defineIntegration(_knexIntegration);

packages/remix/src/index.server.ts

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export {
6868
inboundFiltersIntegration,
6969
initOpenTelemetry,
7070
isInitialized,
71+
knexIntegration,
7172
kafkaIntegration,
7273
koaIntegration,
7374
lastEventId,

0 commit comments

Comments
 (0)