diff --git a/packages/opentelemetry-core/src/metrics/NoopMeter.ts b/packages/opentelemetry-core/src/metrics/NoopMeter.ts index ccff9f1358..9f354cbaa0 100644 --- a/packages/opentelemetry-core/src/metrics/NoopMeter.ts +++ b/packages/opentelemetry-core/src/metrics/NoopMeter.ts @@ -23,6 +23,8 @@ import { MetricOptions, MeasureHandle, SpanContext, + LabelSet, + Labels, } from '@opentelemetry/types'; /** @@ -58,6 +60,10 @@ export class NoopMeter implements Meter { createGauge(name: string, options?: MetricOptions): Metric { return NOOP_GAUGE_METRIC; } + + labels(labels: Labels): LabelSet { + return NOOP_LABEL_SET; + } } export class NoopMetric implements Metric { @@ -67,12 +73,12 @@ export class NoopMetric implements Metric { this._handle = handle; } /** - * Returns a Handle associated with specified label values. + * Returns a Handle associated with specified LabelSet. * It is recommended to keep a reference to the Handle instead of always * calling this method for every operations. - * @param labelValues the list of label values. + * @param labels the canonicalized LabelSet used to associate with this metric handle. */ - getHandle(labelValues: string[]): T { + getHandle(labels: LabelSet): T { return this._handle; } @@ -85,9 +91,10 @@ export class NoopMetric implements Metric { /** * Removes the Handle from the metric, if it is present. - * @param labelValues the list of label values. + * @param labels the canonicalized LabelSet used to associate with this metric handle. */ - removeHandle(labelValues: string[]): void { + removeHandle(labels: LabelSet): void { + // @todo: implement this method return; } @@ -137,3 +144,5 @@ export const NOOP_MEASURE_HANDLE = new NoopMeasureHandle(); export const NOOP_MEASURE_METRIC = new NoopMetric( NOOP_MEASURE_HANDLE ); + +export const NOOP_LABEL_SET = {} as LabelSet; diff --git a/packages/opentelemetry-core/test/metrics/NoopMeter.test.ts b/packages/opentelemetry-core/test/metrics/NoopMeter.test.ts index b37762bc7c..38294975f4 100644 --- a/packages/opentelemetry-core/test/metrics/NoopMeter.test.ts +++ b/packages/opentelemetry-core/test/metrics/NoopMeter.test.ts @@ -24,25 +24,26 @@ import { NOOP_MEASURE_HANDLE, NOOP_MEASURE_METRIC, } from '../../src/metrics/NoopMeter'; +import { Labels } from '@opentelemetry/types'; describe('NoopMeter', () => { it('should not crash', () => { const meter = new NoopMeter(); const counter = meter.createCounter('some-name'); + const labels = {} as Labels; + const labelSet = meter.labels(labels); + // ensure NoopMetric does not crash. counter.setCallback(() => { assert.fail('callback occurred'); }); - counter.getHandle(['val1', 'val2']).add(1); + counter.getHandle(labelSet).add(1); counter.getDefaultHandle().add(1); - counter.removeHandle(['val1', 'val2']); + counter.removeHandle(labelSet); // ensure the correct noop const is returned assert.strictEqual(counter, NOOP_COUNTER_METRIC); - assert.strictEqual( - counter.getHandle(['val1', 'val2']), - NOOP_COUNTER_HANDLE - ); + assert.strictEqual(counter.getHandle(labelSet), NOOP_COUNTER_HANDLE); assert.strictEqual(counter.getDefaultHandle(), NOOP_COUNTER_HANDLE); counter.clear(); @@ -61,10 +62,7 @@ describe('NoopMeter', () => { // ensure the correct noop const is returned assert.strictEqual(measure, NOOP_MEASURE_METRIC); assert.strictEqual(measure.getDefaultHandle(), NOOP_MEASURE_HANDLE); - assert.strictEqual( - measure.getHandle(['val1', 'val2']), - NOOP_MEASURE_HANDLE - ); + assert.strictEqual(measure.getHandle(labelSet), NOOP_MEASURE_HANDLE); const gauge = meter.createGauge('some-name'); gauge.getDefaultHandle().set(1); @@ -72,12 +70,11 @@ describe('NoopMeter', () => { // ensure the correct noop const is returned assert.strictEqual(gauge, NOOP_GAUGE_METRIC); assert.strictEqual(gauge.getDefaultHandle(), NOOP_GAUGE_HANDLE); - assert.strictEqual(gauge.getHandle(['val1', 'val2']), NOOP_GAUGE_HANDLE); + assert.strictEqual(gauge.getHandle(labelSet), NOOP_GAUGE_HANDLE); const options = { component: 'tests', description: 'the testing package', - labelKeys: ['key1', 'key2'], }; const measureWithOptions = meter.createMeasure('some-name', options); diff --git a/packages/opentelemetry-metrics/src/Handle.ts b/packages/opentelemetry-metrics/src/Handle.ts index b6b8095800..335493bffb 100644 --- a/packages/opentelemetry-metrics/src/Handle.ts +++ b/packages/opentelemetry-metrics/src/Handle.ts @@ -23,8 +23,11 @@ import { TimeSeries } from './export/types'; */ export class BaseHandle { protected _data = 0; + protected _labelSet: types.LabelSet; - constructor(private readonly _labels: string[]) {} + constructor(labelSet: types.LabelSet) { + this._labelSet = labelSet; + } /** * Returns the TimeSeries with one or more Point. @@ -34,7 +37,9 @@ export class BaseHandle { */ getTimeSeries(timestamp: types.HrTime): TimeSeries { return { - labelValues: this._labels.map(value => ({ value })), + labelValues: Object.values(this._labelSet.labels).map(value => ({ + value, + })), points: [{ value: this._data, timestamp }], }; } @@ -42,19 +47,18 @@ export class BaseHandle { /** * CounterHandle allows the SDK to observe/record a single metric event. The - * value of single handle in the `Counter` associated with specified label - * values. + * value of single handle in the `Counter` associated with specified LabelSet. */ export class CounterHandle extends BaseHandle implements types.CounterHandle { constructor( + labelSet: types.LabelSet, private readonly _disabled: boolean, private readonly _monotonic: boolean, private readonly _valueType: types.ValueType, - private readonly _labelValues: string[], private readonly _logger: types.Logger, private readonly _onUpdate: Function ) { - super(_labelValues); + super(labelSet); } add(value: number): void { @@ -62,13 +66,17 @@ export class CounterHandle extends BaseHandle implements types.CounterHandle { if (this._monotonic && value < 0) { this._logger.error( - `Monotonic counter cannot descend for ${this._labelValues}` + `Monotonic counter cannot descend for ${Object.values( + this._labelSet.labels + )}` ); return; } if (this._valueType === types.ValueType.INT && !Number.isInteger(value)) { this._logger.warn( - `INT counter cannot accept a floating-point value for ${this._labelValues}, ignoring the fractional digits.` + `INT counter cannot accept a floating-point value for ${Object.values( + this._labelSet.labels + )}, ignoring the fractional digits.` ); value = Math.trunc(value); } @@ -79,18 +87,18 @@ export class CounterHandle extends BaseHandle implements types.CounterHandle { /** * GaugeHandle allows the SDK to observe/record a single metric event. The - * value of single handle in the `Gauge` associated with specified label values. + * value of single handle in the `Gauge` associated with specified LabelSet. */ export class GaugeHandle extends BaseHandle implements types.GaugeHandle { constructor( + labelSet: types.LabelSet, private readonly _disabled: boolean, private readonly _monotonic: boolean, private readonly _valueType: types.ValueType, - private readonly _labelValues: string[], private readonly _logger: types.Logger, private readonly _onUpdate: Function ) { - super(_labelValues); + super(labelSet); } set(value: number): void { @@ -98,14 +106,18 @@ export class GaugeHandle extends BaseHandle implements types.GaugeHandle { if (this._monotonic && value < this._data) { this._logger.error( - `Monotonic gauge cannot descend for ${this._labelValues}` + `Monotonic gauge cannot descend for ${Object.values( + this._labelSet.labels + )}` ); return; } if (this._valueType === types.ValueType.INT && !Number.isInteger(value)) { this._logger.warn( - `INT gauge cannot accept a floating-point value for ${this._labelValues}, ignoring the fractional digits.` + `INT gauge cannot accept a floating-point value for ${Object.values( + this._labelSet.labels + )}, ignoring the fractional digits.` ); value = Math.trunc(value); } diff --git a/packages/opentelemetry-metrics/src/LabelSet.ts b/packages/opentelemetry-metrics/src/LabelSet.ts new file mode 100644 index 0000000000..c17534c2e7 --- /dev/null +++ b/packages/opentelemetry-metrics/src/LabelSet.ts @@ -0,0 +1,39 @@ +/*! + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as types from '@opentelemetry/types'; + +/** + * Canonicalized labels with an unique string identifier. + */ +export class LabelSet implements types.LabelSet { + identifier: string; + labels: types.Labels; + + constructor(identifier: string, labels: types.Labels) { + this.identifier = identifier; + this.labels = labels; + } +} + +/** + * Type guard to remove nulls from arrays + * + * @param value value to be checked for null equality + */ +export function notNull(value: T | null): value is T { + return value !== null; +} diff --git a/packages/opentelemetry-metrics/src/Meter.ts b/packages/opentelemetry-metrics/src/Meter.ts index bbcd2b97dd..d1cb29fce7 100644 --- a/packages/opentelemetry-metrics/src/Meter.ts +++ b/packages/opentelemetry-metrics/src/Meter.ts @@ -29,6 +29,7 @@ import { DEFAULT_CONFIG, MeterConfig, } from './types'; +import { LabelSet } from './LabelSet'; import { ReadableMetric, MetricExporter } from './export/types'; import { notNull } from './Utils'; import { ExportResult } from '@opentelemetry/base'; @@ -187,6 +188,27 @@ export class Meter implements types.Meter { this._metrics.set(name, metric); } + /** + * Provide a pre-computed re-useable LabelSet by + * converting the unordered labels into a canonicalized + * set of lables with an unique identifier, useful for pre-aggregation. + * @param labels user provided unordered Labels. + */ + labels(labels: types.Labels): types.LabelSet { + const keys = Object.keys(labels).sort(); + const identifier = keys.reduce((result, key) => { + if (result.length > 2) { + result += ','; + } + return (result += key + ':' + labels[key]); + }, '|#'); + const sortedLabels: types.Labels = {}; + keys.forEach(key => { + sortedLabels[key] = labels[key]; + }); + return new LabelSet(identifier, sortedLabels); + } + /** * Ensure a metric name conforms to the following rules: * diff --git a/packages/opentelemetry-metrics/src/Metric.ts b/packages/opentelemetry-metrics/src/Metric.ts index 9ef8eab083..f3b50f2884 100644 --- a/packages/opentelemetry-metrics/src/Metric.ts +++ b/packages/opentelemetry-metrics/src/Metric.ts @@ -16,7 +16,6 @@ import * as types from '@opentelemetry/types'; import { hrTime } from '@opentelemetry/core'; -import { hashLabelValues } from './Utils'; import { CounterHandle, GaugeHandle, BaseHandle } from './Handle'; import { MetricOptions } from './types'; import { @@ -47,18 +46,17 @@ export abstract class Metric implements types.Metric { } /** - * Returns a Handle associated with specified label values. + * Returns a Handle associated with specified LabelSet. * It is recommended to keep a reference to the Handle instead of always * calling this method for each operation. - * @param labelValues the list of label values. + * @param labelSet the canonicalized LabelSet used to associate with this metric handle. */ - getHandle(labelValues: string[]): T { - const hash = hashLabelValues(labelValues); - if (this._handles.has(hash)) return this._handles.get(hash)!; + getHandle(labelSet: types.LabelSet): T { + if (this._handles.has(labelSet.identifier)) + return this._handles.get(labelSet.identifier)!; - const handle = this._makeHandle(labelValues); - - this._handles.set(hash, handle); + const handle = this._makeHandle(labelSet); + this._handles.set(labelSet.identifier, handle); return handle; } @@ -73,10 +71,10 @@ export abstract class Metric implements types.Metric { /** * Removes the Handle from the metric, if it is present. - * @param labelValues the list of label values. + * @param labelSet the canonicalized LabelSet used to associate with this metric handle. */ - removeHandle(labelValues: string[]): void { - this._handles.delete(hashLabelValues(labelValues)); + removeHandle(labelSet: types.LabelSet): void { + this._handles.delete(labelSet.identifier); } /** @@ -119,7 +117,7 @@ export abstract class Metric implements types.Metric { }; } - protected abstract _makeHandle(labelValues: string[]): T; + protected abstract _makeHandle(labelSet: types.LabelSet): T; } /** This is a SDK implementation of Counter Metric. */ @@ -137,12 +135,12 @@ export class CounterMetric extends Metric { : MetricDescriptorType.COUNTER_INT64 ); } - protected _makeHandle(labelValues: string[]): CounterHandle { + protected _makeHandle(labelSet: types.LabelSet): CounterHandle { return new CounterHandle( + labelSet, this._disabled, this._monotonic, this._valueType, - labelValues, this._logger, this._onUpdate ); @@ -164,12 +162,12 @@ export class GaugeMetric extends Metric { : MetricDescriptorType.GAUGE_INT64 ); } - protected _makeHandle(labelValues: string[]): GaugeHandle { + protected _makeHandle(labelSet: types.LabelSet): GaugeHandle { return new GaugeHandle( + labelSet, this._disabled, this._monotonic, this._valueType, - labelValues, this._logger, this._onUpdate ); diff --git a/packages/opentelemetry-metrics/src/Utils.ts b/packages/opentelemetry-metrics/src/Utils.ts index 70a338fcaa..e2c8fba463 100644 --- a/packages/opentelemetry-metrics/src/Utils.ts +++ b/packages/opentelemetry-metrics/src/Utils.ts @@ -14,18 +14,6 @@ * limitations under the License. */ -const COMMA_SEPARATOR = ','; - -/** - * Returns a string(comma separated) from the list of label values. - * - * @param labelValues The list of the label values. - * @returns The hashed label values string. - */ -export function hashLabelValues(labelValues: string[]): string { - return labelValues.sort().join(COMMA_SEPARATOR); -} - /** * Type guard to remove nulls from arrays * diff --git a/packages/opentelemetry-metrics/src/export/types.ts b/packages/opentelemetry-metrics/src/export/types.ts index 3f41cedd7d..7b2c114da8 100644 --- a/packages/opentelemetry-metrics/src/export/types.ts +++ b/packages/opentelemetry-metrics/src/export/types.ts @@ -124,7 +124,7 @@ export enum MetricDescriptorType { 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 label keys in the + * all points. The order of label values must match that of LabelSet keys in the * metric descriptor. */ readonly labelValues: LabelValue[]; diff --git a/packages/opentelemetry-metrics/test/Meter.test.ts b/packages/opentelemetry-metrics/test/Meter.test.ts index 1d70f165a0..06fd599abc 100644 --- a/packages/opentelemetry-metrics/test/Meter.test.ts +++ b/packages/opentelemetry-metrics/test/Meter.test.ts @@ -23,6 +23,7 @@ import { MetricDescriptorType, } from '../src'; import * as types from '@opentelemetry/types'; +import { LabelSet } from '../src/LabelSet'; import { NoopLogger, NoopMetric, @@ -35,13 +36,28 @@ const performanceTimeOrigin = hrTime(); describe('Meter', () => { let meter: Meter; - const labelValues = ['value']; + const keya = 'keya'; + const keyb = 'keyb'; + let labels: types.Labels = { [keyb]: 'value2', [keya]: 'value1' }; + let labelSet: types.LabelSet; const hrTime: types.HrTime = [22, 400000000]; beforeEach(() => { meter = new Meter({ logger: new NoopLogger(), }); + labelSet = meter.labels(labels); + }); + + describe('#meter', () => { + it('should re-order labels to a canonicalized set', () => { + const orderedLabels: types.Labels = { + [keya]: 'value1', + [keyb]: 'value2', + }; + const identifier = '|#keya:value1,keyb:value2'; + assert.deepEqual(labelSet, new LabelSet(identifier, orderedLabels)); + }); }); describe('#counter', () => { @@ -63,7 +79,7 @@ describe('Meter', () => { describe('.getHandle()', () => { it('should create a counter handle', () => { const counter = meter.createCounter('name') as CounterMetric; - const handle = counter.getHandle(labelValues); + const handle = counter.getHandle(labelSet); handle.add(10); assert.strictEqual(handle['_data'], 10); handle.add(10); @@ -72,7 +88,7 @@ describe('Meter', () => { it('should return the timeseries', () => { const counter = meter.createCounter('name') as CounterMetric; - const handle = counter.getHandle(['value1', 'value2']); + const handle = counter.getHandle(labelSet); handle.add(20); assert.deepStrictEqual(handle.getTimeSeries(hrTime), { labelValues: [{ value: 'value1' }, { value: 'value2' }], @@ -82,7 +98,7 @@ describe('Meter', () => { it('should add positive values by default', () => { const counter = meter.createCounter('name') as CounterMetric; - const handle = counter.getHandle(labelValues); + const handle = counter.getHandle(labelSet); handle.add(10); assert.strictEqual(handle['_data'], 10); handle.add(-100); @@ -93,7 +109,7 @@ describe('Meter', () => { const counter = meter.createCounter('name', { disabled: true, }) as CounterMetric; - const handle = counter.getHandle(labelValues); + const handle = counter.getHandle(labelSet); handle.add(10); assert.strictEqual(handle['_data'], 0); }); @@ -102,16 +118,16 @@ describe('Meter', () => { const counter = meter.createCounter('name', { monotonic: false, }) as CounterMetric; - const handle = counter.getHandle(labelValues); + const handle = counter.getHandle(labelSet); handle.add(-10); assert.strictEqual(handle['_data'], -10); }); it('should return same handle on same label values', () => { const counter = meter.createCounter('name') as CounterMetric; - const handle = counter.getHandle(labelValues); + const handle = counter.getHandle(labelSet); handle.add(10); - const handle1 = counter.getHandle(labelValues); + const handle1 = counter.getHandle(labelSet); handle1.add(10); assert.strictEqual(handle['_data'], 20); assert.strictEqual(handle, handle1); @@ -121,23 +137,23 @@ describe('Meter', () => { describe('.removeHandle()', () => { it('should remove a counter handle', () => { const counter = meter.createCounter('name') as CounterMetric; - const handle = counter.getHandle(labelValues); + const handle = counter.getHandle(labelSet); assert.strictEqual(counter['_handles'].size, 1); - counter.removeHandle(labelValues); + counter.removeHandle(labelSet); assert.strictEqual(counter['_handles'].size, 0); - const handle1 = counter.getHandle(labelValues); + const handle1 = counter.getHandle(labelSet); assert.strictEqual(counter['_handles'].size, 1); assert.notStrictEqual(handle, handle1); }); it('should not fail when removing non existing handle', () => { const counter = meter.createCounter('name'); - counter.removeHandle([]); + counter.removeHandle(new LabelSet('', {})); }); it('should clear all handles', () => { const counter = meter.createCounter('name') as CounterMetric; - counter.getHandle(labelValues); + counter.getHandle(labelSet); assert.strictEqual(counter['_handles'].size, 1); counter.clear(); assert.strictEqual(counter['_handles'].size, 0); @@ -147,13 +163,13 @@ describe('Meter', () => { describe('.registerMetric()', () => { it('skip already registered Metric', () => { const counter1 = meter.createCounter('name1') as CounterMetric; - counter1.getHandle(labelValues).add(10); + counter1.getHandle(labelSet).add(10); // should skip below metric const counter2 = meter.createCounter('name1', { valueType: types.ValueType.INT, }) as CounterMetric; - counter2.getHandle(labelValues).add(500); + counter2.getHandle(labelSet).add(500); assert.strictEqual(meter.getMetrics().length, 1); const [{ descriptor, timeseries }] = meter.getMetrics(); @@ -216,7 +232,7 @@ describe('Meter', () => { describe('.getHandle()', () => { it('should create a gauge handle', () => { const gauge = meter.createGauge('name') as GaugeMetric; - const handle = gauge.getHandle(labelValues); + const handle = gauge.getHandle(labelSet); handle.set(10); assert.strictEqual(handle['_data'], 10); handle.set(250); @@ -225,7 +241,11 @@ describe('Meter', () => { it('should return the timeseries', () => { const gauge = meter.createGauge('name') as GaugeMetric; - const handle = gauge.getHandle(['v1', 'v2']); + const k1 = 'k1'; + const k2 = 'k2'; + const labels = { [k1]: 'v1', [k2]: 'v2' }; + const LabelSet2 = new LabelSet('|#k1:v1,k2:v2', labels); + const handle = gauge.getHandle(LabelSet2); handle.set(150); assert.deepStrictEqual(handle.getTimeSeries(hrTime), { labelValues: [{ value: 'v1' }, { value: 'v2' }], @@ -235,7 +255,7 @@ describe('Meter', () => { it('should go up and down by default', () => { const gauge = meter.createGauge('name') as GaugeMetric; - const handle = gauge.getHandle(labelValues); + const handle = gauge.getHandle(labelSet); handle.set(10); assert.strictEqual(handle['_data'], 10); handle.set(-100); @@ -246,7 +266,7 @@ describe('Meter', () => { const gauge = meter.createGauge('name', { disabled: true, }) as GaugeMetric; - const handle = gauge.getHandle(labelValues); + const handle = gauge.getHandle(labelSet); handle.set(10); assert.strictEqual(handle['_data'], 0); }); @@ -255,16 +275,16 @@ describe('Meter', () => { const gauge = meter.createGauge('name', { monotonic: true, }) as GaugeMetric; - const handle = gauge.getHandle(labelValues); + const handle = gauge.getHandle(labelSet); handle.set(-10); assert.strictEqual(handle['_data'], 0); }); it('should return same handle on same label values', () => { const gauge = meter.createGauge('name') as GaugeMetric; - const handle = gauge.getHandle(labelValues); + const handle = gauge.getHandle(labelSet); handle.set(10); - const handle1 = gauge.getHandle(labelValues); + const handle1 = gauge.getHandle(labelSet); handle1.set(10); assert.strictEqual(handle['_data'], 10); assert.strictEqual(handle, handle1); @@ -274,23 +294,23 @@ describe('Meter', () => { describe('.removeHandle()', () => { it('should remove the gauge handle', () => { const gauge = meter.createGauge('name') as GaugeMetric; - const handle = gauge.getHandle(labelValues); + const handle = gauge.getHandle(labelSet); assert.strictEqual(gauge['_handles'].size, 1); - gauge.removeHandle(labelValues); + gauge.removeHandle(labelSet); assert.strictEqual(gauge['_handles'].size, 0); - const handle1 = gauge.getHandle(labelValues); + const handle1 = gauge.getHandle(labelSet); assert.strictEqual(gauge['_handles'].size, 1); assert.notStrictEqual(handle, handle1); }); it('should not fail when removing non existing handle', () => { const gauge = meter.createGauge('name'); - gauge.removeHandle([]); + gauge.removeHandle(new LabelSet('', {})); }); it('should clear all handles', () => { const gauge = meter.createGauge('name') as GaugeMetric; - gauge.getHandle(labelValues); + gauge.getHandle(labelSet); assert.strictEqual(gauge['_handles'].size, 1); gauge.clear(); assert.strictEqual(gauge['_handles'].size, 0); @@ -349,11 +369,13 @@ describe('Meter', () => { describe('#getMetrics', () => { it('should create a DOUBLE counter', () => { + const key = 'key'; const counter = meter.createCounter('counter', { description: 'test', - labelKeys: ['key'], + labelKeys: [key], }); - const handle = counter.getHandle(['counter-value']); + const labelSet = meter.labels({ [key]: 'counter-value' }); + const handle = counter.getHandle(labelSet); handle.add(10.45); assert.strictEqual(meter.getMetrics().length, 1); @@ -377,12 +399,14 @@ describe('Meter', () => { }); it('should create a INT counter', () => { + const key = 'key'; const counter = meter.createCounter('counter', { description: 'test', - labelKeys: ['key'], + labelKeys: [key], valueType: types.ValueType.INT, }); - const handle = counter.getHandle(['counter-value']); + const labelSet = meter.labels({ [key]: 'counter-value' }); + const handle = counter.getHandle(labelSet); handle.add(10.45); assert.strictEqual(meter.getMetrics().length, 1); @@ -406,12 +430,15 @@ describe('Meter', () => { }); it('should create a DOUBLE gauge', () => { + const key = 'gauge-key'; const gauge = meter.createGauge('gauge', { - labelKeys: ['gauge-key'], + labelKeys: [key], unit: 'ms', }); - gauge.getHandle(['gauge-value1']).set(200.34); - gauge.getHandle(['gauge-value2']).set(-10.67); + const labelSet1 = meter.labels({ [key]: 'gauge-value1' }); + const labelSet2 = meter.labels({ [key]: 'gauge-value2' }); + gauge.getHandle(labelSet1).set(200.34); + gauge.getHandle(labelSet2).set(-10.67); assert.strictEqual(meter.getMetrics().length, 1); const [{ descriptor, timeseries }] = meter.getMetrics(); @@ -444,13 +471,16 @@ describe('Meter', () => { }); it('should create a INT gauge', () => { + const key = 'gauge-key'; const gauge = meter.createGauge('gauge', { - labelKeys: ['gauge-key'], + labelKeys: [key], unit: 'ms', valueType: types.ValueType.INT, }); - gauge.getHandle(['gauge-value1']).set(200.34); - gauge.getHandle(['gauge-value2']).set(-10.67); + const labelSet1 = meter.labels({ [key]: 'gauge-value1' }); + const labelSet2 = meter.labels({ [key]: 'gauge-value2' }); + gauge.getHandle(labelSet1).set(200.34); + gauge.getHandle(labelSet2).set(-10.67); assert.strictEqual(meter.getMetrics().length, 1); const [{ descriptor, timeseries }] = meter.getMetrics(); @@ -508,8 +538,8 @@ describe('Meter', () => { meter.addExporter(exporter); const gauge = meter.createGauge('name') as GaugeMetric; - const handle = gauge.getHandle(['value1', 'value2']); - handle.set(20); + const labelSet = meter.labels({ value: 'value1', value2: 'value2' }); + gauge.getHandle(labelSet).set(20); }); it('should export a counter when it is updated', done => { @@ -530,8 +560,8 @@ describe('Meter', () => { }); meter.addExporter(exporter); - const handle = counter.getHandle(['value1', 'value2']); - handle.add(20); + const labelSet = meter.labels({ value: 'value1', value2: 'value2' }); + counter.getHandle(labelSet).add(20); }); }); }); diff --git a/packages/opentelemetry-types/src/metrics/Meter.ts b/packages/opentelemetry-types/src/metrics/Meter.ts index 15f03688b6..f9edeaabdd 100644 --- a/packages/opentelemetry-types/src/metrics/Meter.ts +++ b/packages/opentelemetry-types/src/metrics/Meter.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Metric, MetricOptions } from './Metric'; +import { Metric, MetricOptions, Labels, LabelSet } from './Metric'; import { CounterHandle, GaugeHandle, MeasureHandle } from './Handle'; /** @@ -58,4 +58,12 @@ export interface Meter { * @param [options] the metric options. */ createGauge(name: string, options?: MetricOptions): Metric; + + /** + * Provide a pre-computed re-useable LabelSet by + * converting the unordered labels into a canonicalized + * set of lables with an unique identifier, useful for pre-aggregation. + * @param labels user provided unordered Labels. + */ + labels(labels: Labels): LabelSet; } diff --git a/packages/opentelemetry-types/src/metrics/Metric.ts b/packages/opentelemetry-types/src/metrics/Metric.ts index f27633e9a4..0c011d3811 100644 --- a/packages/opentelemetry-types/src/metrics/Metric.ts +++ b/packages/opentelemetry-types/src/metrics/Metric.ts @@ -70,12 +70,12 @@ export enum ValueType { */ export interface Metric { /** - * Returns a Handle associated with specified label values. + * Returns a Handle associated with specified LabelSet. * It is recommended to keep a reference to the Handle instead of always * calling this method for every operations. - * @param labelValues the list of label values. + * @param labels the canonicalized LabelSet used to associate with this metric handle. */ - getHandle(labelValues: string[]): T; + getHandle(labels: LabelSet): T; /** * Returns a Handle for a metric with all labels not set. @@ -84,9 +84,9 @@ export interface Metric { /** * Removes the Handle from the metric, if it is present. - * @param labelValues the list of label values. + * @param labels the canonicalized LabelSet used to associate with this metric handle. */ - removeHandle(labelValues: string[]): void; + removeHandle(labels: LabelSet): void; /** * Clears all timeseries from the Metric. @@ -98,3 +98,16 @@ export interface Metric { */ setCallback(fn: () => void): void; } + +/** + * key-value pairs passed by the user. + */ +export type Labels = Record; + +/** + * Canonicalized labels with an unique string identifier. + */ +export interface LabelSet { + identifier: string; + labels: Labels; +}