diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b605418..8adcb1c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added +- feat: added `zero()` to `Histogram` for setting the metrics for a given label combination to zero + ## [13.1.0] - 2021-01-24 ### Changed diff --git a/README.md b/README.md index 9596e584..67d3f0ad 100644 --- a/README.md +++ b/README.md @@ -326,6 +326,24 @@ xhrRequest(function (err, res) { }); ``` +#### Zeroing metrics with Labels + +Metrics with labels can not be exported before they have been observed at least +once since the possible label values are not known before they're observed. + +For histograms, this can be solved by explicitly zeroing all expected label values: + +```js +const histogram = new client.Histogram({ + name: 'metric_name', + help: 'metric_help', + buckets: [0.1, 5, 15, 50, 100, 500], + labels: ['method'], +}); +histogram.zero({ method: 'GET' }); +histogram.zero({ method: 'POST' }); +``` + #### Strongly typed Labels Typescript can also enforce label names using `as const` diff --git a/index.d.ts b/index.d.ts index cc60a609..264a98d5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -389,6 +389,11 @@ export class Histogram { */ reset(): void; + /** + * Initialize the metrics for the given combination of labels to zero + */ + zero(labels: LabelValues): void; + /** * Return the child for given labels * @param values Label values diff --git a/lib/histogram.js b/lib/histogram.js index 89fc2372..a2c93837 100644 --- a/lib/histogram.js +++ b/lib/histogram.js @@ -73,6 +73,19 @@ class Histogram extends Metric { this.hashMap = {}; } + /** + * Initialize the metrics for the given combination of labels to zero + * @param {object} labels - Object with labels where key is the label key and value is label value. Can only be one level deep + * @returns {void} + */ + zero(labels) { + const hash = hashObject(labels); + this.hashMap[hash] = createBaseValues( + labels, + Object.assign({}, this.bucketValues), + ); + } + /** * Start a timer that could be used to logging durations * @param {object} labels - Object with labels where key is the label key and value is label value. Can only be one level deep diff --git a/test/histogramTest.js b/test/histogramTest.js index 2876aee0..06978ad5 100644 --- a/test/histogramTest.js +++ b/test/histogramTest.js @@ -279,6 +279,48 @@ describe('histogram', () => { }); }); + describe('zero', () => { + beforeEach(() => { + instance = new Histogram({ + name: 'histogram_labels', + help: 'Histogram with labels fn', + labelNames: ['method'], + }); + }); + + it('should zero the given label', async () => { + instance.zero({ method: 'POST' }); + const values = getValuesByLabel( + 'POST', + (await instance.get()).values, + 'method', + ); + values.forEach(bucket => { + expect(bucket.value).toEqual(0); + }); + }); + + it('should export the metric after zeroing', async () => { + instance.zero({ method: 'POST' }); + const values = getValuesByLabel( + 'POST', + (await instance.get()).values, + 'method', + ); + expect(values).not.toHaveLength(0); + }); + + it('should not duplicate the metric', async () => { + instance.zero({ method: 'POST' }); + instance.observe({ method: 'POST' }, 1); + const values = getValuesByName( + 'histogram_labels_count', + (await instance.get()).values, + ); + expect(values).toHaveLength(1); + }); + }); + describe('remove', () => { beforeEach(() => { instance = new Histogram({ @@ -423,6 +465,14 @@ describe('histogram', () => { }) ); } + function getValuesByName(name, values) { + return values.reduce((acc, val) => { + if (val.metricName === name) { + acc.push(val); + } + return acc; + }, []); + } function getValueByLeAndLabel(le, key, label, values) { return values.reduce((acc, val) => { if (val.labels && val.labels.le === le && val.labels[key] === label) {