diff --git a/examples/metrics/metrics/observer.js b/examples/metrics/metrics/observer.js index bb398bb95a..6cced2142c 100644 --- a/examples/metrics/metrics/observer.js +++ b/examples/metrics/metrics/observer.js @@ -1,6 +1,6 @@ 'use strict'; -const { MeterProvider } = require('@opentelemetry/metrics'); +const { MeterProvider, MetricObservable } = require('@opentelemetry/metrics'); const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); const exporter = new PrometheusExporter( @@ -14,7 +14,7 @@ const exporter = new PrometheusExporter( const meter = new MeterProvider({ exporter, - interval: 1000, + interval: 2000, }).getMeter('example-observer'); const otelCpuUsage = meter.createObserver('metric_observer', { @@ -27,9 +27,16 @@ function getCpuUsage() { return Math.random(); } +const observable = new MetricObservable(); + +setInterval(() => { + observable.next(getCpuUsage()); +}, 5000); + otelCpuUsage.setCallback((observerResult) => { observerResult.observe(getCpuUsage, { pid: process.pid, core: '1' }); observerResult.observe(getCpuUsage, { pid: process.pid, core: '2' }); observerResult.observe(getCpuUsage, { pid: process.pid, core: '3' }); observerResult.observe(getCpuUsage, { pid: process.pid, core: '4' }); + observerResult.observe(observable, { pid: process.pid, core: '5' }); }); diff --git a/examples/metrics/metrics/observer.png b/examples/metrics/metrics/observer.png index 7a9184af13..79f77b0c1c 100644 Binary files a/examples/metrics/metrics/observer.png and b/examples/metrics/metrics/observer.png differ diff --git a/packages/opentelemetry-api/src/index.ts b/packages/opentelemetry-api/src/index.ts index 070ed01c05..a4a7406e71 100644 --- a/packages/opentelemetry-api/src/index.ts +++ b/packages/opentelemetry-api/src/index.ts @@ -26,6 +26,7 @@ export * from './metrics/BoundInstrument'; export * from './metrics/Meter'; export * from './metrics/MeterProvider'; export * from './metrics/Metric'; +export * from './metrics/MetricObservable'; export * from './metrics/NoopMeter'; export * from './metrics/NoopMeterProvider'; export * from './metrics/ObserverResult'; diff --git a/packages/opentelemetry-api/src/metrics/MetricObservable.ts b/packages/opentelemetry-api/src/metrics/MetricObservable.ts new file mode 100644 index 0000000000..4e25dce4e5 --- /dev/null +++ b/packages/opentelemetry-api/src/metrics/MetricObservable.ts @@ -0,0 +1,36 @@ +/*! + * Copyright 2020, 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. + */ + +/** + * Metric Observable class to handle asynchronous metrics + */ +export interface MetricObservable { + /** + * Sets the next value for observable metric + * @param value + */ + next: (value: number) => void; + /** + * Subscribes for every value change + * @param callback + */ + subscribe: (callback: (value: number) => void) => void; + /** + * Removes the subscriber + * @param [callback] + */ + unsubscribe: (callback?: (value: number) => void) => void; +} diff --git a/packages/opentelemetry-api/src/metrics/ObserverResult.ts b/packages/opentelemetry-api/src/metrics/ObserverResult.ts index 6fb4edba70..47c3d595c4 100644 --- a/packages/opentelemetry-api/src/metrics/ObserverResult.ts +++ b/packages/opentelemetry-api/src/metrics/ObserverResult.ts @@ -15,11 +15,11 @@ */ import { Labels } from './Metric'; +import { MetricObservable } from './MetricObservable'; /** * Interface that is being used in function setCallback for Observer Metric */ export interface ObserverResult { - observers: Map; - observe(callback: Function, labels: Labels): void; + observe(callback: Function | MetricObservable, labels: Labels): void; } diff --git a/packages/opentelemetry-exporter-collector/package.json b/packages/opentelemetry-exporter-collector/package.json index 5b1e97497d..b6f7cb3e17 100644 --- a/packages/opentelemetry-exporter-collector/package.json +++ b/packages/opentelemetry-exporter-collector/package.json @@ -77,7 +77,7 @@ "ts-node": "^8.6.2", "tslint-consistent-codestyle": "^1.16.0", "tslint-microsoft-contrib": "^6.2.0", - "typescript": "3.6.4", + "typescript": "3.7.2", "webpack": "^4.35.2", "webpack-cli": "^3.3.9", "webpack-merge": "^4.2.2" diff --git a/packages/opentelemetry-metrics/README.md b/packages/opentelemetry-metrics/README.md index eb516f6948..1de26eab56 100644 --- a/packages/opentelemetry-metrics/README.md +++ b/packages/opentelemetry-metrics/README.md @@ -25,8 +25,8 @@ const { MeterProvider } = require('@opentelemetry/metrics'); const meter = new MeterProvider().getMeter('your-meter-name'); const counter = meter.createCounter('metric_name', { - labelKeys: ["pid"], - description: "Example of a counter" + labelKeys: ['pid'], + description: 'Example of a counter' }); const labels = { pid: process.pid }; @@ -34,6 +34,41 @@ const labels = { pid: process.pid }; // Create a BoundInstrument associated with specified label values. const boundCounter = counter.bind(labels); boundCounter.add(10); + +``` + +### Observable +Choose this kind of metric when only last value is important without worry about aggregation + +```js +const { MeterProvider, MetricObservable } = require('@opentelemetry/metrics'); + +// Initialize the Meter to capture measurements in various ways. +const meter = new MeterProvider().getMeter('your-meter-name'); + +const observer = meter.createObserver('metric_name', { + labelKeys: ['pid', 'core'], + description: 'Example of a observer' +}); + +function getCpuUsage() { + return Math.random(); +} + +const metricObservable = new MetricObservable(); + +observer.setCallback((observerResult) => { + // synchronous callback + observerResult.observe(getCpuUsage, { pid: process.pid, core: '1' }); + // asynchronous callback + observerResult.observe(metricObservable, { pid: process.pid, core: '2' }); +}); + +// simulate asynchronous operation +setInterval(()=> { + metricObservable.next(getCpuUsage()); +}, 2000) + ``` See [examples/prometheus](https://github.com/open-telemetry/opentelemetry-js/tree/master/examples/prometheus) for a short example. diff --git a/packages/opentelemetry-metrics/src/Metric.ts b/packages/opentelemetry-metrics/src/Metric.ts index 6d81b4e2f2..cd666a210e 100644 --- a/packages/opentelemetry-metrics/src/Metric.ts +++ b/packages/opentelemetry-metrics/src/Metric.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import * as types from '@opentelemetry/api'; +import * as api from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import { BoundCounter, @@ -30,11 +30,11 @@ import { hashLabels } from './Utils'; /** This is a SDK implementation of {@link Metric} interface. */ export abstract class Metric - implements types.Metric { + implements api.Metric { protected readonly _monotonic: boolean; protected readonly _disabled: boolean; - protected readonly _valueType: types.ValueType; - protected readonly _logger: types.Logger; + protected readonly _valueType: api.ValueType; + protected readonly _logger: api.Logger; private readonly _descriptor: MetricDescriptor; private readonly _instruments: Map = new Map(); @@ -58,7 +58,7 @@ export abstract class Metric * @param labels key-values pairs that are associated with a specific metric * that you want to record. */ - bind(labels: types.Labels): T { + bind(labels: api.Labels): T { const hash = hashLabels(labels); if (this._instruments.has(hash)) return this._instruments.get(hash)!; @@ -71,7 +71,7 @@ export abstract class Metric * Removes the Instrument from the metric, if it is present. * @param labels key-values pairs that are associated with a specific metric. */ - unbind(labels: types.Labels): void { + unbind(labels: api.Labels): void { this._instruments.delete(hashLabels(labels)); } @@ -102,12 +102,12 @@ export abstract class Metric }; } - protected abstract _makeInstrument(labels: types.Labels): T; + protected abstract _makeInstrument(labels: api.Labels): T; } /** This is a SDK implementation of Counter Metric. */ export class CounterMetric extends Metric - implements Pick { + implements Pick { constructor( name: string, options: MetricOptions, @@ -116,7 +116,7 @@ export class CounterMetric extends Metric ) { super(name, options, MetricKind.COUNTER, resource); } - protected _makeInstrument(labels: types.Labels): BoundCounter { + protected _makeInstrument(labels: api.Labels): BoundCounter { return new BoundCounter( labels, this._disabled, @@ -134,13 +134,13 @@ export class CounterMetric extends Metric * @param labels key-values pairs that are associated with a specific metric * that you want to record. */ - add(value: number, labels: types.Labels) { + add(value: number, labels: api.Labels) { this.bind(labels).add(value); } } export class MeasureMetric extends Metric - implements Pick { + implements Pick { protected readonly _absolute: boolean; constructor( @@ -153,7 +153,7 @@ export class MeasureMetric extends Metric this._absolute = options.absolute !== undefined ? options.absolute : true; // Absolute default is true } - protected _makeInstrument(labels: types.Labels): BoundMeasure { + protected _makeInstrument(labels: api.Labels): BoundMeasure { return new BoundMeasure( labels, this._disabled, @@ -165,15 +165,15 @@ export class MeasureMetric extends Metric ); } - record(value: number, labels: types.Labels) { + record(value: number, labels: api.Labels) { this.bind(labels).record(value); } } /** This is a SDK implementation of Observer Metric. */ export class ObserverMetric extends Metric - implements Pick { - private _observerResult: types.ObserverResult = new ObserverResult(); + implements Pick { + private _observerResult = new ObserverResult(); constructor( name: string, @@ -184,7 +184,7 @@ export class ObserverMetric extends Metric super(name, options, MetricKind.OBSERVER, resource); } - protected _makeInstrument(labels: types.Labels): BoundObserver { + protected _makeInstrument(labels: api.Labels): BoundObserver { return new BoundObserver( labels, this._disabled, @@ -196,7 +196,7 @@ export class ObserverMetric extends Metric } getMetricRecord(): MetricRecord[] { - this._observerResult.observers.forEach((callback, labels) => { + this._observerResult.callbackObservers.forEach((callback, labels) => { const instrument = this.bind(labels); instrument.update(callback()); }); @@ -207,7 +207,13 @@ export class ObserverMetric extends Metric * Sets a callback where user can observe value for certain labels * @param callback */ - setCallback(callback: (observerResult: types.ObserverResult) => void): void { + setCallback(callback: (observerResult: api.ObserverResult) => void): void { callback(this._observerResult); + this._observerResult.observers.forEach((observer, labels) => { + observer.subscribe(value => { + const instrument = this.bind(labels); + instrument.update(value); + }); + }); } } diff --git a/packages/opentelemetry-metrics/src/MetricObservable.ts b/packages/opentelemetry-metrics/src/MetricObservable.ts new file mode 100644 index 0000000000..cd52774743 --- /dev/null +++ b/packages/opentelemetry-metrics/src/MetricObservable.ts @@ -0,0 +1,51 @@ +/*! + * Copyright 2020, 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'; + +type Subscriber = (value?: number) => void; + +/** + * Implements the Metric Observable pattern + */ +export class MetricObservable implements api.MetricObservable { + private _subscribers: Subscriber[] = []; + + next(value: number) { + for (const subscriber of this._subscribers) { + subscriber(value); + } + } + + subscribe(subscriber: Function) { + if (typeof subscriber === 'function') { + this._subscribers.push(subscriber as Subscriber); + } + } + + unsubscribe(subscriber?: Function) { + if (typeof subscriber === 'function') { + for (let i = 0, j = this._subscribers.length; i < j; i++) { + if (this._subscribers[i] === subscriber) { + this._subscribers.splice(i, 1); + break; + } + } + } else { + this._subscribers = []; + } + } +} diff --git a/packages/opentelemetry-metrics/src/ObserverResult.ts b/packages/opentelemetry-metrics/src/ObserverResult.ts index 1cb9ecebd4..2ed593351d 100644 --- a/packages/opentelemetry-metrics/src/ObserverResult.ts +++ b/packages/opentelemetry-metrics/src/ObserverResult.ts @@ -15,6 +15,7 @@ */ import { + MetricObservable, ObserverResult as TypeObserverResult, Labels, } from '@opentelemetry/api'; @@ -23,8 +24,17 @@ import { * Implementation of {@link TypeObserverResult} */ export class ObserverResult implements TypeObserverResult { - observers = new Map(); - observe(callback: any, labels: Labels): void { - this.observers.set(labels, callback); + callbackObservers: Map = new Map(); + observers: Map = new Map< + Labels, + MetricObservable + >(); + + observe(callback: Function | MetricObservable, labels: Labels): void { + if (typeof callback === 'function') { + this.callbackObservers.set(labels, callback); + } else { + this.observers.set(labels, callback); + } } } diff --git a/packages/opentelemetry-metrics/src/index.ts b/packages/opentelemetry-metrics/src/index.ts index 113f788dd9..6cb01ff9a8 100644 --- a/packages/opentelemetry-metrics/src/index.ts +++ b/packages/opentelemetry-metrics/src/index.ts @@ -16,8 +16,9 @@ export * from './BoundInstrument'; export * from './Meter'; -export * from './Metric'; export * from './MeterProvider'; +export * from './Metric'; +export * from './MetricObservable'; export * from './export/aggregators'; export * from './export/ConsoleMetricExporter'; export * from './export/types'; diff --git a/packages/opentelemetry-metrics/test/Meter.test.ts b/packages/opentelemetry-metrics/test/Meter.test.ts index 0eaab7410d..7de77a6984 100644 --- a/packages/opentelemetry-metrics/test/Meter.test.ts +++ b/packages/opentelemetry-metrics/test/Meter.test.ts @@ -27,6 +27,7 @@ import { ObserverMetric, MetricRecord, Aggregator, + MetricObservable, } from '../src'; import * as types from '@opentelemetry/api'; import { NoopLogger, hrTime, hrTimeToNanoseconds } from '@opentelemetry/core'; @@ -440,6 +441,7 @@ describe('Meter', () => { }) as ObserverMetric; assert.ok(measure instanceof Metric); }); + it('should set callback and observe value ', () => { const measure = meter.createObserver('name', { description: 'desc', @@ -450,21 +452,28 @@ describe('Meter', () => { return Math.random(); } + const metricObservable = new MetricObservable(); + measure.setCallback((observerResult: types.ObserverResult) => { observerResult.observe(getCpuUsage, { pid: '123', core: '1' }); observerResult.observe(getCpuUsage, { pid: '123', core: '2' }); observerResult.observe(getCpuUsage, { pid: '123', core: '3' }); observerResult.observe(getCpuUsage, { pid: '123', core: '4' }); + observerResult.observe(metricObservable, { pid: '123', core: '5' }); }); + metricObservable.next(0.123); + const metricRecords: MetricRecord[] = measure.getMetricRecord(); - assert.strictEqual(metricRecords.length, 4); + assert.strictEqual(metricRecords.length, 5); - const metric1 = metricRecords[0]; - const metric2 = metricRecords[1]; - const metric3 = metricRecords[2]; - const metric4 = metricRecords[3]; + const metric5 = metricRecords[0]; + assert.strictEqual(hashLabels(metric5.labels), '|#core:5,pid:123'); + const metric1 = metricRecords[1]; + const metric2 = metricRecords[2]; + const metric3 = metricRecords[3]; + const metric4 = metricRecords[4]; assert.strictEqual(hashLabels(metric1.labels), '|#core:1,pid:123'); assert.strictEqual(hashLabels(metric2.labels), '|#core:2,pid:123'); assert.strictEqual(hashLabels(metric3.labels), '|#core:3,pid:123'); @@ -474,6 +483,7 @@ describe('Meter', () => { ensureMetric(metric2); ensureMetric(metric3); ensureMetric(metric4); + ensureMetric(metric5); }); it('should return an observer with resource', () => {