diff --git a/examples/metrics/README.md b/examples/metrics/README.md index 63fd8a5858..ee242cfd26 100644 --- a/examples/metrics/README.md +++ b/examples/metrics/README.md @@ -26,7 +26,7 @@ npm run start:observer ### Prometheus -1. In prometheus search for "metric_observer" +1. In prometheus search for "cpu_core_usage", "cpu_temp_per_app", "cpu_usage_per_app" ### Links @@ -35,7 +35,9 @@ npm run start:observer ### Example -

+![Screenshot of the running example](metrics/observer.png) +![Screenshot of the running example](metrics/observer_batch.png) +![Screenshot of the running example](metrics/observer_batch2.png) ## Useful links diff --git a/examples/metrics/metrics/observer.js b/examples/metrics/metrics/observer.js index 52c424b11a..f455383a05 100644 --- a/examples/metrics/metrics/observer.js +++ b/examples/metrics/metrics/observer.js @@ -1,6 +1,7 @@ 'use strict'; -const { MeterProvider, MetricObservable } = require('@opentelemetry/metrics'); +const { MeterProvider } = require('@opentelemetry/metrics'); +const { ConsoleLogger, LogLevel } = require('@opentelemetry/core'); const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); const exporter = new PrometheusExporter( @@ -19,25 +20,68 @@ const meter = new MeterProvider({ interval: 2000, }).getMeter('example-observer'); -const otelCpuUsage = meter.createObserver('metric_observer', { - monotonic: false, - description: 'Example of a observer', +meter.createValueObserver('cpu_core_usage', { + description: 'Example of a sync value observer with callback', +}, (observerResult) => { // this callback is called once per each interval + observerResult.observe(getRandomValue(), { core: '1' }); + observerResult.observe(getRandomValue(), { core: '2' }); }); -function getCpuUsage() { - return Math.random(); -} +// no callback as they will be updated in batch observer +const tempMetric = meter.createValueObserver('cpu_temp_per_app', { + description: 'Example of sync value observer used with async batch observer', +}); -const observable = new MetricObservable(); +// no callback as they will be updated in batch observer +const cpuUsageMetric = meter.createValueObserver('cpu_usage_per_app', { + description: 'Example of sync value observer used with async batch observer', +}); -setInterval(() => { - observable.next(getCpuUsage()); -}, 5000); +meter.createBatchObserver('metric_batch_observer', (observerBatchResult) => { + Promise.all([ + someAsyncMetrics(), + // simulate waiting + new Promise((resolve, reject) => { + setTimeout(resolve, 300); + }), + ]).then(([apps, waiting]) => { + apps.forEach(app => { + observerBatchResult.observe({ app: app.name, core: '1' }, [ + tempMetric.observation(app.core1.temp), + cpuUsageMetric.observation(app.core1.usage), + ]); + observerBatchResult.observe({ app: app.name, core: '2' }, [ + tempMetric.observation(app.core2.temp), + cpuUsageMetric.observation(app.core2.usage), + ]); + }); + }); + }, { + maxTimeoutUpdateMS: 500, + logger: new ConsoleLogger(LogLevel.DEBUG) + }, +); -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' }); -}); +function someAsyncMetrics() { + return new Promise((resolve) => { + setTimeout(() => { + const stats = [ + { + name: 'app1', + core1: { usage: getRandomValue(), temp: getRandomValue() * 100 }, + core2: { usage: getRandomValue(), temp: getRandomValue() * 100 }, + }, + { + name: 'app2', + core1: { usage: getRandomValue(), temp: getRandomValue() * 100 }, + core2: { usage: getRandomValue(), temp: getRandomValue() * 100 }, + }, + ]; + resolve(stats); + }, 200); + }); +} + +function getRandomValue() { + return Math.random(); +} diff --git a/examples/metrics/metrics/observer.png b/examples/metrics/metrics/observer.png index 79f77b0c1c..b8c3c48910 100644 Binary files a/examples/metrics/metrics/observer.png and b/examples/metrics/metrics/observer.png differ diff --git a/examples/metrics/metrics/observer_batch.png b/examples/metrics/metrics/observer_batch.png new file mode 100644 index 0000000000..32b5f25fe2 Binary files /dev/null and b/examples/metrics/metrics/observer_batch.png differ diff --git a/examples/metrics/metrics/observer_batch2.png b/examples/metrics/metrics/observer_batch2.png new file mode 100644 index 0000000000..a8792e913a Binary files /dev/null and b/examples/metrics/metrics/observer_batch2.png differ diff --git a/examples/metrics/package.json b/examples/metrics/package.json index f412f371a4..e7b8bfce83 100644 --- a/examples/metrics/package.json +++ b/examples/metrics/package.json @@ -26,6 +26,7 @@ "url": "https://github.com/open-telemetry/opentelemetry-js/issues" }, "dependencies": { + "@opentelemetry/core": "^0.9.0", "@opentelemetry/exporter-prometheus": "^0.9.0", "@opentelemetry/metrics": "^0.9.0" }, diff --git a/examples/tracer-web/examples/xml-http-request/index.js b/examples/tracer-web/examples/xml-http-request/index.js index ce22cec2c5..fb1a6529c8 100644 --- a/examples/tracer-web/examples/xml-http-request/index.js +++ b/examples/tracer-web/examples/xml-http-request/index.js @@ -26,16 +26,19 @@ providerWithZone.register({ const webTracerWithZone = providerWithZone.getTracer('example-tracer-web'); -const getData = (url) => new Promise((resolve, _reject) => { +const getData = (url) => new Promise((resolve, reject) => { // eslint-disable-next-line no-undef const req = new XMLHttpRequest(); req.open('GET', url, true); req.setRequestHeader('Content-Type', 'application/json'); req.setRequestHeader('Accept', 'application/json'); - req.send(); req.onload = () => { resolve(); }; + req.onerror = () => { + reject(); + }; + req.send(); }); // example of keeping track of context between async operations @@ -53,6 +56,9 @@ const prepareClickEvent = () => { getData(url1).then((_data) => { webTracerWithZone.getCurrentSpan().addEvent('fetching-span1-completed'); span1.end(); + }, ()=> { + webTracerWithZone.getCurrentSpan().addEvent('fetching-error'); + span1.end(); }); }); } diff --git a/packages/opentelemetry-api/src/index.ts b/packages/opentelemetry-api/src/index.ts index 2f5f81ac99..f9bcf40664 100644 --- a/packages/opentelemetry-api/src/index.ts +++ b/packages/opentelemetry-api/src/index.ts @@ -22,13 +22,14 @@ export * from './context/propagation/NoopHttpTextPropagator'; export * from './context/propagation/setter'; export * from './correlation_context/CorrelationContext'; export * from './correlation_context/EntryValue'; +export * from './metrics/BatchObserverResult'; 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/Observation'; export * from './metrics/ObserverResult'; export * from './trace/attributes'; export * from './trace/Event'; diff --git a/packages/opentelemetry-api/src/metrics/MetricObservable.ts b/packages/opentelemetry-api/src/metrics/BatchObserverResult.ts similarity index 61% rename from packages/opentelemetry-api/src/metrics/MetricObservable.ts rename to packages/opentelemetry-api/src/metrics/BatchObserverResult.ts index 161d2919f4..bae99eb866 100644 --- a/packages/opentelemetry-api/src/metrics/MetricObservable.ts +++ b/packages/opentelemetry-api/src/metrics/BatchObserverResult.ts @@ -13,20 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -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; + +import { Labels } from './Metric'; +import { Observation } from './Observation'; + +/** + * Interface that is being used in callback function for Observer Metric + * for batch + */ +export interface BatchObserverResult { /** - * Removes the subscriber - * @param [callback] + * Used to observe (update) observations for certain labels + * @param labels + * @param observations */ - unsubscribe: (callback?: (value: number) => void) => void; + observe(labels: Labels, observations: Observation[]): void; } diff --git a/packages/opentelemetry-api/src/metrics/BoundInstrument.ts b/packages/opentelemetry-api/src/metrics/BoundInstrument.ts index 5b77158356..4ffbd143c7 100644 --- a/packages/opentelemetry-api/src/metrics/BoundInstrument.ts +++ b/packages/opentelemetry-api/src/metrics/BoundInstrument.ts @@ -44,3 +44,8 @@ export interface BoundValueRecorder { spanContext: SpanContext ): void; } + +/** An Instrument for Base Observer */ +export interface BoundBaseObserver { + update(value: number): void; +} diff --git a/packages/opentelemetry-api/src/metrics/Meter.ts b/packages/opentelemetry-api/src/metrics/Meter.ts index cf6d15df04..eb570a4802 100644 --- a/packages/opentelemetry-api/src/metrics/Meter.ts +++ b/packages/opentelemetry-api/src/metrics/Meter.ts @@ -14,13 +14,17 @@ * limitations under the License. */ +import { BatchObserverResult } from './BatchObserverResult'; import { MetricOptions, Counter, ValueRecorder, - Observer, + ValueObserver, + BatchObserver, + BatchMetricOptions, UpDownCounter, } from './Metric'; +import { ObserverResult } from './ObserverResult'; /** * An interface to allow the recording metrics. @@ -66,9 +70,27 @@ export interface Meter { createUpDownCounter(name: string, options?: MetricOptions): UpDownCounter; /** - * Creates a new `Observer` metric. + * Creates a new `ValueObserver` metric. * @param name the name of the metric. * @param [options] the metric options. + * @param [callback] the observer callback */ - createObserver(name: string, options?: MetricOptions): Observer; + createValueObserver( + name: string, + options?: MetricOptions, + callback?: (observerResult: ObserverResult) => void + ): ValueObserver; + + /** + * Creates a new `BatchObserver` metric, can be used to update many metrics + * at the same time and when operations needs to be async + * @param name the name of the metric. + * @param callback the batch observer callback + * @param [options] the metric batch options. + */ + createBatchObserver( + name: string, + callback: (batchObserverResult: BatchObserverResult) => void, + options?: BatchMetricOptions + ): BatchObserver; } diff --git a/packages/opentelemetry-api/src/metrics/Metric.ts b/packages/opentelemetry-api/src/metrics/Metric.ts index 24f36cfd05..ebf514d6e3 100644 --- a/packages/opentelemetry-api/src/metrics/Metric.ts +++ b/packages/opentelemetry-api/src/metrics/Metric.ts @@ -16,8 +16,12 @@ import { CorrelationContext } from '../correlation_context/CorrelationContext'; import { SpanContext } from '../trace/span_context'; -import { ObserverResult } from './ObserverResult'; -import { BoundCounter, BoundValueRecorder } from './BoundInstrument'; +import { + BoundBaseObserver, + BoundCounter, + BoundValueRecorder, +} from './BoundInstrument'; +import { Logger } from '../common/Logger'; /** * Options needed for metric creation @@ -58,6 +62,18 @@ export interface MetricOptions { * @default {@link ValueType.DOUBLE} */ valueType?: ValueType; + + /** + * User provided logger. + */ + logger?: Logger; +} + +export interface BatchMetricOptions extends MetricOptions { + /** + * Indicates how long the batch metric should wait to update before cancel + */ + maxTimeoutUpdateMS?: number; } /** The Type of value. It describes how the data is reported. */ @@ -148,16 +164,21 @@ export interface ValueRecorder extends UnboundMetric { } /** Base interface for the Observer metrics. */ -export interface Observer extends Metric { - /** - * Sets a callback where user can observe value for certain labels. The - * observers are called periodically to retrieve the value. - * @param callback a function that will be called once to set observers - * for values - */ - setCallback(callback: (observerResult: ObserverResult) => void): void; +export interface BaseObserver extends UnboundMetric { + observation: ( + value: number + ) => { + value: number; + observer: BaseObserver; + }; } +/** Base interface for the Value Observer metrics. */ +export type ValueObserver = BaseObserver; + +/** Base interface for the Batch Observer metrics. */ +export type BatchObserver = Metric; + /** * key-value pairs passed by the user. */ diff --git a/packages/opentelemetry-api/src/metrics/NoopMeter.ts b/packages/opentelemetry-api/src/metrics/NoopMeter.ts index 075a1e544e..9d63f21bb5 100644 --- a/packages/opentelemetry-api/src/metrics/NoopMeter.ts +++ b/packages/opentelemetry-api/src/metrics/NoopMeter.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { BatchObserverResult } from './BatchObserverResult'; import { Meter } from './Meter'; import { MetricOptions, @@ -21,10 +22,16 @@ import { Labels, Counter, ValueRecorder, - Observer, + ValueObserver, + BatchObserver, UpDownCounter, + BaseObserver, } from './Metric'; -import { BoundValueRecorder, BoundCounter } from './BoundInstrument'; +import { + BoundValueRecorder, + BoundCounter, + BoundBaseObserver, +} from './BoundInstrument'; import { CorrelationContext } from '../correlation_context/CorrelationContext'; import { SpanContext } from '../trace/span_context'; import { ObserverResult } from './ObserverResult'; @@ -64,12 +71,29 @@ export class NoopMeter implements Meter { } /** - * Returns constant noop observer. + * Returns constant noop value observer. * @param name the name of the metric. * @param [options] the metric options. + * @param [callback] the value observer callback + */ + createValueObserver( + name: string, + options?: MetricOptions, + callback?: (observerResult: ObserverResult) => void + ): ValueObserver { + return NOOP_VALUE_OBSERVER_METRIC; + } + + /** + * Returns constant noop batch observer. + * @param name the name of the metric. + * @param callback the batch observer callback */ - createObserver(name: string, options?: MetricOptions): Observer { - return NOOP_OBSERVER_METRIC; + createBatchObserver( + name: string, + callback: (batchObserverResult: BatchObserverResult) => void + ): BatchObserver { + return NOOP_BATCH_OBSERVER_METRIC; } } @@ -79,6 +103,7 @@ export class NoopMetric implements UnboundMetric { constructor(instrument: T) { this._instrument = instrument; } + /** * Returns a Bound Instrument associated with specified Labels. * It is recommended to keep a reference to the Bound Instrument instead of @@ -131,10 +156,19 @@ export class NoopValueRecorderMetric extends NoopMetric } } -export class NoopObserverMetric extends NoopMetric implements Observer { - setCallback(callback: (observerResult: ObserverResult) => void): void {} +export class NoopBaseObserverMetric extends NoopMetric + implements BaseObserver { + observation() { + return { + observer: this as BaseObserver, + value: 0, + }; + } } +export class NoopBatchObserverMetric extends NoopMetric + implements BatchObserver {} + export class NoopBoundCounter implements BoundCounter { add(value: number): void { return; @@ -151,6 +185,10 @@ export class NoopBoundValueRecorder implements BoundValueRecorder { } } +export class NoopBoundBaseObserver implements BoundBaseObserver { + update(value: number) {} +} + export const NOOP_METER = new NoopMeter(); export const NOOP_BOUND_COUNTER = new NoopBoundCounter(); export const NOOP_COUNTER_METRIC = new NoopCounterMetric(NOOP_BOUND_COUNTER); @@ -160,4 +198,8 @@ export const NOOP_VALUE_RECORDER_METRIC = new NoopValueRecorderMetric( NOOP_BOUND_VALUE_RECORDER ); -export const NOOP_OBSERVER_METRIC = new NoopObserverMetric(); +export const NOOP_BOUND_BASE_OBSERVER = new NoopBoundBaseObserver(); +export const NOOP_VALUE_OBSERVER_METRIC = new NoopBaseObserverMetric( + NOOP_BOUND_BASE_OBSERVER +); +export const NOOP_BATCH_OBSERVER_METRIC = new NoopBatchObserverMetric(); diff --git a/packages/opentelemetry-api/src/metrics/Observation.ts b/packages/opentelemetry-api/src/metrics/Observation.ts new file mode 100644 index 0000000000..d36f48fb71 --- /dev/null +++ b/packages/opentelemetry-api/src/metrics/Observation.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 { BaseObserver } from './Metric'; + +/** + * Interface for updating value of certain value observer + */ +export interface Observation { + observer: BaseObserver; + value: number; +} diff --git a/packages/opentelemetry-api/src/metrics/ObserverResult.ts b/packages/opentelemetry-api/src/metrics/ObserverResult.ts index 0b7286ac38..7792483ad1 100644 --- a/packages/opentelemetry-api/src/metrics/ObserverResult.ts +++ b/packages/opentelemetry-api/src/metrics/ObserverResult.ts @@ -15,11 +15,10 @@ */ import { Labels } from './Metric'; -import { MetricObservable } from './MetricObservable'; /** - * Interface that is being used in function setCallback for Observer Metric + * Interface that is being used in callback function for Observer Metric */ export interface ObserverResult { - observe(callback: Function | MetricObservable, labels: Labels): void; + observe(value: number, labels: Labels): void; } diff --git a/packages/opentelemetry-exporter-prometheus/src/prometheus.ts b/packages/opentelemetry-exporter-prometheus/src/prometheus.ts index e2b23b33a2..2951ddeeee 100644 --- a/packages/opentelemetry-exporter-prometheus/src/prometheus.ts +++ b/packages/opentelemetry-exporter-prometheus/src/prometheus.ts @@ -14,22 +14,21 @@ * limitations under the License. */ +import * as api from '@opentelemetry/api'; import { ExportResult, - NoopLogger, hrTimeToMilliseconds, + NoopLogger, } from '@opentelemetry/core'; import { - CounterSumAggregator, - LastValue, - MetricExporter, - MetricRecord, + Distribution, + Histogram, MetricDescriptor, + MetricExporter, MetricKind, - ObserverAggregator, + MetricRecord, Sum, } from '@opentelemetry/metrics'; -import * as api from '@opentelemetry/api'; import { createServer, IncomingMessage, Server, ServerResponse } from 'http'; import { Counter, Gauge, Metric, Registry } from 'prom-client'; import * as url from 'url'; @@ -143,18 +142,33 @@ export class PrometheusExporter implements MetricExporter { } if (metric instanceof Gauge) { - if (record.aggregator instanceof CounterSumAggregator) { - metric.set(labels, point.value as Sum); - } else if (record.aggregator instanceof ObserverAggregator) { + if (typeof point.value === 'number') { + if ( + record.descriptor.metricKind === MetricKind.VALUE_OBSERVER || + record.descriptor.metricKind === MetricKind.VALUE_RECORDER + ) { + metric.set( + labels, + point.value, + hrTimeToMilliseconds(point.timestamp) + ); + } else { + metric.set(labels, point.value); + } + } else if ((point.value as Histogram).buckets) { metric.set( labels, - point.value as LastValue, + (point.value as Histogram).sum, + hrTimeToMilliseconds(point.timestamp) + ); + } else if (typeof (point.value as Distribution).max === 'number') { + metric.set( + labels, + (point.value as Distribution).sum, hrTimeToMilliseconds(point.timestamp) ); } } - - // TODO: only counter and gauge are implemented in metrics so far } private _registerMetric(record: MetricRecord): Metric | undefined { @@ -194,7 +208,10 @@ export class PrometheusExporter implements MetricExporter { return new Counter(metricObject); case MetricKind.UP_DOWN_COUNTER: return new Gauge(metricObject); - case MetricKind.OBSERVER: + // case MetricKind.VALUE_RECORDER: + // case MetricKind.SUM_OBSERVER: + // case MetricKind.UP_DOWN_SUM_OBSERVER: + case MetricKind.VALUE_OBSERVER: return new Gauge(metricObject); default: // Other metric types are currently unimplemented diff --git a/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts b/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts index f0a152e9b2..ba499de3f5 100644 --- a/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts +++ b/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts @@ -17,10 +17,9 @@ import { HrTime, ObserverResult } from '@opentelemetry/api'; import { CounterMetric, - CounterSumAggregator, + SumAggregator, Meter, MeterProvider, - ObserverMetric, Point, } from '@opentelemetry/metrics'; import * as assert from 'assert'; @@ -33,15 +32,15 @@ const mockedTimeMS = 1586347902211000; describe('PrometheusExporter', () => { let toPoint: () => Point; before(() => { - toPoint = CounterSumAggregator.prototype.toPoint; - CounterSumAggregator.prototype.toPoint = function (): Point { + toPoint = SumAggregator.prototype.toPoint; + SumAggregator.prototype.toPoint = function (): Point { const point = toPoint.apply(this); point.timestamp = mockedHrTime; return point; }; }); after(() => { - CounterSumAggregator.prototype.toPoint = toPoint; + SumAggregator.prototype.toPoint = toPoint; }); describe('constructor', () => { it('should construct an exporter', () => { @@ -204,35 +203,36 @@ describe('PrometheusExporter', () => { const boundCounter = counter.bind({ key1: 'labelValue1' }); boundCounter.add(10); - meter.collect(); - exporter.export(meter.getBatcher().checkPointSet(), () => { - // This is to test the special case where counters are destroyed - // and recreated in the exporter in order to get around prom-client's - // aggregation and use ours. - boundCounter.add(10); + meter.collect().then(() => { exporter.export(meter.getBatcher().checkPointSet(), () => { - http - .get('http://localhost:9464/metrics', res => { - res.on('data', chunk => { - const body = chunk.toString(); - const lines = body.split('\n'); - - assert.strictEqual( - lines[0], - '# HELP counter a test description' - ); - - assert.deepStrictEqual(lines, [ - '# HELP counter a test description', - '# TYPE counter counter', - `counter{key1="labelValue1"} 20 ${mockedTimeMS}`, - '', - ]); - - done(); - }); - }) - .on('error', errorHandler(done)); + // This is to test the special case where counters are destroyed + // and recreated in the exporter in order to get around prom-client's + // aggregation and use ours. + boundCounter.add(10); + exporter.export(meter.getBatcher().checkPointSet(), () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.strictEqual( + lines[0], + '# HELP counter a test description' + ); + + assert.deepStrictEqual(lines, [ + '# HELP counter a test description', + '# TYPE counter counter', + `counter{key1="labelValue1"} 20 ${mockedTimeMS}`, + '', + ]); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); }); }); }); @@ -242,16 +242,60 @@ describe('PrometheusExporter', () => { return Math.random(); } - const observer = meter.createObserver('metric_observer', { - description: 'a test description', - }) as ObserverMetric; + meter.createValueObserver( + 'metric_observer', + { + description: 'a test description', + }, + (observerResult: ObserverResult) => { + observerResult.observe(getCpuUsage(), { + pid: String(123), + core: '1', + }); + } + ); - observer.setCallback((observerResult: ObserverResult) => { - observerResult.observe(getCpuUsage, { pid: String(123), core: '1' }); + meter.collect().then(() => { + exporter.export(meter.getBatcher().checkPointSet(), () => { + exporter.export(meter.getBatcher().checkPointSet(), () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.strictEqual( + lines[0], + '# HELP metric_observer a test description' + ); + assert.strictEqual(lines[1], '# TYPE metric_observer gauge'); + + const line3 = lines[2].split(' '); + assert.strictEqual( + line3[0], + 'metric_observer{pid="123",core="1"}' + ); + assert.ok( + parseFloat(line3[1]) >= 0 && parseFloat(line3[1]) <= 1 + ); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + }); }); + }); + + it('should export multiple labels', done => { + const counter = meter.createCounter('counter', { + description: 'a test description', + }) as CounterMetric; - meter.collect(); - exporter.export(meter.getBatcher().checkPointSet(), () => { + counter.bind({ counterKey1: 'labelValue1' }).add(10); + counter.bind({ counterKey1: 'labelValue2' }).add(20); + meter.collect().then(() => { exporter.export(meter.getBatcher().checkPointSet(), () => { http .get('http://localhost:9464/metrics', res => { @@ -259,22 +303,13 @@ describe('PrometheusExporter', () => { const body = chunk.toString(); const lines = body.split('\n'); - assert.strictEqual( - lines[0], - '# HELP metric_observer a test description' - ); - - assert.strictEqual(lines[1], '# TYPE metric_observer gauge'); - - const line3 = lines[2].split(' '); - assert.strictEqual( - line3[0], - 'metric_observer{pid="123",core="1"}' - ); - assert.ok( - parseFloat(line3[1]) >= 0 && parseFloat(line3[1]) <= 1 - ); - assert.ok(parseInt(line3[2], 10) <= new Date().getTime()); + assert.deepStrictEqual(lines, [ + '# HELP counter a test description', + '# TYPE counter counter', + `counter{counterKey1="labelValue1"} 10 ${mockedTimeMS}`, + `counter{counterKey1="labelValue2"} 20 ${mockedTimeMS}`, + '', + ]); done(); }); @@ -284,36 +319,6 @@ describe('PrometheusExporter', () => { }); }); - it('should export multiple labels', done => { - const counter = meter.createCounter('counter', { - description: 'a test description', - }) as CounterMetric; - - counter.bind({ counterKey1: 'labelValue1' }).add(10); - counter.bind({ counterKey1: 'labelValue2' }).add(20); - meter.collect(); - exporter.export(meter.getBatcher().checkPointSet(), () => { - http - .get('http://localhost:9464/metrics', res => { - res.on('data', chunk => { - const body = chunk.toString(); - const lines = body.split('\n'); - - assert.deepStrictEqual(lines, [ - '# HELP counter a test description', - '# TYPE counter counter', - `counter{counterKey1="labelValue1"} 10 ${mockedTimeMS}`, - `counter{counterKey1="labelValue2"} 20 ${mockedTimeMS}`, - '', - ]); - - done(); - }); - }) - .on('error', errorHandler(done)); - }); - }); - it('should export a comment if no metrics are registered', done => { exporter.export([], () => { http @@ -336,25 +341,26 @@ describe('PrometheusExporter', () => { const boundCounter = counter.bind({ key1: 'labelValue1' }); boundCounter.add(10); - meter.collect(); - exporter.export(meter.getBatcher().checkPointSet(), () => { - http - .get('http://localhost:9464/metrics', res => { - res.on('data', chunk => { - const body = chunk.toString(); - const lines = body.split('\n'); + meter.collect().then(() => { + exporter.export(meter.getBatcher().checkPointSet(), () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); - assert.deepStrictEqual(lines, [ - '# HELP counter description missing', - '# TYPE counter counter', - `counter{key1="labelValue1"} 10 ${mockedTimeMS}`, - '', - ]); + assert.deepStrictEqual(lines, [ + '# HELP counter description missing', + '# TYPE counter counter', + `counter{key1="labelValue1"} 10 ${mockedTimeMS}`, + '', + ]); - done(); - }); - }) - .on('error', errorHandler(done)); + done(); + }); + }) + .on('error', errorHandler(done)); + }); }); }); @@ -362,25 +368,26 @@ describe('PrometheusExporter', () => { const counter = meter.createCounter('counter.bad-name'); const boundCounter = counter.bind({ key1: 'labelValue1' }); boundCounter.add(10); - meter.collect(); - exporter.export(meter.getBatcher().checkPointSet(), () => { - http - .get('http://localhost:9464/metrics', res => { - res.on('data', chunk => { - const body = chunk.toString(); - const lines = body.split('\n'); + meter.collect().then(() => { + exporter.export(meter.getBatcher().checkPointSet(), () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); - assert.deepStrictEqual(lines, [ - '# HELP counter_bad_name description missing', - '# TYPE counter_bad_name counter', - `counter_bad_name{key1="labelValue1"} 10 ${mockedTimeMS}`, - '', - ]); + assert.deepStrictEqual(lines, [ + '# HELP counter_bad_name description missing', + '# TYPE counter_bad_name counter', + `counter_bad_name{key1="labelValue1"} 10 ${mockedTimeMS}`, + '', + ]); - done(); - }); - }) - .on('error', errorHandler(done)); + done(); + }); + }) + .on('error', errorHandler(done)); + }); }); }); @@ -390,22 +397,23 @@ describe('PrometheusExporter', () => { }); counter.bind({ key1: 'labelValue1' }).add(20); - meter.collect(); - exporter.export(meter.getBatcher().checkPointSet(), () => { - http - .get('http://localhost:9464/metrics', res => { - res.on('data', chunk => { - assert.deepStrictEqual(chunk.toString().split('\n'), [ - '# HELP counter a test description', - '# TYPE counter gauge', - 'counter{key1="labelValue1"} 20', - '', - ]); + meter.collect().then(() => { + exporter.export(meter.getBatcher().checkPointSet(), () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + assert.deepStrictEqual(chunk.toString().split('\n'), [ + '# HELP counter a test description', + '# TYPE counter gauge', + 'counter{key1="labelValue1"} 20', + '', + ]); - done(); - }); - }) - .on('error', errorHandler(done)); + done(); + }); + }) + .on('error', errorHandler(done)); + }); }); }); }); @@ -435,8 +443,8 @@ describe('PrometheusExporter', () => { prefix: 'test_prefix', }); - exporter.startServer(() => { - meter.collect(); + exporter.startServer(async () => { + await meter.collect(); exporter!.export(meter.getBatcher().checkPointSet(), () => { http .get('http://localhost:9464/metrics', res => { @@ -464,8 +472,8 @@ describe('PrometheusExporter', () => { port: 8080, }); - exporter.startServer(() => { - meter.collect(); + exporter.startServer(async () => { + await meter.collect(); exporter!.export(meter.getBatcher().checkPointSet(), () => { http .get('http://localhost:8080/metrics', res => { @@ -493,8 +501,8 @@ describe('PrometheusExporter', () => { endpoint: '/test', }); - exporter.startServer(() => { - meter.collect(); + exporter.startServer(async () => { + await meter.collect(); exporter!.export(meter.getBatcher().checkPointSet(), () => { http .get('http://localhost:9464/test', res => { diff --git a/packages/opentelemetry-metrics/README.md b/packages/opentelemetry-metrics/README.md index de6b733eed..f24668528d 100644 --- a/packages/opentelemetry-metrics/README.md +++ b/packages/opentelemetry-metrics/README.md @@ -74,37 +74,77 @@ boundCounter.add(Math.random() > 0.5 ? 1 : -1); ``` -### Observable +### Value Observer Choose this kind of metric when only last value is important without worry about aggregation ```js -const { MeterProvider, MetricObservable } = require('@opentelemetry/metrics'); +const { MeterProvider } = 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', { - description: 'Example of a observer' +meter.createValueObserver('cpu_core_usage', { + description: 'Example of a sync observer with callback', +}, (observerResult) => { + observerResult.observe(getRandomValue(), { core: '1' }); + observerResult.observe(getRandomValue(), { core: '2' }); }); -function getCpuUsage() { +function getRandomValue() { return Math.random(); } -const metricObservable = new MetricObservable(); +``` + +### Batch Observer + +Choose this kind of metric when you need to update multiple observers with the results of a single async calculation. + +```js +const { MeterProvider } = require('@opentelemetry/metrics'); +const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); + +const exporter = new PrometheusExporter( + { + startServer: true, + }, + () => { + console.log('prometheus scrape endpoint: http://localhost:9464/metrics'); + }, +); + +const meter = new MeterProvider({ + exporter, + interval: 3000, +}).getMeter('example-observer'); + +const cpuUsageMetric = meter.createValueObserver('cpu_usage_per_app', { + description: 'CPU', +}); -observer.setCallback((observerResult) => { - // synchronous callback - observerResult.observe(getCpuUsage, { pid: process.pid, core: '1' }); - // asynchronous callback - observerResult.observe(metricObservable, { pid: process.pid, core: '2' }); +const MemUsageMetric = meter.createValueObserver('mem_usage_per_app', { + description: 'Memory', }); -// simulate asynchronous operation -setInterval(()=> { - metricObservable.next(getCpuUsage()); -}, 2000) +meter.createBatchObserver('metric_batch_observer', (observerBatchResult) => { + getSomeAsyncMetrics().then(metrics => { + observerBatchResult.observe({ app: 'myApp' }, [ + cpuUsageMetric.observation(metrics.value1), + MemUsageMetric.observation(metrics.value2) + ]); + }); +}); + +function getSomeAsyncMetrics() { + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve({ + value1: Math.random(), + value2: Math.random(), + }); + }, 100) + }); +} ``` diff --git a/packages/opentelemetry-metrics/src/BaseObserverMetric.ts b/packages/opentelemetry-metrics/src/BaseObserverMetric.ts new file mode 100644 index 0000000000..61167183df --- /dev/null +++ b/packages/opentelemetry-metrics/src/BaseObserverMetric.ts @@ -0,0 +1,74 @@ +/* + * 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 { BoundObserver } from './BoundInstrument'; +import { Batcher } from './export/Batcher'; +import { MetricKind, MetricRecord } from './export/types'; +import { Metric } from './Metric'; +import { ObserverResult } from './ObserverResult'; + +const NOOP_CALLBACK = () => {}; + +/** + * This is a SDK implementation of Base Observer Metric. + * All observers should extend this class + */ +export abstract class BaseObserverMetric extends Metric + implements api.BaseObserver { + protected _callback: (observerResult: api.ObserverResult) => void; + + constructor( + name: string, + options: api.MetricOptions, + private readonly _batcher: Batcher, + resource: Resource, + metricKind: MetricKind, + instrumentationLibrary: InstrumentationLibrary, + callback?: (observerResult: api.ObserverResult) => void + ) { + super(name, options, metricKind, resource, instrumentationLibrary); + this._callback = callback || NOOP_CALLBACK; + } + + protected _makeInstrument(labels: api.Labels): BoundObserver { + return new BoundObserver( + labels, + this._disabled, + this._valueType, + this._logger, + this._batcher.aggregatorFor(this._descriptor) + ); + } + + getMetricRecord(): Promise { + const observerResult = new ObserverResult(); + this._callback(observerResult); + observerResult.values.forEach((value, labels) => { + const instrument = this.bind(labels); + instrument.update(value); + }); + return super.getMetricRecord(); + } + + observation(value: number) { + return { + value, + observer: this as BaseObserverMetric, + }; + } +} diff --git a/packages/opentelemetry-metrics/src/BatchObserverMetric.ts b/packages/opentelemetry-metrics/src/BatchObserverMetric.ts new file mode 100644 index 0000000000..17e80634c4 --- /dev/null +++ b/packages/opentelemetry-metrics/src/BatchObserverMetric.ts @@ -0,0 +1,91 @@ +/* + * 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 { BatchObserverResult } from './BatchObserverResult'; +import { BoundObserver } from './BoundInstrument'; +import { Batcher } from './export/Batcher'; +import { MetricKind, MetricRecord } from './export/types'; +import { Metric } from './Metric'; + +const NOOP_CALLBACK = () => {}; +const MAX_TIMEOUT_UPDATE_MS = 500; + +/** This is a SDK implementation of Batch Observer Metric. */ +export class BatchObserverMetric extends Metric + implements api.BatchObserver { + private _callback: (observerResult: api.BatchObserverResult) => void; + private _maxTimeoutUpdateMS: number; + + constructor( + name: string, + options: api.BatchMetricOptions, + private readonly _batcher: Batcher, + resource: Resource, + instrumentationLibrary: InstrumentationLibrary, + callback?: (observerResult: api.BatchObserverResult) => void + ) { + super( + name, + options, + MetricKind.VALUE_OBSERVER, + resource, + instrumentationLibrary + ); + this._maxTimeoutUpdateMS = + options.maxTimeoutUpdateMS ?? MAX_TIMEOUT_UPDATE_MS; + this._callback = callback || NOOP_CALLBACK; + } + + protected _makeInstrument(labels: api.Labels): BoundObserver { + return new BoundObserver( + labels, + this._disabled, + this._valueType, + this._logger, + this._batcher.aggregatorFor(this._descriptor) + ); + } + + getMetricRecord(): Promise { + this._logger.debug('getMetricRecord - start'); + return new Promise((resolve, reject) => { + const observerResult = new BatchObserverResult(); + + // cancels after MAX_TIMEOUT_MS - no more waiting for results + const timer = setTimeout(() => { + observerResult.cancelled = true; + // remove callback to prevent user from updating the values later if + // for any reason the observerBatchResult will be referenced + observerResult.onObserveCalled(); + super.getMetricRecord().then(resolve, reject); + this._logger.debug('getMetricRecord - timeout'); + }, this._maxTimeoutUpdateMS); + + // sets callback for each "observe" method + observerResult.onObserveCalled(() => { + clearTimeout(timer); + super.getMetricRecord().then(resolve, reject); + this._logger.debug('getMetricRecord - end'); + }); + + // calls the BatchObserverResult callback + this._callback(observerResult); + }); + } +} diff --git a/packages/opentelemetry-metrics/src/BatchObserverResult.ts b/packages/opentelemetry-metrics/src/BatchObserverResult.ts new file mode 100644 index 0000000000..17f382360a --- /dev/null +++ b/packages/opentelemetry-metrics/src/BatchObserverResult.ts @@ -0,0 +1,60 @@ +/* + * 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'; + +/** + * Implementation of api BatchObserverResult + */ +export class BatchObserverResult implements api.BatchObserverResult { + private _callback: (() => void) | undefined; + private _immediate: NodeJS.Immediate | undefined; + /** + * Cancels the further updates. + * This is used to prevent updating the value of result that took too + * long to update. For example to avoid update after timeout. + * See {@link BatchObserverMetric.getMetricRecord} + */ + cancelled = false; + + /** + * used to save a callback that will be called after the observations are + * updated + * @param [callback] + */ + onObserveCalled(callback?: () => void) { + this._callback = callback; + } + + observe(labels: api.Labels, observations: api.Observation[]): void { + if (this.cancelled || !this._callback) { + return; + } + observations.forEach(observation => { + observation.observer.bind(labels).update(observation.value); + }); + if (!this._immediate) { + this._immediate = setImmediate(() => { + if (typeof this._callback === 'function') { + this._callback(); + // prevent user from updating the values later if for any reason + // the observerBatchResult will be referenced and then try to use + this._callback = undefined; + } + }); + } + } +} diff --git a/packages/opentelemetry-metrics/src/CounterMetric.ts b/packages/opentelemetry-metrics/src/CounterMetric.ts new file mode 100644 index 0000000000..c3f35fb8bb --- /dev/null +++ b/packages/opentelemetry-metrics/src/CounterMetric.ts @@ -0,0 +1,56 @@ +/* + * 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 { BoundCounter } from './BoundInstrument'; +import { Batcher } from './export/Batcher'; +import { MetricKind } from './export/types'; +import { Metric } from './Metric'; + +/** This is a SDK implementation of Counter Metric. */ +export class CounterMetric extends Metric implements api.Counter { + constructor( + name: string, + options: api.MetricOptions, + private readonly _batcher: Batcher, + resource: Resource, + instrumentationLibrary: InstrumentationLibrary + ) { + super(name, options, MetricKind.COUNTER, resource, instrumentationLibrary); + } + protected _makeInstrument(labels: api.Labels): BoundCounter { + return new BoundCounter( + labels, + this._disabled, + this._valueType, + this._logger, + // @todo: consider to set to CounterSumAggregator always. + this._batcher.aggregatorFor(this._descriptor) + ); + } + + /** + * Adds the given value to the current value. Values cannot be negative. + * @param value the value to add. + * @param [labels = {}] key-values pairs that are associated with a specific metric + * that you want to record. + */ + add(value: number, labels: api.Labels = {}) { + this.bind(labels).add(value); + } +} diff --git a/packages/opentelemetry-metrics/src/Meter.ts b/packages/opentelemetry-metrics/src/Meter.ts index 22d4fda93f..0842eefabd 100644 --- a/packages/opentelemetry-metrics/src/Meter.ts +++ b/packages/opentelemetry-metrics/src/Meter.ts @@ -17,20 +17,15 @@ import * as api from '@opentelemetry/api'; import { ConsoleLogger, InstrumentationLibrary } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; +import { BatchObserverMetric } from './BatchObserverMetric'; import { BaseBoundInstrument } from './BoundInstrument'; import { UpDownCounterMetric } from './UpDownCounterMetric'; -import { - Metric, - CounterMetric, - ValueRecorderMetric, - ObserverMetric, -} from './Metric'; -import { - MetricOptions, - DEFAULT_METRIC_OPTIONS, - DEFAULT_CONFIG, - MeterConfig, -} from './types'; +import { CounterMetric } from './CounterMetric'; +import { MetricRecord } from './export/types'; +import { ValueRecorderMetric } from './ValueRecorderMetric'; +import { Metric } from './Metric'; +import { ValueObserverMetric } from './ValueObserverMetric'; +import { DEFAULT_METRIC_OPTIONS, DEFAULT_CONFIG, MeterConfig } from './types'; import { Batcher, UngroupedBatcher } from './export/Batcher'; import { PushController } from './export/Controller'; import { NoopExporter } from './export/NoopExporter'; @@ -77,7 +72,7 @@ export class Meter implements api.Meter { ); return api.NOOP_VALUE_RECORDER_METRIC; } - const opt: MetricOptions = { + const opt: api.MetricOptions = { logger: this._logger, ...DEFAULT_METRIC_OPTIONS, absolute: true, // value recorders are defined as absolute by default @@ -109,7 +104,7 @@ export class Meter implements api.Meter { ); return api.NOOP_COUNTER_METRIC; } - const opt: MetricOptions = { + const opt: api.MetricOptions = { logger: this._logger, ...DEFAULT_METRIC_OPTIONS, ...options, @@ -145,9 +140,9 @@ export class Meter implements api.Meter { ); return api.NOOP_COUNTER_METRIC; } - const opt: MetricOptions = { - logger: this._logger, + const opt: api.MetricOptions = { ...DEFAULT_METRIC_OPTIONS, + logger: this._logger, ...options, }; const upDownCounter = new UpDownCounterMetric( @@ -162,31 +157,71 @@ export class Meter implements api.Meter { } /** - * Creates a new observer metric. + * Creates a new value observer metric. * @param name the name of the metric. * @param [options] the metric options. + * @param [callback] the value observer callback */ - createObserver(name: string, options?: api.MetricOptions): api.Observer { + createValueObserver( + name: string, + options: api.MetricOptions = {}, + callback?: (observerResult: api.ObserverResult) => void + ): api.ValueObserver { if (!this._isValidName(name)) { this._logger.warn( `Invalid metric name ${name}. Defaulting to noop metric implementation.` ); - return api.NOOP_OBSERVER_METRIC; + return api.NOOP_VALUE_OBSERVER_METRIC; } - const opt: MetricOptions = { + const opt: api.MetricOptions = { logger: this._logger, ...DEFAULT_METRIC_OPTIONS, ...options, }; - const observer = new ObserverMetric( + const valueObserver = new ValueObserverMetric( name, opt, this._batcher, this._resource, - this._instrumentationLibrary + this._instrumentationLibrary, + callback + ); + this._registerMetric(name, valueObserver); + return valueObserver; + } + + /** + * Creates a new batch observer metric. + * @param name the name of the metric. + * @param callback the batch observer callback + * @param [options] the metric batch options. + */ + createBatchObserver( + name: string, + callback: (observerResult: api.BatchObserverResult) => void, + options: api.BatchMetricOptions = {} + ): api.BatchObserver { + if (!this._isValidName(name)) { + this._logger.warn( + `Invalid metric name ${name}. Defaulting to noop metric implementation.` + ); + return api.NOOP_BATCH_OBSERVER_METRIC; + } + const opt: api.BatchMetricOptions = { + logger: this._logger, + ...DEFAULT_METRIC_OPTIONS, + ...options, + }; + const batchObserver = new BatchObserverMetric( + name, + opt, + this._batcher, + this._resource, + this._instrumentationLibrary, + callback ); - this._registerMetric(name, observer); - return observer; + this._registerMetric(name, batchObserver); + return batchObserver; } /** @@ -196,11 +231,20 @@ export class Meter implements api.Meter { * each aggregator belonging to the metrics that were created with this * meter instance. */ - collect() { - Array.from(this._metrics.values()).forEach(metric => { - metric.getMetricRecord().forEach(record => { - this._batcher.process(record); + collect(): Promise { + return new Promise((resolve, reject) => { + const metrics: Promise[] = []; + Array.from(this._metrics.values()).forEach(metric => { + metrics.push(metric.getMetricRecord()); }); + Promise.all(metrics) + .then(records => { + records.forEach(metrics => { + metrics.forEach(metric => this._batcher.process(metric)); + }); + resolve(); + }) + .catch(reject); }); } diff --git a/packages/opentelemetry-metrics/src/Metric.ts b/packages/opentelemetry-metrics/src/Metric.ts index 7dcb3ab52c..6edfc3aa92 100644 --- a/packages/opentelemetry-metrics/src/Metric.ts +++ b/packages/opentelemetry-metrics/src/Metric.ts @@ -13,19 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import * as api from '@opentelemetry/api'; +import { NoopLogger } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; -import { - BoundCounter, - BaseBoundInstrument, - BoundValueRecorder, - BoundObserver, -} from './BoundInstrument'; -import { ObserverResult } from './ObserverResult'; -import { MetricOptions } from './types'; -import { MetricKind, MetricDescriptor, MetricRecord } from './export/types'; -import { Batcher } from './export/Batcher'; +import { BaseBoundInstrument } from './BoundInstrument'; +import { MetricDescriptor, MetricKind, MetricRecord } from './export/types'; import { hashLabels } from './Utils'; import { InstrumentationLibrary } from '@opentelemetry/core'; @@ -40,14 +32,17 @@ export abstract class Metric constructor( private readonly _name: string, - private readonly _options: MetricOptions, + private readonly _options: api.MetricOptions, private readonly _kind: MetricKind, readonly resource: Resource, readonly instrumentationLibrary: InstrumentationLibrary ) { - this._disabled = _options.disabled; - this._valueType = _options.valueType; - this._logger = _options.logger; + this._disabled = !!_options.disabled; + this._valueType = + typeof _options.valueType === 'number' + ? _options.valueType + : api.ValueType.DOUBLE; + this._logger = _options.logger ?? new NoopLogger(); this._descriptor = this._getMetricDescriptor(); } @@ -82,21 +77,25 @@ export abstract class Metric this._instruments.clear(); } - getMetricRecord(): MetricRecord[] { - return Array.from(this._instruments.values()).map(instrument => ({ - descriptor: this._descriptor, - labels: instrument.getLabels(), - aggregator: instrument.getAggregator(), - resource: this.resource, - instrumentationLibrary: this.instrumentationLibrary, - })); + getMetricRecord(): Promise { + return new Promise(resolve => { + resolve( + Array.from(this._instruments.values()).map(instrument => ({ + descriptor: this._descriptor, + labels: instrument.getLabels(), + aggregator: instrument.getAggregator(), + resource: this.resource, + instrumentationLibrary: this.instrumentationLibrary, + })) + ); + }); } private _getMetricDescriptor(): MetricDescriptor { return { name: this._name, - description: this._options.description, - unit: this._options.unit, + description: this._options.description || '', + unit: this._options.unit || '1', metricKind: this._kind, valueType: this._valueType, }; @@ -104,121 +103,3 @@ export abstract class Metric protected abstract _makeInstrument(labels: api.Labels): T; } - -/** This is a SDK implementation of Counter Metric. */ -export class CounterMetric extends Metric implements api.Counter { - constructor( - name: string, - options: MetricOptions, - private readonly _batcher: Batcher, - resource: Resource, - instrumentationLibrary: InstrumentationLibrary - ) { - super(name, options, MetricKind.COUNTER, resource, instrumentationLibrary); - } - protected _makeInstrument(labels: api.Labels): BoundCounter { - return new BoundCounter( - labels, - this._disabled, - this._valueType, - this._logger, - // @todo: consider to set to CounterSumAggregator always. - this._batcher.aggregatorFor(this._descriptor) - ); - } - - /** - * Adds the given value to the current value. Values cannot be negative. - * @param value the value to add. - * @param [labels = {}] key-values pairs that are associated with a specific - * metric that you want to record. - */ - add(value: number, labels: api.Labels = {}) { - this.bind(labels).add(value); - } -} - -export class ValueRecorderMetric extends Metric - implements api.ValueRecorder { - protected readonly _absolute: boolean; - - constructor( - name: string, - options: MetricOptions, - private readonly _batcher: Batcher, - resource: Resource, - instrumentationLibrary: InstrumentationLibrary - ) { - super( - name, - options, - MetricKind.VALUE_RECORDER, - resource, - instrumentationLibrary - ); - - this._absolute = options.absolute !== undefined ? options.absolute : true; // Absolute default is true - } - protected _makeInstrument(labels: api.Labels): BoundValueRecorder { - return new BoundValueRecorder( - labels, - this._disabled, - this._absolute, - this._valueType, - this._logger, - this._batcher.aggregatorFor(this._descriptor) - ); - } - - 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 api.Observer { - private _observerResult = new ObserverResult(); - - constructor( - name: string, - options: MetricOptions, - private readonly _batcher: Batcher, - resource: Resource, - instrumentationLibrary: InstrumentationLibrary - ) { - super(name, options, MetricKind.OBSERVER, resource, instrumentationLibrary); - } - - protected _makeInstrument(labels: api.Labels): BoundObserver { - return new BoundObserver( - labels, - this._disabled, - this._valueType, - this._logger, - this._batcher.aggregatorFor(this._descriptor) - ); - } - - getMetricRecord(): MetricRecord[] { - this._observerResult.callbackObservers.forEach((callback, labels) => { - const instrument = this.bind(labels); - instrument.update(callback()); - }); - return super.getMetricRecord(); - } - - /** - * Sets a callback where user can observe value for certain labels - * @param callback - */ - 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 deleted file mode 100644 index f6c751a1e9..0000000000 --- a/packages/opentelemetry-metrics/src/MetricObservable.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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'; - -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 32793dcc9c..60d555641f 100644 --- a/packages/opentelemetry-metrics/src/ObserverResult.ts +++ b/packages/opentelemetry-metrics/src/ObserverResult.ts @@ -15,7 +15,6 @@ */ import { - MetricObservable, ObserverResult as TypeObserverResult, Labels, } from '@opentelemetry/api'; @@ -24,17 +23,9 @@ import { * Implementation of {@link TypeObserverResult} */ export class ObserverResult implements TypeObserverResult { - callbackObservers: Map = new Map(); - observers: Map = new Map< - Labels, - MetricObservable - >(); + values: Map = new Map(); - observe(callback: Function | MetricObservable, labels: Labels): void { - if (typeof callback === 'function') { - this.callbackObservers.set(labels, callback); - } else { - this.observers.set(labels, callback); - } + observe(value: number, labels: Labels): void { + this.values.set(labels, value); } } diff --git a/packages/opentelemetry-metrics/src/UpDownCounterMetric.ts b/packages/opentelemetry-metrics/src/UpDownCounterMetric.ts index 11a468aa6b..14eb1dc6f3 100644 --- a/packages/opentelemetry-metrics/src/UpDownCounterMetric.ts +++ b/packages/opentelemetry-metrics/src/UpDownCounterMetric.ts @@ -18,7 +18,6 @@ import * as api from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import { InstrumentationLibrary } from '@opentelemetry/core'; import { BoundUpDownCounter } from './BoundInstrument'; -import { MetricOptions } from './types'; import { MetricKind } from './export/types'; import { Batcher } from './export/Batcher'; import { Metric } from './Metric'; @@ -28,7 +27,7 @@ export class UpDownCounterMetric extends Metric implements api.UpDownCounter { constructor( name: string, - options: MetricOptions, + options: api.MetricOptions, private readonly _batcher: Batcher, resource: Resource, instrumentationLibrary: InstrumentationLibrary diff --git a/packages/opentelemetry-metrics/src/ValueObserverMetric.ts b/packages/opentelemetry-metrics/src/ValueObserverMetric.ts new file mode 100644 index 0000000000..14ca5418fe --- /dev/null +++ b/packages/opentelemetry-metrics/src/ValueObserverMetric.ts @@ -0,0 +1,44 @@ +/* + * 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 { Batcher } from './export/Batcher'; +import { MetricKind } from './export/types'; + +/** This is a SDK implementation of Value Observer Metric. */ +export class ValueObserverMetric extends BaseObserverMetric + implements api.ValueObserver { + constructor( + name: string, + options: api.MetricOptions, + batcher: Batcher, + resource: Resource, + instrumentationLibrary: InstrumentationLibrary, + callback?: (observerResult: api.ObserverResult) => void + ) { + super( + name, + options, + batcher, + resource, + MetricKind.VALUE_OBSERVER, + instrumentationLibrary, + callback + ); + } +} diff --git a/packages/opentelemetry-metrics/src/ValueRecorderMetric.ts b/packages/opentelemetry-metrics/src/ValueRecorderMetric.ts new file mode 100644 index 0000000000..e23e3daa72 --- /dev/null +++ b/packages/opentelemetry-metrics/src/ValueRecorderMetric.ts @@ -0,0 +1,61 @@ +/* + * 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 { BoundValueRecorder } from './BoundInstrument'; +import { Batcher } from './export/Batcher'; +import { MetricKind } from './export/types'; +import { Metric } from './Metric'; + +/** This is a SDK implementation of Value Recorder Metric. */ +export class ValueRecorderMetric extends Metric + implements api.ValueRecorder { + protected readonly _absolute: boolean; + + constructor( + name: string, + options: api.MetricOptions, + private readonly _batcher: Batcher, + resource: Resource, + instrumentationLibrary: InstrumentationLibrary + ) { + super( + name, + options, + MetricKind.VALUE_RECORDER, + resource, + instrumentationLibrary + ); + + this._absolute = options.absolute !== undefined ? options.absolute : true; // Absolute default is true + } + protected _makeInstrument(labels: api.Labels): BoundValueRecorder { + return new BoundValueRecorder( + labels, + this._disabled, + this._absolute, + this._valueType, + this._logger, + this._batcher.aggregatorFor(this._descriptor) + ); + } + + record(value: number, labels: api.Labels = {}) { + this.bind(labels).record(value); + } +} diff --git a/packages/opentelemetry-metrics/src/export/Batcher.ts b/packages/opentelemetry-metrics/src/export/Batcher.ts index de7d32c1fe..9bcf574d02 100644 --- a/packages/opentelemetry-metrics/src/export/Batcher.ts +++ b/packages/opentelemetry-metrics/src/export/Batcher.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - CounterSumAggregator, - ValueRecorderExactAggregator, - ObserverAggregator, -} from './aggregators'; +import * as aggregators from './aggregators'; import { MetricRecord, MetricKind, @@ -56,11 +52,14 @@ export class UngroupedBatcher extends Batcher { switch (metricDescriptor.metricKind) { case MetricKind.COUNTER: case MetricKind.UP_DOWN_COUNTER: - return new CounterSumAggregator(); - case MetricKind.OBSERVER: - return new ObserverAggregator(); + case MetricKind.SUM_OBSERVER: + case MetricKind.UP_DOWN_SUM_OBSERVER: + return new aggregators.SumAggregator(); + case MetricKind.VALUE_RECORDER: + case MetricKind.VALUE_OBSERVER: + return new aggregators.LastValueAggregator(); default: - return new ValueRecorderExactAggregator(); + return new aggregators.MinMaxSumCountAggregator(); } } diff --git a/packages/opentelemetry-metrics/src/export/aggregators/observer.ts b/packages/opentelemetry-metrics/src/export/aggregators/LastValue.ts similarity index 88% rename from packages/opentelemetry-metrics/src/export/aggregators/observer.ts rename to packages/opentelemetry-metrics/src/export/aggregators/LastValue.ts index 90b193896a..20c049c842 100644 --- a/packages/opentelemetry-metrics/src/export/aggregators/observer.ts +++ b/packages/opentelemetry-metrics/src/export/aggregators/LastValue.ts @@ -18,8 +18,8 @@ import { Aggregator, Point } from '../types'; import { HrTime } from '@opentelemetry/api'; import { hrTime } from '@opentelemetry/core'; -/** Basic aggregator for Observer which keeps the last recorded value. */ -export class ObserverAggregator implements Aggregator { +/** Basic aggregator for LastValue which keeps the last recorded value. */ +export class LastValueAggregator implements Aggregator { private _current: number = 0; private _lastUpdateTime: HrTime = [0, 0]; diff --git a/packages/opentelemetry-metrics/src/export/aggregators/ValueRecorderExact.ts b/packages/opentelemetry-metrics/src/export/aggregators/MinMaxSumCount.ts similarity index 95% rename from packages/opentelemetry-metrics/src/export/aggregators/ValueRecorderExact.ts rename to packages/opentelemetry-metrics/src/export/aggregators/MinMaxSumCount.ts index 5603ee45e7..9be0fe8350 100644 --- a/packages/opentelemetry-metrics/src/export/aggregators/ValueRecorderExact.ts +++ b/packages/opentelemetry-metrics/src/export/aggregators/MinMaxSumCount.ts @@ -20,7 +20,7 @@ import { hrTime } from '@opentelemetry/core'; import { Distribution } from '../types'; /** Basic aggregator keeping all raw values (events, sum, max and min). */ -export class ValueRecorderExactAggregator implements Aggregator { +export class MinMaxSumCountAggregator implements Aggregator { private _distribution: Distribution; private _lastUpdateTime: HrTime = [0, 0]; diff --git a/packages/opentelemetry-metrics/src/export/aggregators/countersum.ts b/packages/opentelemetry-metrics/src/export/aggregators/Sum.ts similarity index 95% rename from packages/opentelemetry-metrics/src/export/aggregators/countersum.ts rename to packages/opentelemetry-metrics/src/export/aggregators/Sum.ts index 182e61ccb4..0475320e5c 100644 --- a/packages/opentelemetry-metrics/src/export/aggregators/countersum.ts +++ b/packages/opentelemetry-metrics/src/export/aggregators/Sum.ts @@ -19,7 +19,7 @@ import { HrTime } from '@opentelemetry/api'; import { hrTime } from '@opentelemetry/core'; /** Basic aggregator which calculates a Sum from individual measurements. */ -export class CounterSumAggregator implements Aggregator { +export class SumAggregator implements Aggregator { private _current: number = 0; private _lastUpdateTime: HrTime = [0, 0]; diff --git a/packages/opentelemetry-metrics/src/export/aggregators/index.ts b/packages/opentelemetry-metrics/src/export/aggregators/index.ts index a46b10a624..0d3f8155b7 100644 --- a/packages/opentelemetry-metrics/src/export/aggregators/index.ts +++ b/packages/opentelemetry-metrics/src/export/aggregators/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export * from './countersum'; -export * from './observer'; -export * from './ValueRecorderExact'; export * from './histogram'; +export * from './MinMaxSumCount'; +export * from './LastValue'; +export * from './Sum'; diff --git a/packages/opentelemetry-metrics/src/export/types.ts b/packages/opentelemetry-metrics/src/export/types.ts index 295b1ebaf1..192cabd414 100644 --- a/packages/opentelemetry-metrics/src/export/types.ts +++ b/packages/opentelemetry-metrics/src/export/types.ts @@ -23,7 +23,6 @@ export enum MetricKind { COUNTER, UP_DOWN_COUNTER, VALUE_RECORDER, - OBSERVER, // @TODO remove later #1146 SUM_OBSERVER, UP_DOWN_SUM_OBSERVER, VALUE_OBSERVER, diff --git a/packages/opentelemetry-metrics/src/index.ts b/packages/opentelemetry-metrics/src/index.ts index cf3eff3026..0be805519a 100644 --- a/packages/opentelemetry-metrics/src/index.ts +++ b/packages/opentelemetry-metrics/src/index.ts @@ -15,11 +15,14 @@ */ export * from './BoundInstrument'; +export * from './CounterMetric'; +export * from './ValueRecorderMetric'; export * from './Meter'; export * from './MeterProvider'; export * from './Metric'; -export * from './MetricObservable'; +export * from './ValueObserverMetric'; export * from './export/aggregators'; +export * from './export/Batcher'; export * from './export/ConsoleMetricExporter'; export * from './export/types'; export * from './UpDownCounterMetric'; diff --git a/packages/opentelemetry-metrics/src/types.ts b/packages/opentelemetry-metrics/src/types.ts index ab05a53e83..0334bac69c 100644 --- a/packages/opentelemetry-metrics/src/types.ts +++ b/packages/opentelemetry-metrics/src/types.ts @@ -15,42 +15,15 @@ */ import { LogLevel } from '@opentelemetry/core'; -import { Logger, ValueType } from '@opentelemetry/api'; +import * as api from '@opentelemetry/api'; import { MetricExporter } from './export/types'; import { Resource } from '@opentelemetry/resources'; import { Batcher } from './export/Batcher'; -/** Options needed for SDK metric creation. */ -export interface MetricOptions { - /** The name of the component that reports the Metric. */ - component?: string; - - /** The description of the Metric. */ - description: string; - - /** The unit of the Metric values. */ - unit: string; - - /** The map of constant labels for the Metric. */ - constantLabels?: Map; - - /** Indicates the metric is a verbose metric that is disabled by default. */ - disabled: boolean; - - /** (Measure only) Asserts that this metric will only accept non-negative values. */ - absolute: boolean; - - /** User provided logger. */ - logger: Logger; - - /** Indicates the type of the recorded value. */ - valueType: ValueType; -} - /** MeterConfig provides an interface for configuring a Meter. */ export interface MeterConfig { /** User provided logger. */ - logger?: Logger; + logger?: api.Logger; /** level of logger. */ logLevel?: LogLevel; @@ -79,5 +52,5 @@ export const DEFAULT_METRIC_OPTIONS = { absolute: false, description: '', unit: '1', - valueType: ValueType.DOUBLE, + valueType: api.ValueType.DOUBLE, }; diff --git a/packages/opentelemetry-metrics/test/Batcher.test.ts b/packages/opentelemetry-metrics/test/Batcher.test.ts index b5daf3b454..f2d3e2d360 100644 --- a/packages/opentelemetry-metrics/test/Batcher.test.ts +++ b/packages/opentelemetry-metrics/test/Batcher.test.ts @@ -35,11 +35,11 @@ describe('Batcher', () => { barCounter = counter.bind({ key: 'bar' }); }); - it('should process a batch', () => { + it('should process a batch', async () => { fooCounter.add(1); barCounter.add(1); barCounter.add(2); - meter.collect(); + await meter.collect(); const checkPointSet = meter.getBatcher().checkPointSet(); assert.strictEqual(checkPointSet.length, 2); for (const record of checkPointSet) { diff --git a/packages/opentelemetry-metrics/test/Meter.test.ts b/packages/opentelemetry-metrics/test/Meter.test.ts index 62e8860c0f..2cd8c42937 100644 --- a/packages/opentelemetry-metrics/test/Meter.test.ts +++ b/packages/opentelemetry-metrics/test/Meter.test.ts @@ -23,21 +23,17 @@ import { Sum, MeterProvider, ValueRecorderMetric, - Distribution, - ObserverMetric, + ValueObserverMetric, MetricRecord, Aggregator, - MetricObservable, MetricDescriptor, + LastValueAggregator, UpDownCounterMetric, } from '../src'; import * as api from '@opentelemetry/api'; import { NoopLogger, hrTime, hrTimeToNanoseconds } from '@opentelemetry/core'; -import { - CounterSumAggregator, - ObserverAggregator, -} from '../src/export/aggregators'; -import { ValueType } from '@opentelemetry/api'; +import { BatchObserverResult } from '../src/BatchObserverResult'; +import { SumAggregator } from '../src/export/aggregators'; import { Resource } from '@opentelemetry/resources'; import { hashLabels } from '../src/Utils'; import { Batcher } from '../src/export/Batcher'; @@ -71,10 +67,10 @@ describe('Meter', () => { assert.ok(counter instanceof Metric); }); - it('should be able to call add() directly on counter', () => { + it('should be able to call add() directly on counter', async () => { const counter = meter.createCounter('name') as CounterMetric; counter.add(10, labels); - meter.collect(); + await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); assert.strictEqual(record1.aggregator.toPoint().value, 10); @@ -92,46 +88,46 @@ describe('Meter', () => { ); }); - it('should be able to call add with no labels', () => { + it('should be able to call add with no labels', async () => { const counter = meter.createCounter('name', { description: 'desc', unit: '1', disabled: false, }); counter.add(1); - meter.collect(); + await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); assert.strictEqual(record1.aggregator.toPoint().value, 1); }); - it('should pipe through resource', () => { + it('should pipe through resource', async () => { const counter = meter.createCounter('name') as CounterMetric; assert.ok(counter.resource instanceof Resource); counter.add(1, { foo: 'bar' }); - const [record] = counter.getMetricRecord(); + const [record] = await counter.getMetricRecord(); assert.ok(record.resource instanceof Resource); }); - it('should pipe through instrumentation library', () => { + it('should pipe through instrumentation library', async () => { const counter = meter.createCounter('name') as CounterMetric; assert.ok(counter.instrumentationLibrary); counter.add(1, { foo: 'bar' }); - const [record] = counter.getMetricRecord(); + const [record] = await counter.getMetricRecord(); const { name, version } = record.instrumentationLibrary; assert.strictEqual(name, 'test-meter'); assert.strictEqual(version, '*'); }); describe('.bind()', () => { - it('should create a counter instrument', () => { + it('should create a counter instrument', async () => { const counter = meter.createCounter('name') as CounterMetric; const boundCounter = counter.bind(labels); boundCounter.add(10); - meter.collect(); + await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); assert.strictEqual(record1.aggregator.toPoint().value, 10); @@ -143,16 +139,16 @@ describe('Meter', () => { const counter = meter.createCounter('name') as CounterMetric; const boundCounter = counter.bind(labels); boundCounter.add(20); - assert.ok(boundCounter.getAggregator() instanceof CounterSumAggregator); + assert.ok(boundCounter.getAggregator() instanceof SumAggregator); assert.strictEqual(boundCounter.getLabels(), labels); }); - it('should add positive values only', () => { + it('should add positive values only', async () => { const counter = meter.createCounter('name') as CounterMetric; const boundCounter = counter.bind(labels); boundCounter.add(10); assert.strictEqual(meter.getBatcher().checkPointSet().length, 0); - meter.collect(); + await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); assert.strictEqual(record1.aggregator.toPoint().value, 10); @@ -160,24 +156,24 @@ describe('Meter', () => { assert.strictEqual(record1.aggregator.toPoint().value, 10); }); - it('should not add the instrument data when disabled', () => { + it('should not add the instrument data when disabled', async () => { const counter = meter.createCounter('name', { disabled: true, }) as CounterMetric; const boundCounter = counter.bind(labels); boundCounter.add(10); - meter.collect(); + await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); assert.strictEqual(record1.aggregator.toPoint().value, 0); }); - it('should return same instrument on same label values', () => { + it('should return same instrument on same label values', async () => { const counter = meter.createCounter('name') as CounterMetric; const boundCounter = counter.bind(labels); boundCounter.add(10); const boundCounter1 = counter.bind(labels); boundCounter1.add(10); - meter.collect(); + await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); assert.strictEqual(record1.aggregator.toPoint().value, 20); @@ -212,7 +208,7 @@ describe('Meter', () => { }); describe('.registerMetric()', () => { - it('skip already registered Metric', () => { + it('skip already registered Metric', async () => { const counter1 = meter.createCounter('name1') as CounterMetric; counter1.bind(labels).add(10); @@ -222,7 +218,7 @@ describe('Meter', () => { }) as CounterMetric; counter2.bind(labels).add(500); - meter.collect(); + await meter.collect(); const record = meter.getBatcher().checkPointSet(); assert.strictEqual(record.length, 1); @@ -231,7 +227,7 @@ describe('Meter', () => { metricKind: MetricKind.COUNTER, name: 'name1', unit: '1', - valueType: ValueType.DOUBLE, + valueType: api.ValueType.DOUBLE, }); assert.strictEqual(record[0].aggregator.toPoint().value, 10); }); @@ -283,10 +279,10 @@ describe('Meter', () => { assert.ok(upDownCounter instanceof Metric); }); - it('should be able to call add() directly on UpDownCounter', () => { + it('should be able to call add() directly on UpDownCounter', async () => { const upDownCounter = meter.createUpDownCounter('name'); upDownCounter.add(10, labels); - meter.collect(); + await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); assert.strictEqual(record1.aggregator.toPoint().value, 10); @@ -304,19 +300,19 @@ describe('Meter', () => { ); }); - it('should be able to call add with no labels', () => { + it('should be able to call add with no labels', async () => { const upDownCounter = meter.createUpDownCounter('name', { description: 'desc', unit: '1', disabled: false, }); upDownCounter.add(1); - meter.collect(); + await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); assert.strictEqual(record1.aggregator.toPoint().value, 1); }); - it('should pipe through resource', () => { + it('should pipe through resource', async () => { const upDownCounter = meter.createUpDownCounter( 'name' ) as UpDownCounterMetric; @@ -324,16 +320,16 @@ describe('Meter', () => { upDownCounter.add(1, { foo: 'bar' }); - const [record] = upDownCounter.getMetricRecord(); + const [record] = await upDownCounter.getMetricRecord(); assert.ok(record.resource instanceof Resource); }); describe('.bind()', () => { - it('should create a UpDownCounter instrument', () => { + it('should create a UpDownCounter instrument', async () => { const upDownCounter = meter.createUpDownCounter('name'); const boundCounter = upDownCounter.bind(labels); boundCounter.add(10); - meter.collect(); + await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); assert.strictEqual(record1.aggregator.toPoint().value, 10); @@ -347,28 +343,28 @@ describe('Meter', () => { ) as UpDownCounterMetric; const boundCounter = upDownCounter.bind(labels); boundCounter.add(20); - assert.ok(boundCounter.getAggregator() instanceof CounterSumAggregator); + assert.ok(boundCounter.getAggregator() instanceof SumAggregator); assert.strictEqual(boundCounter.getLabels(), labels); }); - it('should not add the instrument data when disabled', () => { + it('should not add the instrument data when disabled', async () => { const upDownCounter = meter.createUpDownCounter('name', { disabled: true, }); const boundCounter = upDownCounter.bind(labels); boundCounter.add(10); - meter.collect(); + await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); assert.strictEqual(record1.aggregator.toPoint().value, 0); }); - it('should return same instrument on same label values', () => { + it('should return same instrument on same label values', async () => { const upDownCounter = meter.createUpDownCounter('name'); const boundCounter = upDownCounter.bind(labels); boundCounter.add(10); const boundCounter1 = upDownCounter.bind(labels); boundCounter1.add(10); - meter.collect(); + await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); assert.strictEqual(record1.aggregator.toPoint().value, 20); @@ -407,7 +403,7 @@ describe('Meter', () => { }); describe('.registerMetric()', () => { - it('skip already registered Metric', () => { + it('skip already registered Metric', async () => { const counter1 = meter.createCounter('name1') as CounterMetric; counter1.bind(labels).add(10); @@ -417,7 +413,7 @@ describe('Meter', () => { }) as CounterMetric; counter2.bind(labels).add(500); - meter.collect(); + await meter.collect(); const record = meter.getBatcher().checkPointSet(); assert.strictEqual(record.length, 1); @@ -426,7 +422,7 @@ describe('Meter', () => { metricKind: MetricKind.COUNTER, name: 'name1', unit: '1', - valueType: ValueType.DOUBLE, + valueType: api.ValueType.DOUBLE, }); assert.strictEqual(record[0].aggregator.toPoint().value, 10); }); @@ -501,7 +497,7 @@ describe('Meter', () => { ); }); - it('should pipe through resource', () => { + it('should pipe through resource', async () => { const valueRecorder = meter.createValueRecorder( 'name' ) as ValueRecorderMetric; @@ -509,11 +505,11 @@ describe('Meter', () => { valueRecorder.record(1, { foo: 'bar' }); - const [record] = valueRecorder.getMetricRecord(); + const [record] = await valueRecorder.getMetricRecord(); assert.ok(record.resource instanceof Resource); }); - it('should pipe through instrumentation library', () => { + it('should pipe through instrumentation library', async () => { const valueRecorder = meter.createValueRecorder( 'name' ) as ValueRecorderMetric; @@ -521,7 +517,7 @@ describe('Meter', () => { valueRecorder.record(1, { foo: 'bar' }); - const [record] = valueRecorder.getMetricRecord(); + const [record] = await valueRecorder.getMetricRecord(); const { name, version } = record.instrumentationLibrary; assert.strictEqual(name, 'test-meter'); assert.strictEqual(version, '*'); @@ -559,70 +555,53 @@ describe('Meter', () => { assert.doesNotThrow(() => boundValueRecorder.record(10)); }); - it('should not accept negative values by default', () => { + it('should not accept negative values by default', async () => { const valueRecorder = meter.createValueRecorder('name'); const boundValueRecorder = valueRecorder.bind(labels); boundValueRecorder.record(-10); - meter.collect(); + await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); - assert.deepStrictEqual( - record1.aggregator.toPoint().value as Distribution, - { - count: 0, - max: -Infinity, - min: Infinity, - sum: 0, - } - ); + assert.deepStrictEqual(record1.aggregator.toPoint().value as number, 0); }); - it('should not set the instrument data when disabled', () => { + it('should not set the instrument data when disabled', async () => { const valueRecorder = meter.createValueRecorder('name', { disabled: true, }) as ValueRecorderMetric; const boundValueRecorder = valueRecorder.bind(labels); boundValueRecorder.record(10); - meter.collect(); + await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); - assert.deepStrictEqual( - record1.aggregator.toPoint().value as Distribution, - { - count: 0, - max: -Infinity, - min: Infinity, - sum: 0, - } - ); - }); - - it('should accept negative (and positive) values when absolute is set to false', () => { - const valueRecorder = meter.createValueRecorder('name', { - absolute: false, - }); - const boundValueRecorder = valueRecorder.bind(labels); - boundValueRecorder.record(-10); - boundValueRecorder.record(50); - - meter.collect(); - const [record1] = meter.getBatcher().checkPointSet(); - assert.deepStrictEqual( - record1.aggregator.toPoint().value as Distribution, - { - count: 2, - max: 50, - min: -10, - sum: 40, - } - ); - assert.ok( - hrTimeToNanoseconds(record1.aggregator.toPoint().timestamp) > - hrTimeToNanoseconds(performanceTimeOrigin) - ); - }); + assert.deepStrictEqual(record1.aggregator.toPoint().value as number, 0); + }); + + it( + 'should accept negative (and positive) values when absolute is set' + + ' to false', + async () => { + const valueRecorder = meter.createValueRecorder('name', { + absolute: false, + }); + const boundValueRecorder = valueRecorder.bind(labels); + boundValueRecorder.record(-10); + boundValueRecorder.record(50); + + await meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + assert.deepStrictEqual( + record1.aggregator.toPoint().value as number, + 50 + ); + assert.ok( + hrTimeToNanoseconds(record1.aggregator.toPoint().timestamp) > + hrTimeToNanoseconds(performanceTimeOrigin) + ); + } + ); - it('should return same instrument on same label values', () => { + it('should return same instrument on same label values', async () => { const valueRecorder = meter.createValueRecorder( 'name' ) as ValueRecorderMetric; @@ -630,16 +609,11 @@ describe('Meter', () => { boundValueRecorder1.record(10); const boundValueRecorder2 = valueRecorder.bind(labels); boundValueRecorder2.record(100); - meter.collect(); + await meter.collect(); const [record1] = meter.getBatcher().checkPointSet(); assert.deepStrictEqual( - record1.aggregator.toPoint().value as Distribution, - { - count: 2, - max: 100, - min: 10, - sum: 110, - } + record1.aggregator.toPoint().value as number, + 100 ); assert.strictEqual(boundValueRecorder1, boundValueRecorder2); }); @@ -676,52 +650,48 @@ describe('Meter', () => { }); }); - describe('#observer', () => { - it('should create an observer', () => { - const valueRecorder = meter.createObserver('name') as ObserverMetric; - assert.ok(valueRecorder instanceof Metric); + describe('#valueObserver', () => { + it('should create a value observer', () => { + const valueObserver = meter.createValueObserver( + 'name' + ) as ValueObserverMetric; + assert.ok(valueObserver instanceof Metric); }); it('should create observer with options', () => { - const valueRecorder = meter.createObserver('name', { + const valueObserver = meter.createValueObserver('name', { description: 'desc', unit: '1', disabled: false, - }) as ObserverMetric; - assert.ok(valueRecorder instanceof Metric); - }); - - it('should set callback and observe value ', () => { - const valueRecorder = meter.createObserver('name', { - description: 'desc', - }) as ObserverMetric; + }) as ValueObserverMetric; + assert.ok(valueObserver instanceof Metric); + }); + + it('should set callback and observe value ', async () => { + const valueRecorder = meter.createValueObserver( + 'name', + { + description: 'desc', + }, + (observerResult: api.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' }); + } + ) as ValueObserverMetric; function getCpuUsage() { return Math.random(); } - const metricObservable = new MetricObservable(); - - valueRecorder.setCallback((observerResult: api.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[] = await valueRecorder.getMetricRecord(); + assert.strictEqual(metricRecords.length, 4); - const metricRecords: MetricRecord[] = valueRecorder.getMetricRecord(); - assert.strictEqual(metricRecords.length, 5); - - 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]; + const metric1 = metricRecords[0]; + const metric2 = metricRecords[1]; + const metric3 = metricRecords[2]; + const metric4 = metricRecords[3]; 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'); @@ -731,30 +701,187 @@ describe('Meter', () => { ensureMetric(metric2); ensureMetric(metric3); ensureMetric(metric4); - ensureMetric(metric5); }); - it('should pipe through resource', () => { - const observer = meter.createObserver('name') as ObserverMetric; - assert.ok(observer.resource instanceof Resource); + it('should pipe through resource', async () => { + const valueObserver = meter.createValueObserver('name', {}, result => { + result.observe(42, { foo: 'bar' }); + }) as ValueObserverMetric; + assert.ok(valueObserver.resource instanceof Resource); - observer.setCallback(result => { - result.observe(() => 42, { foo: 'bar' }); - }); - - const [record] = observer.getMetricRecord(); + const [record] = await valueObserver.getMetricRecord(); assert.ok(record.resource instanceof Resource); }); + }); - it('should pipe through instrumentation library', () => { - const observer = meter.createObserver('name') as ObserverMetric; - assert.ok(observer.instrumentationLibrary); + describe('#batchObserver', () => { + it('should create a batch observer', () => { + const measure = meter.createBatchObserver('name', () => {}); + assert.ok(measure instanceof Metric); + }); - observer.setCallback(result => { - result.observe(() => 42, { foo: 'bar' }); + it('should create batch observer with options', () => { + const measure = meter.createBatchObserver('name', () => {}, { + description: 'desc', + unit: '1', + disabled: false, + maxTimeoutUpdateMS: 100, }); + assert.ok(measure instanceof Metric); + }); + + it('should use callback to observe values ', async () => { + const tempMetric = meter.createValueObserver('cpu_temp_per_app', { + description: 'desc', + }) as ValueObserverMetric; + + const cpuUsageMetric = meter.createValueObserver('cpu_usage_per_app', { + description: 'desc', + }) as ValueObserverMetric; + + meter.createBatchObserver( + 'metric_batch_observer', + observerBatchResult => { + interface StatItem { + usage: number; + temp: number; + } + + interface Stat { + name: string; + core1: StatItem; + core2: StatItem; + } + + function someAsyncMetrics() { + return new Promise(resolve => { + const stats: Stat[] = [ + { + name: 'app1', + core1: { usage: 2.1, temp: 67 }, + core2: { usage: 3.1, temp: 69 }, + }, + { + name: 'app2', + core1: { usage: 1.2, temp: 67 }, + core2: { usage: 4.5, temp: 69 }, + }, + ]; + resolve(stats); + }); + } + + Promise.all([ + someAsyncMetrics(), + // simulate waiting + new Promise((resolve, reject) => { + setTimeout(resolve, 1); + }), + ]).then((stats: unknown[]) => { + const apps = (stats[0] as unknown) as Stat[]; + apps.forEach(app => { + observerBatchResult.observe({ app: app.name, core: '1' }, [ + tempMetric.observation(app.core1.temp), + cpuUsageMetric.observation(app.core1.usage), + ]); + observerBatchResult.observe({ app: app.name, core: '2' }, [ + tempMetric.observation(app.core2.temp), + cpuUsageMetric.observation(app.core2.usage), + ]); + }); + }); + } + ); - const [record] = observer.getMetricRecord(); + await meter.collect(); + + const tempMetricRecords: MetricRecord[] = await tempMetric.getMetricRecord(); + const cpuUsageMetricRecords: MetricRecord[] = await cpuUsageMetric.getMetricRecord(); + assert.strictEqual(tempMetricRecords.length, 4); + assert.strictEqual(cpuUsageMetricRecords.length, 4); + + const metric1 = tempMetricRecords[0]; + const metric2 = tempMetricRecords[1]; + const metric3 = tempMetricRecords[2]; + const metric4 = tempMetricRecords[3]; + assert.strictEqual(hashLabels(metric1.labels), '|#app:app1,core:1'); + assert.strictEqual(hashLabels(metric2.labels), '|#app:app1,core:2'); + assert.strictEqual(hashLabels(metric3.labels), '|#app:app2,core:1'); + assert.strictEqual(hashLabels(metric4.labels), '|#app:app2,core:2'); + + ensureMetric(metric1, 'cpu_temp_per_app', 67); + ensureMetric(metric2, 'cpu_temp_per_app', 69); + ensureMetric(metric3, 'cpu_temp_per_app', 67); + ensureMetric(metric4, 'cpu_temp_per_app', 69); + + const metric5 = cpuUsageMetricRecords[0]; + const metric6 = cpuUsageMetricRecords[1]; + const metric7 = cpuUsageMetricRecords[2]; + const metric8 = cpuUsageMetricRecords[3]; + assert.strictEqual(hashLabels(metric1.labels), '|#app:app1,core:1'); + assert.strictEqual(hashLabels(metric2.labels), '|#app:app1,core:2'); + assert.strictEqual(hashLabels(metric3.labels), '|#app:app2,core:1'); + assert.strictEqual(hashLabels(metric4.labels), '|#app:app2,core:2'); + + ensureMetric(metric5, 'cpu_usage_per_app', 2.1); + ensureMetric(metric6, 'cpu_usage_per_app', 3.1); + ensureMetric(metric7, 'cpu_usage_per_app', 1.2); + ensureMetric(metric8, 'cpu_usage_per_app', 4.5); + }); + + it('should not observe values when timeout', done => { + const cpuUsageMetric = meter.createValueObserver('cpu_usage_per_app', { + description: 'desc', + }) as ValueObserverMetric; + + meter.createBatchObserver( + 'metric_batch_observer', + observerBatchResult => { + Promise.all([ + // simulate waiting 11ms + new Promise((resolve, reject) => { + setTimeout(resolve, 11); + }), + ]).then(async () => { + // try to hack to be able to update + (observerBatchResult as BatchObserverResult).cancelled = false; + observerBatchResult.observe({ foo: 'bar' }, [ + cpuUsageMetric.observation(123), + ]); + + // simulate some waiting + await setTimeout(() => {}, 5); + + const cpuUsageMetricRecords: MetricRecord[] = await cpuUsageMetric.getMetricRecord(); + const value = cpuUsageMetric + .bind({ foo: 'bar' }) + .getAggregator() + .toPoint().value as number; + + assert.strictEqual(value, 0); + assert.strictEqual(cpuUsageMetricRecords.length, 0); + done(); + }); + }, + { + maxTimeoutUpdateMS: 10, // timeout after 10ms + } + ); + + meter.collect(); + }); + + it('should pipe through instrumentation library', async () => { + const observer = meter.createValueObserver( + 'name', + {}, + (observerResult: api.ObserverResult) => { + observerResult.observe(42, { foo: 'bar' }); + } + ) as ValueObserverMetric; + assert.ok(observer.instrumentationLibrary); + + const [record] = await observer.getMetricRecord(); const { name, version } = record.instrumentationLibrary; assert.strictEqual(name, 'test-meter'); assert.strictEqual(version, '*'); @@ -762,7 +889,7 @@ describe('Meter', () => { }); describe('#getMetrics', () => { - it('should create a DOUBLE counter', () => { + it('should create a DOUBLE counter', async () => { const key = 'key'; const counter = meter.createCounter('counter', { description: 'test', @@ -771,7 +898,7 @@ describe('Meter', () => { const boundCounter = counter.bind(labels); boundCounter.add(10.45); - meter.collect(); + await meter.collect(); const record = meter.getBatcher().checkPointSet(); assert.strictEqual(record.length, 1); @@ -780,14 +907,14 @@ describe('Meter', () => { description: 'test', metricKind: MetricKind.COUNTER, unit: '1', - valueType: ValueType.DOUBLE, + valueType: api.ValueType.DOUBLE, }); assert.strictEqual(record[0].labels, labels); const value = record[0].aggregator.toPoint().value as Sum; assert.strictEqual(value, 10.45); }); - it('should create a INT counter', () => { + it('should create an INT counter', async () => { const key = 'key'; const counter = meter.createCounter('counter', { description: 'test', @@ -797,7 +924,7 @@ describe('Meter', () => { const boundCounter = counter.bind(labels); boundCounter.add(10.45); - meter.collect(); + await meter.collect(); const record = meter.getBatcher().checkPointSet(); assert.strictEqual(record.length, 1); @@ -806,7 +933,7 @@ describe('Meter', () => { description: 'test', metricKind: MetricKind.COUNTER, unit: '1', - valueType: ValueType.INT, + valueType: api.ValueType.INT, }); assert.strictEqual(record[0].labels, labels); const value = record[0].aggregator.toPoint().value as Sum; @@ -834,20 +961,18 @@ class CustomBatcher extends Batcher { } } -function ensureMetric(metric: MetricRecord) { - assert.ok(metric.aggregator instanceof ObserverAggregator); - assert.ok( - metric.aggregator.toPoint().value >= 0 && - metric.aggregator.toPoint().value <= 1 - ); - assert.ok( - metric.aggregator.toPoint().value >= 0 && - metric.aggregator.toPoint().value <= 1 - ); +function ensureMetric(metric: MetricRecord, name?: string, value?: number) { + assert.ok(metric.aggregator instanceof LastValueAggregator); + const lastValue = metric.aggregator.toPoint().value; + if (typeof value === 'number') { + assert.strictEqual(lastValue, value); + } else { + assert.ok(lastValue >= 0 && lastValue <= 1); + } const descriptor = metric.descriptor; - assert.strictEqual(descriptor.name, 'name'); + assert.strictEqual(descriptor.name, name || 'name'); assert.strictEqual(descriptor.description, 'desc'); assert.strictEqual(descriptor.unit, '1'); - assert.strictEqual(descriptor.metricKind, MetricKind.OBSERVER); - assert.strictEqual(descriptor.valueType, ValueType.DOUBLE); + assert.strictEqual(descriptor.metricKind, MetricKind.VALUE_OBSERVER); + assert.strictEqual(descriptor.valueType, api.ValueType.DOUBLE); } diff --git a/packages/opentelemetry-metrics/test/export/ConsoleMetricExporter.test.ts b/packages/opentelemetry-metrics/test/export/ConsoleMetricExporter.test.ts index 08790ea8c5..b23a14cd81 100644 --- a/packages/opentelemetry-metrics/test/export/ConsoleMetricExporter.test.ts +++ b/packages/opentelemetry-metrics/test/export/ConsoleMetricExporter.test.ts @@ -34,7 +34,7 @@ describe('ConsoleMetricExporter', () => { }); describe('.export()', () => { - it('should export information about metrics', () => { + it('should export information about metrics', async () => { const spyConsole = sinon.spy(console, 'log'); const meter = new MeterProvider().getMeter( @@ -49,7 +49,7 @@ describe('ConsoleMetricExporter', () => { }); boundCounter.add(10); - meter.collect(); + await meter.collect(); consoleExporter.export(meter.getBatcher().checkPointSet(), () => {}); assert.strictEqual(spyConsole.args.length, 3); const [descriptor, labels, value] = spyConsole.args;