From 525cfa49b2e5b56c7c969a61743c839b9a6390eb Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Mon, 6 May 2019 11:55:13 -0700 Subject: [PATCH] Add support for derived cumulative metrics --- packages/opencensus-core/src/index.ts | 3 + .../metrics/cumulative/derived-cumulative.ts | 170 +++++++++++ .../src/metrics/gauges/derived-gauge.ts | 95 +----- packages/opencensus-core/src/metrics/types.ts | 33 +++ packages/opencensus-core/src/metrics/utils.ts | 34 +++ .../test/test-derived-cumulative.ts | 280 ++++++++++++++++++ 6 files changed, 529 insertions(+), 86 deletions(-) create mode 100644 packages/opencensus-core/src/metrics/cumulative/derived-cumulative.ts create mode 100644 packages/opencensus-core/test/test-derived-cumulative.ts diff --git a/packages/opencensus-core/src/index.ts b/packages/opencensus-core/src/index.ts index c1bb8f195..7aa9c9702 100644 --- a/packages/opencensus-core/src/index.ts +++ b/packages/opencensus-core/src/index.ts @@ -23,6 +23,8 @@ export * from './trace/instrumentation/types'; export * from './trace/propagation/types'; export * from './exporters/types'; export * from './common/types'; +export * from './metrics/types'; +export * from './metrics/cumulative/types'; export * from './metrics/gauges/types'; export {Metric, MetricDescriptor, TimeSeries, MetricDescriptorType, LabelKey, LabelValue, Point as TimeSeriesPoint, DistributionValue, BucketOptions, Bucket as DistributionBucket, SummaryValue, Explicit, Exemplar, Timestamp, Snapshot, ValueAtPercentile, MetricProducerManager, MetricProducer} from './metrics/export/types'; @@ -74,6 +76,7 @@ export * from './metrics/metric-registry'; // Cumulative CLASSES export * from './metrics/cumulative/cumulative'; +export * from './metrics/cumulative/derived-cumulative'; // GAUGES CLASSES export * from './metrics/gauges/derived-gauge'; diff --git a/packages/opencensus-core/src/metrics/cumulative/derived-cumulative.ts b/packages/opencensus-core/src/metrics/cumulative/derived-cumulative.ts new file mode 100644 index 000000000..f4827042b --- /dev/null +++ b/packages/opencensus-core/src/metrics/cumulative/derived-cumulative.ts @@ -0,0 +1,170 @@ +/** + * Copyright 2019, OpenCensus 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 + * + * http://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 {getTimestampWithProcessHRTime} from '../../common/time-util'; +import {validateArrayElementsNotNull, validateNotNull} from '../../common/validations'; +import {LabelKey, LabelValue, Metric, MetricDescriptor, MetricDescriptorType, TimeSeries, Timestamp} from '../export/types'; +import {Meter} from '../types'; +import {AccessorInterface} from '../types'; +import * as util from '../utils'; + +type ValueExtractor = () => number; + +interface CumulativeEntry { + readonly labelValues: LabelValue[]; + readonly extractor: ValueExtractor; + prevValue: number; +} + +/** + * DerivedCumulative metric is used to record aggregated metrics that + * represents a single numerical value accumulated over a time interval. + */ +export class DerivedCumulative implements Meter { + private metricDescriptor: MetricDescriptor; + private labelKeysLength: number; + private registeredPoints: Map = new Map(); + private extractor?: ValueExtractor; + private readonly constantLabelValues: LabelValue[]; + private startTime: Timestamp; + + /** + * Constructs a new DerivedCumulative instance. + * + * @param {string} name The name of the metric. + * @param {string} description The description of the metric. + * @param {string} unit The unit of the metric. + * @param {MetricDescriptorType} type The type of metric. + * @param {LabelKey[]} labelKeys The list of the label keys. + * @param {Map} constantLabels The map of constant + * labels for the Metric. + * @param {Timestamp} startTime The time when the cumulative metric start + * measuring the value. + */ + constructor( + name: string, description: string, unit: string, + type: MetricDescriptorType, labelKeys: LabelKey[], + readonly constantLabels: Map, + startTime: Timestamp) { + this.labelKeysLength = labelKeys.length; + const keysAndConstantKeys = [...labelKeys, ...constantLabels.keys()]; + this.constantLabelValues = [...constantLabels.values()]; + + this.metricDescriptor = + {name, description, unit, type, labelKeys: keysAndConstantKeys}; + this.startTime = startTime; + } + + /** + * Creates a TimeSeries. The value of a single point in the TimeSeries is + * observed from a obj. The ValueExtractor is invoked whenever + * metrics are collected, meaning the reported value is up-to-date. + * + * @param {LabelValue[]} labelValues The list of the label values. + * @param objOrFn obj The obj to get the size or length or value from. If + * multiple options are available, the value (ToValueInterface) takes + * precedence first, followed by length and size. e.g value -> length -> + * size. + * fn is the function that will be called to get the current value + * of the cumulative. + */ + createTimeSeries(labelValues: LabelValue[], objOrFn: AccessorInterface): + void { + validateArrayElementsNotNull( + validateNotNull(labelValues, 'labelValues'), 'labelValue'); + validateNotNull(objOrFn, 'obj'); + + const hash = util.hashLabelValues(labelValues); + if (this.registeredPoints.has(hash)) { + throw new Error( + 'A different time series with the same labels already exists.'); + } + if (this.labelKeysLength !== labelValues.length) { + throw new Error('Label Keys and Label Values don\'t have same size'); + } + + if (objOrFn instanceof Function) { + this.extractor = objOrFn; + } else if (util.isToValueInterface(objOrFn)) { + this.extractor = () => objOrFn.getValue(); + } else if (util.isLengthAttributeInterface(objOrFn)) { + this.extractor = () => objOrFn.length; + } else if (util.isLengthMethodInterface(objOrFn)) { + this.extractor = () => objOrFn.length(); + } else if (util.isSizeAttributeInterface(objOrFn)) { + this.extractor = () => objOrFn.size; + } else if (util.isSizeMethodInterface(objOrFn)) { + this.extractor = () => objOrFn.size(); + } else { + throw new Error('Unknown interface/object type'); + } + + this.registeredPoints.set( + hash, {labelValues, extractor: this.extractor, prevValue: 0}); + } + + /** + * Removes the TimeSeries from the cumulative metric, if it is present. i.e. + * references to previous Point objects are invalid (not part of the + * metric). + * + * @param {LabelValue[]} labelValues The list of label values. + */ + removeTimeSeries(labelValues: LabelValue[]): void { + validateNotNull(labelValues, 'labelValues'); + this.registeredPoints.delete(util.hashLabelValues(labelValues)); + } + + /** + * Removes all TimeSeries from the cumulative metric. i.e. references to all + * previous Point objects are invalid (not part of the metric). + */ + clear(): void { + this.registeredPoints.clear(); + } + + /** + * Provides a Metric with one or more TimeSeries. + * + * @returns {Metric} The Metric, or null if TimeSeries is not present in + * Metric. + */ + getMetric(): Metric|null { + if (this.registeredPoints.size === 0) { + return null; + } + const timestamp: Timestamp = getTimestampWithProcessHRTime(); + return { + descriptor: this.metricDescriptor, + timeseries: Array.from( + this.registeredPoints, + ([_, cumulativeEntry]) => { + const newValue = cumulativeEntry.extractor(); + const value = newValue > cumulativeEntry.prevValue ? + newValue : + cumulativeEntry.prevValue; + cumulativeEntry.prevValue = value; + + return { + labelValues: + [...cumulativeEntry.labelValues, ...this.constantLabelValues], + points: [{value, timestamp}], + startTimestamp: this.startTime + } as TimeSeries; + }) + }; + } +} diff --git a/packages/opencensus-core/src/metrics/gauges/derived-gauge.ts b/packages/opencensus-core/src/metrics/gauges/derived-gauge.ts index d7139813d..efd1795c8 100644 --- a/packages/opencensus-core/src/metrics/gauges/derived-gauge.ts +++ b/packages/opencensus-core/src/metrics/gauges/derived-gauge.ts @@ -18,42 +18,8 @@ import {getTimestampWithProcessHRTime} from '../../common/time-util'; import {validateArrayElementsNotNull, validateNotNull} from '../../common/validations'; import {LabelKey, LabelValue, Metric, MetricDescriptor, MetricDescriptorType, TimeSeries, Timestamp} from '../export/types'; import * as types from '../types'; -import {hashLabelValues} from '../utils'; - -/** - * Interface for objects with "length()" method. - */ -export interface LengthMethodInterface { - length(): number; -} - -/** - * Interface for objects with "length" attribute (e.g. Array). - */ -export interface LengthAttributeInterface { - length: number; -} - -/** - * Interface for objects with "size" method. - */ -export interface SizeMethodInterface { - size(): number; -} - -/** - * Interface for objects with "size" attribute (e.g. Map, Set). - */ -export interface SizeAttributeInterface { - size: number; -} - -/** - * Interface for objects with "getValue" method. - */ -export interface ToValueInterface { - getValue(): number; -} +import {AccessorInterface} from '../types'; +import * as util from '../utils'; type ValueExtractor = () => number; @@ -62,14 +28,6 @@ interface GaugeEntry { readonly extractor: ValueExtractor; } -interface AccessorFunction { - (): number; -} - -export type AccessorInterface = - LengthAttributeInterface|LengthMethodInterface|SizeAttributeInterface| - SizeMethodInterface|ToValueInterface|AccessorFunction; - /** * DerivedGauge metric */ @@ -83,8 +41,6 @@ export class DerivedGauge implements types.Meter { private static readonly LABEL_VALUE = 'labelValue'; private static readonly LABEL_VALUES = 'labelValues'; private static readonly OBJECT = 'obj'; - private static readonly NUMBER = 'number'; - private static readonly FUNCTION = 'function'; private static readonly ERROR_MESSAGE_INVALID_SIZE = 'Label Keys and Label Values don\'t have same size'; private static readonly ERROR_MESSAGE_DUPLICATE_TIME_SERIES = @@ -115,39 +71,6 @@ export class DerivedGauge implements types.Meter { {name, description, unit, type, labelKeys: keysAndConstantKeys}; } - // Checks if the specified collection is a LengthAttributeInterface. - // tslint:disable-next-line:no-any - protected static isLengthAttributeInterface(obj: any): - obj is LengthAttributeInterface { - return obj && typeof obj.length === DerivedGauge.NUMBER; - } - - // Checks if the specified collection is a LengthMethodInterface. - // tslint:disable-next-line:no-any - protected static isLengthMethodInterface(obj: any): - obj is LengthMethodInterface { - return obj && typeof obj.length === DerivedGauge.FUNCTION; - } - - // Checks if the specified collection is a SizeAttributeInterface. - // tslint:disable-next-line:no-any - protected static isSizeAttributeInterface(obj: any): - obj is SizeAttributeInterface { - return obj && typeof obj.size === DerivedGauge.NUMBER; - } - - // Checks if the specified collection is a SizeMethodInterface. - // tslint:disable-next-line:no-any - protected static isSizeMethodInterface(obj: any): obj is SizeMethodInterface { - return obj && typeof obj.size === DerivedGauge.FUNCTION; - } - - // Checks if the specified callbackFn is a ToValueInterface. - // tslint:disable-next-line:no-any - protected static isToValueInterface(obj: any): obj is ToValueInterface { - return obj && typeof obj.getValue === DerivedGauge.FUNCTION; - } - /** * Creates a TimeSeries. The value of a single point in the TimeSeries is * observed from a obj or a function. The ValueExtractor is invoked whenever @@ -168,7 +91,7 @@ export class DerivedGauge implements types.Meter { DerivedGauge.LABEL_VALUE); validateNotNull(objOrFn, DerivedGauge.OBJECT); - const hash = hashLabelValues(labelValues); + const hash = util.hashLabelValues(labelValues); if (this.registeredPoints.has(hash)) { throw new Error(DerivedGauge.ERROR_MESSAGE_DUPLICATE_TIME_SERIES); } @@ -178,15 +101,15 @@ export class DerivedGauge implements types.Meter { if (objOrFn instanceof Function) { this.extractor = objOrFn; - } else if (DerivedGauge.isToValueInterface(objOrFn)) { + } else if (util.isToValueInterface(objOrFn)) { this.extractor = () => objOrFn.getValue(); - } else if (DerivedGauge.isLengthAttributeInterface(objOrFn)) { + } else if (util.isLengthAttributeInterface(objOrFn)) { this.extractor = () => objOrFn.length; - } else if (DerivedGauge.isLengthMethodInterface(objOrFn)) { + } else if (util.isLengthMethodInterface(objOrFn)) { this.extractor = () => objOrFn.length(); - } else if (DerivedGauge.isSizeAttributeInterface(objOrFn)) { + } else if (util.isSizeAttributeInterface(objOrFn)) { this.extractor = () => objOrFn.size; - } else if (DerivedGauge.isSizeMethodInterface(objOrFn)) { + } else if (util.isSizeMethodInterface(objOrFn)) { this.extractor = () => objOrFn.size(); } else { throw new Error(DerivedGauge.ERROR_MESSAGE_UNKNOWN_INTERFACE); @@ -204,7 +127,7 @@ export class DerivedGauge implements types.Meter { */ removeTimeSeries(labelValues: LabelValue[]): void { validateNotNull(labelValues, DerivedGauge.LABEL_VALUES); - this.registeredPoints.delete(hashLabelValues(labelValues)); + this.registeredPoints.delete(util.hashLabelValues(labelValues)); } /** diff --git a/packages/opencensus-core/src/metrics/types.ts b/packages/opencensus-core/src/metrics/types.ts index f75664a14..0394f6124 100644 --- a/packages/opencensus-core/src/metrics/types.ts +++ b/packages/opencensus-core/src/metrics/types.ts @@ -42,3 +42,36 @@ export interface MetricOptions { // TODO(mayurkale): Add resource information. // https://github.com/census-instrumentation/opencensus-specs/pull/248 } + +/** Interface for objects with "length()" method. */ +export interface LengthMethodInterface { + length(): number; +} + +/** Interface for objects with "length" attribute (e.g. Array). */ +export interface LengthAttributeInterface { + length: number; +} + +/** Interface for objects with "size" method. */ +export interface SizeMethodInterface { + size(): number; +} + +/** Interface for objects with "size" attribute (e.g. Map, Set). */ +export interface SizeAttributeInterface { + size: number; +} + +/** Interface for objects with "getValue" method. */ +export interface ToValueInterface { + getValue(): number; +} + +export interface AccessorFunction { + (): number; +} + +export type AccessorInterface = + LengthAttributeInterface|LengthMethodInterface|SizeAttributeInterface| + SizeMethodInterface|ToValueInterface|AccessorFunction; diff --git a/packages/opencensus-core/src/metrics/utils.ts b/packages/opencensus-core/src/metrics/utils.ts index 0024bdaa7..58298b078 100644 --- a/packages/opencensus-core/src/metrics/utils.ts +++ b/packages/opencensus-core/src/metrics/utils.ts @@ -15,6 +15,7 @@ */ import {LabelValue} from './export/types'; +import {LengthAttributeInterface, LengthMethodInterface, SizeAttributeInterface, SizeMethodInterface, ToValueInterface} from './types'; const COMMA_SEPARATOR = ','; const UNSET_LABEL_VALUE: LabelValue = { @@ -40,3 +41,36 @@ export function hashLabelValues(labelValues: LabelValue[]): string { export function initializeDefaultLabels(count: number): LabelValue[] { return new Array(count).fill(UNSET_LABEL_VALUE); } + +// Checks if the specified collection is a LengthAttributeInterface. +// tslint:disable-next-line:no-any +export function isLengthAttributeInterface(obj: any): + obj is LengthAttributeInterface { + return obj && typeof obj.length === 'number'; +} + +// Checks if the specified collection is a LengthMethodInterface. +// tslint:disable-next-line:no-any +export function isLengthMethodInterface(obj: any): + obj is LengthMethodInterface { + return obj && typeof obj.length === 'function'; +} + +// Checks if the specified collection is a SizeAttributeInterface. +// tslint:disable-next-line:no-any +export function isSizeAttributeInterface(obj: any): + obj is SizeAttributeInterface { + return obj && typeof obj.size === 'number'; +} + +// Checks if the specified collection is a SizeMethodInterface. +// tslint:disable-next-line:no-any +export function isSizeMethodInterface(obj: any): obj is SizeMethodInterface { + return obj && typeof obj.size === 'function'; +} + +// Checks if the specified callbackFn is a ToValueInterface. +// tslint:disable-next-line:no-any +export function isToValueInterface(obj: any): obj is ToValueInterface { + return obj && typeof obj.getValue === 'function'; +} diff --git a/packages/opencensus-core/test/test-derived-cumulative.ts b/packages/opencensus-core/test/test-derived-cumulative.ts new file mode 100644 index 000000000..c50658a42 --- /dev/null +++ b/packages/opencensus-core/test/test-derived-cumulative.ts @@ -0,0 +1,280 @@ +/** + * Copyright 2019, OpenCensus 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 + * + * http://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 {TEST_ONLY} from '../src/common/time-util'; +import {DerivedCumulative} from '../src/metrics/cumulative/derived-cumulative'; +import {LabelKey, LabelValue, MetricDescriptorType, Timestamp} from '../src/metrics/export/types'; + +const METRIC_NAME = 'metric-name'; +const METRIC_DESCRIPTION = 'metric-description'; +const UNIT = '1'; +const CUMULATIVE_INT64 = MetricDescriptorType.CUMULATIVE_INT64; +const CUMULATIVE_DOUBLE = MetricDescriptorType.CUMULATIVE_DOUBLE; +const LABEL_KEYS: LabelKey[] = [{key: 'code', description: 'desc'}]; +const LABEL_VALUES_200: LabelValue[] = [{value: '200'}]; +const LABEL_VALUES_400: LabelValue[] = [{value: '400'}]; +const LABEL_VALUES_EXRTA: LabelValue[] = [{value: '200'}, {value: '400'}]; +const EMPTY_CONSTANT_LABELS = new Map(); +const CONSTANT_LABELS = new Map(); +CONSTANT_LABELS.set({key: 'host', description: 'host'}, {value: 'localhost'}); + +describe('DerivedCumulative', () => { + let instance: DerivedCumulative; + const realHrtimeFn = process.hrtime; + const realNowFn = Date.now; + const mockedTime: Timestamp = {seconds: 1450000100, nanos: 1e7}; + const expectedMetricDescriptor = { + name: METRIC_NAME, + description: METRIC_DESCRIPTION, + unit: UNIT, + type: CUMULATIVE_INT64, + labelKeys: LABEL_KEYS + }; + const startime: Timestamp = {seconds: 1400000100, nanos: 1e7}; + + beforeEach(() => { + instance = new DerivedCumulative( + METRIC_NAME, METRIC_DESCRIPTION, UNIT, CUMULATIVE_INT64, LABEL_KEYS, + EMPTY_CONSTANT_LABELS, startime); + + process.hrtime = () => [100, 1e7]; + Date.now = () => 1450000000000; + // Force the clock to recalibrate the time offset with the mocked time + TEST_ONLY.setHrtimeReference(); + }); + + afterEach(() => { + process.hrtime = realHrtimeFn; + Date.now = realNowFn; + // Reset the hrtime reference so that it uses a real clock again. + TEST_ONLY.resetHrtimeFunctionCache(); + }); + + describe('createTimeSeries()', () => { + it('should throw an error when the keys and values dont have same size', + () => { + assert.throws(() => { + instance.createTimeSeries(LABEL_VALUES_EXRTA, new Map()); + }, /^Error: Label Keys and Label Values don't have same size$/); + }); + + it('should return a Metric', () => { + const map = new Map(); + map.set('key', 'value'); + instance.createTimeSeries(LABEL_VALUES_200, map); + map.set('key1', 'value1'); + + let metric = instance.getMetric(); + assert.notEqual(metric, null); + assert.deepStrictEqual(metric!.descriptor, expectedMetricDescriptor); + assert.equal(metric!.timeseries.length, 1); + assert.deepStrictEqual(metric!.timeseries, [{ + labelValues: LABEL_VALUES_200, + points: [{value: 2, timestamp: mockedTime}], + startTimestamp: startime + }]); + // add data in collection + map.set('key2', 'value2'); + map.set('key3', 'value3'); + + // add new timeseries with length-method + const arr = new Array(5).fill('test'); + instance.createTimeSeries(LABEL_VALUES_400, { + size: () => arr.length, + }); + + metric = instance.getMetric(); + assert.deepStrictEqual(metric!.descriptor, expectedMetricDescriptor); + assert.equal(metric!.timeseries.length, 2); + assert.deepStrictEqual(metric!.timeseries, [ + { + labelValues: LABEL_VALUES_200, + points: [{value: 4, timestamp: mockedTime}], + startTimestamp: startime + }, + { + labelValues: LABEL_VALUES_400, + points: [{value: 5, timestamp: mockedTime}], + startTimestamp: startime + } + ]); + }); + + it('should return a Metric (INT64) - custom object', () => { + class QueueManager { + getValue(): number { + return 45; + } + } + const queue = new QueueManager(); + instance.createTimeSeries(LABEL_VALUES_200, queue); + const metric = instance.getMetric(); + assert.notEqual(metric, null); + assert.deepStrictEqual(metric!.descriptor, expectedMetricDescriptor); + assert.equal(metric!.timeseries.length, 1); + assert.deepStrictEqual(metric!.timeseries, [{ + labelValues: LABEL_VALUES_200, + points: [{value: 45, timestamp: mockedTime}], + startTimestamp: startime + }]); + }); + + it('should return a Metric value from a function', () => { + class QueueManager { + private depth = 0; + get pendingJobs() { + return this.depth; + } + addJob() { + this.depth++; + } + } + const queue = new QueueManager(); + queue.addJob(); + instance.createTimeSeries(LABEL_VALUES_200, () => { + return queue.pendingJobs; + }); + + let metric = instance.getMetric(); + assert.notEqual(metric, null); + assert.deepStrictEqual(metric!.descriptor, expectedMetricDescriptor); + assert.equal(metric!.timeseries.length, 1); + assert.deepStrictEqual(metric!.timeseries, [{ + labelValues: LABEL_VALUES_200, + points: [{value: 1, timestamp: mockedTime}], + startTimestamp: startime + }]); + // Simulate a adding multiple jobs in queue + queue.addJob(); + queue.addJob(); + queue.addJob(); + metric = instance.getMetric(); + assert.equal(metric!.timeseries.length, 1); + assert.deepStrictEqual( + metric!.timeseries[0].points, [{value: 4, timestamp: mockedTime}]); + }); + + it('should return a Metric (Double) - custom object', () => { + class QueueManager { + getValue(): number { + return 0.7; + } + } + const queue = new QueueManager(); + const doubleInstance = new DerivedCumulative( + METRIC_NAME, METRIC_DESCRIPTION, UNIT, CUMULATIVE_DOUBLE, LABEL_KEYS, + EMPTY_CONSTANT_LABELS, startime); + doubleInstance.createTimeSeries(LABEL_VALUES_200, queue); + const metric = doubleInstance.getMetric(); + assert.notEqual(metric, null); + assert.deepStrictEqual(metric!.descriptor, { + name: METRIC_NAME, + description: METRIC_DESCRIPTION, + unit: UNIT, + type: CUMULATIVE_DOUBLE, + labelKeys: LABEL_KEYS + }); + assert.equal(metric!.timeseries.length, 1); + assert.deepStrictEqual(metric!.timeseries, [{ + labelValues: LABEL_VALUES_200, + points: [{value: 0.7, timestamp: mockedTime}], + startTimestamp: startime + }]); + }); + + it('should ignore dropping (decreasing) value', () => { + const map = new Map(); + instance.createTimeSeries(LABEL_VALUES_200, map); + map.set('key', 'value'); + map.set('key1', 'value1'); + let metric = instance.getMetric(); + assert.deepStrictEqual(metric!.descriptor, expectedMetricDescriptor); + assert.equal(metric!.timeseries.length, 1); + assert.deepStrictEqual(metric!.timeseries, [{ + labelValues: LABEL_VALUES_200, + points: [{value: 2, timestamp: mockedTime}], + startTimestamp: startime + }]); + + map.clear(); + metric = instance.getMetric(); + assert.equal(metric!.timeseries.length, 1); + assert.deepStrictEqual(metric!.timeseries, [{ + labelValues: LABEL_VALUES_200, + points: [{value: 2, timestamp: mockedTime}], + startTimestamp: startime + }]); + }); + + it('should not create same timeseries again', () => { + const map = new Map(); + instance.createTimeSeries(LABEL_VALUES_200, map); + map.set('key', 'value'); + const metric = instance.getMetric(); + assert.notEqual(metric, null); + assert.deepStrictEqual(metric!.descriptor, expectedMetricDescriptor); + assert.equal(metric!.timeseries.length, 1); + assert.deepStrictEqual(metric!.timeseries, [{ + labelValues: LABEL_VALUES_200, + points: [{value: 1, timestamp: mockedTime}], + startTimestamp: startime + }]); + + // create timeseries with same labels. + assert.throws(() => { + instance.createTimeSeries(LABEL_VALUES_200, map); + }, /^Error: A different time series with the same labels already exists.$/); + }); + }); + + describe('removeTimeSeries()', () => { + it('should remove TimeSeries', () => { + const arr: string[] = []; + instance.createTimeSeries(LABEL_VALUES_200, arr); + arr.push('test'); + let metric = instance.getMetric(); + assert.notEqual(metric, null); + assert.deepStrictEqual(metric!.descriptor, expectedMetricDescriptor); + instance.removeTimeSeries(LABEL_VALUES_200); + metric = instance.getMetric(); + assert.deepStrictEqual(metric, null); + }); + }); + + describe('clear()', () => { + it('should clear all TimeSeries', () => { + const map = new Map(); + instance.createTimeSeries(LABEL_VALUES_200, { + size: () => map.size, + }); + map.set('key', 'value'); + + const arr: string[] = []; + instance.createTimeSeries(LABEL_VALUES_400, { + size: () => arr.length, + }); + arr.push('test'); + let metric = instance.getMetric(); + assert.notEqual(metric, null); + assert.deepStrictEqual(metric!.descriptor, expectedMetricDescriptor); + assert.equal(metric!.timeseries.length, 2); + instance.clear(); + metric = instance.getMetric(); + assert.deepStrictEqual(metric, null); + }); + }); +});