diff --git a/plugins/node/opentelemetry-instrumentation-pg/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-pg/src/instrumentation.ts index d1f0a3f0cb..e7c3da7a5f 100644 --- a/plugins/node/opentelemetry-instrumentation-pg/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-pg/src/instrumentation.ts @@ -19,13 +19,16 @@ import { InstrumentationNodeModuleDefinition, safeExecuteInTheMiddle, } from '@opentelemetry/instrumentation'; - import { context, trace, Span, SpanStatusCode, SpanKind, + Histogram, + ValueType, + Attributes, + HrTime, UpDownCounter, } from '@opentelemetry/api'; import type * as pgTypes from 'pg'; @@ -42,12 +45,27 @@ import * as utils from './utils'; import { addSqlCommenterComment } from '@opentelemetry/sql-common'; import { PACKAGE_NAME, PACKAGE_VERSION } from './version'; import { SpanNames } from './enums/SpanNames'; +import { + hrTime, + hrTimeDuration, + hrTimeToMilliseconds, +} from '@opentelemetry/core'; +import { + DBSYSTEMVALUES_POSTGRESQL, + SEMATTRS_DB_SYSTEM, + ATTR_ERROR_TYPE, + ATTR_SERVER_PORT, + ATTR_SERVER_ADDRESS, +} from '@opentelemetry/semantic-conventions'; import { METRIC_DB_CLIENT_CONNECTION_COUNT, METRIC_DB_CLIENT_CONNECTION_PENDING_REQUESTS, + METRIC_DB_CLIENT_OPERATION_DURATION, + ATTR_DB_NAMESPACE, } from '@opentelemetry/semantic-conventions/incubating'; export class PgInstrumentation extends InstrumentationBase { + private _operationDuration!: Histogram; private _connectionsCount!: UpDownCounter; private _connectionPendingRequests!: UpDownCounter; // Pool events connect, acquire, release and remove can be called @@ -66,6 +84,20 @@ export class PgInstrumentation extends InstrumentationBase { + if (key in attributes) { + metricsAttributes[key] = attributes[key]; + } + }); + + const durationSeconds = + hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime())) / 1000; + this._operationDuration.record(durationSeconds, metricsAttributes); + } + private _getClientQueryPatch() { const plugin = this; return (original: typeof pgTypes.Client.prototype.query) => { @@ -196,6 +249,7 @@ export class PgInstrumentation extends InstrumentationBase { + plugin.recordOperationDuration(attributes, startTime); + }; + const instrumentationConfig = plugin.getConfig(); const span = utils.handleConfigQuery.call( @@ -251,7 +316,8 @@ export class PgInstrumentation extends InstrumentationBase { const metrics = resourceMetrics.scopeMetrics[0].metrics; assert.strictEqual( - metrics[0].descriptor.name, + metrics[1].descriptor.name, 'db.client.connection.count' ); assert.strictEqual( - metrics[0].descriptor.description, + metrics[1].descriptor.description, 'The number of connections that are currently in state described by the state attribute.' ); assert.strictEqual( - metrics[0].dataPoints[0].attributes[ + metrics[1].dataPoints[0].attributes[ ATTR_DB_CLIENT_CONNECTION_STATE ], 'used' ); assert.strictEqual( - metrics[0].dataPoints[0].value, + metrics[1].dataPoints[0].value, 1, 'expected to have 1 used connection' ); assert.strictEqual( - metrics[0].dataPoints[1].attributes[ + metrics[1].dataPoints[1].attributes[ ATTR_DB_CLIENT_CONNECTION_STATE ], 'idle' ); assert.strictEqual( - metrics[0].dataPoints[1].value, + metrics[1].dataPoints[1].value, 0, 'expected to have 0 idle connections' ); assert.strictEqual( - metrics[1].descriptor.name, + metrics[2].descriptor.name, 'db.client.connection.pending_requests' ); assert.strictEqual( - metrics[1].descriptor.description, + metrics[2].descriptor.description, 'The number of current pending requests for an open connection.' ); assert.strictEqual( - metrics[1].dataPoints[0].value, + metrics[2].dataPoints[0].value, 0, 'expected to have 0 pending requests' ); diff --git a/plugins/node/opentelemetry-instrumentation-pg/test/pg.test.ts b/plugins/node/opentelemetry-instrumentation-pg/test/pg.test.ts index eb2e847328..b394893399 100644 --- a/plugins/node/opentelemetry-instrumentation-pg/test/pg.test.ts +++ b/plugins/node/opentelemetry-instrumentation-pg/test/pg.test.ts @@ -30,6 +30,7 @@ import { InMemorySpanExporter, SimpleSpanProcessor, } from '@opentelemetry/sdk-trace-base'; +import { DataPoint, Histogram } from '@opentelemetry/sdk-metrics'; import * as assert from 'assert'; import type * as pg from 'pg'; import * as sinon from 'sinon'; @@ -50,6 +51,7 @@ import { SEMATTRS_NET_PEER_PORT, SEMATTRS_DB_USER, DBSYSTEMVALUES_POSTGRESQL, + ATTR_ERROR_TYPE, } from '@opentelemetry/semantic-conventions'; import { addSqlCommenterComment } from '@opentelemetry/sql-common'; @@ -960,4 +962,56 @@ describe('pg', () => { }); }); }); + + describe('pg metrics', () => { + let metricReader: testUtils.TestMetricReader; + + beforeEach(() => { + metricReader = testUtils.initMeterProvider(instrumentation); + }); + + it('should generate db.client.operation.duration metric', done => { + client.query('SELECT NOW()', async (_, ret) => { + assert.ok(ret, 'query should be executed'); + + const { resourceMetrics, errors } = await metricReader.collect(); + assert.deepEqual( + errors, + [], + 'expected no errors from the callback during metric collection' + ); + + const metrics = resourceMetrics.scopeMetrics[0].metrics; + assert.strictEqual( + metrics[0].descriptor.name, + 'db.client.operation.duration' + ); + assert.strictEqual( + metrics[0].descriptor.description, + 'Duration of database client operations.' + ); + const dataPoint = metrics[0].dataPoints[0]; + assert.strictEqual( + dataPoint.attributes[SEMATTRS_DB_SYSTEM], + DBSYSTEMVALUES_POSTGRESQL + ); + assert.strictEqual(dataPoint.attributes[ATTR_ERROR_TYPE], undefined); + + const v = (dataPoint as DataPoint).value; + v.min = v.min ? v.min : 0; + v.max = v.max ? v.max : 0; + assert.equal( + v.min > 0, + true, + 'expect min value for Histogram to be greater than 0' + ); + assert.equal( + v.max > 0, + true, + 'expect max value for Histogram to be greater than 0' + ); + done(); + }); + }); + }); });