Skip to content

Commit fb514f7

Browse files
authored
feat(pg-instrumentation): record exceptions as span events (#3182)
1 parent c9d86c3 commit fb514f7

File tree

4 files changed

+252
-39
lines changed

4 files changed

+252
-39
lines changed

packages/instrumentation-pg/src/instrumentation.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,8 @@ export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConf
339339
values: Array.isArray(args[1]) ? args[1] : undefined,
340340
}
341341
: firstArgIsQueryObjectWithText
342-
? (arg0 as utils.ObjectWithText)
343-
: undefined;
342+
? (arg0 as utils.ObjectWithText)
343+
: undefined;
344344

345345
const attributes: Attributes = {
346346
[ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_POSTGRESQL,
@@ -469,6 +469,9 @@ export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConf
469469
try {
470470
result = original.apply(this, args as never);
471471
} catch (e: unknown) {
472+
if (e instanceof Error) {
473+
span.recordException(utils.sanitizedErrorMessage(e));
474+
}
472475
span.setStatus({
473476
code: SpanStatusCode.ERROR,
474477
message: utils.getErrorMessage(e),
@@ -491,6 +494,9 @@ export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConf
491494
})
492495
.catch((error: Error) => {
493496
return new Promise((_, reject) => {
497+
if (error instanceof Error) {
498+
span.recordException(utils.sanitizedErrorMessage(error));
499+
}
494500
span.setStatus({
495501
code: SpanStatusCode.ERROR,
496502
message: error.message,
@@ -612,6 +618,9 @@ function handleConnectResult(span: Span, connectResult: unknown) {
612618
return result;
613619
})
614620
.catch((error: unknown) => {
621+
if (error instanceof Error) {
622+
span.recordException(utils.sanitizedErrorMessage(error));
623+
}
615624
span.setStatus({
616625
code: SpanStatusCode.ERROR,
617626
message: utils.getErrorMessage(error),

packages/instrumentation-pg/src/utils.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,9 @@ export function patchCallback(
342342
if (Object.prototype.hasOwnProperty.call(err, 'code')) {
343343
attributes[ATTR_ERROR_TYPE] = (err as any)['code'];
344344
}
345-
345+
if (err instanceof Error) {
346+
span.recordException(sanitizedErrorMessage(err));
347+
}
346348
span.setStatus({
347349
code: SpanStatusCode.ERROR,
348350
message: err.message,
@@ -412,6 +414,9 @@ export function patchCallbackPGPool(
412414
done: any
413415
) {
414416
if (err) {
417+
if (err instanceof Error) {
418+
span.recordException(sanitizedErrorMessage(err));
419+
}
415420
span.setStatus({
416421
code: SpanStatusCode.ERROR,
417422
message: err.message,
@@ -428,6 +433,9 @@ export function patchClientConnectCallback(span: Span, cb: Function): Function {
428433
err: Error
429434
) {
430435
if (err) {
436+
if (err instanceof Error) {
437+
span.recordException(sanitizedErrorMessage(err));
438+
}
431439
span.setStatus({
432440
code: SpanStatusCode.ERROR,
433441
message: err.message,
@@ -460,3 +468,14 @@ export type ObjectWithText = {
460468
text: string;
461469
[k: string]: unknown;
462470
};
471+
472+
/**
473+
* Generates a sanitized message for the error.
474+
* Only includes the error type and PostgreSQL error code, omitting any sensitive details.
475+
*/
476+
export function sanitizedErrorMessage(error: unknown): string {
477+
const name = (error as any)?.name ?? 'PostgreSQLError';
478+
const code = (error as any)?.code ?? 'UNKNOWN';
479+
480+
return `PostgreSQL error of type '${name}' occurred (code: ${code})`;
481+
}

packages/instrumentation-pg/test/pg-pool.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,127 @@ describe('pg-pool', () => {
530530
});
531531
});
532532

533+
describe('exception event recording', () => {
534+
const queryText = 'SELECT foo FROM nonexistent_table';
535+
536+
it('should record exceptions as events on spans for a query to a nonexistent table (callback)', done => {
537+
const parentSpan = provider
538+
.getTracer('test-pg-pool')
539+
.startSpan('test span');
540+
context.with(trace.setSpan(context.active(), parentSpan), () => {
541+
pool.query(queryText, err => {
542+
assert.notEqual(err, null, 'Expected query to throw an error');
543+
544+
const spans = memoryExporter.getFinishedSpans();
545+
546+
const querySpan = spans.find(
547+
s =>
548+
s.attributes?.[ATTR_DB_STATEMENT] &&
549+
String(s.attributes[ATTR_DB_STATEMENT]).includes(
550+
'nonexistent_table'
551+
)
552+
);
553+
assert.ok(
554+
querySpan,
555+
'Expected a span for the nonexistent table query'
556+
);
557+
558+
const exceptionEvents = querySpan.events.filter(
559+
e => e.name === 'exception'
560+
);
561+
assert.ok(
562+
exceptionEvents.length > 0,
563+
'Expected at least one exception event'
564+
);
565+
566+
exceptionEvents.forEach(e => {
567+
const attrs = e.attributes!;
568+
const code = '42P01';
569+
570+
const message = attrs['exception.message'];
571+
console.log('exception message:', message);
572+
assert.ok(message, 'exception.message should exist');
573+
assert.strictEqual(
574+
typeof message,
575+
'string',
576+
'exception.message should be a string'
577+
);
578+
579+
if (typeof message === 'string') {
580+
assert.ok(
581+
message.includes(code),
582+
`exception.message should include the Postgres error code ${code}`
583+
);
584+
}
585+
});
586+
587+
memoryExporter.reset();
588+
done();
589+
});
590+
});
591+
});
592+
593+
it('should record exceptions as events on spans for a query to a nonexistent table (async-await)', async () => {
594+
const parentSpan = provider
595+
.getTracer('test-pg-pool')
596+
.startSpan('test span');
597+
598+
await context.with(
599+
trace.setSpan(context.active(), parentSpan),
600+
async () => {
601+
try {
602+
await pool.query(queryText);
603+
assert.fail('Expected query to throw an error');
604+
} catch {
605+
const spans = memoryExporter.getFinishedSpans();
606+
607+
const querySpan = spans.find(
608+
s =>
609+
s.attributes?.[ATTR_DB_STATEMENT] &&
610+
String(s.attributes[ATTR_DB_STATEMENT]).includes(
611+
'nonexistent_table'
612+
)
613+
);
614+
assert.ok(
615+
querySpan,
616+
'Expected a span for the nonexistent table query'
617+
);
618+
619+
const exceptionEvents = querySpan.events.filter(
620+
e => e.name === 'exception'
621+
);
622+
assert.ok(
623+
exceptionEvents.length > 0,
624+
'Expected at least one exception event'
625+
);
626+
627+
exceptionEvents.forEach(e => {
628+
const attrs = e.attributes!;
629+
const code = '42P01';
630+
631+
const message = attrs['exception.message'];
632+
assert.ok(message, 'exception.message should exist');
633+
assert.strictEqual(
634+
typeof message,
635+
'string',
636+
'exception.message should be a string'
637+
);
638+
639+
if (typeof message === 'string') {
640+
assert.ok(
641+
message.includes(code),
642+
`exception.message should include the Postgres error code ${code}`
643+
);
644+
}
645+
});
646+
647+
memoryExporter.reset();
648+
}
649+
}
650+
);
651+
});
652+
});
653+
533654
describe('pg metrics', () => {
534655
let metricReader: testUtils.TestMetricReader;
535656

packages/instrumentation-pg/test/pg.test.ts

Lines changed: 100 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,6 @@ const DEFAULT_ATTRIBUTES = {
8383
const unsetStatus: SpanStatus = {
8484
code: SpanStatusCode.UNSET,
8585
};
86-
const errorStatus: SpanStatus = {
87-
code: SpanStatusCode.ERROR,
88-
};
8986

9087
const runCallbackTest = (
9188
span: Span | null,
@@ -161,6 +158,8 @@ describe('pg', () => {
161158
});
162159

163160
beforeEach(() => {
161+
memoryExporter.reset();
162+
164163
contextManager = new AsyncLocalStorageContextManager().enable();
165164
context.setGlobalContextManager(contextManager);
166165

@@ -192,47 +191,54 @@ describe('pg', () => {
192191
return /node_modules[/\\]pg/.test(src);
193192
};
194193

195-
assert.throws(
196-
() => {
197-
(client as any).query();
194+
const errorThrowCases = [
195+
{ fn: () => (client as any).query(), desc: 'no args provided' },
196+
{ fn: () => (client as any).query(null), desc: 'null as only arg' },
197+
{
198+
fn: () => (client as any).query(undefined),
199+
desc: 'undefined as only arg',
198200
},
199-
assertPgError,
200-
'pg should throw when no args provided'
201-
);
202-
runCallbackTest(null, DEFAULT_ATTRIBUTES, [], errorStatus);
203-
memoryExporter.reset();
201+
];
204202

205-
assert.throws(
206-
() => {
207-
(client as any).query(null);
208-
},
209-
assertPgError,
210-
'pg should throw when null provided as only arg'
211-
);
212-
runCallbackTest(null, DEFAULT_ATTRIBUTES, [], errorStatus);
213-
memoryExporter.reset();
203+
errorThrowCases.forEach(({ fn, desc }) => {
204+
assert.throws(fn, assertPgError, `pg should throw when ${desc}`);
205+
const spans = memoryExporter.getFinishedSpans();
206+
assert.ok(spans.length > 0, 'No spans recorded');
214207

215-
assert.throws(
216-
() => {
217-
(client as any).query(undefined);
218-
},
219-
assertPgError,
220-
'pg should throw when undefined provided as only arg'
221-
);
222-
runCallbackTest(null, DEFAULT_ATTRIBUTES, [], errorStatus);
223-
memoryExporter.reset();
208+
const exceptionEvents = spans[0].events.filter(
209+
e => e.name === 'exception'
210+
);
211+
212+
assert.strictEqual(
213+
exceptionEvents.length,
214+
1,
215+
'Expected one exception event'
216+
);
217+
218+
const event = exceptionEvents[0];
219+
assert.strictEqual(
220+
event.attributes!['exception.message'],
221+
"PostgreSQL error of type 'TypeError' occurred (code: UNKNOWN)"
222+
);
223+
assert.ok(event.time.length === 2, 'Event time should be a HrTime array');
224+
225+
memoryExporter.reset();
226+
});
224227

225228
assert.doesNotThrow(
226229
() =>
227230
(client as any).query({ foo: 'bar' }, undefined, () => {
228-
runCallbackTest(
229-
null,
230-
{
231-
...DEFAULT_ATTRIBUTES,
232-
},
233-
[],
234-
errorStatus
231+
const spans = memoryExporter.getFinishedSpans();
232+
assert.strictEqual(spans.length, 1);
233+
const exceptionEvents = spans[0].events.filter(
234+
e => e.name === 'exception'
235+
);
236+
assert.strictEqual(
237+
exceptionEvents.length,
238+
1,
239+
'Expected 1 exception event'
235240
);
241+
memoryExporter.reset();
236242
}),
237243
'pg should not throw when invalid config args are provided'
238244
);
@@ -982,6 +988,64 @@ describe('pg', () => {
982988
});
983989
});
984990

991+
describe('exception event recording', () => {
992+
const assertExceptionEvents = (pgSpan: any) => {
993+
assert.strictEqual(
994+
pgSpan.status.code,
995+
SpanStatusCode.ERROR,
996+
'Span should have ERROR status'
997+
);
998+
999+
const exceptionEvents = pgSpan.events.filter(
1000+
(e: { name: string }) => e.name === 'exception'
1001+
);
1002+
assert.ok(
1003+
exceptionEvents.length > 0,
1004+
'Expected at least one exception event'
1005+
);
1006+
1007+
exceptionEvents.forEach((err: { attributes: any }) => {
1008+
const attrs = err.attributes!;
1009+
assert.ok(attrs['exception.message'], 'Expected exception.message');
1010+
const code = '42P01';
1011+
assert.ok(
1012+
attrs['exception.message'].includes(code),
1013+
`Expected exception.message to include error code ${code}`
1014+
);
1015+
});
1016+
};
1017+
1018+
it('should record exceptions as events on spans for a query to a nonexistent table (callback)', done => {
1019+
client.query('SELECT foo FROM nonexistent_table', err => {
1020+
assert.notEqual(err, null, 'Expected query to throw an error');
1021+
1022+
const spans = memoryExporter.getFinishedSpans();
1023+
assert.strictEqual(spans.length, 1, 'Expected one finished span');
1024+
const pgSpan = spans[0];
1025+
1026+
assertExceptionEvents(pgSpan);
1027+
1028+
memoryExporter.reset();
1029+
done();
1030+
});
1031+
});
1032+
1033+
it('should record exceptions as events on spans for a query to a nonexistent table (promise)', async () => {
1034+
try {
1035+
await client.query('SELECT foo FROM nonexistent_table');
1036+
assert.fail('Expected query to throw an error');
1037+
} catch {
1038+
const spans = memoryExporter.getFinishedSpans();
1039+
assert.strictEqual(spans.length, 1, 'Expected one finished span');
1040+
const pgSpan = spans[0];
1041+
1042+
assertExceptionEvents(pgSpan);
1043+
1044+
memoryExporter.reset();
1045+
}
1046+
});
1047+
});
1048+
9851049
describe('pg metrics', () => {
9861050
let metricReader: testUtils.TestMetricReader;
9871051

0 commit comments

Comments
 (0)