diff --git a/packages/opentelemetry-metrics/src/export/Batcher.ts b/packages/opentelemetry-metrics/src/export/Batcher.ts index 7caec738381..2e09a3a0f59 100644 --- a/packages/opentelemetry-metrics/src/export/Batcher.ts +++ b/packages/opentelemetry-metrics/src/export/Batcher.ts @@ -18,7 +18,7 @@ import { CounterSumAggregator, MeasureExactAggregator, ObserverAggregator, -} from './Aggregator'; +} from './aggregators'; import { MetricRecord, MetricKind, Aggregator } from './types'; /** diff --git a/packages/opentelemetry-metrics/src/export/ConsoleMetricExporter.ts b/packages/opentelemetry-metrics/src/export/ConsoleMetricExporter.ts index 78e5efc069f..0440f895814 100644 --- a/packages/opentelemetry-metrics/src/export/ConsoleMetricExporter.ts +++ b/packages/opentelemetry-metrics/src/export/ConsoleMetricExporter.ts @@ -14,13 +14,7 @@ * limitations under the License. */ -import { - MetricExporter, - MetricRecord, - MetricKind, - Sum, - Distribution, -} from './types'; +import { MetricExporter, MetricRecord, Distribution, Histogram } from './types'; import { ExportResult } from '@opentelemetry/base'; /** @@ -35,25 +29,26 @@ export class ConsoleMetricExporter implements MetricExporter { for (const metric of metrics) { console.log(metric.descriptor); console.log(metric.labels); - switch (metric.descriptor.metricKind) { - case MetricKind.COUNTER: - const sum = metric.aggregator.toPoint().value as Sum; - console.log('value: ' + sum); - break; - default: - const distribution = metric.aggregator.toPoint() - .value as Distribution; - console.log( - 'min: ' + - distribution.min + - ', max: ' + - distribution.max + - ', count: ' + - distribution.count + - ', sum: ' + - distribution.sum - ); - break; + const point = metric.aggregator.toPoint(); + if (typeof point.value === 'number') { + console.log('value: ' + point.value); + } else if (typeof (point.value as Histogram).buckets === 'object') { + const histogram = point.value as Histogram; + console.log( + `count: ${histogram.count}, sum: ${histogram.sum}, buckets: ${histogram.buckets}` + ); + } else { + const distribution = point.value as Distribution; + console.log( + 'min: ' + + distribution.min + + ', max: ' + + distribution.max + + ', count: ' + + distribution.count + + ', sum: ' + + distribution.sum + ); } } return resultCallback(ExportResult.SUCCESS); diff --git a/packages/opentelemetry-metrics/src/export/aggregators/countersum.ts b/packages/opentelemetry-metrics/src/export/aggregators/countersum.ts new file mode 100644 index 00000000000..6b1fdc1ee98 --- /dev/null +++ b/packages/opentelemetry-metrics/src/export/aggregators/countersum.ts @@ -0,0 +1,37 @@ +/*! + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Aggregator, Point } from '../types'; +import { HrTime } from '@opentelemetry/api'; +import { hrTime } from '@opentelemetry/core'; + +/** Basic aggregator which calculates a Sum from individual measurements. */ +export class CounterSumAggregator implements Aggregator { + private _current: number = 0; + private _lastUpdateTime: HrTime = [0, 0]; + + update(value: number): void { + this._current += value; + this._lastUpdateTime = hrTime(); + } + + toPoint(): Point { + return { + value: this._current, + timestamp: this._lastUpdateTime, + }; + } +} diff --git a/packages/opentelemetry-metrics/src/export/aggregators/histogram.ts b/packages/opentelemetry-metrics/src/export/aggregators/histogram.ts new file mode 100644 index 00000000000..beae9090b9b --- /dev/null +++ b/packages/opentelemetry-metrics/src/export/aggregators/histogram.ts @@ -0,0 +1,81 @@ +/*! + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Aggregator, Point, Histogram } from '../types'; +import { HrTime } from '@opentelemetry/api'; +import { hrTime } from '@opentelemetry/core'; + +/** + * Basic aggregator which observes events and counts them in pre-defined buckets + * and provides the total sum and count of all observations. + */ +export class HistogramAggregator implements Aggregator { + private _lastCheckpoint: Histogram; + private _currentCheckpoint: Histogram; + private _lastCheckpointTime: HrTime; + private readonly _boundaries: number[]; + + constructor(boundaries: number[]) { + if (boundaries === undefined || boundaries.length === 0) { + throw new Error(`HistogramAggregator should be created with boundaries.`); + } + // we need to an ordered set to be able to correctly compute count for each + // boundary since we'll iterate on each in order. + this._boundaries = boundaries.sort(); + this._lastCheckpoint = this._newEmptyCheckpoint(); + this._lastCheckpointTime = hrTime(); + this._currentCheckpoint = this._newEmptyCheckpoint(); + } + + update(value: number): void { + this._currentCheckpoint.count += 1; + this._currentCheckpoint.sum += value; + + for (let i = 0; i < this._boundaries.length; i++) { + if (value < this._boundaries[i]) { + this._currentCheckpoint.buckets.counts[i] += 1; + return; + } + } + + // value is above all observed boundaries + this._currentCheckpoint.buckets.counts[this._boundaries.length] += 1; + } + + reset(): void { + this._lastCheckpointTime = hrTime(); + this._lastCheckpoint = this._currentCheckpoint; + this._currentCheckpoint = this._newEmptyCheckpoint(); + } + + toPoint(): Point { + return { + value: this._lastCheckpoint, + timestamp: this._lastCheckpointTime, + }; + } + + private _newEmptyCheckpoint(): Histogram { + return { + buckets: { + boundaries: this._boundaries, + counts: this._boundaries.map(() => 0).concat([0]), + }, + sum: 0, + count: 0, + }; + } +} diff --git a/packages/opentelemetry-metrics/src/export/aggregators/index.ts b/packages/opentelemetry-metrics/src/export/aggregators/index.ts new file mode 100644 index 00000000000..938e1ad6e38 --- /dev/null +++ b/packages/opentelemetry-metrics/src/export/aggregators/index.ts @@ -0,0 +1,20 @@ +/*! + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './countersum'; +export * from './observer'; +export * from './measureexact'; +export * from './histogram'; diff --git a/packages/opentelemetry-metrics/src/export/Aggregator.ts b/packages/opentelemetry-metrics/src/export/aggregators/measureexact.ts similarity index 61% rename from packages/opentelemetry-metrics/src/export/Aggregator.ts rename to packages/opentelemetry-metrics/src/export/aggregators/measureexact.ts index d8bd4373b9d..abbad0b4306 100644 --- a/packages/opentelemetry-metrics/src/export/Aggregator.ts +++ b/packages/opentelemetry-metrics/src/export/aggregators/measureexact.ts @@ -14,45 +14,10 @@ * limitations under the License. */ -import { Aggregator, Distribution, Point } from './types'; +import { Aggregator, Point } from '../types'; import { HrTime } from '@opentelemetry/api'; import { hrTime } from '@opentelemetry/core'; - -/** Basic aggregator which calculates a Sum from individual measurements. */ -export class CounterSumAggregator implements Aggregator { - private _current: number = 0; - private _lastUpdateTime: HrTime = [0, 0]; - - update(value: number): void { - this._current += value; - this._lastUpdateTime = hrTime(); - } - - toPoint(): Point { - return { - value: this._current, - timestamp: this._lastUpdateTime, - }; - } -} - -/** Basic aggregator for Observer which keeps the last recorded value. */ -export class ObserverAggregator implements Aggregator { - private _current: number = 0; - private _lastUpdateTime: HrTime = [0, 0]; - - update(value: number): void { - this._current = value; - this._lastUpdateTime = hrTime(); - } - - toPoint(): Point { - return { - value: this._current, - timestamp: this._lastUpdateTime, - }; - } -} +import { Distribution } from '../types'; /** Basic aggregator keeping all raw values (events, sum, max and min). */ export class MeasureExactAggregator implements Aggregator { diff --git a/packages/opentelemetry-metrics/src/export/aggregators/observer.ts b/packages/opentelemetry-metrics/src/export/aggregators/observer.ts new file mode 100644 index 00000000000..d1ba176c30f --- /dev/null +++ b/packages/opentelemetry-metrics/src/export/aggregators/observer.ts @@ -0,0 +1,37 @@ +/*! + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { 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 { + private _current: number = 0; + private _lastUpdateTime: HrTime = [0, 0]; + + update(value: number): void { + this._current = value; + this._lastUpdateTime = hrTime(); + } + + toPoint(): Point { + return { + value: this._current, + timestamp: this._lastUpdateTime, + }; + } +} diff --git a/packages/opentelemetry-metrics/src/export/types.ts b/packages/opentelemetry-metrics/src/export/types.ts index ff4f1173eb0..40b631e0269 100644 --- a/packages/opentelemetry-metrics/src/export/types.ts +++ b/packages/opentelemetry-metrics/src/export/types.ts @@ -37,6 +37,33 @@ export interface Distribution { sum: number; } +export interface Histogram { + /** + * Buckets are implemented using two different array: + * - boundaries contains every boundary (which are upper boundary for each slice) + * - counts contains count of event for each slice + * + * Note that we'll always have n+1 (where n is the number of boundaries) slice + * because we need to count event that are above the highest boundary. This is the + * reason why it's not implement using array of object, because the last slice + * dont have any boundary. + * + * Example if we measure the values: [5, 30, 5, 40, 5, 15, 15, 15, 25] + * with the boundaries [ 10, 20, 30 ], we will have the following state: + * + * buckets: { + * boundaries: [10, 20, 30], + * counts: [3, 3, 2, 1], + * } + */ + buckets: { + boundaries: number[]; + counts: number[]; + }; + sum: number; + count: number; +} + export interface MetricRecord { readonly descriptor: MetricDescriptor; readonly labels: Labels; @@ -80,6 +107,6 @@ export interface Aggregator { } export interface Point { - value: Sum | LastValue | Distribution; + value: Sum | LastValue | Distribution | Histogram; timestamp: HrTime; } diff --git a/packages/opentelemetry-metrics/src/index.ts b/packages/opentelemetry-metrics/src/index.ts index c9f158528f1..113f788dd9d 100644 --- a/packages/opentelemetry-metrics/src/index.ts +++ b/packages/opentelemetry-metrics/src/index.ts @@ -18,7 +18,6 @@ export * from './BoundInstrument'; export * from './Meter'; export * from './Metric'; export * from './MeterProvider'; -export * from './export/Aggregator'; +export * from './export/aggregators'; export * from './export/ConsoleMetricExporter'; export * from './export/types'; -export * from './export/Aggregator'; diff --git a/packages/opentelemetry-metrics/test/Meter.test.ts b/packages/opentelemetry-metrics/test/Meter.test.ts index dd028bec585..0eaab7410db 100644 --- a/packages/opentelemetry-metrics/test/Meter.test.ts +++ b/packages/opentelemetry-metrics/test/Meter.test.ts @@ -33,7 +33,7 @@ import { NoopLogger, hrTime, hrTimeToNanoseconds } from '@opentelemetry/core'; import { CounterSumAggregator, ObserverAggregator, -} from '../src/export/Aggregator'; +} from '../src/export/aggregators'; import { ValueType } from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import { hashLabels } from '../src/Utils'; diff --git a/packages/opentelemetry-metrics/test/export/aggregators/histogram.test.ts b/packages/opentelemetry-metrics/test/export/aggregators/histogram.test.ts new file mode 100644 index 00000000000..efb1c6519af --- /dev/null +++ b/packages/opentelemetry-metrics/test/export/aggregators/histogram.test.ts @@ -0,0 +1,158 @@ +/*! + * Copyright 2019, 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 assert from 'assert'; +import { HistogramAggregator } from '../../../src/export/aggregators'; +import { Histogram } from '../../../src'; + +describe('HistogramAggregator', () => { + describe('constructor()', () => { + it('should construct a histogramAggregator', () => { + assert.doesNotThrow(() => { + new HistogramAggregator([1, 2]); + }); + }); + + it('should sort boundaries', () => { + const aggregator = new HistogramAggregator([500, 300, 700]); + const point = aggregator.toPoint().value as Histogram; + assert.deepEqual(point.buckets.boundaries, [300, 500, 700]); + }); + + it('should throw if no boundaries are defined', () => { + // @ts-ignore + assert.throws(() => new HistogramAggregator()); + assert.throws(() => new HistogramAggregator([])); + }); + }); + + describe('.update()', () => { + it('should not update checkpoint', () => { + const aggregator = new HistogramAggregator([100, 200]); + aggregator.update(150); + const point = aggregator.toPoint().value as Histogram; + assert.equal(point.count, 0); + assert.equal(point.sum, 0); + }); + + it('should update the second bucket', () => { + const aggregator = new HistogramAggregator([100, 200]); + aggregator.update(150); + aggregator.reset(); + const point = aggregator.toPoint().value as Histogram; + assert.equal(point.count, 1); + assert.equal(point.sum, 150); + assert.equal(point.buckets.counts[0], 0); + assert.equal(point.buckets.counts[1], 1); + assert.equal(point.buckets.counts[2], 0); + }); + + it('should update the second bucket', () => { + const aggregator = new HistogramAggregator([100, 200]); + aggregator.update(50); + aggregator.reset(); + const point = aggregator.toPoint().value as Histogram; + assert.equal(point.count, 1); + assert.equal(point.sum, 50); + assert.equal(point.buckets.counts[0], 1); + assert.equal(point.buckets.counts[1], 0); + assert.equal(point.buckets.counts[2], 0); + }); + + it('should update the third bucket since value is above all boundaries', () => { + const aggregator = new HistogramAggregator([100, 200]); + aggregator.update(250); + aggregator.reset(); + const point = aggregator.toPoint().value as Histogram; + assert.equal(point.count, 1); + assert.equal(point.sum, 250); + assert.equal(point.buckets.counts[0], 0); + assert.equal(point.buckets.counts[1], 0); + assert.equal(point.buckets.counts[2], 1); + }); + }); + + describe('.count', () => { + it('should return last checkpoint count', () => { + const aggregator = new HistogramAggregator([100]); + let point = aggregator.toPoint().value as Histogram; + assert.equal(point.count, point.count); + aggregator.update(10); + aggregator.reset(); + point = aggregator.toPoint().value as Histogram; + assert.equal(point.count, 1); + assert.equal(point.count, point.count); + }); + }); + + describe('.sum', () => { + it('should return last checkpoint sum', () => { + const aggregator = new HistogramAggregator([100]); + let point = aggregator.toPoint().value as Histogram; + assert.equal(point.sum, point.sum); + aggregator.update(10); + aggregator.reset(); + point = aggregator.toPoint().value as Histogram; + assert.equal(point.sum, 10); + }); + }); + + describe('.reset()', () => { + it('should create a empty checkoint by default', () => { + const aggregator = new HistogramAggregator([100]); + const point = aggregator.toPoint().value as Histogram; + assert.deepEqual(point.buckets.boundaries, [100]); + assert(point.buckets.counts.every(count => count === 0)); + // should contains one bucket for each boundary + one for values outside of the largest boundary + assert.equal(point.buckets.counts.length, 2); + assert.deepEqual(point.buckets.boundaries, [100]); + assert.equal(point.count, 0); + assert.equal(point.sum, 0); + }); + + it('should update checkpoint', () => { + const aggregator = new HistogramAggregator([100]); + aggregator.update(10); + aggregator.reset(); + const point = aggregator.toPoint().value as Histogram; + assert.equal(point.count, 1); + assert.equal(point.sum, 10); + assert.deepEqual(point.buckets.boundaries, [100]); + assert.equal(point.buckets.counts.length, 2); + assert.deepEqual(point.buckets.counts, [1, 0]); + }); + }); + + describe('.toPoint()', () => { + it('should return default checkpoint', () => { + const aggregator = new HistogramAggregator([100]); + const point = aggregator.toPoint().value as Histogram; + assert.deepEqual(aggregator.toPoint().value, point); + assert(aggregator.toPoint().timestamp.every(nbr => nbr > 0)); + }); + + it('should return last checkpoint if updated', () => { + const aggregator = new HistogramAggregator([100]); + aggregator.update(100); + aggregator.reset(); + assert( + aggregator + .toPoint() + .timestamp.every(nbr => typeof nbr === 'number' && nbr !== 0) + ); + }); + }); +});