From a5d948ca73d7912635f4621adb3c028969eb1e1d Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Sun, 12 Jul 2026 22:03:29 -0400 Subject: [PATCH] fix(counter): use counter total as exemplar value instead of increment Counter exemplars previously recorded the increment amount (default 1) instead of the counter's current total. This meant that after multiple increments, exemplar markers in tools like Grafana would all sit at the bottom of the graph regardless of the actual counter value. Change incWithExemplar to pass the store entry's current value to updateExemplar, matching the behavior of other Prometheus clients. Fixes #621 Signed-off-by: Chen Zhang --- CHANGELOG.md | 2 ++ lib/counter.js | 3 ++- test/exemplarsTest.js | 25 +++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41b3c037..119df208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ This release marks our first release under the Prometheus umbrella. - Drop support for Node.js versions 16, 18, 20, 21 and 23 - Metric internal storage ('hashMap') changed to a separate object, LabelMap. If you have subclassed the built-in metric types you may need to adjust your code. +- Counter exemplar values now record the counter's total value instead of the increment + amount, matching the behavior of other Prometheus clients (Fixes [#621](https://github.com/prometheus/client_js/issues/621)) ### Changed diff --git a/lib/counter.js b/lib/counter.js index 7835c8de..bc40f596 100644 --- a/lib/counter.js +++ b/lib/counter.js @@ -82,7 +82,8 @@ class Counter extends Metric { exemplarLabels = this.defaultExemplarLabelSet, } = {}) { this.incWithoutExemplar(labels, value); - this.updateExemplar(labels, exemplarLabels, value); + const entry = this.store.entry(labels); + this.updateExemplar(labels, exemplarLabels, entry.value); } updateExemplar(labels, exemplarLabels, value) { diff --git a/test/exemplarsTest.js b/test/exemplarsTest.js index 0ad65883..6f431e61 100644 --- a/test/exemplarsTest.js +++ b/test/exemplarsTest.js @@ -60,6 +60,31 @@ describe('Exemplars', () => { ); }); + it('should use the counter total as the exemplar value, not the increment', async () => { + const localRegistry = new Registry(); + localRegistry.setContentType(regType); + const counterInstance = new Counter({ + name: 'counter_exemplar_total_test', + help: 'help', + labelNames: ['method', 'code'], + enableExemplars: true, + registers: [localRegistry], + }); + counterInstance.inc({ + labels: { method: 'get', code: '200' }, + exemplarLabels: { traceId: 'trace_id_1' }, + }); + counterInstance.inc({ + value: 3, + labels: { method: 'get', code: '200' }, + exemplarLabels: { traceId: 'trace_id_2' }, + }); + const vals = await counterInstance.get(); + expect(vals.values[0].value).toEqual(4); + expect(vals.values[0].exemplar.value).toEqual(4); + expect(vals.values[0].exemplar.labelSet.traceId).toEqual('trace_id_2'); + }); + it('should make histogram with exemplars on multiple buckets', async () => { const histogramInstance = new Histogram({ name: 'histogram_exemplar_test',