Skip to content

Commit 050fee0

Browse files
feat(instrumentation-pg): implementation of metric operation duration (#2380)
Co-authored-by: Marc Pichler <[email protected]>
1 parent 0d6ebde commit 050fee0

File tree

4 files changed

+136
-15
lines changed

4 files changed

+136
-15
lines changed

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

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ import {
1919
InstrumentationNodeModuleDefinition,
2020
safeExecuteInTheMiddle,
2121
} from '@opentelemetry/instrumentation';
22-
2322
import {
2423
context,
2524
trace,
2625
Span,
2726
SpanStatusCode,
2827
SpanKind,
28+
Histogram,
29+
ValueType,
30+
Attributes,
31+
HrTime,
2932
UpDownCounter,
3033
} from '@opentelemetry/api';
3134
import type * as pgTypes from 'pg';
@@ -42,12 +45,27 @@ import * as utils from './utils';
4245
import { addSqlCommenterComment } from '@opentelemetry/sql-common';
4346
import { PACKAGE_NAME, PACKAGE_VERSION } from './version';
4447
import { SpanNames } from './enums/SpanNames';
48+
import {
49+
hrTime,
50+
hrTimeDuration,
51+
hrTimeToMilliseconds,
52+
} from '@opentelemetry/core';
53+
import {
54+
DBSYSTEMVALUES_POSTGRESQL,
55+
SEMATTRS_DB_SYSTEM,
56+
ATTR_ERROR_TYPE,
57+
ATTR_SERVER_PORT,
58+
ATTR_SERVER_ADDRESS,
59+
} from '@opentelemetry/semantic-conventions';
4560
import {
4661
METRIC_DB_CLIENT_CONNECTION_COUNT,
4762
METRIC_DB_CLIENT_CONNECTION_PENDING_REQUESTS,
63+
METRIC_DB_CLIENT_OPERATION_DURATION,
64+
ATTR_DB_NAMESPACE,
4865
} from '@opentelemetry/semantic-conventions/incubating';
4966

5067
export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConfig> {
68+
private _operationDuration!: Histogram;
5169
private _connectionsCount!: UpDownCounter;
5270
private _connectionPendingRequests!: UpDownCounter;
5371
// Pool events connect, acquire, release and remove can be called
@@ -66,6 +84,20 @@ export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConf
6684
}
6785

6886
override _updateMetricInstruments() {
87+
this._operationDuration = this.meter.createHistogram(
88+
METRIC_DB_CLIENT_OPERATION_DURATION,
89+
{
90+
description: 'Duration of database client operations.',
91+
unit: 's',
92+
valueType: ValueType.DOUBLE,
93+
advice: {
94+
explicitBucketBoundaries: [
95+
0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10,
96+
],
97+
},
98+
}
99+
);
100+
69101
this._connectionsCounter = {
70102
idle: 0,
71103
pending: 0,
@@ -188,6 +220,27 @@ export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConf
188220
};
189221
}
190222

223+
private recordOperationDuration(attributes: Attributes, startTime: HrTime) {
224+
const metricsAttributes: Attributes = {};
225+
const keysToCopy = [
226+
SEMATTRS_DB_SYSTEM,
227+
ATTR_DB_NAMESPACE,
228+
ATTR_ERROR_TYPE,
229+
ATTR_SERVER_PORT,
230+
ATTR_SERVER_ADDRESS,
231+
];
232+
233+
keysToCopy.forEach(key => {
234+
if (key in attributes) {
235+
metricsAttributes[key] = attributes[key];
236+
}
237+
});
238+
239+
const durationSeconds =
240+
hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime())) / 1000;
241+
this._operationDuration.record(durationSeconds, metricsAttributes);
242+
}
243+
191244
private _getClientQueryPatch() {
192245
const plugin = this;
193246
return (original: typeof pgTypes.Client.prototype.query) => {
@@ -196,6 +249,7 @@ export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConf
196249
if (utils.shouldSkipInstrumentation(plugin.getConfig())) {
197250
return original.apply(this, args as never);
198251
}
252+
const startTime = hrTime();
199253

200254
// client.query(text, cb?), client.query(text, values, cb?), and
201255
// client.query(configObj, cb?) are all valid signatures. We construct
@@ -221,6 +275,17 @@ export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConf
221275
? (arg0 as utils.ObjectWithText)
222276
: undefined;
223277

278+
const attributes: Attributes = {
279+
[SEMATTRS_DB_SYSTEM]: DBSYSTEMVALUES_POSTGRESQL,
280+
[ATTR_DB_NAMESPACE]: this.database,
281+
[ATTR_SERVER_PORT]: this.connectionParameters.port,
282+
[ATTR_SERVER_ADDRESS]: this.connectionParameters.host,
283+
};
284+
285+
const recordDuration = () => {
286+
plugin.recordOperationDuration(attributes, startTime);
287+
};
288+
224289
const instrumentationConfig = plugin.getConfig();
225290

226291
const span = utils.handleConfigQuery.call(
@@ -251,7 +316,8 @@ export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConf
251316
args[args.length - 1] = utils.patchCallback(
252317
instrumentationConfig,
253318
span,
254-
args[args.length - 1] as PostgresCallback // nb: not type safe.
319+
args[args.length - 1] as PostgresCallback, // nb: not type safe.
320+
recordDuration
255321
);
256322

257323
// If a parent span exists, bind the callback
@@ -266,7 +332,8 @@ export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConf
266332
let callback = utils.patchCallback(
267333
plugin.getConfig(),
268334
span,
269-
queryConfig.callback as PostgresCallback // nb: not type safe.
335+
queryConfig.callback as PostgresCallback, // nb: not type safe.
336+
recordDuration
270337
);
271338

272339
// If a parent span existed, bind the callback
@@ -324,7 +391,6 @@ export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConf
324391
try {
325392
result = original.apply(this, args as never);
326393
} catch (e: unknown) {
327-
// span.recordException(e);
328394
span.setStatus({
329395
code: SpanStatusCode.ERROR,
330396
message: utils.getErrorMessage(e),

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,15 +243,15 @@ export function handleExecutionResult(
243243
export function patchCallback(
244244
instrumentationConfig: PgInstrumentationConfig,
245245
span: Span,
246-
cb: PostgresCallback
246+
cb: PostgresCallback,
247+
recordDuration: { (): void }
247248
): PostgresCallback {
248249
return function patchedCallback(
249250
this: PgClientExtended,
250251
err: Error,
251252
res: object
252253
) {
253254
if (err) {
254-
// span.recordException(err);
255255
span.setStatus({
256256
code: SpanStatusCode.ERROR,
257257
message: err.message,
@@ -260,6 +260,7 @@ export function patchCallback(
260260
handleExecutionResult(instrumentationConfig, span, res);
261261
}
262262

263+
recordDuration();
263264
span.end();
264265
cb.call(this, err, res);
265266
};

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -520,46 +520,46 @@ describe('pg-pool', () => {
520520

521521
const metrics = resourceMetrics.scopeMetrics[0].metrics;
522522
assert.strictEqual(
523-
metrics[0].descriptor.name,
523+
metrics[1].descriptor.name,
524524
'db.client.connection.count'
525525
);
526526
assert.strictEqual(
527-
metrics[0].descriptor.description,
527+
metrics[1].descriptor.description,
528528
'The number of connections that are currently in state described by the state attribute.'
529529
);
530530
assert.strictEqual(
531-
metrics[0].dataPoints[0].attributes[
531+
metrics[1].dataPoints[0].attributes[
532532
ATTR_DB_CLIENT_CONNECTION_STATE
533533
],
534534
'used'
535535
);
536536
assert.strictEqual(
537-
metrics[0].dataPoints[0].value,
537+
metrics[1].dataPoints[0].value,
538538
1,
539539
'expected to have 1 used connection'
540540
);
541541
assert.strictEqual(
542-
metrics[0].dataPoints[1].attributes[
542+
metrics[1].dataPoints[1].attributes[
543543
ATTR_DB_CLIENT_CONNECTION_STATE
544544
],
545545
'idle'
546546
);
547547
assert.strictEqual(
548-
metrics[0].dataPoints[1].value,
548+
metrics[1].dataPoints[1].value,
549549
0,
550550
'expected to have 0 idle connections'
551551
);
552552

553553
assert.strictEqual(
554-
metrics[1].descriptor.name,
554+
metrics[2].descriptor.name,
555555
'db.client.connection.pending_requests'
556556
);
557557
assert.strictEqual(
558-
metrics[1].descriptor.description,
558+
metrics[2].descriptor.description,
559559
'The number of current pending requests for an open connection.'
560560
);
561561
assert.strictEqual(
562-
metrics[1].dataPoints[0].value,
562+
metrics[2].dataPoints[0].value,
563563
0,
564564
'expected to have 0 pending requests'
565565
);

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
InMemorySpanExporter,
3131
SimpleSpanProcessor,
3232
} from '@opentelemetry/sdk-trace-base';
33+
import { DataPoint, Histogram } from '@opentelemetry/sdk-metrics';
3334
import * as assert from 'assert';
3435
import type * as pg from 'pg';
3536
import * as sinon from 'sinon';
@@ -50,6 +51,7 @@ import {
5051
SEMATTRS_NET_PEER_PORT,
5152
SEMATTRS_DB_USER,
5253
DBSYSTEMVALUES_POSTGRESQL,
54+
ATTR_ERROR_TYPE,
5355
} from '@opentelemetry/semantic-conventions';
5456
import { addSqlCommenterComment } from '@opentelemetry/sql-common';
5557

@@ -960,4 +962,56 @@ describe('pg', () => {
960962
});
961963
});
962964
});
965+
966+
describe('pg metrics', () => {
967+
let metricReader: testUtils.TestMetricReader;
968+
969+
beforeEach(() => {
970+
metricReader = testUtils.initMeterProvider(instrumentation);
971+
});
972+
973+
it('should generate db.client.operation.duration metric', done => {
974+
client.query('SELECT NOW()', async (_, ret) => {
975+
assert.ok(ret, 'query should be executed');
976+
977+
const { resourceMetrics, errors } = await metricReader.collect();
978+
assert.deepEqual(
979+
errors,
980+
[],
981+
'expected no errors from the callback during metric collection'
982+
);
983+
984+
const metrics = resourceMetrics.scopeMetrics[0].metrics;
985+
assert.strictEqual(
986+
metrics[0].descriptor.name,
987+
'db.client.operation.duration'
988+
);
989+
assert.strictEqual(
990+
metrics[0].descriptor.description,
991+
'Duration of database client operations.'
992+
);
993+
const dataPoint = metrics[0].dataPoints[0];
994+
assert.strictEqual(
995+
dataPoint.attributes[SEMATTRS_DB_SYSTEM],
996+
DBSYSTEMVALUES_POSTGRESQL
997+
);
998+
assert.strictEqual(dataPoint.attributes[ATTR_ERROR_TYPE], undefined);
999+
1000+
const v = (dataPoint as DataPoint<Histogram>).value;
1001+
v.min = v.min ? v.min : 0;
1002+
v.max = v.max ? v.max : 0;
1003+
assert.equal(
1004+
v.min > 0,
1005+
true,
1006+
'expect min value for Histogram to be greater than 0'
1007+
);
1008+
assert.equal(
1009+
v.max > 0,
1010+
true,
1011+
'expect max value for Histogram to be greater than 0'
1012+
);
1013+
done();
1014+
});
1015+
});
1016+
});
9631017
});

0 commit comments

Comments
 (0)