From d35ac4f8830b085ff9964c208f38c74030a71b4c Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Tue, 18 Feb 2020 11:21:07 -0800 Subject: [PATCH] feat: Metrics SDK - aggregator, batcher, controller (#738) * add Batcher and Aggregator * JSDoc comments * final patch * Descriptor to MetricDescriptor * merge createMeasure PR and fix the tests * move Aggregator interface to types --- .../src/prometheus.ts | 89 +++-- .../test/prometheus.test.ts | 33 +- .../src/BoundInstrument.ts | 137 ++++--- packages/opentelemetry-metrics/src/Meter.ts | 80 ++-- packages/opentelemetry-metrics/src/Metric.ts | 76 ++-- .../src/export/Aggregator.ts | 75 ++++ .../src/export/Batcher.ts | 67 ++++ .../src/export/ConsoleMetricExporter.ts | 55 ++- .../src/export/Controller.ts | 50 +++ .../opentelemetry-metrics/src/export/types.ts | 348 +++-------------- packages/opentelemetry-metrics/src/types.ts | 9 +- .../opentelemetry-metrics/test/Meter.test.ts | 358 ++++++++++-------- .../test/MeterProvider.test.ts | 2 +- .../test/export/ConsoleMetricExporter.test.ts | 28 +- .../test/mocks/Exporter.ts | 4 +- 15 files changed, 674 insertions(+), 737 deletions(-) create mode 100644 packages/opentelemetry-metrics/src/export/Aggregator.ts create mode 100644 packages/opentelemetry-metrics/src/export/Batcher.ts create mode 100644 packages/opentelemetry-metrics/src/export/Controller.ts diff --git a/packages/opentelemetry-exporter-prometheus/src/prometheus.ts b/packages/opentelemetry-exporter-prometheus/src/prometheus.ts index 65553bbee89..4dfa9cd8342 100644 --- a/packages/opentelemetry-exporter-prometheus/src/prometheus.ts +++ b/packages/opentelemetry-exporter-prometheus/src/prometheus.ts @@ -17,17 +17,20 @@ import { ExportResult } from '@opentelemetry/base'; import { NoopLogger } from '@opentelemetry/core'; import { - LabelValue, - MetricDescriptor, - MetricDescriptorType, MetricExporter, - ReadableMetric, + MetricRecord, + MetricDescriptor, + LastValue, + MetricKind, + Sum, } from '@opentelemetry/metrics'; import * as types from '@opentelemetry/api'; import { createServer, IncomingMessage, Server, ServerResponse } from 'http'; import { Counter, Gauge, labelValues, Metric, Registry } from 'prom-client'; import * as url from 'url'; import { ExporterConfig } from './export/types'; +import { LabelSet } from '@opentelemetry/metrics/build/src/LabelSet'; +import { CounterSumAggregator } from '@opentelemetry/metrics/build/src/export/Aggregator'; export class PrometheusExporter implements MetricExporter { static readonly DEFAULT_OPTIONS = { @@ -81,13 +84,10 @@ export class PrometheusExporter implements MetricExporter { * be a no-op and the exporter should reach into the metrics when the export endpoint is * called. As there is currently no interface to do this, this is our only option. * - * @param readableMetrics Metrics to be sent to the prometheus backend + * @param records Metrics to be sent to the prometheus backend * @param cb result callback to be called on finish */ - export( - readableMetrics: ReadableMetric[], - cb: (result: ExportResult) => void - ) { + export(records: MetricRecord[], cb: (result: ExportResult) => void) { if (!this._server) { // It is conceivable that the _server may not be started as it is an async startup // However unlikely, if this happens the caller may retry the export @@ -97,8 +97,8 @@ export class PrometheusExporter implements MetricExporter { this._logger.debug('Prometheus exporter export'); - for (const readableMetric of readableMetrics) { - this._updateMetric(readableMetric); + for (const record of records) { + this._updateMetric(record); } cb(ExportResult.SUCCESS); @@ -117,32 +117,33 @@ export class PrometheusExporter implements MetricExporter { /** * Updates the value of a single metric in the registry * - * @param readableMetric Metric value to be saved + * @param record Metric value to be saved */ - private _updateMetric(readableMetric: ReadableMetric) { - const metric = this._registerMetric(readableMetric); + private _updateMetric(record: MetricRecord) { + const metric = this._registerMetric(record); if (!metric) return; - const labelKeys = readableMetric.descriptor.labelKeys; + const labelKeys = record.descriptor.labelKeys; + const value = record.aggregator.value(); if (metric instanceof Counter) { - for (const ts of readableMetric.timeseries) { - // Prometheus counter saves internal state and increments by given value. - // ReadableMetric value is the current state, not the delta to be incremented by. - // Currently, _registerMetric creates a new counter every time the value changes, - // so the increment here behaves as a set value (increment from 0) - metric.inc( - this._getLabelValues(labelKeys, ts.labelValues), - ts.points[0].value as number - ); - } + // Prometheus counter saves internal state and increments by given value. + // ReadableMetric value is the current state, not the delta to be incremented by. + // Currently, _registerMetric creates a new counter every time the value changes, + // so the increment here behaves as a set value (increment from 0) + metric.inc(this._getLabelValues(labelKeys, record.labels), value as Sum); } if (metric instanceof Gauge) { - for (const ts of readableMetric.timeseries) { + if (record.aggregator instanceof CounterSumAggregator) { + metric.set( + this._getLabelValues(labelKeys, record.labels), + value as Sum + ); + } else { metric.set( - this._getLabelValues(labelKeys, ts.labelValues), - ts.points[0].value as number + this._getLabelValues(labelKeys, record.labels), + (value as LastValue).value ); } } @@ -150,18 +151,19 @@ export class PrometheusExporter implements MetricExporter { // TODO: only counter and gauge are implemented in metrics so far } - private _getLabelValues(keys: string[], values: LabelValue[]) { + private _getLabelValues(keys: string[], values: LabelSet) { const labelValues: labelValues = {}; + const labels = values.labels; for (let i = 0; i < keys.length; i++) { - if (values[i].value !== null) { - labelValues[keys[i]] = values[i].value!; + if (labels[keys[i]] !== null) { + labelValues[keys[i]] = labels[keys[i]]; } } return labelValues; } - private _registerMetric(readableMetric: ReadableMetric): Metric | undefined { - const metricName = this._getPrometheusMetricName(readableMetric.descriptor); + private _registerMetric(record: MetricRecord): Metric | undefined { + const metricName = this._getPrometheusMetricName(record.descriptor); const metric = this._registry.getSingleMetric(metricName); /** @@ -177,31 +179,26 @@ export class PrometheusExporter implements MetricExporter { this._registry.removeSingleMetric(metricName); } else if (metric) return metric; - return this._newMetric(readableMetric, metricName); + return this._newMetric(record, metricName); } - private _newMetric( - readableMetric: ReadableMetric, - name: string - ): Metric | undefined { + private _newMetric(record: MetricRecord, name: string): Metric | undefined { const metricObject = { name, // prom-client throws with empty description which is our default - help: readableMetric.descriptor.description || 'description missing', - labelNames: readableMetric.descriptor.labelKeys, + help: record.descriptor.description || 'description missing', + labelNames: record.descriptor.labelKeys, // list of registries to register the newly created metric registers: [this._registry], }; - switch (readableMetric.descriptor.type) { - case MetricDescriptorType.COUNTER_DOUBLE: - case MetricDescriptorType.COUNTER_INT64: + switch (record.descriptor.metricKind) { + case MetricKind.COUNTER: // there is no such thing as a non-monotonic counter in prometheus - return readableMetric.descriptor.monotonic + return record.descriptor.monotonic ? new Counter(metricObject) : new Gauge(metricObject); - case MetricDescriptorType.GAUGE_DOUBLE: - case MetricDescriptorType.GAUGE_INT64: + case MetricKind.GAUGE: 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 1edd1fb136d..c0df60de33d 100644 --- a/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts +++ b/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts @@ -187,12 +187,13 @@ describe('PrometheusExporter', () => { const boundCounter = counter.bind(meter.labels({ key1: 'labelValue1' })); boundCounter.add(10); - exporter.export(meter.getMetrics(), () => { + 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); - exporter.export(meter.getMetrics(), () => { + exporter.export(meter.getBatcher().checkPointSet(), () => { http .get('http://localhost:9464/metrics', res => { res.on('data', chunk => { @@ -227,7 +228,8 @@ describe('PrometheusExporter', () => { const boundGauge = gauge.bind(meter.labels({ key1: 'labelValue1' })); boundGauge.set(10); - exporter.export([gauge.get()!], () => { + meter.collect(); + exporter.export(meter.getBatcher().checkPointSet(), () => { http .get('http://localhost:9464/metrics', res => { res.on('data', chunk => { @@ -259,9 +261,10 @@ describe('PrometheusExporter', () => { labelKeys: ['counterKey1'], }) as CounterMetric; - gauge.bind(meter.labels({ key1: 'labelValue1' })).set(10); - counter.bind(meter.labels({ key1: 'labelValue1' })).add(10); - exporter.export([gauge.get()!, counter.get()!], () => { + gauge.bind(meter.labels({ gaugeKey1: 'labelValue1' })).set(10); + counter.bind(meter.labels({ counterKey1: 'labelValue1' })).add(10); + meter.collect(); + exporter.export(meter.getBatcher().checkPointSet(), () => { http .get('http://localhost:9464/metrics', res => { res.on('data', chunk => { @@ -308,7 +311,8 @@ describe('PrometheusExporter', () => { const boundGauge = gauge.bind(meter.labels({ key1: 'labelValue1' })); boundGauge.set(10); - exporter.export([gauge.get()!], () => { + meter.collect(); + exporter.export(meter.getBatcher().checkPointSet(), () => { http .get('http://localhost:9464/metrics', res => { res.on('data', chunk => { @@ -333,7 +337,8 @@ describe('PrometheusExporter', () => { const gauge = meter.createGauge('gauge.bad-name') as GaugeMetric; const boundGauge = gauge.bind(meter.labels({ key1: 'labelValue1' })); boundGauge.set(10); - exporter.export([gauge.get()!], () => { + meter.collect(); + exporter.export(meter.getBatcher().checkPointSet(), () => { http .get('http://localhost:9464/metrics', res => { res.on('data', chunk => { @@ -362,7 +367,8 @@ describe('PrometheusExporter', () => { }); counter.bind(meter.labels({ key1: 'labelValue1' })).add(20); - exporter.export(meter.getMetrics(), () => { + meter.collect(); + exporter.export(meter.getBatcher().checkPointSet(), () => { http .get('http://localhost:9464/metrics', res => { res.on('data', chunk => { @@ -407,7 +413,8 @@ describe('PrometheusExporter', () => { }); exporter.startServer(() => { - exporter!.export(meter.getMetrics(), () => { + meter.collect(); + exporter!.export(meter.getBatcher().checkPointSet(), () => { http .get('http://localhost:9464/metrics', res => { res.on('data', chunk => { @@ -435,7 +442,8 @@ describe('PrometheusExporter', () => { }); exporter.startServer(() => { - exporter!.export(meter.getMetrics(), () => { + meter.collect(); + exporter!.export(meter.getBatcher().checkPointSet(), () => { http .get('http://localhost:8080/metrics', res => { res.on('data', chunk => { @@ -463,7 +471,8 @@ describe('PrometheusExporter', () => { }); exporter.startServer(() => { - exporter!.export(meter.getMetrics(), () => { + meter.collect(); + exporter!.export(meter.getBatcher().checkPointSet(), () => { http .get('http://localhost:9464/test', res => { res.on('data', chunk => { diff --git a/packages/opentelemetry-metrics/src/BoundInstrument.ts b/packages/opentelemetry-metrics/src/BoundInstrument.ts index b72176fae1f..15351bb8e44 100644 --- a/packages/opentelemetry-metrics/src/BoundInstrument.ts +++ b/packages/opentelemetry-metrics/src/BoundInstrument.ts @@ -15,33 +15,51 @@ */ import * as types from '@opentelemetry/api'; -import { TimeSeries } from './export/types'; +import { Aggregator } from './export/types'; /** * This class represent the base to BoundInstrument, which is responsible for generating * the TimeSeries. */ export class BaseBoundInstrument { - protected _data = 0; protected _labelSet: types.LabelSet; + protected _logger: types.Logger; + protected _monotonic: boolean; - constructor(labelSet: types.LabelSet) { + constructor( + labelSet: types.LabelSet, + logger: types.Logger, + monotonic: boolean, + private readonly _disabled: boolean, + private readonly _valueType: types.ValueType, + private readonly _aggregator: Aggregator + ) { this._labelSet = labelSet; + this._logger = logger; + this._monotonic = monotonic; + } + + update(value: number): void { + if (this._disabled) return; + + if (this._valueType === types.ValueType.INT && !Number.isInteger(value)) { + this._logger.warn( + `INT value type cannot accept a floating-point value for ${Object.values( + this._labelSet.labels + )}, ignoring the fractional digits.` + ); + value = Math.trunc(value); + } + + this._aggregator.update(value); } - /** - * Returns the TimeSeries with one or more Point. - * - * @param timestamp The time at which the instrument is recorded. - * @returns The TimeSeries. - */ - getTimeSeries(timestamp: types.HrTime): TimeSeries { - return { - labelValues: Object.values(this._labelSet.labels).map(value => ({ - value, - })), - points: [{ value: this._data, timestamp }], - }; + getLabelSet(): types.LabelSet { + return this._labelSet; + } + + getAggregator(): Aggregator { + return this._aggregator; } } @@ -53,18 +71,16 @@ export class BoundCounter extends BaseBoundInstrument implements types.BoundCounter { constructor( labelSet: types.LabelSet, - private readonly _disabled: boolean, - private readonly _monotonic: boolean, - private readonly _valueType: types.ValueType, - private readonly _logger: types.Logger, - private readonly _onUpdate: Function + disabled: boolean, + monotonic: boolean, + valueType: types.ValueType, + logger: types.Logger, + aggregator: Aggregator ) { - super(labelSet); + super(labelSet, logger, monotonic, disabled, valueType, aggregator); } add(value: number): void { - if (this._disabled) return; - if (this._monotonic && value < 0) { this._logger.error( `Monotonic counter cannot descend for ${Object.values( @@ -73,16 +89,8 @@ export class BoundCounter extends BaseBoundInstrument ); return; } - if (this._valueType === types.ValueType.INT && !Number.isInteger(value)) { - this._logger.warn( - `INT counter cannot accept a floating-point value for ${Object.values( - this._labelSet.labels - )}, ignoring the fractional digits.` - ); - value = Math.trunc(value); - } - this._data = this._data + value; - this._onUpdate(); + + this.update(value); } } @@ -92,21 +100,21 @@ export class BoundCounter extends BaseBoundInstrument */ export class BoundGauge extends BaseBoundInstrument implements types.BoundGauge { + private _current: number = 0; + constructor( labelSet: types.LabelSet, - private readonly _disabled: boolean, - private readonly _monotonic: boolean, - private readonly _valueType: types.ValueType, - private readonly _logger: types.Logger, - private readonly _onUpdate: Function + disabled: boolean, + monotonic: boolean, + valueType: types.ValueType, + logger: types.Logger, + aggregator: Aggregator ) { - super(labelSet); + super(labelSet, logger, monotonic, disabled, valueType, aggregator); } set(value: number): void { - if (this._disabled) return; - - if (this._monotonic && value < this._data) { + if (this._monotonic && value < this._current) { this._logger.error( `Monotonic gauge cannot descend for ${Object.values( this._labelSet.labels @@ -115,16 +123,8 @@ export class BoundGauge extends BaseBoundInstrument return; } - if (this._valueType === types.ValueType.INT && !Number.isInteger(value)) { - this._logger.warn( - `INT gauge cannot accept a floating-point value for ${Object.values( - this._labelSet.labels - )}, ignoring the fractional digits.` - ); - value = Math.trunc(value); - } - this._data = value; - this._onUpdate(); + this._current = value; + this.update(value); } } @@ -133,15 +133,19 @@ export class BoundGauge extends BaseBoundInstrument */ export class BoundMeasure extends BaseBoundInstrument implements types.BoundMeasure { + private readonly _absolute: boolean; + constructor( labelSet: types.LabelSet, - private readonly _disabled: boolean, - private readonly _absolute: boolean, - private readonly _valueType: types.ValueType, - private readonly _logger: types.Logger, - private readonly _onUpdate: Function + disabled: boolean, + monotonic: boolean, + absolute: boolean, + valueType: types.ValueType, + logger: types.Logger, + aggregator: Aggregator ) { - super(labelSet); + super(labelSet, logger, monotonic, disabled, valueType, aggregator); + this._absolute = absolute; } record( @@ -149,8 +153,6 @@ export class BoundMeasure extends BaseBoundInstrument distContext?: types.DistributedContext, spanContext?: types.SpanContext ): void { - if (this._disabled) return; - if (this._absolute && value < 0) { this._logger.error( `Absolute measure cannot contain negative values for ${Object.values( @@ -160,17 +162,6 @@ export class BoundMeasure extends BaseBoundInstrument return; } - if (this._valueType === types.ValueType.INT && !Number.isInteger(value)) { - this._logger.warn( - `INT measure cannot accept a floating-point value for ${Object.values( - this._labelSet.labels - )}; truncating the value.` - ); - value = Math.trunc(value); - } - - //@todo: implement this._data logic - - this._onUpdate(); + this.update(value); } } diff --git a/packages/opentelemetry-metrics/src/Meter.ts b/packages/opentelemetry-metrics/src/Meter.ts index 9d3122b5de7..d17fb7fd265 100644 --- a/packages/opentelemetry-metrics/src/Meter.ts +++ b/packages/opentelemetry-metrics/src/Meter.ts @@ -25,9 +25,9 @@ import { MeterConfig, } from './types'; import { LabelSet } from './LabelSet'; -import { ReadableMetric, MetricExporter } from './export/types'; -import { notNull } from './Utils'; -import { ExportResult } from '@opentelemetry/base'; +import { Batcher, UngroupedBatcher } from './export/Batcher'; +import { PushController } from './export/Controller'; +import { NoopExporter } from '../test/mocks/Exporter'; /** * Meter is an implementation of the {@link Meter} interface. @@ -35,8 +35,7 @@ import { ExportResult } from '@opentelemetry/base'; export class Meter implements types.Meter { private readonly _logger: types.Logger; private readonly _metrics = new Map>(); - private readonly _exporters: MetricExporter[] = []; - + private readonly _batcher: Batcher; readonly labels = Meter.labels; /** @@ -44,6 +43,11 @@ export class Meter implements types.Meter { */ constructor(config: MeterConfig = DEFAULT_CONFIG) { this._logger = config.logger || new ConsoleLogger(config.logLevel); + this._batcher = new UngroupedBatcher(); + // start the push controller + const exporter = config.exporter || new NoopExporter(); + const interval = config.interval; + new PushController(this, exporter, interval); } /** @@ -62,17 +66,14 @@ export class Meter implements types.Meter { return types.NOOP_MEASURE_METRIC; } const opt: MetricOptions = { - // Measures are defined as absolute by default - absolute: true, + absolute: true, // Measures are defined as absolute by default monotonic: false, // not applicable to measure, set to false logger: this._logger, ...DEFAULT_METRIC_OPTIONS, ...options, }; - const measure = new MeasureMetric(name, opt, () => { - this._exportOneMetric(name); - }); + const measure = new MeasureMetric(name, opt, this._batcher); this._registerMetric(name, measure); return measure; } @@ -95,16 +96,13 @@ export class Meter implements types.Meter { return types.NOOP_COUNTER_METRIC; } const opt: MetricOptions = { - // Counters are defined as monotonic by default - monotonic: true, + monotonic: true, // Counters are defined as monotonic by default absolute: false, // not applicable to counter, set to false logger: this._logger, ...DEFAULT_METRIC_OPTIONS, ...options, }; - const counter = new CounterMetric(name, opt, () => { - this._exportOneMetric(name); - }); + const counter = new CounterMetric(name, opt, this._batcher); this._registerMetric(name, counter); return counter; } @@ -128,37 +126,34 @@ export class Meter implements types.Meter { return types.NOOP_GAUGE_METRIC; } const opt: MetricOptions = { - // Gauges are defined as non-monotonic by default - monotonic: false, + monotonic: false, // Gauges are defined as non-monotonic by default absolute: false, // not applicable for gauges, set to false logger: this._logger, ...DEFAULT_METRIC_OPTIONS, ...options, }; - const gauge = new GaugeMetric(name, opt, () => { - this._exportOneMetric(name); - }); + const gauge = new GaugeMetric(name, opt, this._batcher); this._registerMetric(name, gauge); return gauge; } /** - * Gets a collection of Metrics to be exported. - * @returns The list of metrics. + * Collects all the metrics created with this `Meter` for export. + * + * Utilizes the batcher to create checkpoints of the current values in + * each aggregator belonging to the metrics that were created with this + * meter instance. */ - getMetrics(): ReadableMetric[] { - return Array.from(this._metrics.values()) - .map(metric => metric.get()) - .filter(notNull); + collect() { + Array.from(this._metrics.values()).forEach(metric => { + metric.getMetricRecord().forEach(record => { + this._batcher.process(record); + }); + }); } - /** - * Add an exporter to the list of registered exporters - * - * @param exporter {@Link MetricExporter} to add to the list of registered exporters - */ - addExporter(exporter: MetricExporter) { - this._exporters.push(exporter); + getBatcher(): Batcher { + return this._batcher; } /** @@ -182,25 +177,6 @@ export class Meter implements types.Meter { return new LabelSet(identifier, sortedLabels); } - /** - * Send a single metric by name to all registered exporters - */ - private _exportOneMetric(name: string) { - const metric = this._metrics.get(name); - if (!metric) return; - - const readableMetric = metric.get(); - if (!readableMetric) return; - - for (const exporter of this._exporters) { - exporter.export([readableMetric], result => { - if (result !== ExportResult.SUCCESS) { - this._logger.error(`Failed to export ${name}`); - } - }); - } - } - /** * Registers metric to register. * @param name The name of the metric. diff --git a/packages/opentelemetry-metrics/src/Metric.ts b/packages/opentelemetry-metrics/src/Metric.ts index 65321f3c02b..017421b3f45 100644 --- a/packages/opentelemetry-metrics/src/Metric.ts +++ b/packages/opentelemetry-metrics/src/Metric.ts @@ -15,7 +15,6 @@ */ import * as types from '@opentelemetry/api'; -import { hrTime } from '@opentelemetry/core'; import { BoundCounter, BoundGauge, @@ -23,11 +22,8 @@ import { BoundMeasure, } from './BoundInstrument'; import { MetricOptions } from './types'; -import { - ReadableMetric, - MetricDescriptor, - MetricDescriptorType, -} from './export/types'; +import { MetricKind, MetricDescriptor, MetricRecord } from './export/types'; +import { Batcher } from './export/Batcher'; /** This is a SDK implementation of {@link Metric} interface. */ export abstract class Metric @@ -36,19 +32,19 @@ export abstract class Metric protected readonly _disabled: boolean; protected readonly _valueType: types.ValueType; protected readonly _logger: types.Logger; - private readonly _metricDescriptor: MetricDescriptor; + private readonly _descriptor: MetricDescriptor; private readonly _instruments: Map = new Map(); constructor( private readonly _name: string, private readonly _options: MetricOptions, - private readonly _type: MetricDescriptorType + private readonly _kind: MetricKind ) { this._monotonic = _options.monotonic; this._disabled = _options.disabled; this._valueType = _options.valueType; this._logger = _options.logger; - this._metricDescriptor = this._getMetricDescriptor(); + this._descriptor = this._getMetricDescriptor(); } /** @@ -97,21 +93,12 @@ export abstract class Metric return; } - /** - * Provides a ReadableMetric with one or more TimeSeries. - * @returns The ReadableMetric, or null if TimeSeries is not present in - * Metric. - */ - get(): ReadableMetric | null { - if (this._instruments.size === 0) return null; - - const timestamp = hrTime(); - return { - descriptor: this._metricDescriptor, - timeseries: Array.from(this._instruments, ([_, instrument]) => - instrument.getTimeSeries(timestamp) - ), - }; + getMetricRecord(): MetricRecord[] { + return Array.from(this._instruments.values()).map(instrument => ({ + descriptor: this._descriptor, + labels: instrument.getLabelSet(), + aggregator: instrument.getAggregator(), + })); } private _getMetricDescriptor(): MetricDescriptor { @@ -119,8 +106,9 @@ export abstract class Metric name: this._name, description: this._options.description, unit: this._options.unit, + metricKind: this._kind, + valueType: this._valueType, labelKeys: this._options.labelKeys, - type: this._type, monotonic: this._monotonic, }; } @@ -134,15 +122,9 @@ export class CounterMetric extends Metric constructor( name: string, options: MetricOptions, - private readonly _onUpdate: Function + private readonly _batcher: Batcher ) { - super( - name, - options, - options.valueType === types.ValueType.DOUBLE - ? MetricDescriptorType.COUNTER_DOUBLE - : MetricDescriptorType.COUNTER_INT64 - ); + super(name, options, MetricKind.COUNTER); } protected _makeInstrument(labelSet: types.LabelSet): BoundCounter { return new BoundCounter( @@ -151,7 +133,8 @@ export class CounterMetric extends Metric this._monotonic, this._valueType, this._logger, - this._onUpdate + // @todo: consider to set to CounterSumAggregator always. + this._batcher.aggregatorFor(MetricKind.COUNTER) ); } @@ -171,15 +154,9 @@ export class GaugeMetric extends Metric constructor( name: string, options: MetricOptions, - private readonly _onUpdate: Function + private readonly _batcher: Batcher ) { - super( - name, - options, - options.valueType === types.ValueType.DOUBLE - ? MetricDescriptorType.GAUGE_DOUBLE - : MetricDescriptorType.GAUGE_INT64 - ); + super(name, options, MetricKind.GAUGE); } protected _makeInstrument(labelSet: types.LabelSet): BoundGauge { return new BoundGauge( @@ -188,7 +165,7 @@ export class GaugeMetric extends Metric this._monotonic, this._valueType, this._logger, - this._onUpdate + this._batcher.aggregatorFor(MetricKind.GAUGE) ); } @@ -209,15 +186,9 @@ export class MeasureMetric extends Metric constructor( name: string, options: MetricOptions, - private readonly _onUpdate: Function + private readonly _batcher: Batcher ) { - super( - name, - options, - options.valueType === types.ValueType.DOUBLE - ? MetricDescriptorType.MEASURE_DOUBLE - : MetricDescriptorType.MEASURE_INT64 - ); + super(name, options, MetricKind.MEASURE); this._absolute = options.absolute !== undefined ? options.absolute : true; // Absolute default is true } @@ -225,10 +196,11 @@ export class MeasureMetric extends Metric return new BoundMeasure( labelSet, this._disabled, + this._monotonic, this._absolute, this._valueType, this._logger, - this._onUpdate + this._batcher.aggregatorFor(MetricKind.MEASURE) ); } diff --git a/packages/opentelemetry-metrics/src/export/Aggregator.ts b/packages/opentelemetry-metrics/src/export/Aggregator.ts new file mode 100644 index 00000000000..2a84b168ded --- /dev/null +++ b/packages/opentelemetry-metrics/src/export/Aggregator.ts @@ -0,0 +1,75 @@ +/*! + * 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 { Distribution, Sum, LastValue, Aggregator } from './types'; +import { hrTime } from '@opentelemetry/core'; +import * as types from '@opentelemetry/api'; + +/** Basic aggregator which calculates a Sum from individual measurements. */ +export class CounterSumAggregator implements Aggregator { + private _current: number = 0; + + update(value: number): void { + this._current += value; + } + + value(): Sum { + return this._current; + } +} + +/** Basic aggregator which keeps the last recorded value and timestamp. */ +export class GaugeAggregator implements Aggregator { + private _current: number = 0; + private _timestamp: types.HrTime = hrTime(); + + update(value: number): void { + this._current = value; + this._timestamp = hrTime(); + } + + value(): LastValue { + return { + value: this._current, + timestamp: this._timestamp, + }; + } +} + +/** Basic aggregator keeping all raw values (events, sum, max and min). */ +export class MeasureExactAggregator implements Aggregator { + private _distribution: Distribution; + + constructor() { + this._distribution = { + min: Infinity, + max: -Infinity, + sum: 0, + count: 0, + }; + } + + update(value: number): void { + this._distribution.count++; + this._distribution.sum += value; + this._distribution.min = Math.min(this._distribution.min, value); + this._distribution.max = Math.max(this._distribution.max, value); + } + + value(): Distribution { + return this._distribution; + } +} diff --git a/packages/opentelemetry-metrics/src/export/Batcher.ts b/packages/opentelemetry-metrics/src/export/Batcher.ts new file mode 100644 index 00000000000..d8ba2e49619 --- /dev/null +++ b/packages/opentelemetry-metrics/src/export/Batcher.ts @@ -0,0 +1,67 @@ +/*! + * 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 { + CounterSumAggregator, + GaugeAggregator, + MeasureExactAggregator, +} from './Aggregator'; +import { MetricRecord, MetricKind, Aggregator } from './types'; + +/** + * Base class for all batcher types. + * + * The batcher is responsible for storing the aggregators and aggregated + * values received from updates from metrics in the meter. The stored values + * will be sent to an exporter for exporting. + */ +export abstract class Batcher { + protected readonly _batchMap = new Map(); + + /** Returns an aggregator based off metric kind. */ + abstract aggregatorFor(metricKind: MetricKind): Aggregator; + + /** Stores record information to be ready for exporting. */ + abstract process(record: MetricRecord): void; + + checkPointSet(): MetricRecord[] { + return Array.from(this._batchMap.values()); + } +} + +/** + * Batcher which retains all dimensions/labels. It accepts all records and + * passes them for exporting. + */ +export class UngroupedBatcher extends Batcher { + aggregatorFor(metricKind: MetricKind): Aggregator { + switch (metricKind) { + case MetricKind.COUNTER: + return new CounterSumAggregator(); + case MetricKind.GAUGE: + return new GaugeAggregator(); + default: + return new MeasureExactAggregator(); + } + } + + process(record: MetricRecord): void { + this._batchMap.set( + record.descriptor.name + record.labels.identifier, + record + ); + } +} diff --git a/packages/opentelemetry-metrics/src/export/ConsoleMetricExporter.ts b/packages/opentelemetry-metrics/src/export/ConsoleMetricExporter.ts index 5702d7258b7..7524d4a96c5 100644 --- a/packages/opentelemetry-metrics/src/export/ConsoleMetricExporter.ts +++ b/packages/opentelemetry-metrics/src/export/ConsoleMetricExporter.ts @@ -14,7 +14,14 @@ * limitations under the License. */ -import { MetricExporter, ReadableMetric } from './types'; +import { + MetricExporter, + MetricRecord, + MetricKind, + Sum, + LastValue, + Distribution, +} from './types'; import { ExportResult } from '@opentelemetry/base'; /** @@ -23,30 +30,36 @@ import { ExportResult } from '@opentelemetry/base'; */ export class ConsoleMetricExporter implements MetricExporter { export( - metrics: ReadableMetric[], + metrics: MetricRecord[], resultCallback: (result: ExportResult) => void ): void { for (const metric of metrics) { - const descriptor = metric.descriptor; - const timeseries = metric.timeseries; - console.log({ - name: descriptor.name, - description: descriptor.description, - }); - - for (const ts of timeseries) { - const labels = descriptor.labelKeys - .map((k, i) => [k, ts.labelValues[i]]) - .reduce( - (p, c) => ({ - ...p, - [c[0] as string]: typeof c[1] === 'string' ? c[1] : c[1].value, - }), - {} + console.log(metric.descriptor); + console.log(metric.labels.labels); + switch (metric.descriptor.metricKind) { + case MetricKind.COUNTER: + const sum = metric.aggregator.value() as Sum; + console.log('value: ' + sum); + break; + case MetricKind.GAUGE: + const lastValue = metric.aggregator.value() as LastValue; + console.log( + 'value: ' + lastValue.value + ', timestamp: ' + lastValue.timestamp + ); + break; + default: + const distribution = metric.aggregator.value() as Distribution; + console.log( + 'min: ' + + distribution.min + + ', max: ' + + distribution.max + + ', count: ' + + distribution.count + + ', sum: ' + + distribution.sum ); - for (const point of ts.points) { - console.log({ labels, value: point.value }); - } + break; } } return resultCallback(ExportResult.SUCCESS); diff --git a/packages/opentelemetry-metrics/src/export/Controller.ts b/packages/opentelemetry-metrics/src/export/Controller.ts new file mode 100644 index 00000000000..ccef3213eb6 --- /dev/null +++ b/packages/opentelemetry-metrics/src/export/Controller.ts @@ -0,0 +1,50 @@ +/*! + * 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 { unrefTimer } from '@opentelemetry/core'; +import { Meter } from '../Meter'; +import { MetricExporter } from './types'; +import { ExportResult } from '@opentelemetry/base'; + +const DEFAULT_EXPORT_INTERVAL = 60_000; + +export class Controller {} + +/** Controller organizes a periodic push of metric data. */ +export class PushController extends Controller { + private _timer: NodeJS.Timeout; + + constructor( + private readonly _meter: Meter, + private readonly _exporter: MetricExporter, + interval: number = DEFAULT_EXPORT_INTERVAL + ) { + super(); + this._timer = setInterval(() => { + this._collect(); + }, interval); + unrefTimer(this._timer); + } + + private _collect() { + this._meter.collect(); + this._exporter.export(this._meter.getBatcher().checkPointSet(), result => { + if (result !== ExportResult.SUCCESS) { + // @todo: log error + } + }); + } +} diff --git a/packages/opentelemetry-metrics/src/export/types.ts b/packages/opentelemetry-metrics/src/export/types.ts index 559b9658d85..551b171cf76 100644 --- a/packages/opentelemetry-metrics/src/export/types.ts +++ b/packages/opentelemetry-metrics/src/export/types.ts @@ -14,331 +14,71 @@ * limitations under the License. */ -/** - * This is based on - * opencensus-node/packages/opencensus-core/src/metrics/export/types.ts - * - * Proto definition: - * opentelemetry-proto/opentelemetry/proto/metrics/v1/metrics.proto - */ +import { HrTime, ValueType } from '@opentelemetry/api'; +import { ExportResult } from '@opentelemetry/base'; +import { LabelSet } from '../LabelSet'; + +/** The kind of metric. */ +export enum MetricKind { + COUNTER, + GAUGE, + MEASURE, +} -import { HrTime } from '@opentelemetry/api'; -import { Resource, ExportResult } from '@opentelemetry/base'; +/** Sum returns an aggregated sum. */ +export type Sum = number; -export interface ReadableMetric { - /** - * The descriptor of the Metric. This is an optimization for network wire - * size, from data-model perspective a Metric contains always - * a MetricDescriptor. - * In case of a streaming RPC can be sent only - * the first time a metric is reported to save network traffic. - */ - readonly descriptor: MetricDescriptor; +/** LastValue returns the latest value that was aggregated. */ +export type LastValue = { + value: number; + timestamp: HrTime; +}; - /** - * One or more timeseries for a single metric, where each timeseries has - * one or more points. - */ - readonly timeseries: TimeSeries[]; +export interface Distribution { + min: number; + max: number; + count: number; + sum: number; +} - // The resource for the metric. If unset, it may be set to a default value - // provided for a sequence of messages in an RPC stream. - resource?: Resource; +export interface MetricRecord { + readonly descriptor: MetricDescriptor; + readonly labels: LabelSet; + readonly aggregator: Aggregator; } -/** Properties of a Metric type and its schema */ export interface MetricDescriptor { - /** The metric type, including its DNS name prefix. It must be unique. */ readonly name: string; - /** - * A detailed description of the metric, which can be used in documentation. - */ readonly description: string; - /** - * The unit in which the metric value is reported. Follows the format - * described by http://unitsofmeasure.org/ucum.html. - */ readonly unit: string; - /** MetricDescriptor type */ - readonly type: MetricDescriptorType; - /** - * Metric may only increase - * - * This property is not in the .proto file, but is included here because - * it is required for correct export of prometheus metrics - */ - readonly monotonic: boolean; - /** The label keys associated with the metric descriptor. */ + readonly metricKind: MetricKind; + readonly valueType: ValueType; readonly labelKeys: string[]; -} - -/** - * The kind of metric. It describes how the data is reported. - * - * A gauge is an instantaneous measurement of a value. - * - * A cumulative measurement is a value accumulated over a time interval. In - * a time series, cumulative measurements should have the same start time, - * increasing values and increasing end times, until an event resets the - * cumulative value to zero and sets a new start time for the following - * points. - */ -export enum MetricDescriptorType { - /** Do not use this default value. */ - UNSPECIFIED, - /** Integer gauge. The value can go both up and down. */ - GAUGE_INT64, - /** Floating point gauge. The value can go both up and down. */ - GAUGE_DOUBLE, - /** - * Distribution gauge measurement aka histogram. - * The count and sum can go both up and - * down. Recorded values are always >= 0. - * Used in scenarios like a snapshot of time the current items in a queue - * have spent there. - */ - GAUGE_HISTOGRAM, - /** - * Integer counter measurement. The value cannot decrease, if resets - * then the start_time should also be reset. - */ - COUNTER_INT64, - /** - * Floating point counter measurement. The value cannot decrease, if - * resets then the start_time should also be reset. Recorded values are - * always >= 0. - */ - COUNTER_DOUBLE, - /** - * Histogram cumulative measurement. The count and sum cannot decrease, - * if reset then the start_time should also be reset. - */ - CUMULATIVE_HISTOGRAM, - /** - * Some frameworks implemented Histograms as a summary of observations - * (usually things like request durations and response sizes). While it - * also provides a total count of observations and a sum of all observed - * values, it calculates configurable percentiles over a sliding time - * window. This is not recommended, since it cannot be aggregated. - */ - SUMMARY, - /** - * Integer measure. The value can be positive or negative. - */ - MEASURE_INT64, - /** - * Floating point measure. The value can be positive or negative. - */ - MEASURE_DOUBLE, -} - -/** - * A collection of data points that describes the time-varying values - * of a metric. - */ -export interface TimeSeries { - /** - * The set of label values that uniquely identify this timeseries. Applies to - * all points. The order of label values must match that of LabelSet keys in the - * metric descriptor. - */ - readonly labelValues: LabelValue[]; - - /** - * The data points of this timeseries. Point.value type MUST match the - * MetricDescriptor.type. - */ - readonly points: Point[]; -} - -/** The LabelValue type. null value indicates an unset. */ -export interface LabelValue { - /** The value for the label. */ - readonly value: string | null; -} - -/** A timestamped measurement. */ -export interface Point { - /** - * Must be present for counter/cumulative metrics. The time when the - * cumulative value was reset to zero. The cumulative value is over the time - * interval (start_timestamp, timestamp]. If not specified, the backend can - * use the previous recorded value. - */ - readonly startTimestamp?: HrTime; - - /** - * The moment when this point was recorded. Inclusive. - * If not specified, the timestamp will be decided by the backend. - */ - readonly timestamp: HrTime; - - /** - * The actual point value. - * 64-bit integer or 64-bit double-precision floating-point number - * or distribution value - * or summary value. This is not recommended, since it cannot be aggregated. - */ - readonly value: number | HistogramValue | SummaryValue; -} - -/** - * Histograms contains summary statistics for a population of values. It - * optionally contains a histogram representing the distribution of those - * values across a set of buckets. - */ -export interface HistogramValue { - /** - * The number of values in the population. Must be non-negative. This value - * must equal the sum of the values in bucket_counts if a histogram is - * provided. - */ - readonly count: number; - - /** - * The sum of values in this population. Optional since some systems don't - * expose this. If count is zero then this field must be zero or not set - * (if not supported). - */ - readonly sum?: number; - - /** - * Don't change bucket boundaries within a TimeSeries if your backend doesn't - * support this. To save network bandwidth this field can be sent only the - * first time a metric is sent when using a streaming RPC. - */ - readonly bucketOptions?: BucketOptions; - /** DistributionValue buckets */ - readonly buckets: Bucket[]; -} - -/** - * The start_timestamp only applies to the count and sum in the SummaryValue. - */ -export interface SummaryValue { - /** - * The total number of recorded values since start_time. Optional since - * some systems don't expose this. - */ - readonly count: number; - - /** - * The total sum of recorded values since start_time. Optional since some - * systems don't expose this. If count is zero then this field must be zero. - * This field must be unset if the sum is not available. - */ - readonly sum?: number; - - /** Values calculated over an arbitrary time window. */ - // TODO: Change it to required when Exemplar functionality will be added. - readonly snapshot?: Snapshot; -} - -/** - * Properties of a BucketOptions. - * A Distribution may optionally contain a histogram of the values in the - * population. The bucket boundaries for that histogram are described by - * BucketOptions. - * - * If bucket_options has no type, then there is no histogram associated with - * the Distribution. - */ -export interface BucketOptions { - /** Bucket with explicit bounds. */ - readonly explicit: Explicit; -} - -/** - * Properties of an Explicit. - * Specifies a set of buckets with arbitrary upper-bounds. - * This defines size(bounds) + 1 (= N) buckets. The boundaries for bucket - * index i are: - * - * [0, bucket_bounds[i]) for i == 0 - * [bucket_bounds[i-1], bucket_bounds[i]) for 0 < i < N-1 - * [bucket_bounds[i-1], +infinity) for i == N-1 - */ -export interface Explicit { - /** The values must be strictly increasing and > 0. */ - readonly bounds: number[]; - // TODO: If OpenMetrics decides to support (a, b] intervals we should add - // support for these by defining a boolean value here which decides what - // type of intervals to use. -} - -/** Properties of a Bucket. */ -export interface Bucket { - /** - * The number of values in each bucket of the histogram, as described in - * bucket_bounds. - */ - readonly count: number; - /** - * If the distribution does not have a histogram, then omit this field. - */ - readonly exemplar?: Exemplar; -} - -/** - * Exemplars are example points that may be used to annotate aggregated - * Distribution values. They are metadata that gives information about a - * particular value added to a Distribution bucket. - */ -export interface Exemplar { - /** - * Value of the exemplar point. It determines which bucket the exemplar - * belongs to. - */ - readonly value: number; - /** The observation (sampling) time of the above value. */ - readonly timestamp: HrTime; - /** Contextual information about the example value. */ - readonly attachments: { [key: string]: string }; -} - -/** - * The values in this message can be reset at arbitrary unknown times, with - * the requirement that all of them are reset at the same time. - */ -export interface Snapshot { - /** - * The number of values in the snapshot. Optional since some systems don't - * expose this. - */ - readonly count: number; - /** - * The sum of values in the snapshot. Optional since some systems don't - * expose this. If count is zero then this field must be zero or not set - * (if not supported). - */ - readonly sum?: number; - /** - * A list of values at different percentiles of the distribution calculated - * from the current snapshot. The percentiles must be strictly increasing. - */ - readonly percentileValues: ValueAtPercentile[]; -} - -/** - * Represents the value at a given percentile of a distribution. - */ -export interface ValueAtPercentile { - /** The percentile of a distribution. Must be in the interval (0.0, 100.0]. */ - readonly percentile: number; - /** The value at the given percentile of a distribution. */ - readonly value: number; + readonly monotonic: boolean; } /** * Base interface that represents a metric exporter */ export interface MetricExporter { - /** Exports the list of a given {@link ReadableMetric} */ + /** Exports the list of a given {@link MetricRecord} */ export( - metrics: ReadableMetric[], + metrics: MetricRecord[], resultCallback: (result: ExportResult) => void ): void; /** Stops the exporter. */ shutdown(): void; } + +/** + * Base interface for aggregators. Aggregators are responsible for holding + * aggregated values and taking a snapshot of these values upon export. + */ +export interface Aggregator { + /** Updates the current with the new value. */ + update(value: number): void; + + /** Returns snapshot of the current value. */ + value(): Sum | LastValue | Distribution; +} diff --git a/packages/opentelemetry-metrics/src/types.ts b/packages/opentelemetry-metrics/src/types.ts index cb53e67dad1..31a2a8e4863 100644 --- a/packages/opentelemetry-metrics/src/types.ts +++ b/packages/opentelemetry-metrics/src/types.ts @@ -16,6 +16,7 @@ import { LogLevel } from '@opentelemetry/core'; import { Logger, ValueType } from '@opentelemetry/api'; +import { MetricExporter } from './export/types'; /** Options needed for SDK metric creation. */ export interface MetricOptions { @@ -55,8 +56,14 @@ export interface MeterConfig { /** User provided logger. */ logger?: Logger; - /** level of logger. */ + /** level of logger. */ logLevel?: LogLevel; + + /** Metric exporter. */ + exporter?: MetricExporter; + + /** Metric collect interval */ + interval?: number; } /** Default Meter configuration. */ diff --git a/packages/opentelemetry-metrics/test/Meter.test.ts b/packages/opentelemetry-metrics/test/Meter.test.ts index a155cc7ed95..2b42e43aa34 100644 --- a/packages/opentelemetry-metrics/test/Meter.test.ts +++ b/packages/opentelemetry-metrics/test/Meter.test.ts @@ -20,14 +20,21 @@ import { Metric, CounterMetric, GaugeMetric, - MetricDescriptorType, + LastValue, + MetricKind, + Sum, + MeterProvider, MeasureMetric, + Distribution, } from '../src'; import * as types from '@opentelemetry/api'; import { LabelSet } from '../src/LabelSet'; import { NoopLogger, hrTime, hrTimeToMilliseconds } from '@opentelemetry/core'; -import { NoopExporter } from './mocks/Exporter'; -import { MeterProvider } from '../src/MeterProvider'; +import { + CounterSumAggregator, + GaugeAggregator, +} from '../src/export/Aggregator'; +import { ValueType } from '@opentelemetry/api'; const performanceTimeOrigin = hrTime(); @@ -37,7 +44,6 @@ describe('Meter', () => { const keyb = 'keyb'; let labels: types.Labels = { [keyb]: 'value2', [keya]: 'value1' }; let labelSet: types.LabelSet; - const hrTime: types.HrTime = [22, 400000000]; beforeEach(() => { meter = new MeterProvider({ @@ -76,9 +82,12 @@ describe('Meter', () => { it('should be able to call add() directly on counter', () => { const counter = meter.createCounter('name') as CounterMetric; counter.add(10, labelSet); - assert.strictEqual(counter.bind(labelSet)['_data'], 10); + meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + + assert.strictEqual(record1.aggregator.value(), 10); counter.add(10, labelSet); - assert.strictEqual(counter.bind(labelSet)['_data'], 20); + assert.strictEqual(record1.aggregator.value(), 20); }); describe('.bind()', () => { @@ -86,28 +95,33 @@ describe('Meter', () => { const counter = meter.createCounter('name') as CounterMetric; const boundCounter = counter.bind(labelSet); boundCounter.add(10); - assert.strictEqual(boundCounter['_data'], 10); + meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + + assert.strictEqual(record1.aggregator.value(), 10); boundCounter.add(10); - assert.strictEqual(boundCounter['_data'], 20); + assert.strictEqual(record1.aggregator.value(), 20); }); - it('should return the timeseries', () => { + it('should return the aggregator', () => { const counter = meter.createCounter('name') as CounterMetric; const boundCounter = counter.bind(labelSet); boundCounter.add(20); - assert.deepStrictEqual(boundCounter.getTimeSeries(hrTime), { - labelValues: [{ value: 'value1' }, { value: 'value2' }], - points: [{ value: 20, timestamp: hrTime }], - }); + assert.ok(boundCounter.getAggregator() instanceof CounterSumAggregator); + assert.strictEqual(boundCounter.getLabelSet(), labelSet); }); it('should add positive values by default', () => { const counter = meter.createCounter('name') as CounterMetric; const boundCounter = counter.bind(labelSet); boundCounter.add(10); - assert.strictEqual(boundCounter['_data'], 10); + assert.strictEqual(meter.getBatcher().checkPointSet().length, 0); + meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + + assert.strictEqual(record1.aggregator.value(), 10); boundCounter.add(-100); - assert.strictEqual(boundCounter['_data'], 10); + assert.strictEqual(record1.aggregator.value(), 10); }); it('should not add the instrument data when disabled', () => { @@ -116,7 +130,9 @@ describe('Meter', () => { }) as CounterMetric; const boundCounter = counter.bind(labelSet); boundCounter.add(10); - assert.strictEqual(boundCounter['_data'], 0); + meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + assert.strictEqual(record1.aggregator.value(), 0); }); it('should add negative value when monotonic is set to false', () => { @@ -125,7 +141,9 @@ describe('Meter', () => { }) as CounterMetric; const boundCounter = counter.bind(labelSet); boundCounter.add(-10); - assert.strictEqual(boundCounter['_data'], -10); + meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + assert.strictEqual(record1.aggregator.value(), -10); }); it('should return same instrument on same label values', () => { @@ -134,7 +152,10 @@ describe('Meter', () => { boundCounter.add(10); const boundCounter1 = counter.bind(labelSet); boundCounter1.add(10); - assert.strictEqual(boundCounter['_data'], 20); + meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + + assert.strictEqual(record1.aggregator.value(), 20); assert.strictEqual(boundCounter, boundCounter1); }); }); @@ -176,16 +197,20 @@ describe('Meter', () => { }) as CounterMetric; counter2.bind(labelSet).add(500); - assert.strictEqual(meter.getMetrics().length, 1); - const [{ descriptor, timeseries }] = meter.getMetrics(); - assert.deepStrictEqual(descriptor.name, 'name1'); - assert.deepStrictEqual( - descriptor.type, - MetricDescriptorType.COUNTER_DOUBLE - ); - assert.strictEqual(timeseries.length, 1); - assert.strictEqual(timeseries[0].points.length, 1); - assert.strictEqual(timeseries[0].points[0].value, 10); + meter.collect(); + const record = meter.getBatcher().checkPointSet(); + + assert.strictEqual(record.length, 1); + assert.deepStrictEqual(record[0].descriptor, { + description: '', + labelKeys: [], + metricKind: MetricKind.COUNTER, + monotonic: true, + name: 'name1', + unit: '1', + valueType: ValueType.DOUBLE, + }); + assert.strictEqual(record[0].aggregator.value(), 10); }); }); @@ -237,9 +262,17 @@ describe('Meter', () => { it('should be able to call set() directly on gauge', () => { const gauge = meter.createGauge('name') as GaugeMetric; gauge.set(10, labelSet); - assert.strictEqual(gauge.bind(labelSet)['_data'], 10); + meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + + assert.strictEqual((record1.aggregator.value() as LastValue).value, 10); + assert.ok( + hrTimeToMilliseconds( + (record1.aggregator.value() as LastValue).timestamp + ) > hrTimeToMilliseconds(performanceTimeOrigin) + ); gauge.set(250, labelSet); - assert.strictEqual(gauge.bind(labelSet)['_data'], 250); + assert.strictEqual((record1.aggregator.value() as LastValue).value, 250); }); describe('.bind()', () => { @@ -247,12 +280,23 @@ describe('Meter', () => { const gauge = meter.createGauge('name') as GaugeMetric; const boundGauge = gauge.bind(labelSet); boundGauge.set(10); - assert.strictEqual(boundGauge['_data'], 10); + meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + + assert.strictEqual((record1.aggregator.value() as LastValue).value, 10); + assert.ok( + hrTimeToMilliseconds( + (record1.aggregator.value() as LastValue).timestamp + ) > hrTimeToMilliseconds(performanceTimeOrigin) + ); boundGauge.set(250); - assert.strictEqual(boundGauge['_data'], 250); + assert.strictEqual( + (record1.aggregator.value() as LastValue).value, + 250 + ); }); - it('should return the timeseries', () => { + it('should return the aggregator', () => { const gauge = meter.createGauge('name') as GaugeMetric; const k1 = 'k1'; const k2 = 'k2'; @@ -260,19 +304,24 @@ describe('Meter', () => { const LabelSet2 = new LabelSet('|#k1:v1,k2:v2', labels); const boundGauge = gauge.bind(LabelSet2); boundGauge.set(150); - assert.deepStrictEqual(boundGauge.getTimeSeries(hrTime), { - labelValues: [{ value: 'v1' }, { value: 'v2' }], - points: [{ value: 150, timestamp: hrTime }], - }); + assert.ok(boundGauge.getAggregator() instanceof GaugeAggregator); + assert.strictEqual(boundGauge.getLabelSet(), LabelSet2); }); it('should go up and down by default', () => { const gauge = meter.createGauge('name') as GaugeMetric; const boundGauge = gauge.bind(labelSet); boundGauge.set(10); - assert.strictEqual(boundGauge['_data'], 10); + assert.strictEqual(meter.getBatcher().checkPointSet().length, 0); + meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + + assert.strictEqual((record1.aggregator.value() as LastValue).value, 10); boundGauge.set(-100); - assert.strictEqual(boundGauge['_data'], -100); + assert.strictEqual( + (record1.aggregator.value() as LastValue).value, + -100 + ); }); it('should not set the instrument data when disabled', () => { @@ -281,7 +330,9 @@ describe('Meter', () => { }) as GaugeMetric; const boundGauge = gauge.bind(labelSet); boundGauge.set(10); - assert.strictEqual(boundGauge['_data'], 0); + meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + assert.strictEqual((record1.aggregator.value() as LastValue).value, 0); }); it('should not set negative value when monotonic is set to true', () => { @@ -290,7 +341,9 @@ describe('Meter', () => { }) as GaugeMetric; const boundGauge = gauge.bind(labelSet); boundGauge.set(-10); - assert.strictEqual(boundGauge['_data'], 0); + meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + assert.strictEqual((record1.aggregator.value() as LastValue).value, 0); }); it('should return same instrument on same label values', () => { @@ -299,7 +352,10 @@ describe('Meter', () => { boundGauge.set(10); const boundGauge1 = gauge.bind(labelSet); boundGauge1.set(10); - assert.strictEqual(boundGauge['_data'], 10); + meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + + assert.strictEqual((record1.aggregator.value() as LastValue).value, 10); assert.strictEqual(boundGauge, boundGauge1); }); }); @@ -361,7 +417,7 @@ describe('Meter', () => { describe('#measure', () => { it('should create a measure', () => { - const measure = meter.createMeasure('name') as MeasureMetric; + const measure = meter.createMeasure('name'); assert.ok(measure instanceof Metric); }); @@ -433,7 +489,15 @@ describe('Meter', () => { }) as MeasureMetric; const boundMeasure = measure.bind(labelSet); boundMeasure.record(10); - assert.strictEqual(boundMeasure['_data'], 0); + + meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + assert.deepStrictEqual(record1.aggregator.value() as Distribution, { + count: 0, + max: -Infinity, + min: Infinity, + sum: 0, + }); }); it('should accept negative (and positive) values when monotonic is set to false', () => { @@ -446,8 +510,14 @@ describe('Meter', () => { boundMeasure1.record(10); const boundMeasure2 = measure.bind(labelSet); boundMeasure2.record(100); - // @todo: re-add once record is implemented - // assert.strictEqual(boundMeasure1['_data'], 100); + meter.collect(); + const [record1] = meter.getBatcher().checkPointSet(); + assert.deepStrictEqual(record1.aggregator.value() as Distribution, { + count: 2, + max: 100, + min: 10, + sum: 110, + }); assert.strictEqual(boundMeasure1, boundMeasure2); }); }); @@ -490,25 +560,22 @@ describe('Meter', () => { const boundCounter = counter.bind(labelSet); boundCounter.add(10.45); - assert.strictEqual(meter.getMetrics().length, 1); - const [{ descriptor, timeseries }] = meter.getMetrics(); - assert.deepStrictEqual(descriptor, { + meter.collect(); + const record = meter.getBatcher().checkPointSet(); + + assert.strictEqual(record.length, 1); + assert.deepStrictEqual(record[0].descriptor, { name: 'counter', - monotonic: true, description: 'test', + metricKind: MetricKind.COUNTER, + monotonic: true, unit: '1', - type: MetricDescriptorType.COUNTER_DOUBLE, + valueType: ValueType.DOUBLE, labelKeys: ['key'], }); - assert.strictEqual(timeseries.length, 1); - const [{ labelValues, points }] = timeseries; - assert.deepStrictEqual(labelValues, [{ value: 'counter-value' }]); - assert.strictEqual(points.length, 1); - assert.strictEqual(points[0].value, 10.45); - assert.ok( - hrTimeToMilliseconds(points[0].timestamp) > - hrTimeToMilliseconds(performanceTimeOrigin) - ); + assert.strictEqual(record[0].labels, labelSet); + const value = record[0].aggregator.value() as Sum; + assert.strictEqual(value, 10.45); }); it('should create a INT counter', () => { @@ -522,25 +589,22 @@ describe('Meter', () => { const boundCounter = counter.bind(labelSet); boundCounter.add(10.45); - assert.strictEqual(meter.getMetrics().length, 1); - const [{ descriptor, timeseries }] = meter.getMetrics(); - assert.deepStrictEqual(descriptor, { + meter.collect(); + const record = meter.getBatcher().checkPointSet(); + + assert.strictEqual(record.length, 1); + assert.deepStrictEqual(record[0].descriptor, { name: 'counter', description: 'test', + metricKind: MetricKind.COUNTER, monotonic: true, unit: '1', - type: MetricDescriptorType.COUNTER_INT64, + valueType: ValueType.INT, labelKeys: ['key'], }); - assert.strictEqual(timeseries.length, 1); - const [{ labelValues, points }] = timeseries; - assert.deepStrictEqual(labelValues, [{ value: 'counter-value' }]); - assert.strictEqual(points.length, 1); - assert.strictEqual(points[0].value, 10); - assert.ok( - hrTimeToMilliseconds(points[0].timestamp) > - hrTimeToMilliseconds(performanceTimeOrigin) - ); + assert.strictEqual(record[0].labels, labelSet); + const value = record[0].aggregator.value() as Sum; + assert.strictEqual(value, 10); }); it('should create a DOUBLE gauge', () => { @@ -554,33 +618,41 @@ describe('Meter', () => { gauge.bind(labelSet1).set(200.34); gauge.bind(labelSet2).set(-10.67); - assert.strictEqual(meter.getMetrics().length, 1); - const [{ descriptor, timeseries }] = meter.getMetrics(); - assert.deepStrictEqual(descriptor, { - name: 'gauge', - monotonic: false, + meter.collect(); + const record = meter.getBatcher().checkPointSet(); + + assert.strictEqual(record.length, 2); + assert.deepStrictEqual(record[0].descriptor, { description: '', - unit: 'ms', - type: MetricDescriptorType.GAUGE_DOUBLE, labelKeys: ['gauge-key'], + metricKind: MetricKind.GAUGE, + monotonic: false, + name: 'gauge', + unit: 'ms', + valueType: ValueType.DOUBLE, }); - assert.strictEqual(timeseries.length, 2); - const [ - { labelValues: labelValues1, points: points1 }, - { labelValues: labelValues2, points: points2 }, - ] = timeseries; - assert.deepStrictEqual(labelValues1, [{ value: 'gauge-value1' }]); - assert.strictEqual(points1.length, 1); - assert.strictEqual(points1[0].value, 200.34); + assert.strictEqual(record[0].labels, labelSet1); + let lastValue = record[0].aggregator.value() as LastValue; + assert.strictEqual(lastValue.value, 200.34); assert.ok( - hrTimeToMilliseconds(points1[0].timestamp) > + hrTimeToMilliseconds(lastValue.timestamp) > hrTimeToMilliseconds(performanceTimeOrigin) ); - assert.deepStrictEqual(labelValues2, [{ value: 'gauge-value2' }]); - assert.strictEqual(points2.length, 1); - assert.strictEqual(points2[0].value, -10.67); + + assert.deepStrictEqual(record[1].descriptor, { + description: '', + labelKeys: ['gauge-key'], + metricKind: MetricKind.GAUGE, + monotonic: false, + name: 'gauge', + unit: 'ms', + valueType: ValueType.DOUBLE, + }); + assert.strictEqual(record[1].labels, labelSet2); + lastValue = record[1].aggregator.value() as LastValue; + assert.strictEqual(lastValue.value, -10.67); assert.ok( - hrTimeToMilliseconds(points2[0].timestamp) > + hrTimeToMilliseconds(lastValue.timestamp) > hrTimeToMilliseconds(performanceTimeOrigin) ); }); @@ -597,87 +669,43 @@ describe('Meter', () => { gauge.bind(labelSet1).set(200.34); gauge.bind(labelSet2).set(-10.67); - assert.strictEqual(meter.getMetrics().length, 1); - const [{ descriptor, timeseries }] = meter.getMetrics(); - assert.deepStrictEqual(descriptor, { - name: 'gauge', - monotonic: false, + meter.collect(); + const record = meter.getBatcher().checkPointSet(); + + assert.strictEqual(record.length, 2); + assert.deepStrictEqual(record[0].descriptor, { description: '', - unit: 'ms', - type: MetricDescriptorType.GAUGE_INT64, labelKeys: ['gauge-key'], + metricKind: MetricKind.GAUGE, + monotonic: false, + name: 'gauge', + unit: 'ms', + valueType: ValueType.INT, }); - assert.strictEqual(timeseries.length, 2); - const [ - { labelValues: labelValues1, points: points1 }, - { labelValues: labelValues2, points: points2 }, - ] = timeseries; - assert.deepStrictEqual(labelValues1, [{ value: 'gauge-value1' }]); - assert.strictEqual(points1.length, 1); - assert.strictEqual(points1[0].value, 200); + assert.strictEqual(record[0].labels, labelSet1); + let lastValue = record[0].aggregator.value() as LastValue; + assert.strictEqual(lastValue.value, 200); assert.ok( - hrTimeToMilliseconds(points1[0].timestamp) > + hrTimeToMilliseconds(lastValue.timestamp) > hrTimeToMilliseconds(performanceTimeOrigin) ); - assert.deepStrictEqual(labelValues2, [{ value: 'gauge-value2' }]); - assert.strictEqual(points2.length, 1); - assert.strictEqual(points2[0].value, -10); + + assert.deepStrictEqual(record[1].descriptor, { + description: '', + labelKeys: ['gauge-key'], + metricKind: MetricKind.GAUGE, + monotonic: false, + name: 'gauge', + unit: 'ms', + valueType: ValueType.INT, + }); + assert.strictEqual(record[1].labels, labelSet2); + lastValue = record[1].aggregator.value() as LastValue; + assert.strictEqual(lastValue.value, -10); assert.ok( - hrTimeToMilliseconds(points2[0].timestamp) > + hrTimeToMilliseconds(lastValue.timestamp) > hrTimeToMilliseconds(performanceTimeOrigin) ); }); }); - - describe('Exporters', () => { - it('should register an exporter', () => { - const exporter = new NoopExporter(); - meter.addExporter(exporter); - assert.equal(meter['_exporters'].length, 1); - }); - - it('should export a gauge when it is updated', done => { - const exporter = new NoopExporter(); - exporter.on('export', metrics => { - assert.equal(metrics[0].descriptor.name, 'name'); - assert.equal(metrics[0].timeseries[0].points[0].value, 20); - assert.deepEqual(metrics[0].timeseries[0].labelValues, [ - { - value: 'value1', - }, - { - value: 'value2', - }, - ]); - done(); - }); - - meter.addExporter(exporter); - const gauge = meter.createGauge('name'); - const labelSet = meter.labels({ value: 'value1', value2: 'value2' }); - gauge.bind(labelSet).set(20); - }); - - it('should export a counter when it is updated', done => { - const counter = meter.createCounter('name'); - const exporter = new NoopExporter(); - exporter.on('export', metrics => { - assert.equal(metrics[0].descriptor.name, 'name'); - assert.equal(metrics[0].timeseries[0].points[0].value, 20); - assert.deepEqual(metrics[0].timeseries[0].labelValues, [ - { - value: 'value1', - }, - { - value: 'value2', - }, - ]); - done(); - }); - - meter.addExporter(exporter); - const labelSet = meter.labels({ value: 'value1', value2: 'value2' }); - counter.bind(labelSet).add(20); - }); - }); }); diff --git a/packages/opentelemetry-metrics/test/MeterProvider.test.ts b/packages/opentelemetry-metrics/test/MeterProvider.test.ts index 552b3d4feea..82877759b36 100644 --- a/packages/opentelemetry-metrics/test/MeterProvider.test.ts +++ b/packages/opentelemetry-metrics/test/MeterProvider.test.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { NoopLogger } from '@opentelemetry/core'; import * as assert from 'assert'; import { MeterProvider, Meter } from '../src'; +import { NoopLogger } from '@opentelemetry/core'; describe('MeterProvider', () => { describe('constructor', () => { diff --git a/packages/opentelemetry-metrics/test/export/ConsoleMetricExporter.test.ts b/packages/opentelemetry-metrics/test/export/ConsoleMetricExporter.test.ts index 1652baad07d..8a8c5139af3 100644 --- a/packages/opentelemetry-metrics/test/export/ConsoleMetricExporter.test.ts +++ b/packages/opentelemetry-metrics/test/export/ConsoleMetricExporter.test.ts @@ -16,8 +16,8 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { ConsoleMetricExporter } from '../../src'; -import { MeterProvider } from '../../src/MeterProvider'; +import { ConsoleMetricExporter, MeterProvider, MetricKind } from '../../src'; +import { ValueType } from '@opentelemetry/api'; describe('ConsoleMetricExporter', () => { let consoleExporter: ConsoleMetricExporter; @@ -40,7 +40,6 @@ describe('ConsoleMetricExporter', () => { const meter = new MeterProvider().getMeter( 'test-console-metric-exporter' ); - meter.addExporter(consoleExporter); const gauge = meter.createGauge('gauge', { description: 'a test description', labelKeys: ['key1', 'key2'], @@ -49,16 +48,29 @@ describe('ConsoleMetricExporter', () => { meter.labels({ key1: 'labelValue1', key2: 'labelValue2' }) ); boundGauge.set(10); - const [descriptor, timeseries] = spyConsole.args; + + meter.collect(); + consoleExporter.export(meter.getBatcher().checkPointSet(), () => {}); + assert.strictEqual(spyConsole.args.length, 3); + const [descriptor, labels, value] = spyConsole.args; assert.deepStrictEqual(descriptor, [ - { description: 'a test description', name: 'gauge' }, + { + description: 'a test description', + labelKeys: ['key1', 'key2'], + metricKind: MetricKind.GAUGE, + monotonic: false, + name: 'gauge', + unit: '1', + valueType: ValueType.DOUBLE, + }, ]); - assert.deepStrictEqual(timeseries, [ + assert.deepStrictEqual(labels, [ { - labels: { key1: 'labelValue1', key2: 'labelValue2' }, - value: 10, + key1: 'labelValue1', + key2: 'labelValue2', }, ]); + assert.ok(value[0].includes('value: 10, timestamp:')); }); }); }); diff --git a/packages/opentelemetry-metrics/test/mocks/Exporter.ts b/packages/opentelemetry-metrics/test/mocks/Exporter.ts index 9ff760cdaff..7ce7f656895 100644 --- a/packages/opentelemetry-metrics/test/mocks/Exporter.ts +++ b/packages/opentelemetry-metrics/test/mocks/Exporter.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { MetricExporter, ReadableMetric } from '../../src/export/types'; +import { MetricExporter, MetricRecord } from '../../src/export/types'; import { ExportResult } from '@opentelemetry/base'; import { EventEmitter } from 'events'; export class NoopExporter extends EventEmitter implements MetricExporter { export( - metrics: ReadableMetric[], + metrics: MetricRecord[], resultCallback: (result: ExportResult) => void ): void { this.emit('export', metrics, resultCallback);