Skip to content

Commit a8465a2

Browse files
authored
feat: postgresql responseHook support (#528)
1 parent a52deec commit a8465a2

File tree

7 files changed

+346
-18
lines changed

7 files changed

+346
-18
lines changed

plugins/node/opentelemetry-instrumentation-pg/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ PostgreSQL instrumentation has few options available to choose from. You can set
4646
| Options | Type | Description |
4747
| ------- | ---- | ----------- |
4848
| [`enhancedDatabaseReporting`](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-pg/src/pg.ts#L48) | `boolean` | If true, additional information about query parameters and results will be attached (as `attributes`) to spans representing database operations |
49+
| `responseHook` | `PgInstrumentationExecutionResponseHook` (function) | Function for adding custom attributes from db response |
4950

5051
## Supported Versions
5152

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,8 @@
1515
*/
1616

1717
export * from './instrumentation';
18+
export {
19+
PgInstrumentationConfig,
20+
PgInstrumentationExecutionResponseHook,
21+
PgResponseHookInformation,
22+
} from './types';

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

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import {
1717
isWrapped,
1818
InstrumentationBase,
19-
InstrumentationConfig,
2019
InstrumentationNodeModuleDefinition,
2120
} from '@opentelemetry/instrumentation';
2221

@@ -36,6 +35,7 @@ import {
3635
PostgresCallback,
3736
PgPoolExtended,
3837
PgPoolCallback,
38+
PgInstrumentationConfig,
3939
} from './types';
4040
import * as utils from './utils';
4141
import { AttributeNames } from './enums/AttributeNames';
@@ -45,17 +45,7 @@ import {
4545
} from '@opentelemetry/semantic-conventions';
4646
import { VERSION } from './version';
4747

48-
export interface PgInstrumentationConfig extends InstrumentationConfig {
49-
/**
50-
* If true, additional information about query parameters and
51-
* results will be attached (as `attributes`) to spans representing
52-
* database operations.
53-
*/
54-
enhancedDatabaseReporting?: boolean;
55-
}
56-
5748
const PG_POOL_COMPONENT = 'pg-pool';
58-
5949
export class PgInstrumentation extends InstrumentationBase {
6050
static readonly COMPONENT = 'pg';
6151

@@ -134,7 +124,7 @@ export class PgInstrumentation extends InstrumentationBase {
134124
span = utils.handleParameterizedQuery.call(
135125
this,
136126
plugin.tracer,
137-
plugin._config as InstrumentationConfig & PgInstrumentationConfig,
127+
plugin.getConfig() as PgInstrumentationConfig,
138128
query,
139129
params
140130
);
@@ -146,7 +136,7 @@ export class PgInstrumentation extends InstrumentationBase {
146136
span = utils.handleConfigQuery.call(
147137
this,
148138
plugin.tracer,
149-
plugin._config as InstrumentationConfig & PgInstrumentationConfig,
139+
plugin.getConfig() as PgInstrumentationConfig,
150140
queryConfig
151141
);
152142
} else {
@@ -164,6 +154,7 @@ export class PgInstrumentation extends InstrumentationBase {
164154
if (typeof args[args.length - 1] === 'function') {
165155
// Patch ParameterQuery callback
166156
args[args.length - 1] = utils.patchCallback(
157+
plugin.getConfig() as PgInstrumentationConfig,
167158
span,
168159
args[args.length - 1] as PostgresCallback
169160
);
@@ -179,6 +170,7 @@ export class PgInstrumentation extends InstrumentationBase {
179170
) {
180171
// Patch ConfigQuery callback
181172
let callback = utils.patchCallback(
173+
plugin.getConfig() as PgInstrumentationConfig,
182174
span,
183175
(args[0] as NormalizedQueryConfig).callback!
184176
);
@@ -202,6 +194,11 @@ export class PgInstrumentation extends InstrumentationBase {
202194
.then((result: unknown) => {
203195
// Return a pass-along promise which ends the span and then goes to user's orig resolvers
204196
return new Promise(resolve => {
197+
utils.handleExecutionResult(
198+
plugin.getConfig() as PgInstrumentationConfig,
199+
span,
200+
result
201+
);
205202
span.end();
206203
resolve(result);
207204
});

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,31 @@
1616

1717
import * as pgTypes from 'pg';
1818
import * as pgPoolTypes from 'pg-pool';
19+
import type * as api from '@opentelemetry/api';
20+
import { InstrumentationConfig } from '@opentelemetry/instrumentation';
21+
22+
export interface PgResponseHookInformation {
23+
data: pgTypes.QueryResult | pgTypes.QueryArrayResult;
24+
}
25+
26+
export interface PgInstrumentationExecutionResponseHook {
27+
(span: api.Span, responseInfo: PgResponseHookInformation): void;
28+
}
29+
30+
export interface PgInstrumentationConfig extends InstrumentationConfig {
31+
/**
32+
* If true, additional information about query parameters will be attached (as `attributes`) to spans representing
33+
*/
34+
enhancedDatabaseReporting?: boolean;
35+
36+
/**
37+
* Hook that allows adding custom span attributes based on the data
38+
* returned from "query" Pg actions.
39+
*
40+
* @default undefined
41+
*/
42+
responseHook?: PgInstrumentationExecutionResponseHook;
43+
}
1944

2045
export type PostgresCallback = (err: Error, res: object) => unknown;
2146

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Span, SpanStatusCode, Tracer, SpanKind } from '@opentelemetry/api';
17+
import {
18+
Span,
19+
SpanStatusCode,
20+
Tracer,
21+
SpanKind,
22+
diag,
23+
} from '@opentelemetry/api';
1824
import { AttributeNames } from './enums/AttributeNames';
1925
import {
2026
SemanticAttributes,
@@ -27,9 +33,11 @@ import {
2733
PgClientConnectionParams,
2834
PgPoolCallback,
2935
PgPoolExtended,
36+
PgInstrumentationConfig,
3037
} from './types';
3138
import * as pgTypes from 'pg';
32-
import { PgInstrumentation, PgInstrumentationConfig } from './';
39+
import { PgInstrumentation } from './';
40+
import { safeExecuteInTheMiddle } from '@opentelemetry/instrumentation';
3341

3442
function arrayStringifyHelper(arr: Array<unknown>): string {
3543
return '[' + arr.toString() + ']';
@@ -161,7 +169,30 @@ export function handleInvalidQuery(
161169
return result;
162170
}
163171

172+
export function handleExecutionResult(
173+
config: PgInstrumentationConfig,
174+
span: Span,
175+
pgResult: pgTypes.QueryResult | pgTypes.QueryArrayResult | unknown
176+
) {
177+
if (typeof config.responseHook === 'function') {
178+
safeExecuteInTheMiddle(
179+
() => {
180+
config.responseHook!(span, {
181+
data: pgResult as pgTypes.QueryResult | pgTypes.QueryArrayResult,
182+
});
183+
},
184+
err => {
185+
if (err) {
186+
diag.error('Error running response hook', err);
187+
}
188+
},
189+
true
190+
);
191+
}
192+
}
193+
164194
export function patchCallback(
195+
instrumentationConfig: PgInstrumentationConfig,
165196
span: Span,
166197
cb: PostgresCallback
167198
): PostgresCallback {
@@ -176,7 +207,10 @@ export function patchCallback(
176207
code: SpanStatusCode.ERROR,
177208
message: err.message,
178209
});
210+
} else {
211+
handleExecutionResult(instrumentationConfig, span, res);
179212
}
213+
180214
span.end();
181215
cb.call(this, err, res);
182216
};

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

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import {
2424
trace,
2525
} from '@opentelemetry/api';
2626
import { BasicTracerProvider } from '@opentelemetry/tracing';
27-
import { PgInstrumentation } from '../src';
27+
import {
28+
PgInstrumentation,
29+
PgInstrumentationConfig,
30+
PgResponseHookInformation,
31+
} from '../src';
2832
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
2933
import * as testUtils from '@opentelemetry/test-utils';
3034
import {
@@ -95,6 +99,11 @@ const runCallbackTest = (
9599
};
96100

97101
describe('[email protected]', () => {
102+
function create(config: PgInstrumentationConfig = {}) {
103+
instrumentation.setConfig(config);
104+
instrumentation.enable();
105+
}
106+
98107
let pool: pgPool<pg.Client>;
99108
let contextManager: AsyncHooksContextManager;
100109
let instrumentation: PgInstrumentation;
@@ -130,6 +139,7 @@ describe('[email protected]', () => {
130139
if (testPostgresLocally) {
131140
testUtils.cleanUpDocker('postgres');
132141
}
142+
133143
pool.end(() => {
134144
done();
135145
});
@@ -288,5 +298,154 @@ describe('[email protected]', () => {
288298
assert.strictEqual(resNoPromise, undefined, 'No promise is returned');
289299
});
290300
});
301+
302+
describe('when specifying a responseHook configuration', () => {
303+
const dataAttributeName = 'pg_data';
304+
const query = 'SELECT 0::text';
305+
const events: TimedEvent[] = [];
306+
307+
describe('AND valid responseHook', () => {
308+
const pgPoolattributes = {
309+
...DEFAULT_PGPOOL_ATTRIBUTES,
310+
};
311+
const pgAttributes = {
312+
...DEFAULT_PG_ATTRIBUTES,
313+
[SemanticAttributes.DB_STATEMENT]: query,
314+
[dataAttributeName]: '{"rowCount":1}',
315+
};
316+
317+
beforeEach(async () => {
318+
const config: PgInstrumentationConfig = {
319+
enhancedDatabaseReporting: true,
320+
responseHook: (
321+
span: Span,
322+
responseInfo: PgResponseHookInformation
323+
) =>
324+
span.setAttribute(
325+
dataAttributeName,
326+
JSON.stringify({ rowCount: responseInfo?.data.rowCount })
327+
),
328+
};
329+
330+
create(config);
331+
});
332+
333+
it('should attach response hook data to resulting spans for query with callback ', done => {
334+
const parentSpan = provider
335+
.getTracer('test-pg-pool')
336+
.startSpan('test span');
337+
context.with(trace.setSpan(context.active(), parentSpan), () => {
338+
const resNoPromise = pool.query(query, (err, result) => {
339+
if (err) {
340+
return done(err);
341+
}
342+
runCallbackTest(
343+
parentSpan,
344+
pgPoolattributes,
345+
events,
346+
unsetStatus,
347+
2,
348+
0
349+
);
350+
runCallbackTest(
351+
parentSpan,
352+
pgAttributes,
353+
events,
354+
unsetStatus,
355+
2,
356+
1
357+
);
358+
done();
359+
});
360+
assert.strictEqual(
361+
resNoPromise,
362+
undefined,
363+
'No promise is returned'
364+
);
365+
});
366+
});
367+
368+
it('should attach response hook data to resulting spans for query returning a Promise', async () => {
369+
const span = provider
370+
.getTracer('test-pg-pool')
371+
.startSpan('test span');
372+
await context.with(
373+
trace.setSpan(context.active(), span),
374+
async () => {
375+
const result = await pool.query(query);
376+
runCallbackTest(
377+
span,
378+
pgPoolattributes,
379+
events,
380+
unsetStatus,
381+
2,
382+
0
383+
);
384+
runCallbackTest(span, pgAttributes, events, unsetStatus, 2, 1);
385+
assert.ok(result, 'pool.query() returns a promise');
386+
}
387+
);
388+
});
389+
});
390+
391+
describe('AND invalid responseHook', () => {
392+
const pgPoolattributes = {
393+
...DEFAULT_PGPOOL_ATTRIBUTES,
394+
};
395+
const pgAttributes = {
396+
...DEFAULT_PG_ATTRIBUTES,
397+
[SemanticAttributes.DB_STATEMENT]: query,
398+
};
399+
400+
beforeEach(async () => {
401+
create({
402+
enhancedDatabaseReporting: true,
403+
responseHook: (
404+
span: Span,
405+
responseInfo: PgResponseHookInformation
406+
) => {
407+
throw 'some kind of failure!';
408+
},
409+
});
410+
});
411+
412+
it('should not do any harm when throwing an exception', done => {
413+
const parentSpan = provider
414+
.getTracer('test-pg-pool')
415+
.startSpan('test span');
416+
context.with(trace.setSpan(context.active(), parentSpan), () => {
417+
const resNoPromise = pool.query(query, (err, result) => {
418+
if (err) {
419+
return done(err);
420+
}
421+
assert.ok(result);
422+
423+
runCallbackTest(
424+
parentSpan,
425+
pgPoolattributes,
426+
events,
427+
unsetStatus,
428+
2,
429+
0
430+
);
431+
runCallbackTest(
432+
parentSpan,
433+
pgAttributes,
434+
events,
435+
unsetStatus,
436+
2,
437+
1
438+
);
439+
done();
440+
});
441+
assert.strictEqual(
442+
resNoPromise,
443+
undefined,
444+
'No promise is returned'
445+
);
446+
});
447+
});
448+
});
449+
});
291450
});
292451
});

0 commit comments

Comments
 (0)