diff --git a/CHANGELOG.md b/CHANGELOG.md index 21d3db71b..46e9917ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. ## Unreleased - +- Gauge: Add support for constant labels. ## 0.0.10 - 2019-04-03 - Add optional `compressedSize` and `uncompressedSize` params to `Span.addMessageEvent` diff --git a/packages/opencensus-core/src/common/validations.ts b/packages/opencensus-core/src/common/validations.ts index 4211a94b2..d3cc62e39 100644 --- a/packages/opencensus-core/src/common/validations.ts +++ b/packages/opencensus-core/src/common/validations.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import {LabelKey, LabelValue} from '../metrics/export/types'; + /** * Validates that an object reference passed as a parameter to the calling * method is not null. @@ -43,3 +45,24 @@ export function validateArrayElementsNotNull( throw new Error(`${errorMessage} elements should not be a NULL`); } } + +/** Throws an error if any of the map elements is null. */ +export function validateMapElementNotNull( + map: Map, errorMessage: string) { + for (const [key, value] of map.entries()) { + if (key == null || value == null) { + throw new Error(`${errorMessage} elements should not be a NULL`); + } + } +} + +/** Throws an error if any of the array element present in the map. */ +export function validateDuplicateKeys( + keys: LabelKey[], constantLabels: Map) { + const keysAndConstantKeys = + new Set([...keys, ...constantLabels.keys()].map(k => k.key)); + if (keysAndConstantKeys.size !== (keys.length + constantLabels.size)) { + throw new Error( + `The keys from LabelKeys should not be present in constantLabels or LabelKeys should not contains duplicate keys`); + } +} diff --git a/packages/opencensus-core/src/metrics/gauges/derived-gauge.ts b/packages/opencensus-core/src/metrics/gauges/derived-gauge.ts index 65f14ccdb..90e45bfc8 100644 --- a/packages/opencensus-core/src/metrics/gauges/derived-gauge.ts +++ b/packages/opencensus-core/src/metrics/gauges/derived-gauge.ts @@ -73,6 +73,7 @@ export class DerivedGauge implements types.Meter { private labelKeysLength: number; private registeredPoints: Map = new Map(); private extractor?: ValueExtractor; + private readonly constantLabelValues: LabelValue[]; private static readonly LABEL_VALUE = 'labelValue'; private static readonly LABEL_VALUES = 'labelValues'; @@ -94,12 +95,19 @@ export class DerivedGauge implements types.Meter { * @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. */ constructor( name: string, description: string, unit: string, - type: MetricDescriptorType, labelKeys: LabelKey[]) { - this.metricDescriptor = {name, description, unit, type, labelKeys}; + type: MetricDescriptorType, labelKeys: LabelKey[], + readonly constantLabels: Map) { this.labelKeysLength = labelKeys.length; + const keysAndConstantKeys = [...labelKeys, ...constantLabels.keys()]; + this.constantLabelValues = [...constantLabels.values()]; + + this.metricDescriptor = + {name, description, unit, type, labelKeys: keysAndConstantKeys}; } // Checks if the specified collection is a LengthAttributeInterface. @@ -211,7 +219,8 @@ export class DerivedGauge implements types.Meter { timeseries: Array.from( this.registeredPoints, ([_, gaugeEntry]) => ({ - labelValues: gaugeEntry.labelValues, + labelValues: + [...gaugeEntry.labelValues, ...this.constantLabelValues], points: [{value: gaugeEntry.extractor(), timestamp}] } as TimeSeries)) }; diff --git a/packages/opencensus-core/src/metrics/gauges/gauge.ts b/packages/opencensus-core/src/metrics/gauges/gauge.ts index da7b13432..2a2788708 100644 --- a/packages/opencensus-core/src/metrics/gauges/gauge.ts +++ b/packages/opencensus-core/src/metrics/gauges/gauge.ts @@ -28,6 +28,7 @@ export class Gauge implements types.Meter { private labelKeysLength: number; private defaultLabelValues: LabelValue[]; private registeredPoints: Map = new Map(); + private readonly constantLabelValues: LabelValue[]; private static readonly LABEL_VALUE = 'labelValue'; private static readonly LABEL_VALUES = 'labelValues'; @@ -42,12 +43,19 @@ export class Gauge implements types.Meter { * @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. */ constructor( name: string, description: string, unit: string, - type: MetricDescriptorType, readonly labelKeys: LabelKey[]) { - this.metricDescriptor = {name, description, unit, type, labelKeys}; + type: MetricDescriptorType, readonly labelKeys: LabelKey[], + readonly constantLabels: Map) { this.labelKeysLength = labelKeys.length; + const keysAndConstantKeys = [...labelKeys, ...constantLabels.keys()]; + this.constantLabelValues = [...constantLabels.values()]; + + this.metricDescriptor = + {name, description, unit, type, labelKeys: keysAndConstantKeys}; this.defaultLabelValues = initializeDefaultLabels(this.labelKeysLength); } @@ -116,7 +124,7 @@ export class Gauge implements types.Meter { throw new Error(Gauge.ERROR_MESSAGE_INVALID_SIZE); } - const point = new PointEntry(labelValues); + const point = new PointEntry([...labelValues, ...this.constantLabelValues]); this.registeredPoints.set(hash, point); return point; } diff --git a/packages/opencensus-core/src/metrics/metric-registry.ts b/packages/opencensus-core/src/metrics/metric-registry.ts index 99f3954a9..f6ed0537c 100644 --- a/packages/opencensus-core/src/metrics/metric-registry.ts +++ b/packages/opencensus-core/src/metrics/metric-registry.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import {validateArrayElementsNotNull, validateNotNull} from '../common/validations'; -import {MeasureUnit,} from '../stats/types'; +import {validateArrayElementsNotNull, validateDuplicateKeys, validateMapElementNotNull, validateNotNull} from '../common/validations'; +import {MeasureUnit} from '../stats/types'; import {BaseMetricProducer} from './export/base-metric-producer'; import {Metric, MetricDescriptorType, MetricProducer} from './export/types'; import {DerivedGauge} from './gauges/derived-gauge'; @@ -31,9 +31,11 @@ export class MetricRegistry { private static readonly NAME = 'name'; private static readonly LABEL_KEY = 'labelKey'; + private static readonly CONSTANT_LABELS = 'constantLabels'; private static readonly DEFAULT_DESCRIPTION = ''; private static readonly DEFAULT_UNIT = MeasureUnit.UNIT; private static readonly DEFAULT_LABEL_KEYS = []; + private static readonly DEFAULT_CONSTANT_LABEL = new Map(); constructor() { this.metricProducer = new MetricProducerForRegistry(this.registeredMetrics); @@ -54,13 +56,18 @@ export class MetricRegistry { const unit = (options && options.unit) || MetricRegistry.DEFAULT_UNIT; const labelKeys = (options && options.labelKeys) || MetricRegistry.DEFAULT_LABEL_KEYS; - // TODO (mayurkale): Add support for constantLabels + const constantLabels = (options && options.constantLabels) || + MetricRegistry.DEFAULT_CONSTANT_LABEL; + // TODO(mayurkale): Add support for resource validateArrayElementsNotNull(labelKeys, MetricRegistry.LABEL_KEY); + validateMapElementNotNull(constantLabels, MetricRegistry.CONSTANT_LABELS); + validateDuplicateKeys(labelKeys, constantLabels); + const labelKeysCopy = Object.assign([], labelKeys); const int64Gauge = new Gauge( validateNotNull(name, MetricRegistry.NAME), description, unit, - MetricDescriptorType.GAUGE_INT64, labelKeysCopy); + MetricDescriptorType.GAUGE_INT64, labelKeysCopy, constantLabels); this.registerMetric(name, int64Gauge); return int64Gauge; } @@ -80,13 +87,18 @@ export class MetricRegistry { const unit = (options && options.unit) || MetricRegistry.DEFAULT_UNIT; const labelKeys = (options && options.labelKeys) || MetricRegistry.DEFAULT_LABEL_KEYS; - // TODO (mayurkale): Add support for constantLabels + const constantLabels = (options && options.constantLabels) || + MetricRegistry.DEFAULT_CONSTANT_LABEL; + // TODO(mayurkale): Add support for resource validateArrayElementsNotNull(labelKeys, MetricRegistry.LABEL_KEY); + validateMapElementNotNull(constantLabels, MetricRegistry.CONSTANT_LABELS); + validateDuplicateKeys(labelKeys, constantLabels); + const labelKeysCopy = Object.assign([], labelKeys); const doubleGauge = new Gauge( validateNotNull(name, MetricRegistry.NAME), description, unit, - MetricDescriptorType.GAUGE_DOUBLE, labelKeysCopy); + MetricDescriptorType.GAUGE_DOUBLE, labelKeysCopy, constantLabels); this.registerMetric(name, doubleGauge); return doubleGauge; } @@ -106,13 +118,18 @@ export class MetricRegistry { const unit = (options && options.unit) || MetricRegistry.DEFAULT_UNIT; const labelKeys = (options && options.labelKeys) || MetricRegistry.DEFAULT_LABEL_KEYS; - // TODO (mayurkale): Add support for constantLabels + const constantLabels = (options && options.constantLabels) || + MetricRegistry.DEFAULT_CONSTANT_LABEL; + // TODO(mayurkale): Add support for resource validateArrayElementsNotNull(labelKeys, MetricRegistry.LABEL_KEY); + validateMapElementNotNull(constantLabels, MetricRegistry.CONSTANT_LABELS); + validateDuplicateKeys(labelKeys, constantLabels); + const labelKeysCopy = Object.assign([], labelKeys); const derivedInt64Gauge = new DerivedGauge( validateNotNull(name, MetricRegistry.NAME), description, unit, - MetricDescriptorType.GAUGE_INT64, labelKeysCopy); + MetricDescriptorType.GAUGE_INT64, labelKeysCopy, constantLabels); this.registerMetric(name, derivedInt64Gauge); return derivedInt64Gauge; } @@ -132,13 +149,18 @@ export class MetricRegistry { const unit = (options && options.unit) || MetricRegistry.DEFAULT_UNIT; const labelKeys = (options && options.labelKeys) || MetricRegistry.DEFAULT_LABEL_KEYS; - // TODO (mayurkale): Add support for constantLabels + const constantLabels = (options && options.constantLabels) || + MetricRegistry.DEFAULT_CONSTANT_LABEL; + // TODO(mayurkale): Add support for resource validateArrayElementsNotNull(labelKeys, MetricRegistry.LABEL_KEY); + validateMapElementNotNull(constantLabels, MetricRegistry.CONSTANT_LABELS); + validateDuplicateKeys(labelKeys, constantLabels); + const labelKeysCopy = Object.assign([], labelKeys); const derivedDoubleGauge = new DerivedGauge( validateNotNull(name, MetricRegistry.NAME), description, unit, - MetricDescriptorType.GAUGE_DOUBLE, labelKeysCopy); + MetricDescriptorType.GAUGE_DOUBLE, labelKeysCopy, constantLabels); this.registerMetric(name, derivedDoubleGauge); return derivedDoubleGauge; } diff --git a/packages/opencensus-core/test/test-derived-gauge.ts b/packages/opencensus-core/test/test-derived-gauge.ts index 159ea6450..0becbf0db 100644 --- a/packages/opencensus-core/test/test-derived-gauge.ts +++ b/packages/opencensus-core/test/test-derived-gauge.ts @@ -15,7 +15,6 @@ */ import * as assert from 'assert'; - import {TEST_ONLY} from '../src/common/time-util'; import {LabelKey, LabelValue, MetricDescriptorType, Timestamp} from '../src/metrics/export/types'; import {DerivedGauge} from '../src/metrics/gauges/derived-gauge'; @@ -29,6 +28,9 @@ 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('DerivedGauge', () => { let instance: DerivedGauge; @@ -45,7 +47,8 @@ describe('DerivedGauge', () => { beforeEach(() => { instance = new DerivedGauge( - METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_INT64, LABEL_KEYS); + METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_INT64, LABEL_KEYS, + EMPTY_CONSTANT_LABELS); process.hrtime = () => [100, 1e7]; Date.now = () => 1450000000000; @@ -148,7 +151,8 @@ describe('DerivedGauge', () => { } const obj = new QueueManager(); const doubleInstance = new DerivedGauge( - METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_DOUBLE, LABEL_KEYS); + METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_DOUBLE, LABEL_KEYS, + EMPTY_CONSTANT_LABELS); doubleInstance.createTimeSeries(LABEL_VALUES_200, obj); const metric = doubleInstance.getMetric(); assert.notEqual(metric, null); @@ -171,6 +175,39 @@ describe('DerivedGauge', () => { }]); }); + it('should return a Metric (Double) - custom object', () => { + class QueueManager { + getValue(): number { + return 0.7; + } + } + const obj = new QueueManager(); + const doubleInstance = new DerivedGauge( + METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_DOUBLE, LABEL_KEYS, + CONSTANT_LABELS); + doubleInstance.createTimeSeries(LABEL_VALUES_200, obj); + const metric = doubleInstance.getMetric(); + assert.notEqual(metric, null); + assert.deepStrictEqual(metric!.descriptor, { + name: METRIC_NAME, + description: METRIC_DESCRIPTION, + unit: UNIT, + type: GAUGE_DOUBLE, + labelKeys: [...LABEL_KEYS, ...Array.from(CONSTANT_LABELS.keys())] + }); + assert.equal(metric!.timeseries.length, 1); + assert.deepStrictEqual( + metric!.timeseries, [{ + labelValues: + [...LABEL_VALUES_200, ...Array.from(CONSTANT_LABELS.values())], + points: [{ + value: 0.7, + timestamp: + {nanos: mockedTime.nanos, seconds: mockedTime.seconds} + }] + }]); + }); + it('should not create same timeseries again', () => { const map = new Map(); instance.createTimeSeries(LABEL_VALUES_200, map); diff --git a/packages/opencensus-core/test/test-gauge.ts b/packages/opencensus-core/test/test-gauge.ts index c1bdd2112..9dc0613f5 100644 --- a/packages/opencensus-core/test/test-gauge.ts +++ b/packages/opencensus-core/test/test-gauge.ts @@ -32,6 +32,9 @@ const LABEL_VALUES_EXRTA: LabelValue[] = [{value: '200'}, {value: '400'}]; const UNSET_LABEL_VALUE: LabelValue = { value: null }; +const EMPTY_CONSTANT_LABELS = new Map(); +const CONSTANT_LABELS = new Map(); +CONSTANT_LABELS.set({key: 'host', description: 'host'}, {value: 'localhost'}); describe('GAUGE_INT64', () => { let instance: Gauge; @@ -48,7 +51,8 @@ describe('GAUGE_INT64', () => { beforeEach(() => { instance = new Gauge( - METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_INT64, LABEL_KEYS); + METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_INT64, LABEL_KEYS, + EMPTY_CONSTANT_LABELS); process.hrtime = () => [100, 1e7]; Date.now = () => 1450000000000; @@ -168,7 +172,8 @@ describe('GAUGE_INT64', () => { it('should return same timeseries for interchanged labels', () => { instance = new Gauge( METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_INT64, - [{key: 'k1', description: 'desc'}, {key: 'k2', description: 'desc'}]); + [{key: 'k1', description: 'desc'}, {key: 'k2', description: 'desc'}], + EMPTY_CONSTANT_LABELS); const point = instance.getOrCreateTimeSeries([{value: '200'}, {value: '400'}]); point.add(200); @@ -178,11 +183,40 @@ describe('GAUGE_INT64', () => { const metric = instance.getMetric(); assert.equal(metric!.timeseries.length, 1); }); - it('should create same labelValues as labelKeys', () => { - instance = new Gauge(METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_INT64, [ + + it('should add constant labels', () => { + instance = new Gauge( + METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_INT64, + [{key: 'k1', description: 'desc'}, {key: 'k2', description: 'desc'}], + CONSTANT_LABELS); + const point = + instance.getOrCreateTimeSeries([{value: '200'}, {value: '400'}]); + point.add(200); + const metric = instance.getMetric(); + assert.equal(metric!.timeseries.length, 1); + assert.deepStrictEqual(metric!.descriptor.labelKeys, [ {key: 'k1', description: 'desc'}, {key: 'k2', description: 'desc'}, - {key: 'k3', description: 'desc'} + {key: 'host', description: 'host'} ]); + assert.deepStrictEqual( + metric!.timeseries, [{ + labelValues: [{value: '200'}, {value: '400'}, {value: 'localhost'}], + points: [{ + value: 200, + timestamp: + {nanos: mockedTime.nanos, seconds: mockedTime.seconds} + }] + }]); + }); + + it('should create same labelValues as labelKeys', () => { + instance = new Gauge( + METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_INT64, + [ + {key: 'k1', description: 'desc'}, {key: 'k2', description: 'desc'}, + {key: 'k3', description: 'desc'} + ], + EMPTY_CONSTANT_LABELS); const point = instance.getDefaultTimeSeries(); point.add(200); const metric = instance.getMetric(); @@ -279,7 +313,8 @@ describe('GAUGE_DOUBLE', () => { beforeEach(() => { instance = new Gauge( - METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_DOUBLE, LABEL_KEYS); + METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_DOUBLE, LABEL_KEYS, + EMPTY_CONSTANT_LABELS); process.hrtime = () => [100, 1e7]; Date.now = () => 1450000000000; @@ -428,11 +463,13 @@ describe('GAUGE_DOUBLE', () => { ]); }); it('should create same labelValues as labelKeys', () => { - instance = - new Gauge(METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_DOUBLE, [ + instance = new Gauge( + METRIC_NAME, METRIC_DESCRIPTION, UNIT, GAUGE_DOUBLE, + [ {key: 'k1', description: 'desc'}, {key: 'k2', description: 'desc'}, {key: 'k3', description: 'desc'} - ]); + ], + EMPTY_CONSTANT_LABELS); const point = instance.getDefaultTimeSeries(); point.add(10.1); const metric = instance.getMetric(); diff --git a/packages/opencensus-core/test/test-metric-registry.ts b/packages/opencensus-core/test/test-metric-registry.ts index 3d156b85b..8d2e7cc05 100644 --- a/packages/opencensus-core/test/test-metric-registry.ts +++ b/packages/opencensus-core/test/test-metric-registry.ts @@ -102,6 +102,16 @@ describe('addInt64Gauge', () => { point.timestamp, {seconds: mockedTime.seconds, nanos: mockedTime.nanos}); }); + + it('should throw an error when the duplicate keys in labelKeys and constantLabels', + () => { + const constantLabels = new Map(); + constantLabels.set({key: 'k1'}, {value: 'v1'}); + const labelKeys = [{key: 'k1', description: 'desc'}]; + assert.throws(() => { + registry.addInt64Gauge(METRIC_NAME, {constantLabels, labelKeys}); + }, /^Error: The keys from LabelKeys should not be present in constantLabels or LabelKeys should not contains duplicate keys$/); + }); }); describe('addDoubleGauge', () => { @@ -160,6 +170,20 @@ describe('addDoubleGauge', () => { }, /^Error: A metric with the name metric-name has already been registered.$/); }); + it('should throw an error when the constant labels elements are null', () => { + let constantLabels = new Map(); + constantLabels.set({key: 'k1'}, null); + assert.throws(() => { + registry.addDoubleGauge(METRIC_NAME, {constantLabels}); + }, /^Error: constantLabels elements should not be a NULL$/); + + constantLabels = new Map(); + constantLabels.set(null, null); + assert.throws(() => { + registry.addDoubleGauge(METRIC_NAME, {constantLabels}); + }, /^Error: constantLabels elements should not be a NULL$/); + }); + it('should return a metric without options', () => { const doubleGauge = registry.addDoubleGauge(METRIC_NAME); const pointEntry = doubleGauge.getDefaultTimeSeries(); @@ -241,6 +265,25 @@ describe('addDerivedInt64Gauge', () => { }, /^Error: A metric with the name metric-name has already been registered.$/); }); + it('should throw an error when the duplicate keys in labelKeys and constantLabels', + () => { + const constantLabels = new Map(); + constantLabels.set({key: 'k1'}, {value: 'v1'}); + const labelKeys = [{key: 'k1', description: 'desc'}]; + assert.throws(() => { + registry.addDerivedInt64Gauge( + METRIC_NAME, {constantLabels, labelKeys}); + }, /^Error: The keys from LabelKeys should not be present in constantLabels or LabelKeys should not contains duplicate keys$/); + }); + + it('should throw an error when the constant labels elements are null', () => { + const constantLabels = new Map(); + constantLabels.set({key: 'k1'}, null); + assert.throws(() => { + registry.addDerivedInt64Gauge(METRIC_NAME, {constantLabels}); + }, /^Error: constantLabels elements should not be a NULL$/); + }); + it('should return a metric without options', () => { const map = new Map(); map.set('key', 'value');