From c09f29947323a3d31f0ef339de6ba31eee3a82db Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Sat, 25 Nov 2023 22:05:48 -0500 Subject: [PATCH 1/7] add `defaultLabels` as an option for all metrics --- lib/counter.js | 12 ++-- lib/gauge.js | 12 ++-- lib/histogram.js | 34 ++++++--- lib/metric.js | 14 +++- lib/summary.js | 22 ++++-- test/counterTest.js | 105 +++++++++++++++++++++++++++ test/gaugeTest.js | 160 +++++++++++++++++++++++++++++++++++++++++ test/histogramTest.js | 162 ++++++++++++++++++++++++++++++++++++++++++ test/summaryTest.js | 137 +++++++++++++++++++++++++++++++++++ 9 files changed, 633 insertions(+), 25 deletions(-) diff --git a/lib/counter.js b/lib/counter.js index 22a440ec..26fab532 100644 --- a/lib/counter.js +++ b/lib/counter.js @@ -19,7 +19,6 @@ class Counter extends Metric { constructor(config) { super(config); this.type = 'counter'; - this.defaultLabels = {}; this.defaultValue = 1; this.defaultExemplarLabelSet = {}; if (config.enableExemplars) { @@ -40,11 +39,12 @@ class Counter extends Metric { incWithoutExemplar(labels, value) { let hash = ''; if (isObject(labels)) { + labels = { ...this.defaultLabels, ...labels }; hash = hashObject(labels, this.sortedLabelNames); validateLabel(this.labelNames, labels); } else { value = labels; - labels = {}; + labels = this.defaultLabels; } if (value && !Number.isFinite(value)) { @@ -76,10 +76,11 @@ class Counter extends Metric { * @returns {void} */ incWithExemplar({ - labels = this.defaultLabels, + labels, value = this.defaultValue, exemplarLabels = this.defaultExemplarLabelSet, } = {}) { + labels = { ...this.defaultLabels, ...labels }; const res = this.incWithoutExemplar(labels, value); this.updateExemplar(exemplarLabels, value, res.labelHash); } @@ -128,8 +129,9 @@ class Counter extends Metric { } remove(...args) { - const labels = getLabels(this.labelNames, args) || {}; - validateLabel(this.labelNames, labels); + const labelsArgs = getLabels(this.labelNames, args); + validateLabel(this.labelNames, labelsArgs); + const labels = { ...this.defaultLabels, ...labelsArgs }; return removeLabels.call(this, this.hashMap, labels, this.sortedLabelNames); } } diff --git a/lib/gauge.js b/lib/gauge.js index 3725bf01..43b1564f 100644 --- a/lib/gauge.js +++ b/lib/gauge.js @@ -30,7 +30,7 @@ class Gauge extends Metric { */ set(labels, value) { value = getValueArg(labels, value); - labels = getLabelArg(labels); + labels = { ...this.defaultLabels, ...getLabelArg(labels) }; set(this, labels, value); } @@ -53,7 +53,7 @@ class Gauge extends Metric { */ inc(labels, value) { value = getValueArg(labels, value); - labels = getLabelArg(labels); + labels = { ...this.defaultLabels, ...getLabelArg(labels) }; if (value === undefined) value = 1; setDelta(this, labels, value); } @@ -66,7 +66,7 @@ class Gauge extends Metric { */ dec(labels, value) { value = getValueArg(labels, value); - labels = getLabelArg(labels); + labels = { ...this.defaultLabels, ...getLabelArg(labels) }; if (value === undefined) value = 1; setDelta(this, labels, -value); } @@ -120,6 +120,7 @@ class Gauge extends Metric { } _getValue(labels) { + labels = { ...this.defaultLabels, ...labels }; const hash = hashObject(labels || {}, this.sortedLabelNames); return this.hashMap[hash] ? this.hashMap[hash].value : 0; } @@ -137,8 +138,9 @@ class Gauge extends Metric { } remove(...args) { - const labels = getLabels(this.labelNames, args); - validateLabel(this.labelNames, labels); + const labelsArgs = getLabels(this.labelNames, args); + validateLabel(this.labelNames, labelsArgs); + const labels = { ...this.defaultLabels, ...labelsArgs }; removeLabels.call(this, this.hashMap, labels, this.sortedLabelNames); } } diff --git a/lib/histogram.js b/lib/histogram.js index 723e0192..653615a5 100644 --- a/lib/histogram.js +++ b/lib/histogram.js @@ -22,7 +22,6 @@ class Histogram extends Metric { }); this.type = 'histogram'; - this.defaultLabels = {}; this.defaultExemplarLabelSet = {}; this.enableExemplars = false; @@ -72,19 +71,28 @@ class Histogram extends Metric { * @returns {void} */ observeWithoutExemplar(labels, value) { - observe.call(this, labels === 0 ? 0 : labels || {})(value); + labels = isObject(labels) + ? { ...this.defaultLabels, ...labels } + : labels ?? this.defaultLabels; + observe.call(this, labels)(value); } observeWithExemplar({ - labels = this.defaultLabels, + labels, value, exemplarLabels = this.defaultExemplarLabelSet, } = {}) { - observe.call(this, labels === 0 ? 0 : labels || {})(value); + labels = isObject(labels) + ? { ...this.defaultLabels, ...labels } + : labels ?? this.defaultLabels; + observe.call(this, labels)(value); this.updateExemplar(labels, value, exemplarLabels); } updateExemplar(labels, value, exemplarLabels) { + labels = isObject(labels) + ? { ...this.defaultLabels, ...labels } + : labels ?? this.defaultLabels; const hash = hashObject(labels, this.sortedLabelNames); const bound = findBound(this.upperBounds, value); const { bucketExemplars } = this.hashMap[hash]; @@ -134,6 +142,9 @@ class Histogram extends Metric { * @returns {void} */ zero(labels) { + labels = isObject(labels) + ? { ...this.defaultLabels, ...labels } + : labels ?? this.defaultLabels; const hash = hashObject(labels, this.sortedLabelNames); this.hashMap[hash] = createBaseValues( labels, @@ -155,14 +166,20 @@ class Histogram extends Metric { * }); */ startTimer(labels, exemplarLabels) { + labels = isObject(labels) + ? { ...this.defaultLabels, ...labels } + : labels ?? this.defaultLabels; return this.enableExemplars ? startTimerWithExemplar.call(this, labels, exemplarLabels)() : startTimer.call(this, labels)(); } labels(...args) { - const labels = getLabels(this.labelNames, args); - validateLabel(this.labelNames, labels); + const labelsArg = getLabels(this.labelNames, args); + validateLabel(this.labelNames, labelsArg); + const labels = isObject(labelsArg) + ? { ...this.defaultLabels, ...labelsArg } + : labelsArg ?? this.defaultLabels; return { observe: observe.call(this, labels), startTimer: startTimer.call(this, labels), @@ -170,8 +187,9 @@ class Histogram extends Metric { } remove(...args) { - const labels = getLabels(this.labelNames, args); - validateLabel(this.labelNames, labels); + const labelsArgs = getLabels(this.labelNames, args); + validateLabel(this.labelNames, labelsArgs); + const labels = { ...this.defaultLabels, ...labelsArgs }; removeLabels.call(this, this.hashMap, labels, this.sortedLabelNames); } } diff --git a/lib/metric.js b/lib/metric.js index a2bdf304..08c8b0a8 100644 --- a/lib/metric.js +++ b/lib/metric.js @@ -2,7 +2,11 @@ const Registry = require('./registry'); const { isObject } = require('./util'); -const { validateMetricName, validateLabelName } = require('./validation'); +const { + validateMetricName, + validateLabelName, + validateLabel, +} = require('./validation'); /** * @abstract @@ -19,6 +23,7 @@ class Metric { registers: [Registry.globalRegistry], aggregator: 'sum', enableExemplars: false, + defaultLabels: {}, }, defaults, config, @@ -39,6 +44,13 @@ class Metric { if (!validateLabelName(this.labelNames)) { throw new Error('Invalid label name'); } + try { + validateLabel(this.labelNames, this.defaultLabels); + } catch (cause) { + const error = new Error('Invalid default label values'); + error.cause = cause; + throw error; + } if (this.collect && typeof this.collect !== 'function') { throw new Error('Optional "collect" parameter must be a function'); diff --git a/lib/summary.js b/lib/summary.js index 28fdb07a..8731a44d 100644 --- a/lib/summary.js +++ b/lib/summary.js @@ -4,7 +4,7 @@ 'use strict'; const util = require('util'); -const { getLabels, hashObject, removeLabels } = require('./util'); +const { getLabels, hashObject, removeLabels, isObject } = require('./util'); const { validateLabel } = require('./validation'); const { Metric } = require('./metric'); const timeWindowQuantiles = require('./timeWindowQuantiles'); @@ -45,7 +45,10 @@ class Summary extends Metric { * @returns {void} */ observe(labels, value) { - observe.call(this, labels === 0 ? 0 : labels || {})(value); + labels = isObject(labels) + ? { ...this.defaultLabels, ...labels } + : labels ?? this.defaultLabels; + observe.call(this, labels)(value); } async get() { @@ -100,12 +103,18 @@ class Summary extends Metric { * }); */ startTimer(labels) { + labels = isObject(labels) + ? { ...this.defaultLabels, ...labels } + : labels ?? this.defaultLabels; return startTimer.call(this, labels)(); } labels(...args) { - const labels = getLabels(this.labelNames, args); - validateLabel(this.labelNames, labels); + const labelsArg = getLabels(this.labelNames, args); + validateLabel(this.labelNames, labelsArg); + const labels = isObject(labelsArg) + ? { ...this.defaultLabels, ...labelsArg } + : labelsArg ?? this.defaultLabels; return { observe: observe.call(this, labels), startTimer: startTimer.call(this, labels), @@ -113,8 +122,9 @@ class Summary extends Metric { } remove(...args) { - const labels = getLabels(this.labelNames, args); - validateLabel(this.labelNames, labels); + const labelsArgs = getLabels(this.labelNames, args); + validateLabel(this.labelNames, labelsArgs); + const labels = { ...this.defaultLabels, ...labelsArgs }; removeLabels.call(this, this.hashMap, labels, this.sortedLabelNames); } } diff --git a/test/counterTest.js b/test/counterTest.js index 0ee0fc2d..66817255 100644 --- a/test/counterTest.js +++ b/test/counterTest.js @@ -8,6 +8,7 @@ describe.each([ ])('counter with %s registry', (tag, regType) => { const Counter = require('../index').Counter; const globalRegistry = require('../index').register; + /** @type {Counter} */ let instance; beforeEach(() => { @@ -104,6 +105,86 @@ describe.each([ expect(values[0].value).toEqual(100); }); }); + + describe('default label values', () => { + beforeEach(() => { + instance = new Counter({ + name: 'counter_test', + help: 'help', + labelNames: ['method', 'endpoint', 'protocol'], + defaultLabels: { + protocol: 'https', + }, + }); + }); + + it("then throws an error on construction if labels don't match up", () => { + expect.assertions(4); + try { + new Counter({ + name: 'counter_test_2', + help: 'help', + labelNames: ['method', 'endpoint', 'protocol'], + defaultLabels: { + a_bad_label: 'oh noooo', + }, + }); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error.message).toEqual('Invalid default label values'); + expect(error.cause).toBeInstanceOf(Error); + expect(error.cause.message).toEqual( + "Added label \"a_bad_label\" is not included in initial labelset: [ 'method', 'endpoint', 'protocol' ]", + ); + } + }); + + describe('inc', () => { + it('should increment label value with provided value plus any default labels', async () => { + instance.labels({ method: 'GET', endpoint: '/test' }).inc(100); + const values = (await instance.get()).values; + expect(values[0].value).toEqual(100); + expect(values[0].labels).toEqual({ + method: 'GET', + endpoint: '/test', + protocol: 'https', + }); + }); + + it('allows specifying value for default label', async () => { + instance.labels('GET', '/test', 'http').inc(100); + const values = (await instance.get()).values; + expect(values[0].value).toEqual(100); + expect(values[0].labels).toEqual({ + method: 'GET', + endpoint: '/test', + protocol: 'http', + }); + }); + }); + + describe('remove', () => { + it('then removes without specifying default labels', async () => { + instance.labels({ method: 'GET', endpoint: '/test' }).inc(100); + instance.labels({ method: 'POST', endpoint: '/test' }).inc(100); + + instance.remove({ method: 'POST', endpoint: '/test' }); + + const values = (await instance.get()).values; + expect(values.length).toEqual(1); + }); + + it('then removes with specifying default labels', async () => { + instance.labels({ method: 'GET', endpoint: '/test' }).inc(100); + instance.labels({ method: 'POST', endpoint: '/test' }).inc(100); + + instance.remove('POST', '/test', 'https'); + + const values = (await instance.get()).values; + expect(values.length).toEqual(1); + }); + }); + }); }); describe('remove', () => { @@ -231,5 +312,29 @@ describe.each([ expect((await instance.get()).values[0].labels.serial).toEqual('12345'); expect((await instance.get()).values[0].labels.active).toEqual('no'); }); + it('should reset the counter, incl default labels', async () => { + const instance = new Counter({ + name: 'test_metric', + help: 'Another test metric', + labelNames: ['serial', 'active', 'color'], + defaultLabels: { color: 'red' }, + }); + + instance.inc({ serial: '12345', active: 'yes' }, 12); + expect((await instance.get()).values[0].value).toEqual(12); + expect((await instance.get()).values[0].labels.serial).toEqual('12345'); + expect((await instance.get()).values[0].labels.active).toEqual('yes'); + expect((await instance.get()).values[0].labels.color).toEqual('red'); + + instance.inc({ serial: '12345', active: 'yes', color: 'blue' }, 12); + expect((await instance.get()).values[1].value).toEqual(12); + expect((await instance.get()).values[1].labels.serial).toEqual('12345'); + expect((await instance.get()).values[1].labels.active).toEqual('yes'); + expect((await instance.get()).values[1].labels.color).toEqual('blue'); + + instance.reset(); + + expect((await instance.get()).values).toEqual([]); + }); }); }); diff --git a/test/gaugeTest.js b/test/gaugeTest.js index c7d9ec59..a3403f2c 100644 --- a/test/gaugeTest.js +++ b/test/gaugeTest.js @@ -8,6 +8,7 @@ describe.each([ ])('gauge with %s registry', (tag, regType) => { const Gauge = require('../index').Gauge; const globalRegistry = require('../index').register; + /** @type { Gauge } */ let instance; beforeEach(() => { @@ -168,6 +169,141 @@ describe.each([ }); }); + describe('default label values', () => { + beforeEach(() => { + instance = new Gauge({ + name: 'gauge_test_2', + help: 'help', + labelNames: ['method', 'endpoint', 'protocol'], + defaultLabels: { + protocol: 'https', + }, + }); + instance.setToCurrentTime; + instance.startTimer; + instance.labels; + }); + + it("then throws an error on construction if labels don't match up", () => { + expect.assertions(4); + try { + new Gauge({ + name: 'gauge_test_2', + help: 'help', + labelNames: ['method', 'endpoint', 'protocol'], + defaultLabels: { + a_bad_label: 'oh noooo', + }, + }); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error.message).toEqual('Invalid default label values'); + expect(error.cause).toBeInstanceOf(Error); + expect(error.cause.message).toEqual( + "Added label \"a_bad_label\" is not included in initial labelset: [ 'method', 'endpoint', 'protocol' ]", + ); + } + }); + + describe('inc', () => { + it('should increment label value with provided value plus any default labels', async () => { + instance.labels({ method: 'GET', endpoint: '/test' }).inc(100); + const values = (await instance.get()).values; + expect(values[0].value).toEqual(100); + expect(values[0].labels).toEqual({ + method: 'GET', + endpoint: '/test', + protocol: 'https', + }); + }); + + it('allows specifying value for default label', async () => { + instance.labels('GET', '/test', 'http').inc(100); + const values = (await instance.get()).values; + expect(values[0].value).toEqual(100); + expect(values[0].labels).toEqual({ + method: 'GET', + endpoint: '/test', + protocol: 'http', + }); + }); + }); + + describe('set', () => { + it('should set label value with provided value plus any default labels', async () => { + instance.labels({ method: 'GET', endpoint: '/test' }).inc(100); + instance.labels({ method: 'GET', endpoint: '/test' }).set(12); + const values = (await instance.get()).values; + expect(values[0].value).toEqual(12); + expect(values[0].labels).toEqual({ + method: 'GET', + endpoint: '/test', + protocol: 'https', + }); + }); + + it('allows specifying value for default label', async () => { + instance.labels('GET', '/test', 'http').inc(100); + instance.labels('GET', '/test', 'http').set(12); + const values = (await instance.get()).values; + expect(values[0].value).toEqual(12); + expect(values[0].labels).toEqual({ + method: 'GET', + endpoint: '/test', + protocol: 'http', + }); + }); + }); + + describe('dec', () => { + it('should increment label value with provided value plus any default labels', async () => { + instance.labels({ method: 'GET', endpoint: '/test' }).inc(100); + instance.labels({ method: 'GET', endpoint: '/test' }).dec(50); + const values = (await instance.get()).values; + expect(values[0].value).toEqual(50); + expect(values[0].labels).toEqual({ + method: 'GET', + endpoint: '/test', + protocol: 'https', + }); + }); + + it('allows specifying value for default label', async () => { + instance.labels('GET', '/test', 'http').inc(100); + instance.labels('GET', '/test', 'http').dec(50); + const values = (await instance.get()).values; + expect(values[0].value).toEqual(50); + expect(values[0].labels).toEqual({ + method: 'GET', + endpoint: '/test', + protocol: 'http', + }); + }); + }); + + describe('remove', () => { + it('then removes without specifying default labels', async () => { + instance.labels({ method: 'GET', endpoint: '/test' }).inc(100); + instance.labels({ method: 'POST', endpoint: '/test' }).inc(100); + + instance.remove({ method: 'POST', endpoint: '/test' }); + + const values = (await instance.get()).values; + expect(values.length).toEqual(1); + }); + + it('then removes with specifying default labels', async () => { + instance.labels({ method: 'GET', endpoint: '/test' }).inc(100); + instance.labels({ method: 'POST', endpoint: '/test' }).inc(100); + + instance.remove('POST', '/test', 'https'); + + const values = (await instance.get()).values; + expect(values.length).toEqual(1); + }); + }); + }); + describe('with remove', () => { beforeEach(() => { instance = new Gauge({ @@ -270,6 +406,30 @@ describe.each([ expect((await instance.get()).values[0].labels.serial).toEqual('12345'); expect((await instance.get()).values[0].labels.active).toEqual('no'); }); + it('should reset the gauge, incl default labels', async () => { + const instance = new Gauge({ + name: 'test_metric', + help: 'Another test metric', + labelNames: ['serial', 'active', 'color'], + defaultLabels: { color: 'red' }, + }); + + instance.inc({ serial: '12345', active: 'yes' }, 12); + expect((await instance.get()).values[0].value).toEqual(12); + expect((await instance.get()).values[0].labels.serial).toEqual('12345'); + expect((await instance.get()).values[0].labels.active).toEqual('yes'); + expect((await instance.get()).values[0].labels.color).toEqual('red'); + + instance.inc({ serial: '12345', active: 'yes', color: 'blue' }, 12); + expect((await instance.get()).values[1].value).toEqual(12); + expect((await instance.get()).values[1].labels.serial).toEqual('12345'); + expect((await instance.get()).values[1].labels.active).toEqual('yes'); + expect((await instance.get()).values[1].labels.color).toEqual('blue'); + + instance.reset(); + + expect((await instance.get()).values).toEqual([]); + }); }); async function expectValue(val) { diff --git a/test/histogramTest.js b/test/histogramTest.js index 5ec89f97..447ad5ee 100644 --- a/test/histogramTest.js +++ b/test/histogramTest.js @@ -8,6 +8,7 @@ describe.each([ ])('histogram with %s registry', (tag, regType) => { const Histogram = require('../index').Histogram; const globalRegistry = require('../index').register; + /** @type {Histogram} */ let instance; beforeEach(() => { @@ -422,6 +423,167 @@ describe.each([ }); }); + describe('default label values', () => { + beforeEach(() => { + instance = new Histogram({ + name: 'histogram_test', + help: 'help', + labelNames: ['method', 'endpoint', 'protocol'], + defaultLabels: { + protocol: 'https', + }, + }); + }); + + it("then throws an error on construction if labels don't match up", () => { + expect.assertions(4); + try { + new Histogram({ + name: 'histogram_test_2', + help: 'help', + labelNames: ['method', 'endpoint', 'protocol'], + defaultLabels: { + a_bad_label: 'oh noooo', + }, + }); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error.message).toEqual('Invalid default label values'); + expect(error.cause).toBeInstanceOf(Error); + expect(error.cause.message).toEqual( + "Added label \"a_bad_label\" is not included in initial labelset: [ 'method', 'endpoint', 'protocol' ]", + ); + } + }); + + describe('observe', () => { + it('should record label value with provided value plus any default labels', async () => { + instance.labels({ method: 'GET', endpoint: '/test' }).observe(1); + const res = getValueByLeAndLabel( + 5, + 'method', + 'GET', + (await instance.get()).values, + ); + expect(res.value).toEqual(1); + expect(res.labels).toEqual({ + le: 5, + method: 'GET', + endpoint: '/test', + protocol: 'https', + }); + }); + + it('allows specifying value for default label', async () => { + instance.labels('GET', '/test', 'http').observe(1); + const res = getValueByLeAndLabel( + 5, + 'method', + 'GET', + (await instance.get()).values, + ); + expect(res.value).toEqual(1); + expect(res.labels).toEqual({ + le: 5, + method: 'GET', + endpoint: '/test', + protocol: 'http', + }); + }); + }); + + describe('remove', () => { + it('then removes without specifying default labels', async () => { + instance.labels({ method: 'GET', endpoint: '/test' }).observe(1); + instance.labels({ method: 'POST', endpoint: '/test' }).observe(1); + + instance.remove({ method: 'POST', endpoint: '/test' }); + + const values = (await instance.get()).values; + const res = getValueByLeAndLabel( + 5, + 'method', + 'GET', + (await instance.get()).values, + ); + expect(res.value).toEqual(1); + expect(res.labels).toEqual({ + le: 5, + method: 'GET', + endpoint: '/test', + protocol: 'https', + }); + }); + + it('then removes with specifying default labels', async () => { + instance.labels({ method: 'GET', endpoint: '/test' }).observe(1); + instance.labels({ method: 'POST', endpoint: '/test' }).observe(1); + + instance.remove('POST', '/test', 'https'); + + const values = (await instance.get()).values; + const res = getValueByLeAndLabel( + 5, + 'method', + 'GET', + (await instance.get()).values, + ); + expect(res.value).toEqual(1); + expect(res.labels).toEqual({ + le: 5, + method: 'GET', + endpoint: '/test', + protocol: 'https', + }); + }); + }); + + describe('reset', () => { + it('should reset the histogram, incl default labels', async () => { + const instance = new Histogram({ + name: 'test_metric', + help: 'Another test metric', + labelNames: ['serial', 'active', 'color'], + defaultLabels: { color: 'red' }, + }); + + instance.observe({ serial: '12345', active: 'yes' }, 5); + instance.observe({ serial: '12345', active: 'yes' }, 3); + let res = getValueByLeAndLabel( + 5, + 'active', + 'yes', + (await instance.get()).values, + ); + expect(res.value).toEqual(2); + expect(res.labels).toEqual({ + le: 5, + active: 'yes', + color: 'red', + serial: '12345', + }); + + instance.observe( + { serial: '12345', active: 'yes', color: 'blue' }, + 4, + ); + res = getValueByLeAndLabel( + 5, + 'color', + 'blue', + (await instance.get()).values, + ); + expect(res.value).toEqual(1); + expect(res.labels).toEqual({ + le: 5, + active: 'yes', + color: 'blue', + serial: '12345', + }); + }); + }); + }); + describe('without registry', () => { beforeEach(() => { instance = new Histogram({ diff --git a/test/summaryTest.js b/test/summaryTest.js index 306c9b8b..ab5a9288 100644 --- a/test/summaryTest.js +++ b/test/summaryTest.js @@ -8,6 +8,7 @@ describe.each([ ])('summary with %s registry', (tag, regType) => { const Summary = require('../index').Summary; const globalRegistry = require('../index').register; + /** @type {Summary} */ let instance; beforeEach(() => { @@ -451,6 +452,142 @@ describe.each([ }); }); }); + + describe('default label values', () => { + beforeEach(() => { + instance = new Summary({ + name: 'summary_test_1', + help: 'help', + labelNames: ['method', 'endpoint', 'protocol'], + defaultLabels: { + protocol: 'https', + }, + }); + }); + + it("then throws an error on construction if labels don't match up", () => { + expect.assertions(4); + try { + new Summary({ + name: 'summary_test_2', + help: 'help', + labelNames: ['method', 'endpoint', 'protocol'], + defaultLabels: { + a_bad_label: 'oh noooo', + }, + }); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error.message).toEqual('Invalid default label values'); + expect(error.cause).toBeInstanceOf(Error); + expect(error.cause.message).toEqual( + "Added label \"a_bad_label\" is not included in initial labelset: [ 'method', 'endpoint', 'protocol' ]", + ); + } + }); + + describe('observe', () => { + it('should record label value with provided value plus any default labels', async () => { + instance.labels({ method: 'GET', endpoint: '/test' }).observe(1); + const values = (await instance.get()).values; + + expect(values[0].labels.quantile).toEqual(0.01); + expect(values[0].value).toEqual(1); + expect(values[0].labels).toEqual({ + quantile: 0.01, + method: 'GET', + endpoint: '/test', + protocol: 'https', + }); + }); + + it('allows specifying value for default label', async () => { + instance.labels('GET', '/test', 'http').observe(1); + const values = (await instance.get()).values; + + expect(values[0].labels.quantile).toEqual(0.01); + expect(values[0].value).toEqual(1); + expect(values[0].labels).toEqual({ + quantile: 0.01, + method: 'GET', + endpoint: '/test', + protocol: 'http', + }); + }); + }); + + describe('remove', () => { + it('then removes without specifying default labels', async () => { + instance.labels({ method: 'GET', endpoint: '/test' }).observe(1); + instance.labels({ method: 'POST', endpoint: '/test' }).observe(1); + + instance.remove({ method: 'POST', endpoint: '/test' }); + + const values = (await instance.get()).values; + expect(values[0].labels.quantile).toEqual(0.01); + expect(values[0].value).toEqual(1); + expect(values[0].labels).toEqual({ + quantile: 0.01, + method: 'GET', + endpoint: '/test', + protocol: 'https', + }); + }); + + it('then removes with specifying default labels', async () => { + instance.labels({ method: 'GET', endpoint: '/test' }).observe(1); + instance.labels({ method: 'POST', endpoint: '/test' }).observe(1); + + instance.remove('POST', '/test', 'https'); + + const values = (await instance.get()).values; + expect(values[0].labels.quantile).toEqual(0.01); + expect(values[0].value).toEqual(1); + expect(values[0].labels).toEqual({ + quantile: 0.01, + method: 'GET', + endpoint: '/test', + protocol: 'https', + }); + }); + }); + + describe('reset', () => { + it('should reset the summary, incl default labels', async () => { + const instance = new Summary({ + name: 'test_metric', + help: 'Another test metric', + labelNames: ['serial', 'active', 'color'], + defaultLabels: { color: 'red' }, + }); + + instance.observe({ serial: '12345', active: 'yes' }, 5); + let values = (await instance.get()).values; + expect(values[0].labels.quantile).toEqual(0.01); + expect(values[0].value).toEqual(5); + expect(values[0].labels).toEqual({ + quantile: 0.01, + active: 'yes', + color: 'red', + serial: '12345', + }); + + instance.observe( + { serial: '12345', active: 'yes', color: 'blue' }, + 4, + ); + values = (await instance.get()).values; + expect(values[0].labels.quantile).toEqual(0.01); + expect(values[0].value).toEqual(5); + expect(values[0].labels).toEqual({ + quantile: 0.01, + active: 'yes', + color: 'red', + serial: '12345', + }); + }); + }); + }); }); }); describe('without registry', () => { From 8e638633b82bcecc9012ee65d93445e89f6d4c12 Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Sat, 25 Nov 2023 22:06:03 -0500 Subject: [PATCH 2/7] update eslint parser --- .eslintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 067ab963..5055dbf3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,7 +10,7 @@ "es6": true }, "parserOptions": { - "ecmaVersion": 2019 + "ecmaVersion": 2020 }, "rules": { "no-underscore-dangle": "off", From 5305ff00f0b620e3b805a49a52184d7a4a965130 Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Sat, 25 Nov 2023 22:11:45 -0500 Subject: [PATCH 3/7] add `defaultLabels` to typescript types --- index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/index.d.ts b/index.d.ts index 13e0051e..4f9cd8bb 100644 --- a/index.d.ts +++ b/index.d.ts @@ -214,6 +214,7 @@ interface MetricConfiguration { | Registry | Registry )[]; + defaultLabels: Partial>; aggregator?: Aggregator; collect?: CollectFunction; enableExemplars?: boolean; From 35b00e3b0d9b269d1f4aea4dd1848b7d21c401ba Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Sat, 25 Nov 2023 22:23:19 -0500 Subject: [PATCH 4/7] update documentation --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/README.md b/README.md index a0d0684d..364daf3d 100644 --- a/README.md +++ b/README.md @@ -393,6 +393,53 @@ Default labels will be overridden if there is a name conflict. `register.clear()` will clear default labels. +##### Per-metric default label values + +Static labels values may also be applied per-metric: + +```js +const counter = new client.Counter({ + name: 'metric_name', + help: 'metric_help', + labels: ['method', 'endpoint', 'protocol'], + defaultLabels: { + protocol: 'https', + }, +}); + +// will be recorded with method: "GET", endpoint: "/test", protocol: "https" +counter.labels({method: 'GET', endpoint: '/test' }).inc(); +counter.inc({method: 'GET', endpoint: '/test' }); +``` + +Default values can also be overridden when recording a value: + +```js +// the following are all equivalent: +counter.labels('GET', '/test', 'http').inc(); + +counter.labels({ + method: 'GET', + endpoint: '/test', + protocol: 'http' +}).inc(); + +counter.inc({ + method: 'GET', + endpoint: '/test', + protocol: 'http' +}); +``` + +Note that the following shorthand _can't_ be used, unless all labels are specified, including labels with a default value: + +```js +// this is good +counter.labels('GET', '/test', 'https').inc(); +// this will throw an error +counter.labels('GET', '/test').inc(); +``` + ### Exemplars The exemplars defined in the OpenMetrics specification can be enabled on Counter From 1fed26f6907214a1dddfd49423155a1cc7a783c2 Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Sat, 25 Nov 2023 22:29:09 -0500 Subject: [PATCH 5/7] rename variable --- lib/counter.js | 6 +++--- lib/gauge.js | 6 +++--- lib/histogram.js | 16 ++++++++-------- lib/summary.js | 16 ++++++++-------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/counter.js b/lib/counter.js index 26fab532..7e1592fd 100644 --- a/lib/counter.js +++ b/lib/counter.js @@ -129,9 +129,9 @@ class Counter extends Metric { } remove(...args) { - const labelsArgs = getLabels(this.labelNames, args); - validateLabel(this.labelNames, labelsArgs); - const labels = { ...this.defaultLabels, ...labelsArgs }; + const labelsFromArgs = getLabels(this.labelNames, args); + validateLabel(this.labelNames, labelsFromArgs); + const labels = { ...this.defaultLabels, ...labelsFromArgs }; return removeLabels.call(this, this.hashMap, labels, this.sortedLabelNames); } } diff --git a/lib/gauge.js b/lib/gauge.js index 43b1564f..b4da38fd 100644 --- a/lib/gauge.js +++ b/lib/gauge.js @@ -138,9 +138,9 @@ class Gauge extends Metric { } remove(...args) { - const labelsArgs = getLabels(this.labelNames, args); - validateLabel(this.labelNames, labelsArgs); - const labels = { ...this.defaultLabels, ...labelsArgs }; + const labelsFromArgs = getLabels(this.labelNames, args); + validateLabel(this.labelNames, labelsFromArgs); + const labels = { ...this.defaultLabels, ...labelsFromArgs }; removeLabels.call(this, this.hashMap, labels, this.sortedLabelNames); } } diff --git a/lib/histogram.js b/lib/histogram.js index 653615a5..219d306d 100644 --- a/lib/histogram.js +++ b/lib/histogram.js @@ -175,11 +175,11 @@ class Histogram extends Metric { } labels(...args) { - const labelsArg = getLabels(this.labelNames, args); - validateLabel(this.labelNames, labelsArg); - const labels = isObject(labelsArg) - ? { ...this.defaultLabels, ...labelsArg } - : labelsArg ?? this.defaultLabels; + const labelsFromArgs = getLabels(this.labelNames, args); + validateLabel(this.labelNames, labelsFromArgs); + const labels = isObject(labelsFromArgs) + ? { ...this.defaultLabels, ...labelsFromArgs } + : labelsFromArgs ?? this.defaultLabels; return { observe: observe.call(this, labels), startTimer: startTimer.call(this, labels), @@ -187,9 +187,9 @@ class Histogram extends Metric { } remove(...args) { - const labelsArgs = getLabels(this.labelNames, args); - validateLabel(this.labelNames, labelsArgs); - const labels = { ...this.defaultLabels, ...labelsArgs }; + const labelsFromArgs = getLabels(this.labelNames, args); + validateLabel(this.labelNames, labelsFromArgs); + const labels = { ...this.defaultLabels, ...labelsFromArgs }; removeLabels.call(this, this.hashMap, labels, this.sortedLabelNames); } } diff --git a/lib/summary.js b/lib/summary.js index 8731a44d..a9dc33a6 100644 --- a/lib/summary.js +++ b/lib/summary.js @@ -110,11 +110,11 @@ class Summary extends Metric { } labels(...args) { - const labelsArg = getLabels(this.labelNames, args); - validateLabel(this.labelNames, labelsArg); - const labels = isObject(labelsArg) - ? { ...this.defaultLabels, ...labelsArg } - : labelsArg ?? this.defaultLabels; + const labelsFromArgs = getLabels(this.labelNames, args); + validateLabel(this.labelNames, labelsFromArgs); + const labels = isObject(labelsFromArgs) + ? { ...this.defaultLabels, ...labelsFromArgs } + : labelsFromArgs ?? this.defaultLabels; return { observe: observe.call(this, labels), startTimer: startTimer.call(this, labels), @@ -122,9 +122,9 @@ class Summary extends Metric { } remove(...args) { - const labelsArgs = getLabels(this.labelNames, args); - validateLabel(this.labelNames, labelsArgs); - const labels = { ...this.defaultLabels, ...labelsArgs }; + const labelsFromArgs = getLabels(this.labelNames, args); + validateLabel(this.labelNames, labelsFromArgs); + const labels = { ...this.defaultLabels, ...labelsFromArgs }; removeLabels.call(this, this.hashMap, labels, this.sortedLabelNames); } } From 42ecc56ca3ea75077a2fa3b03cba0a478d59226e Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Sun, 26 Nov 2023 08:04:04 -0500 Subject: [PATCH 6/7] fix ci --- README.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 364daf3d..e7960fff 100644 --- a/README.md +++ b/README.md @@ -402,14 +402,14 @@ const counter = new client.Counter({ name: 'metric_name', help: 'metric_help', labels: ['method', 'endpoint', 'protocol'], - defaultLabels: { - protocol: 'https', - }, + defaultLabels: { + protocol: 'https', + }, }); // will be recorded with method: "GET", endpoint: "/test", protocol: "https" -counter.labels({method: 'GET', endpoint: '/test' }).inc(); -counter.inc({method: 'GET', endpoint: '/test' }); +counter.labels({ method: 'GET', endpoint: '/test' }).inc(); +counter.inc({ method: 'GET', endpoint: '/test' }); ``` Default values can also be overridden when recording a value: @@ -418,16 +418,18 @@ Default values can also be overridden when recording a value: // the following are all equivalent: counter.labels('GET', '/test', 'http').inc(); -counter.labels({ - method: 'GET', - endpoint: '/test', - protocol: 'http' -}).inc(); +counter + .labels({ + method: 'GET', + endpoint: '/test', + protocol: 'http', + }) + .inc(); counter.inc({ - method: 'GET', - endpoint: '/test', - protocol: 'http' + method: 'GET', + endpoint: '/test', + protocol: 'http', }); ``` From f3803671408c1db65fd8ed9e14ef21f8679eb484 Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Sun, 26 Nov 2023 08:07:19 -0500 Subject: [PATCH 7/7] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e917d0a2..be5030ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ project adheres to [Semantic Versioning](http://semver.org/). - Allow Pushgateway to now require job names for compatibility with Gravel Gateway. - Allow `histogram.startTime()` to be used with exemplars. +- added `defaultLabels` option to metric classes ## [15.0.0] - 2023-10-09