Skip to content

Commit 510ba3e

Browse files
feat(node): Add postgresjs instrumentation (#16665)
Resolves: #15621 Adds instrumentation for https://github.com/porsager/postgres Sampled event: (Emitted from the integration tests added here): [Link](https://sentry-sdks.sentry.io/insights/backend/summary/trace/72c94a37c9907cc2c7f4bef9c56b0196/?fov=0%2C32.09936037659645&node=span-b3505cfada7dea73&project=5429215&query=transaction.op%3Atransaction&referrer=insights-backend-overview&source=performance_transaction_summary&statsPeriod=5m&timestamp=1750718572&transaction=Test%20Transaction) This implementation patches `connection` and `query` classes to create database transactions: - From `connection`, we pick up the database `name`, `url` and `port` to use in the db query spans - For each `query` instance, we create a `db` span - This implementation does not create a separate span for each `cursor` used Initially, I implemented a way to capture `db.operation` (as `command` is available when the query resolves) but it seems the ingestion extracts the operation anyway, so I removed it. Also added sanitization/normalization for raw query, which we use as the span description, also seems to be normalized by the ingestion engine. We can remove it too if it's not worth having, as it creates a possibly-unnecessary performance overhead on the SDK side. --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent f916be1 commit 510ba3e

File tree

14 files changed

+643
-0
lines changed

14 files changed

+643
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"node-cron": "^3.0.3",
6262
"node-schedule": "^2.1.1",
6363
"pg": "8.16.0",
64+
"postgres": "^3.4.7",
6465
"proxy": "^2.1.1",
6566
"redis-4": "npm:redis@^4.6.14",
6667
"reflect-metadata": "0.2.1",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
version: '3.9'
2+
3+
services:
4+
db:
5+
image: postgres:13
6+
restart: always
7+
container_name: integration-tests-postgresjs
8+
ports:
9+
- '5444:5432'
10+
environment:
11+
POSTGRES_USER: test
12+
POSTGRES_PASSWORD: test
13+
POSTGRES_DB: test_db
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
10+
11+
// Stop the process from exiting before the transaction is sent
12+
setInterval(() => {}, 1000);
13+
14+
const postgres = require('postgres');
15+
16+
const sql = postgres({ port: 5444, user: 'test', password: 'test', database: 'test_db' });
17+
18+
async function run() {
19+
await Sentry.startSpan(
20+
{
21+
name: 'Test Transaction',
22+
op: 'transaction',
23+
},
24+
async () => {
25+
try {
26+
await sql`
27+
CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"));
28+
`;
29+
30+
await sql`
31+
INSERT INTO "User" ("email", "name") VALUES ('Foo', 'bar@baz.com');
32+
`;
33+
34+
await sql`
35+
UPDATE "User" SET "name" = 'Foo' WHERE "email" = 'bar@baz.com';
36+
`;
37+
38+
await sql`
39+
SELECT * FROM "User" WHERE "email" = 'bar@baz.com';
40+
`;
41+
42+
await sql`SELECT * from generate_series(1,1000) as x `.cursor(10, async rows => {
43+
await Promise.all(rows);
44+
});
45+
46+
await sql`
47+
DROP TABLE "User";
48+
`;
49+
50+
// This will be captured as an error as the table no longer exists
51+
await sql`
52+
SELECT * FROM "User" WHERE "email" = 'foo@baz.com';
53+
`;
54+
} finally {
55+
await sql.end();
56+
}
57+
},
58+
);
59+
}
60+
61+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
62+
run();
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import { describe, expect, test } from 'vitest';
2+
import { createRunner } from '../../../utils/runner';
3+
4+
const EXISTING_TEST_EMAIL = 'bar@baz.com';
5+
const NON_EXISTING_TEST_EMAIL = 'foo@baz.com';
6+
7+
describe('postgresjs auto instrumentation', () => {
8+
test('should auto-instrument `postgres` package', { timeout: 60_000 }, async () => {
9+
const EXPECTED_TRANSACTION = {
10+
transaction: 'Test Transaction',
11+
spans: expect.arrayContaining([
12+
expect.objectContaining({
13+
data: expect.objectContaining({
14+
'db.namespace': 'test_db',
15+
'db.system.name': 'postgres',
16+
'db.operation.name': 'CREATE TABLE',
17+
'db.query.text':
18+
'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))',
19+
'sentry.op': 'db',
20+
'sentry.origin': 'auto.db.otel.postgres',
21+
'server.address': 'localhost',
22+
'server.port': 5444,
23+
}),
24+
description:
25+
'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))',
26+
op: 'db',
27+
status: 'ok',
28+
origin: 'auto.db.otel.postgres',
29+
parent_span_id: expect.any(String),
30+
span_id: expect.any(String),
31+
start_timestamp: expect.any(Number),
32+
timestamp: expect.any(Number),
33+
trace_id: expect.any(String),
34+
}),
35+
expect.objectContaining({
36+
data: expect.objectContaining({
37+
'db.namespace': 'test_db',
38+
'db.system.name': 'postgres',
39+
'db.operation.name': 'SELECT',
40+
'db.query.text':
41+
"select b.oid, b.typarray from pg_catalog.pg_type a left join pg_catalog.pg_type b on b.oid = a.typelem where a.typcategory = 'A' group by b.oid, b.typarray order by b.oid",
42+
'sentry.op': 'db',
43+
'sentry.origin': 'auto.db.otel.postgres',
44+
'server.address': 'localhost',
45+
'server.port': 5444,
46+
}),
47+
description:
48+
"select b.oid, b.typarray from pg_catalog.pg_type a left join pg_catalog.pg_type b on b.oid = a.typelem where a.typcategory = 'A' group by b.oid, b.typarray order by b.oid",
49+
op: 'db',
50+
status: 'ok',
51+
origin: 'auto.db.otel.postgres',
52+
parent_span_id: expect.any(String),
53+
span_id: expect.any(String),
54+
start_timestamp: expect.any(Number),
55+
timestamp: expect.any(Number),
56+
trace_id: expect.any(String),
57+
}),
58+
expect.objectContaining({
59+
data: expect.objectContaining({
60+
'db.namespace': 'test_db',
61+
'db.system.name': 'postgres',
62+
'db.operation.name': 'INSERT',
63+
'db.query.text': `INSERT INTO "User" ("email", "name") VALUES ('Foo', '${EXISTING_TEST_EMAIL}')`,
64+
'sentry.origin': 'auto.db.otel.postgres',
65+
'sentry.op': 'db',
66+
'server.address': 'localhost',
67+
'server.port': 5444,
68+
}),
69+
description: `INSERT INTO "User" ("email", "name") VALUES ('Foo', '${EXISTING_TEST_EMAIL}')`,
70+
op: 'db',
71+
status: 'ok',
72+
origin: 'auto.db.otel.postgres',
73+
parent_span_id: expect.any(String),
74+
span_id: expect.any(String),
75+
start_timestamp: expect.any(Number),
76+
timestamp: expect.any(Number),
77+
trace_id: expect.any(String),
78+
}),
79+
expect.objectContaining({
80+
data: expect.objectContaining({
81+
'db.namespace': 'test_db',
82+
'db.system.name': 'postgres',
83+
'db.operation.name': 'UPDATE',
84+
'db.query.text': `UPDATE "User" SET "name" = 'Foo' WHERE "email" = '${EXISTING_TEST_EMAIL}'`,
85+
'sentry.op': 'db',
86+
'sentry.origin': 'auto.db.otel.postgres',
87+
'server.address': 'localhost',
88+
'server.port': 5444,
89+
}),
90+
description: `UPDATE "User" SET "name" = 'Foo' WHERE "email" = '${EXISTING_TEST_EMAIL}'`,
91+
op: 'db',
92+
status: 'ok',
93+
origin: 'auto.db.otel.postgres',
94+
parent_span_id: expect.any(String),
95+
span_id: expect.any(String),
96+
start_timestamp: expect.any(Number),
97+
timestamp: expect.any(Number),
98+
trace_id: expect.any(String),
99+
}),
100+
expect.objectContaining({
101+
data: expect.objectContaining({
102+
'db.namespace': 'test_db',
103+
'db.system.name': 'postgres',
104+
'db.operation.name': 'SELECT',
105+
'db.query.text': `SELECT * FROM "User" WHERE "email" = '${EXISTING_TEST_EMAIL}'`,
106+
'sentry.op': 'db',
107+
'sentry.origin': 'auto.db.otel.postgres',
108+
'server.address': 'localhost',
109+
'server.port': 5444,
110+
}),
111+
description: `SELECT * FROM "User" WHERE "email" = '${EXISTING_TEST_EMAIL}'`,
112+
op: 'db',
113+
status: 'ok',
114+
origin: 'auto.db.otel.postgres',
115+
parent_span_id: expect.any(String),
116+
span_id: expect.any(String),
117+
start_timestamp: expect.any(Number),
118+
timestamp: expect.any(Number),
119+
trace_id: expect.any(String),
120+
}),
121+
expect.objectContaining({
122+
data: expect.objectContaining({
123+
'db.namespace': 'test_db',
124+
'db.system.name': 'postgres',
125+
'db.operation.name': 'SELECT',
126+
'db.query.text': 'SELECT * from generate_series(?,?) as x',
127+
'sentry.op': 'db',
128+
'sentry.origin': 'auto.db.otel.postgres',
129+
'server.address': 'localhost',
130+
'server.port': 5444,
131+
}),
132+
description: 'SELECT * from generate_series(?,?) as x',
133+
op: 'db',
134+
status: 'ok',
135+
origin: 'auto.db.otel.postgres',
136+
parent_span_id: expect.any(String),
137+
span_id: expect.any(String),
138+
start_timestamp: expect.any(Number),
139+
timestamp: expect.any(Number),
140+
trace_id: expect.any(String),
141+
}),
142+
expect.objectContaining({
143+
data: expect.objectContaining({
144+
'db.namespace': 'test_db',
145+
'db.system.name': 'postgres',
146+
'db.operation.name': 'DROP TABLE',
147+
'db.query.text': 'DROP TABLE "User"',
148+
'sentry.op': 'db',
149+
'sentry.origin': 'auto.db.otel.postgres',
150+
'server.address': 'localhost',
151+
'server.port': 5444,
152+
}),
153+
description: 'DROP TABLE "User"',
154+
op: 'db',
155+
status: 'ok',
156+
origin: 'auto.db.otel.postgres',
157+
parent_span_id: expect.any(String),
158+
span_id: expect.any(String),
159+
start_timestamp: expect.any(Number),
160+
timestamp: expect.any(Number),
161+
trace_id: expect.any(String),
162+
}),
163+
expect.objectContaining({
164+
data: expect.objectContaining({
165+
'db.namespace': 'test_db',
166+
'db.system.name': 'postgres',
167+
// No db.operation.name here, as this is an errored span
168+
'db.response.status_code': '42P01',
169+
'error.type': 'PostgresError',
170+
'db.query.text': `SELECT * FROM "User" WHERE "email" = '${NON_EXISTING_TEST_EMAIL}'`,
171+
'sentry.op': 'db',
172+
'sentry.origin': 'auto.db.otel.postgres',
173+
'server.address': 'localhost',
174+
'server.port': 5444,
175+
}),
176+
description: `SELECT * FROM "User" WHERE "email" = '${NON_EXISTING_TEST_EMAIL}'`,
177+
op: 'db',
178+
status: 'unknown_error',
179+
origin: 'auto.db.otel.postgres',
180+
parent_span_id: expect.any(String),
181+
span_id: expect.any(String),
182+
start_timestamp: expect.any(Number),
183+
timestamp: expect.any(Number),
184+
trace_id: expect.any(String),
185+
}),
186+
]),
187+
};
188+
189+
const EXPECTED_ERROR_EVENT = {
190+
event_id: expect.any(String),
191+
contexts: {
192+
trace: {
193+
trace_id: expect.any(String),
194+
span_id: expect.any(String),
195+
},
196+
},
197+
exception: {
198+
values: [
199+
{
200+
type: 'PostgresError',
201+
value: 'relation "User" does not exist',
202+
stacktrace: expect.objectContaining({
203+
frames: expect.arrayContaining([
204+
expect.objectContaining({
205+
function: 'handle',
206+
module: 'postgres.cjs.src:connection',
207+
filename: expect.any(String),
208+
lineno: expect.any(Number),
209+
colno: expect.any(Number),
210+
}),
211+
]),
212+
}),
213+
},
214+
],
215+
},
216+
};
217+
218+
await createRunner(__dirname, 'scenario.js')
219+
.withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] })
220+
.expect({ transaction: EXPECTED_TRANSACTION })
221+
.expect({ event: EXPECTED_ERROR_EVENT })
222+
.start()
223+
.completed();
224+
});
225+
});

packages/astro/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export {
8484
onUnhandledRejectionIntegration,
8585
parameterize,
8686
postgresIntegration,
87+
postgresJsIntegration,
8788
prismaIntegration,
8889
childProcessIntegration,
8990
createSentryWinstonTransport,

packages/aws-serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export {
9999
redisIntegration,
100100
tediousIntegration,
101101
postgresIntegration,
102+
postgresJsIntegration,
102103
prismaIntegration,
103104
childProcessIntegration,
104105
createSentryWinstonTransport,

packages/bun/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export {
120120
redisIntegration,
121121
tediousIntegration,
122122
postgresIntegration,
123+
postgresJsIntegration,
123124
prismaIntegration,
124125
hapiIntegration,
125126
setupHapiErrorHandler,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export {
9999
redisIntegration,
100100
tediousIntegration,
101101
postgresIntegration,
102+
postgresJsIntegration,
102103
prismaIntegration,
103104
hapiIntegration,
104105
setupHapiErrorHandler,

packages/node/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export { mysqlIntegration } from './integrations/tracing/mysql';
2323
export { mysql2Integration } from './integrations/tracing/mysql2';
2424
export { redisIntegration } from './integrations/tracing/redis';
2525
export { postgresIntegration } from './integrations/tracing/postgres';
26+
export { postgresJsIntegration } from './integrations/tracing/postgresjs';
2627
export { prismaIntegration } from './integrations/tracing/prisma';
2728
export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi';
2829
export { koaIntegration, setupKoaErrorHandler } from './integrations/tracing/koa';

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { instrumentMongoose, mongooseIntegration } from './mongoose';
1515
import { instrumentMysql, mysqlIntegration } from './mysql';
1616
import { instrumentMysql2, mysql2Integration } from './mysql2';
1717
import { instrumentPostgres, postgresIntegration } from './postgres';
18+
import { instrumentPostgresJs, postgresJsIntegration } from './postgresjs';
1819
import { prismaIntegration } from './prisma';
1920
import { instrumentRedis, redisIntegration } from './redis';
2021
import { instrumentTedious, tediousIntegration } from './tedious';
@@ -44,6 +45,7 @@ export function getAutoPerformanceIntegrations(): Integration[] {
4445
amqplibIntegration(),
4546
lruMemoizerIntegration(),
4647
vercelAIIntegration(),
48+
postgresJsIntegration(),
4749
];
4850
}
4951

@@ -75,5 +77,6 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) =>
7577
instrumentGenericPool,
7678
instrumentAmqplib,
7779
instrumentVercelAi,
80+
instrumentPostgresJs,
7881
];
7982
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy