From eb9c05a7e9394dbe26802b9461e4eef863dd0242 Mon Sep 17 00:00:00 2001 From: Afnan-0206 <2102508519@svyasa-sas.edu.in> Date: Sat, 19 Sep 2026 22:07:33 +0530 Subject: [PATCH] feat: add zero() method to Summary (#481) Add Summary.prototype.zero(labels) to initialize a labelled Summary metric to zero across all quantiles, sum, and count prior to observations, matching Histogram.prototype.zero(). Signed-off-by: Afnan-0206 <2102508519@svyasa-sas.edu.in> --- index.d.ts | 5 ++ lib/summary.js | 42 ++++++++++------- test/summaryTest.js | 109 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 16 deletions(-) diff --git a/index.d.ts b/index.d.ts index 6e60e8bf..aadbd733 100644 --- a/index.d.ts +++ b/index.d.ts @@ -755,6 +755,11 @@ export class Summary { */ 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/summary.js b/lib/summary.js index 87d18003..52a0cecd 100644 --- a/lib/summary.js +++ b/lib/summary.js @@ -39,14 +39,9 @@ class Summary extends Metric { this.store = new LabelMap(this.labelNames); if (this.labelNames.length === 0) { - this.store.getOrAdd({}, storedLabels => { - return { - labels: storedLabels, - td: new timeWindowQuantiles(this.maxAgeSeconds, this.ageBuckets), - count: 0, - sum: 0, - }; - }); + this.store.getOrAdd({}, storedLabels => + createBaseValues(storedLabels, this.maxAgeSeconds, this.ageBuckets), + ); } } @@ -96,6 +91,18 @@ class Summary extends Metric { } } + /** + * 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) { + this.store.validate(labels); + this.store.merge(labels, { + value: createBaseValues(labels, this.maxAgeSeconds, this.ageBuckets), + }); + } + /** * 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 @@ -179,14 +186,8 @@ function observe(labels) { const summaryOfLabel = this.store.getOrAdd( labelValuePair.labels, - storedLabels => { - return { - labels: storedLabels, - td: new timeWindowQuantiles(this.maxAgeSeconds, this.ageBuckets), - count: 0, - sum: 0, - }; - }, + storedLabels => + createBaseValues(storedLabels, this.maxAgeSeconds, this.ageBuckets), ); summaryOfLabel.td.push(labelValuePair.value); @@ -212,4 +213,13 @@ function convertLabelsAndValues(labels, value) { }; } +function createBaseValues(labels, maxAgeSeconds, ageBuckets) { + return { + labels, + td: new timeWindowQuantiles(maxAgeSeconds, ageBuckets), + count: 0, + sum: 0, + }; +} + module.exports = Summary; diff --git a/test/summaryTest.js b/test/summaryTest.js index 8f4d17f3..960561c1 100644 --- a/test/summaryTest.js +++ b/test/summaryTest.js @@ -487,6 +487,115 @@ describe.each([ }); }); }); + + describe('zero', () => { + beforeEach(() => { + globalRegistry.clear(); + instance = new Summary({ + name: 'summary_labels', + help: 'Summary 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', + ); + expect(values).not.toHaveLength(0); + values.forEach(val => { + expect(val.value).toEqual(0); + }); + + const sumValues = getValuesByName('summary_labels_sum', values); + expect(sumValues).toHaveLength(1); + expect(sumValues[0].value).toEqual(0); + + const countValues = getValuesByName('summary_labels_count', values); + expect(countValues).toHaveLength(1); + expect(countValues[0].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( + 'summary_labels_count', + (await instance.get()).values, + ); + expect(values).toHaveLength(1); + expect(values[0].value).toEqual(1); + + const sumValues = getValuesByName( + 'summary_labels_sum', + (await instance.get()).values, + ); + expect(sumValues).toHaveLength(1); + expect(sumValues[0].value).toEqual(1); + }); + + it('should reset the label combination if zero is called after observe', async () => { + instance.observe({ method: 'POST' }, 50); + let countValues = getValuesByName( + 'summary_labels_count', + (await instance.get()).values, + ); + expect(countValues[0].value).toEqual(1); + + instance.zero({ method: 'POST' }); + const values = getValuesByLabel( + 'POST', + (await instance.get()).values, + 'method', + ); + values.forEach(val => { + expect(val.value).toEqual(0); + }); + + countValues = getValuesByName('summary_labels_count', values); + expect(countValues[0].value).toEqual(0); + + const sumValues = getValuesByName('summary_labels_sum', values); + expect(sumValues[0].value).toEqual(0); + }); + + it('should throw error when invalid label names are provided', () => { + expect(() => { + instance.zero({ invalid_label: 'POST' }); + }).toThrow(/not included in initial labelset/); + }); + + function getValuesByName(name, values) { + return values.reduce((acc, val) => { + if (val.metricName === name) { + acc.push(val); + } + return acc; + }, []); + } + + function getValuesByLabel(label, values, key) { + return values.reduce((acc, val) => { + if (val.labels && val.labels[key] === label) { + acc.push(val); + } + return acc; + }, []); + } + }); }); }); describe('without registry', () => {