Skip to content

Commit b520d04

Browse files
authored
fix(instrumentation-pg): connection string parsing (#2715)
1 parent 353dbb0 commit b520d04

File tree

4 files changed

+110
-5
lines changed

4 files changed

+110
-5
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export interface PgPoolOptionsParams {
5353
user: string;
5454
idleTimeoutMillis: number; // the minimum amount of time that an object may sit idle in the pool before it is eligible for eviction due to idle time
5555
maxClient: number; // maximum size of the pool
56+
connectionString?: string; // connection string if provided directly
5657
}
5758

5859
export const EVENT_LISTENERS_SET = Symbol(

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

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,28 @@ export function parseNormalizedOperationName(queryText: string) {
106106
return sqlCommand.endsWith(';') ? sqlCommand.slice(0, -1) : sqlCommand;
107107
}
108108

109-
export function getConnectionString(params: PgParsedConnectionParams) {
109+
export function parseAndMaskConnectionString(connectionString: string): string {
110+
try {
111+
// Parse the connection string
112+
const url = new URL(connectionString);
113+
114+
// Remove all auth information (username and password)
115+
url.username = '';
116+
url.password = '';
117+
118+
return url.toString();
119+
} catch (e) {
120+
// If parsing fails, return a generic connection string
121+
return 'postgresql://localhost:5432/';
122+
}
123+
}
124+
125+
export function getConnectionString(
126+
params: PgParsedConnectionParams | PgPoolOptionsParams
127+
) {
128+
if ('connectionString' in params && params.connectionString) {
129+
return parseAndMaskConnectionString(params.connectionString);
130+
}
110131
const host = params.host || 'localhost';
111132
const port = params.port || 5432;
112133
const database = params.database || '';
@@ -138,13 +159,22 @@ export function getSemanticAttributesFromConnection(
138159
}
139160

140161
export function getSemanticAttributesFromPool(params: PgPoolOptionsParams) {
162+
let url: URL | undefined;
163+
try {
164+
url = params.connectionString
165+
? new URL(params.connectionString)
166+
: undefined;
167+
} catch (e) {
168+
url = undefined;
169+
}
170+
141171
return {
142172
[SEMATTRS_DB_SYSTEM]: DBSYSTEMVALUES_POSTGRESQL,
143-
[SEMATTRS_DB_NAME]: params.database, // required
173+
[SEMATTRS_DB_NAME]: url?.pathname.slice(1) ?? params.database, // required
144174
[SEMATTRS_DB_CONNECTION_STRING]: getConnectionString(params), // required
145-
[SEMATTRS_NET_PEER_NAME]: params.host, // required
146-
[SEMATTRS_NET_PEER_PORT]: getPort(params.port),
147-
[SEMATTRS_DB_USER]: params.user,
175+
[SEMATTRS_NET_PEER_NAME]: url?.hostname ?? params.host, // required
176+
[SEMATTRS_NET_PEER_PORT]: Number(url?.port) || getPort(params.port),
177+
[SEMATTRS_DB_USER]: url?.username ?? params.user,
148178
[AttributeNames.IDLE_TIMEOUT_MILLIS]: params.idleTimeoutMillis,
149179
[AttributeNames.MAX_CLIENT]: params.maxClient,
150180
};

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,36 @@ describe('pg-pool', () => {
221221
});
222222
});
223223

224+
// Test connection string support
225+
it('should handle connection string in pool options', async () => {
226+
const connectionString = `postgresql://${CONFIG.user}:${CONFIG.password}@${CONFIG.host}:${CONFIG.port}/${CONFIG.database}`;
227+
const poolWithConnString = new pgPool({
228+
connectionString,
229+
idleTimeoutMillis: CONFIG.idleTimeoutMillis,
230+
});
231+
232+
const expectedAttributes = {
233+
[SEMATTRS_DB_SYSTEM]: DBSYSTEMVALUES_POSTGRESQL,
234+
[SEMATTRS_DB_NAME]: CONFIG.database,
235+
[SEMATTRS_NET_PEER_NAME]: CONFIG.host,
236+
[SEMATTRS_DB_CONNECTION_STRING]: `postgresql://${CONFIG.host}:${CONFIG.port}/${CONFIG.database}`,
237+
[SEMATTRS_NET_PEER_PORT]: CONFIG.port,
238+
[SEMATTRS_DB_USER]: CONFIG.user,
239+
[AttributeNames.IDLE_TIMEOUT_MILLIS]: CONFIG.idleTimeoutMillis,
240+
};
241+
242+
const events: TimedEvent[] = [];
243+
const span = provider.getTracer('test-pg-pool').startSpan('test span');
244+
245+
await context.with(trace.setSpan(context.active(), span), async () => {
246+
const client = await poolWithConnString.connect();
247+
runCallbackTest(span, expectedAttributes, events, unsetStatus, 2, 1);
248+
client.release();
249+
});
250+
251+
await poolWithConnString.end();
252+
});
253+
224254
// callback - checkout a client
225255
it('should not return a promise if callback is provided', done => {
226256
const pgPoolAttributes = {

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,4 +254,48 @@ describe('utils.ts', () => {
254254
);
255255
});
256256
});
257+
258+
describe('.parseAndMaskConnectionString()', () => {
259+
it('should remove all auth information from connection string', () => {
260+
const connectionString =
261+
'postgresql://user:password123@localhost:5432/dbname';
262+
assert.strictEqual(
263+
utils.parseAndMaskConnectionString(connectionString),
264+
'postgresql://localhost:5432/dbname'
265+
);
266+
});
267+
268+
it('should remove username when no password is present', () => {
269+
const connectionString = 'postgresql://user@localhost:5432/dbname';
270+
assert.strictEqual(
271+
utils.parseAndMaskConnectionString(connectionString),
272+
'postgresql://localhost:5432/dbname'
273+
);
274+
});
275+
276+
it('should preserve connection string when no auth is present', () => {
277+
const connectionString = 'postgresql://localhost:5432/dbname';
278+
assert.strictEqual(
279+
utils.parseAndMaskConnectionString(connectionString),
280+
'postgresql://localhost:5432/dbname'
281+
);
282+
});
283+
284+
it('should preserve query parameters while removing auth', () => {
285+
const connectionString =
286+
'postgresql://user:pass@localhost/dbname?sslmode=verify-full&application_name=myapp';
287+
assert.strictEqual(
288+
utils.parseAndMaskConnectionString(connectionString),
289+
'postgresql://localhost/dbname?sslmode=verify-full&application_name=myapp'
290+
);
291+
});
292+
293+
it('should handle invalid connection string', () => {
294+
const connectionString = 'not-a-valid-url';
295+
assert.strictEqual(
296+
utils.parseAndMaskConnectionString(connectionString),
297+
'postgresql://localhost:5432/'
298+
);
299+
});
300+
});
257301
});

0 commit comments

Comments
 (0)