Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(instrumentation-pg): implementation of metric operation duration #2380

Merged
merged 27 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
143c81b
operation duration metric
maryliag Jul 31, 2024
c58027b
use temporary const for sem attr
maryliag Jul 31, 2024
bbac895
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js-con…
maryliag Aug 13, 2024
962e4e8
fix lint
maryliag Aug 13, 2024
194ca30
remove unsed type
maryliag Aug 13, 2024
eaabf75
add test
maryliag Aug 20, 2024
a3c518e
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js-con…
maryliag Aug 20, 2024
c344db5
add more tests
maryliag Aug 20, 2024
a66144d
fix test
maryliag Aug 20, 2024
4adc2a6
cleanup
maryliag Aug 20, 2024
41577ea
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js-con…
maryliag Aug 20, 2024
3be63a3
cleanup
maryliag Aug 21, 2024
fbf14a8
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js-con…
maryliag Aug 21, 2024
bed20e1
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js-con…
maryliag Aug 27, 2024
ceb6fad
add timeout for test
maryliag Aug 27, 2024
1aa1838
add missing done
maryliag Aug 27, 2024
0d89c87
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js-con…
maryliag Sep 4, 2024
d7a78d0
use sem conv
maryliag Sep 4, 2024
6af47fa
fix lint
maryliag Sep 4, 2024
426bcc5
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js-con…
maryliag Sep 12, 2024
aed321d
remove error attribute
maryliag Sep 13, 2024
bc87a34
cleanup
maryliag Sep 13, 2024
8467309
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js-con…
maryliag Sep 13, 2024
942b5be
Merge branch 'main' into db-metric-duration
pichlermarc Sep 18, 2024
26075d1
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js-con…
maryliag Sep 23, 2024
e04e8d0
Merge branch 'db-metric-duration' of github.com:maryliag/opentelemetr…
maryliag Sep 23, 2024
fa0b0fd
Merge branch 'main' into db-metric-duration
pichlermarc Sep 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
InstrumentationNodeModuleDefinition,
safeExecuteInTheMiddle,
} from '@opentelemetry/instrumentation';

import {
context,
trace,
Span,
SpanStatusCode,
SpanKind,
Histogram,
ValueType,
Attributes,
HrTime,
} from '@opentelemetry/api';
import type * as pgTypes from 'pg';
import type * as pgPoolTypes from 'pg-pool';
Expand All @@ -41,12 +44,45 @@
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_OPERATION_DURATION,
ATTR_DB_NAMESPACE,
} from '@opentelemetry/semantic-conventions/incubating';

export class PgInstrumentation extends InstrumentationBase<PgInstrumentationConfig> {
private _operationDuration!: Histogram;
constructor(config: PgInstrumentationConfig = {}) {
super(PACKAGE_NAME, PACKAGE_VERSION, config);
}

override _updateMetricInstruments() {
this._operationDuration = this.meter.createHistogram(
METRIC_DB_CLIENT_OPERATION_DURATION,
{
description: 'Duration of database client operations.',
unit: 's',
valueType: ValueType.DOUBLE,
advice: {
explicitBucketBoundaries: [
0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10,
],
},
}
);
}

protected init() {
const modulePG = new InstrumentationNodeModuleDefinition(
'pg',
Expand Down Expand Up @@ -146,6 +182,27 @@
};
}

private recordOperationDuration(attributes: Attributes, startTime: HrTime) {
const metricsAttributes: Attributes = {};
const keysToCopy = [
SEMATTRS_DB_SYSTEM,
ATTR_DB_NAMESPACE,
ATTR_ERROR_TYPE,
ATTR_SERVER_PORT,
ATTR_SERVER_ADDRESS,
];

keysToCopy.forEach(key => {
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) => {
Expand All @@ -154,6 +211,7 @@
if (utils.shouldSkipInstrumentation(plugin.getConfig())) {
return original.apply(this, args as never);
}
const startTime = hrTime();

// client.query(text, cb?), client.query(text, values, cb?), and
// client.query(configObj, cb?) are all valid signatures. We construct
Expand All @@ -179,6 +237,17 @@
? (arg0 as utils.ObjectWithText)
: undefined;

const attributes: Attributes = {
[SEMATTRS_DB_SYSTEM]: DBSYSTEMVALUES_POSTGRESQL,
[ATTR_DB_NAMESPACE]: this.database,
[ATTR_SERVER_PORT]: this.connectionParameters.port,
[ATTR_SERVER_ADDRESS]: this.connectionParameters.host,
};

const recordDuration = () => {
plugin.recordOperationDuration(attributes, startTime);
};

const instrumentationConfig = plugin.getConfig();

const span = utils.handleConfigQuery.call(
Expand Down Expand Up @@ -209,7 +278,9 @@
args[args.length - 1] = utils.patchCallback(
instrumentationConfig,
span,
args[args.length - 1] as PostgresCallback // nb: not type safe.
args[args.length - 1] as PostgresCallback, // nb: not type safe.
attributes,
recordDuration
);

// If a parent span exists, bind the callback
Expand All @@ -224,7 +295,9 @@
let callback = utils.patchCallback(
plugin.getConfig(),
span,
queryConfig.callback as PostgresCallback // nb: not type safe.
queryConfig.callback as PostgresCallback, // nb: not type safe.
attributes,
recordDuration
);

// If a parent span existed, bind the callback
Expand Down Expand Up @@ -282,12 +355,12 @@
try {
result = original.apply(this, args as never);
} catch (e: unknown) {
// span.recordException(e);
span.setStatus({
code: SpanStatusCode.ERROR,
message: utils.getErrorMessage(e),
});
span.end();
attributes[ATTR_ERROR_TYPE] = utils.getErrorMessage(e);
throw e;
}

Expand All @@ -309,6 +382,7 @@
message: error.message,
});
span.end();
attributes[ATTR_ERROR_TYPE] = utils.getErrorMessage(error);

Check warning on line 385 in plugins/node/opentelemetry-instrumentation-pg/src/instrumentation.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L385 was not covered by tests
reject(error);
});
});
Expand Down
8 changes: 6 additions & 2 deletions plugins/node/opentelemetry-instrumentation-pg/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
Tracer,
SpanKind,
diag,
Attributes,
} from '@opentelemetry/api';
import { AttributeNames } from './enums/AttributeNames';
import {
Expand Down Expand Up @@ -236,15 +237,17 @@ export function handleExecutionResult(
export function patchCallback(
instrumentationConfig: PgInstrumentationConfig,
span: Span,
cb: PostgresCallback
cb: PostgresCallback,
attributes: Attributes,
recordDuration: { (): void }
): PostgresCallback {
return function patchedCallback(
this: PgClientExtended,
err: Error,
res: object
) {
if (err) {
// span.recordException(err);
attributes['error.type'] = getErrorMessage(err);
maryliag marked this conversation as resolved.
Show resolved Hide resolved
span.setStatus({
code: SpanStatusCode.ERROR,
message: err.message,
Expand All @@ -253,6 +256,7 @@ export function patchCallback(
handleExecutionResult(instrumentationConfig, span, res);
}

recordDuration();
span.end();
cb.call(this, err, res);
};
Expand Down
100 changes: 100 additions & 0 deletions plugins/node/opentelemetry-instrumentation-pg/test/pg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -960,4 +962,102 @@ 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<Histogram>).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();
});
});

it('should generate db.client.operation.duration metric with error attribute', done => {
client.query('SELECT test()', async (err, ret) => {
assert.notEqual(err, null);
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],
'function test() does not exist'
);
maryliag marked this conversation as resolved.
Show resolved Hide resolved

const v = (dataPoint as DataPoint<Histogram>).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();
});
});
});
});