Skip to content

Commit 5f214eb

Browse files
authored
feat(pg): Patch client inside lib and lib/pg-native (#2563)
1 parent bcf1da7 commit 5f214eb

File tree

4 files changed

+172
-32
lines changed

4 files changed

+172
-32
lines changed

plugins/node/opentelemetry-instrumentation-pg/src/instrumentation.ts

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
InstrumentationBase,
1919
InstrumentationNodeModuleDefinition,
2020
safeExecuteInTheMiddle,
21+
InstrumentationNodeModuleFile,
2122
} from '@opentelemetry/instrumentation';
2223
import {
2324
context,
@@ -67,6 +68,12 @@ import {
6768
ATTR_DB_OPERATION_NAME,
6869
} from './semconv';
6970

71+
function extractModuleExports(module: any) {
72+
return module[Symbol.toStringTag] === 'Module'
73+
? module.default // ESM
74+
: module; // CommonJS
75+
}
76+
7077
export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConfig> {
7178
private _operationDuration!: Histogram;
7279
private _connectionsCount!: UpDownCounter;
@@ -125,45 +132,38 @@ export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConf
125132
}
126133

127134
protected init() {
128-
const modulePG = new InstrumentationNodeModuleDefinition(
129-
'pg',
130-
['>=8.0.3 <9'],
131-
(module: any) => {
132-
const moduleExports: typeof pgTypes =
133-
module[Symbol.toStringTag] === 'Module'
134-
? module.default // ESM
135-
: module; // CommonJS
136-
if (isWrapped(moduleExports.Client.prototype.query)) {
137-
this._unwrap(moduleExports.Client.prototype, 'query');
138-
}
135+
const SUPPORTED_PG_VERSIONS = ['>=8.0.3 <9'];
139136

140-
if (isWrapped(moduleExports.Client.prototype.connect)) {
141-
this._unwrap(moduleExports.Client.prototype, 'connect');
142-
}
137+
const modulePgNativeClient = new InstrumentationNodeModuleFile(
138+
'pg/lib/native/client.js',
139+
SUPPORTED_PG_VERSIONS,
140+
this._patchPgClient.bind(this),
141+
this._unpatchPgClient.bind(this)
142+
);
143143

144-
this._wrap(
145-
moduleExports.Client.prototype,
146-
'query',
147-
this._getClientQueryPatch() as any
148-
);
144+
const modulePgClient = new InstrumentationNodeModuleFile(
145+
'pg/lib/client.js',
146+
SUPPORTED_PG_VERSIONS,
147+
this._patchPgClient.bind(this),
148+
this._unpatchPgClient.bind(this)
149+
);
149150

150-
this._wrap(
151-
moduleExports.Client.prototype,
152-
'connect',
153-
this._getClientConnectPatch() as any
154-
);
151+
const modulePG = new InstrumentationNodeModuleDefinition(
152+
'pg',
153+
SUPPORTED_PG_VERSIONS,
154+
(module: any) => {
155+
const moduleExports = extractModuleExports(module);
155156

157+
this._patchPgClient(moduleExports.Client);
156158
return module;
157159
},
158160
(module: any) => {
159-
const moduleExports: typeof pgTypes =
160-
module[Symbol.toStringTag] === 'Module'
161-
? module.default // ESM
162-
: module; // CommonJS
163-
if (isWrapped(moduleExports.Client.prototype.query)) {
164-
this._unwrap(moduleExports.Client.prototype, 'query');
165-
}
166-
}
161+
const moduleExports = extractModuleExports(module);
162+
163+
this._unpatchPgClient(moduleExports.Client);
164+
return module;
165+
},
166+
[modulePgClient, modulePgNativeClient]
167167
);
168168

169169
const modulePGPool = new InstrumentationNodeModuleDefinition(
@@ -190,6 +190,50 @@ export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConf
190190
return [modulePG, modulePGPool];
191191
}
192192

193+
private _patchPgClient(module: any) {
194+
if (!module) {
195+
return;
196+
}
197+
198+
const moduleExports = extractModuleExports(module);
199+
200+
if (isWrapped(moduleExports.prototype.query)) {
201+
this._unwrap(moduleExports.prototype, 'query');
202+
}
203+
204+
if (isWrapped(moduleExports.prototype.connect)) {
205+
this._unwrap(moduleExports.prototype, 'connect');
206+
}
207+
208+
this._wrap(
209+
moduleExports.prototype,
210+
'query',
211+
this._getClientQueryPatch() as any
212+
);
213+
214+
this._wrap(
215+
moduleExports.prototype,
216+
'connect',
217+
this._getClientConnectPatch() as any
218+
);
219+
220+
return module;
221+
}
222+
223+
private _unpatchPgClient(module: any) {
224+
const moduleExports = extractModuleExports(module);
225+
226+
if (isWrapped(moduleExports.prototype.query)) {
227+
this._unwrap(moduleExports.prototype, 'query');
228+
}
229+
230+
if (isWrapped(moduleExports.prototype.connect)) {
231+
this._unwrap(moduleExports.prototype, 'connect');
232+
}
233+
234+
return module;
235+
}
236+
193237
private _getClientConnectPatch() {
194238
const plugin = this;
195239
return (original: PgClientConnect) => {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Use postgres from an ES module:
18+
// node --experimental-loader=@opentelemetry/instrumentation/hook.mjs use-pg.mjs
19+
20+
import { trace } from '@opentelemetry/api';
21+
import { createTestNodeSdk } from '@opentelemetry/contrib-test-utils';
22+
import assert from 'assert';
23+
24+
import { PgInstrumentation } from '../../build/src/index.js';
25+
26+
const CONFIG = {
27+
user: process.env.POSTGRES_USER || 'postgres',
28+
password: process.env.POSTGRES_PASSWORD || 'postgres',
29+
database: process.env.POSTGRES_DB || 'postgres',
30+
host: process.env.POSTGRES_HOST || 'localhost',
31+
port: process.env.POSTGRES_PORT
32+
? parseInt(process.env.POSTGRES_PORT, 10)
33+
: 54320,
34+
};
35+
36+
const sdk = createTestNodeSdk({
37+
serviceName: 'use-pg',
38+
instrumentations: [new PgInstrumentation()],
39+
});
40+
sdk.start();
41+
42+
import pg from 'pg';
43+
const client = new pg.Client(CONFIG);
44+
45+
await client.connect();
46+
47+
const tracer = trace.getTracer();
48+
49+
await tracer.startActiveSpan('test-span', async span => {
50+
const res = await client.query('SELECT NOW()');
51+
52+
assert.ok(res);
53+
span.end();
54+
55+
await client.end();
56+
await sdk.shutdown();
57+
});

plugins/node/opentelemetry-instrumentation-pg/test/pg-pool.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ describe('pg-pool', () => {
114114
function create(config: PgInstrumentationConfig = {}) {
115115
instrumentation.setConfig(config);
116116
instrumentation.enable();
117+
118+
// Disable and enable the instrumentation to visit unwrap calls
119+
instrumentation.disable();
120+
instrumentation.enable();
117121
}
118122

119123
let pool: pgPool<pg.Client>;

plugins/node/opentelemetry-instrumentation-pg/test/pg.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ describe('pg', () => {
108108
function create(config: PgInstrumentationConfig = {}) {
109109
instrumentation.setConfig(config);
110110
instrumentation.enable();
111+
112+
// Disable and enable the instrumentation to visit unwrap calls
113+
instrumentation.disable();
114+
instrumentation.enable();
111115
}
112116

113117
let postgres: typeof pg;
@@ -152,13 +156,15 @@ describe('pg', () => {
152156

153157
postgres = require('pg');
154158
client = new postgres.Client(CONFIG);
159+
155160
await client.connect();
156161
});
157162

158163
after(async () => {
159164
if (testPostgresLocally) {
160165
testUtils.cleanUpDocker('postgres');
161166
}
167+
162168
await client.end();
163169
});
164170

@@ -1087,3 +1093,32 @@ describe('pg', () => {
10871093
});
10881094
});
10891095
});
1096+
1097+
describe('pg (ESM)', () => {
1098+
it('should work with ESM usage', async () => {
1099+
await testUtils.runTestFixture({
1100+
cwd: __dirname,
1101+
argv: ['fixtures/use-pg.mjs'],
1102+
env: {
1103+
NODE_OPTIONS:
1104+
'--experimental-loader=@opentelemetry/instrumentation/hook.mjs',
1105+
NODE_NO_WARNINGS: '1',
1106+
},
1107+
checkResult: (err, stdout, stderr) => {
1108+
assert.ifError(err);
1109+
},
1110+
checkCollector: (collector: testUtils.TestCollector) => {
1111+
const spans = collector.sortedSpans;
1112+
1113+
assert.strictEqual(spans.length, 3);
1114+
1115+
assert.strictEqual(spans[0].name, 'pg.connect');
1116+
assert.strictEqual(spans[0].kind, 3);
1117+
assert.strictEqual(spans[1].name, 'test-span');
1118+
assert.strictEqual(spans[1].kind, 1);
1119+
assert.strictEqual(spans[2].name, 'pg.query:SELECT otel_pg_database');
1120+
assert.strictEqual(spans[2].kind, 3);
1121+
},
1122+
});
1123+
});
1124+
});

0 commit comments

Comments
 (0)