diff --git a/packages/opentelemetry-api/src/metrics/Metric.ts b/packages/opentelemetry-api/src/metrics/Metric.ts index d495762e279..ee7fd29ab44 100644 --- a/packages/opentelemetry-api/src/metrics/Metric.ts +++ b/packages/opentelemetry-api/src/metrics/Metric.ts @@ -179,6 +179,9 @@ export type ValueObserver = BaseObserver; /** Base interface for the UpDownSumObserver metrics. */ export type UpDownSumObserver = BaseObserver; +/** Base interface for the SumObserver metrics. */ +export type SumObserver = BaseObserver; + /** Base interface for the Batch Observer metrics. */ export type BatchObserver = Metric; diff --git a/packages/opentelemetry-api/src/metrics/NoopMeter.ts b/packages/opentelemetry-api/src/metrics/NoopMeter.ts index 5667160372b..47df1ab970d 100644 --- a/packages/opentelemetry-api/src/metrics/NoopMeter.ts +++ b/packages/opentelemetry-api/src/metrics/NoopMeter.ts @@ -202,7 +202,13 @@ export const NOOP_BOUND_BASE_OBSERVER = new NoopBoundBaseObserver(); export const NOOP_VALUE_OBSERVER_METRIC = new NoopBaseObserverMetric( NOOP_BOUND_BASE_OBSERVER ); + export const NOOP_UP_DOWN_SUM_OBSERVER_METRIC = new NoopBaseObserverMetric( NOOP_BOUND_BASE_OBSERVER ); + +export const NOOP_SUM_OBSERVER_METRIC = new NoopBaseObserverMetric( + NOOP_BOUND_BASE_OBSERVER +); + export const NOOP_BATCH_OBSERVER_METRIC = new NoopBatchObserverMetric(); diff --git a/packages/opentelemetry-metrics/README.md b/packages/opentelemetry-metrics/README.md index 2e2f1ad3937..7f200f9418e 100644 --- a/packages/opentelemetry-metrics/README.md +++ b/packages/opentelemetry-metrics/README.md @@ -153,6 +153,45 @@ function getRandomValue() { ``` +### Sum Observer + +Choose this kind of metric when collecting a sum that never decreases. +The callback can be sync or async. + +```js +const { MeterProvider } = require('@opentelemetry/metrics'); + +const meter = new MeterProvider().getMeter('your-meter-name'); + +// async callback in case you need to wait for values +meter.createSumObserver('example_metric', { + description: 'Example of an async sum observer with callback', +}, async (observerResult) => { + const value = await getAsyncValue(); + observerResult.observe(value, { label: '1' }); +}); + +function getAsyncValue() { + return new Promise((resolve) => { + setTimeout(() => { + resolve(Math.random()); + }, 100) + }); +} + +// sync callback in case you don't need to wait for values +meter.createSumObserver('example_metric', { + description: 'Example of a sync sum observer with callback', +}, (observerResult) => { + const value = getRandomValue(); + observerResult.observe(value, { label: '1' }); +}); + +function getRandomValue() { + return Math.random(); +} +``` + ### Batch Observer Choose this kind of metric when you need to update multiple observers with the results of a single async calculation. diff --git a/packages/opentelemetry-metrics/src/BaseObserverMetric.ts b/packages/opentelemetry-metrics/src/BaseObserverMetric.ts index 3065a34a077..733d701ce3e 100644 --- a/packages/opentelemetry-metrics/src/BaseObserverMetric.ts +++ b/packages/opentelemetry-metrics/src/BaseObserverMetric.ts @@ -55,8 +55,12 @@ export abstract class BaseObserverMetric extends Metric ); } + protected createObserverResult(): ObserverResult { + return new ObserverResult(); + } + async getMetricRecord(): Promise { - const observerResult = new ObserverResult(); + const observerResult = this.createObserverResult(); await this._callback(observerResult); observerResult.values.forEach((value, labels) => { const instrument = this.bind(labels); diff --git a/packages/opentelemetry-metrics/src/Meter.ts b/packages/opentelemetry-metrics/src/Meter.ts index 950296ac1a0..e6c33621137 100644 --- a/packages/opentelemetry-metrics/src/Meter.ts +++ b/packages/opentelemetry-metrics/src/Meter.ts @@ -25,6 +25,7 @@ import { UpDownSumObserverMetric } from './UpDownSumObserverMetric'; import { ValueRecorderMetric } from './ValueRecorderMetric'; import { Metric } from './Metric'; import { ValueObserverMetric } from './ValueObserverMetric'; +import { SumObserverMetric } from './SumObserverMetric'; import { DEFAULT_METRIC_OPTIONS, DEFAULT_CONFIG, MeterConfig } from './types'; import { Batcher, UngroupedBatcher } from './export/Batcher'; import { PushController } from './export/Controller'; @@ -190,6 +191,34 @@ export class Meter implements api.Meter { return valueObserver; } + createSumObserver( + name: string, + options: api.MetricOptions = {}, + callback?: (observerResult: api.ObserverResult) => unknown + ): api.SumObserver { + if (!this._isValidName(name)) { + this._logger.warn( + `Invalid metric name ${name}. Defaulting to noop metric implementation.` + ); + return api.NOOP_SUM_OBSERVER_METRIC; + } + const opt: api.MetricOptions = { + logger: this._logger, + ...DEFAULT_METRIC_OPTIONS, + ...options, + }; + const sumObserver = new SumObserverMetric( + name, + opt, + this._batcher, + this._resource, + this._instrumentationLibrary, + callback + ); + this._registerMetric(name, sumObserver); + return sumObserver; + } + /** * Creates a new `UpDownSumObserver` metric. * @param name the name of the metric. diff --git a/packages/opentelemetry-metrics/src/MonotonicObserverResult.ts b/packages/opentelemetry-metrics/src/MonotonicObserverResult.ts new file mode 100644 index 00000000000..480c196169d --- /dev/null +++ b/packages/opentelemetry-metrics/src/MonotonicObserverResult.ts @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Labels } from '@opentelemetry/api'; +import { ObserverResult } from './ObserverResult'; +export class MonotonicObserverResult extends ObserverResult { + observe(value: number, labels: Labels): void { + if (value >= 0) { + this.values.set(labels, value); + } + } +} diff --git a/packages/opentelemetry-metrics/src/SumObserverMetric.ts b/packages/opentelemetry-metrics/src/SumObserverMetric.ts new file mode 100644 index 00000000000..892ca934664 --- /dev/null +++ b/packages/opentelemetry-metrics/src/SumObserverMetric.ts @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as api from '@opentelemetry/api'; +import { InstrumentationLibrary } from '@opentelemetry/core'; +import { Resource } from '@opentelemetry/resources'; +import { BaseObserverMetric } from './BaseObserverMetric'; +import { ObserverResult } from './ObserverResult'; +import { MonotonicObserverResult } from './MonotonicObserverResult'; +import { Batcher } from './export/Batcher'; +import { MetricKind } from './export/types'; + +/** This is a SDK implementation of SumObserver Metric. */ +export class SumObserverMetric extends BaseObserverMetric + implements api.SumObserver { + constructor( + name: string, + options: api.MetricOptions, + batcher: Batcher, + resource: Resource, + instrumentationLibrary: InstrumentationLibrary, + callback?: (observerResult: api.ObserverResult) => unknown + ) { + super( + name, + options, + batcher, + resource, + MetricKind.SUM_OBSERVER, + instrumentationLibrary, + callback + ); + } + + protected createObserverResult(): ObserverResult { + return new MonotonicObserverResult(); + } +} diff --git a/packages/opentelemetry-metrics/test/Meter.test.ts b/packages/opentelemetry-metrics/test/Meter.test.ts index da9fb32ce8d..eb6f3d3d0a2 100644 --- a/packages/opentelemetry-metrics/test/Meter.test.ts +++ b/packages/opentelemetry-metrics/test/Meter.test.ts @@ -36,6 +36,7 @@ import * as api from '@opentelemetry/api'; import { NoopLogger, hrTime, hrTimeToNanoseconds } from '@opentelemetry/core'; import { BatchObserverResult } from '../src/BatchObserverResult'; import { SumAggregator } from '../src/export/aggregators'; +import { SumObserverMetric } from '../src/SumObserverMetric'; import { Resource } from '@opentelemetry/resources'; import { UpDownSumObserverMetric } from '../src/UpDownSumObserverMetric'; import { hashLabels } from '../src/Utils'; @@ -683,6 +684,142 @@ describe('Meter', () => { }); }); + describe('#SumObserverMetric', () => { + it('should create an Sum observer', () => { + const sumObserver = meter.createSumObserver('name') as SumObserverMetric; + assert.ok(sumObserver instanceof Metric); + }); + + it('should return noop observer when name is invalid', () => { + const spy = sinon.stub(meter['_logger'], 'warn'); + const sumObserver = meter.createSumObserver('na me'); + assert.ok(sumObserver === api.NOOP_SUM_OBSERVER_METRIC); + const args = spy.args[0]; + assert.ok( + args[0], + 'Invalid metric name na me. Defaulting to noop metric implementation.' + ); + }); + + it('should create observer with options', () => { + const sumObserver = meter.createSumObserver('name', { + description: 'desc', + unit: '1', + disabled: false, + }) as SumObserverMetric; + assert.ok(sumObserver instanceof Metric); + }); + + it('should set callback and observe value ', async () => { + let counter = 0; + function getValue() { + if (++counter % 2 == 0) { + return -1; + } + return 3; + } + const sumObserver = meter.createSumObserver( + 'name', + { + description: 'desc', + }, + (observerResult: api.ObserverResult) => { + // simulate async + return new Promise(resolve => { + setTimeout(() => { + observerResult.observe(getValue(), { pid: '123', core: '1' }); + resolve(); + }, 1); + }); + } + ) as SumObserverMetric; + + let metricRecords = await sumObserver.getMetricRecord(); + assert.strictEqual(metricRecords.length, 1); + let point = metricRecords[0].aggregator.toPoint(); + assert.strictEqual(point.value, 3); + assert.strictEqual( + hashLabels(metricRecords[0].labels), + '|#core:1,pid:123' + ); + + metricRecords = await sumObserver.getMetricRecord(); + assert.strictEqual(metricRecords.length, 1); + point = metricRecords[0].aggregator.toPoint(); + assert.strictEqual(point.value, 3); + + metricRecords = await sumObserver.getMetricRecord(); + assert.strictEqual(metricRecords.length, 1); + point = metricRecords[0].aggregator.toPoint(); + assert.strictEqual(point.value, 6); + }); + + it('should set callback and observe value when callback returns nothing', async () => { + const sumObserver = meter.createSumObserver( + 'name', + { + description: 'desc', + }, + (observerResult: api.ObserverResult) => { + observerResult.observe(1, { pid: '123', core: '1' }); + } + ) as SumObserverMetric; + + const metricRecords = await sumObserver.getMetricRecord(); + assert.strictEqual(metricRecords.length, 1); + }); + + it( + 'should set callback and observe value when callback returns anything' + + ' but Promise', + async () => { + const sumObserver = meter.createSumObserver( + 'name', + { + description: 'desc', + }, + (observerResult: api.ObserverResult) => { + observerResult.observe(1, { pid: '123', core: '1' }); + return '1'; + } + ) as SumObserverMetric; + + const metricRecords = await sumObserver.getMetricRecord(); + assert.strictEqual(metricRecords.length, 1); + } + ); + + it('should reject getMetricRecord when callback throws an error', async () => { + const sumObserver = meter.createSumObserver( + 'name', + { + description: 'desc', + }, + (observerResult: api.ObserverResult) => { + observerResult.observe(1, { pid: '123', core: '1' }); + throw new Error('Boom'); + } + ) as SumObserverMetric; + await sumObserver + .getMetricRecord() + .then() + .catch(e => { + assert.strictEqual(e.message, 'Boom'); + }); + }); + + it('should pipe through resource', async () => { + const sumObserver = meter.createSumObserver('name', {}, result => { + result.observe(42, { foo: 'bar' }); + return Promise.resolve(); + }) as SumObserverMetric; + assert.ok(sumObserver.resource instanceof Resource); + + const [record] = await sumObserver.getMetricRecord(); + assert.ok(record.resource instanceof Resource); + }); + }); + describe('#ValueObserver', () => { it('should create a value observer', () => { const valueObserver = meter.createValueObserver(