From 75ad4617659a950ba1ec10f9ae2a4aec376d535d Mon Sep 17 00:00:00 2001 From: p0rtale Date: Wed, 25 Jun 2025 16:16:37 +0300 Subject: [PATCH 1/6] Add fixed label keys support --- metrics/api.lua | 12 +++---- metrics/collectors/shared.lua | 31 ++++++++++++------ test/collectors/counter_test.lua | 47 +++++++++++++++++++++++++++ test/collectors/gauge_test.lua | 54 ++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 15 deletions(-) diff --git a/metrics/api.lua b/metrics/api.lua index d36f1c9a..b08e6697 100644 --- a/metrics/api.lua +++ b/metrics/api.lua @@ -65,16 +65,16 @@ local function clear() registry:clear() end -local function counter(name, help, metainfo) - checks('string', '?string', '?table') +local function counter(name, help, metainfo, label_keys) + checks('string', '?string', '?table', '?table') - return registry:find_or_create(Counter, name, help, metainfo) + return registry:find_or_create(Counter, name, help, metainfo, label_keys) end -local function gauge(name, help, metainfo) - checks('string', '?string', '?table') +local function gauge(name, help, metainfo, label_keys) + checks('string', '?string', '?table', '?table') - return registry:find_or_create(Gauge, name, help, metainfo) + return registry:find_or_create(Gauge, name, help, metainfo, label_keys) end local function histogram(name, help, buckets, metainfo) diff --git a/metrics/collectors/shared.lua b/metrics/collectors/shared.lua index 2cb195b4..335f6edd 100644 --- a/metrics/collectors/shared.lua +++ b/metrics/collectors/shared.lua @@ -24,7 +24,7 @@ function Shared:new_class(kind, method_names) return setmetatable(class, {__index = methods}) end -function Shared:new(name, help, metainfo) +function Shared:new(name, help, metainfo, label_keys) metainfo = table.copy(metainfo) or {} if not name then @@ -35,6 +35,7 @@ function Shared:new(name, help, metainfo) help = help or "", observations = {}, label_pairs = {}, + label_keys = label_keys, metainfo = metainfo, }, self) end @@ -43,21 +44,33 @@ function Shared:set_registry(registry) self.registry = registry end -function Shared.make_key(label_pairs) +function Shared.make_key(label_pairs, label_keys) if type(label_pairs) ~= 'table' then return "" end + local parts = {} - for k, v in pairs(label_pairs) do - table.insert(parts, k .. '\t' .. v) + if label_keys ~= nil then + for _, label_key in ipairs(label_keys) do + local label_value = label_pairs[label_key] + if label_value == nil then + error(string.format("Label key '%s' is missing", label_key)) + end + table.insert(parts, label_value) + end + else + for k, v in pairs(label_pairs) do + table.insert(parts, k .. '\t' .. v) + end + table.sort(parts) end - table.sort(parts) + return table.concat(parts, '\t') end function Shared:remove(label_pairs) assert(label_pairs, 'label pairs is a required parameter') - local key = self.make_key(label_pairs) + local key = self.make_key(label_pairs, self.label_keys) self.observations[key] = nil self.label_pairs[key] = nil end @@ -67,7 +80,7 @@ function Shared:set(num, label_pairs) error("Collector set value should be a number") end num = num or 0 - local key = self.make_key(label_pairs) + local key = self.make_key(label_pairs, self.label_keys) self.observations[key] = num self.label_pairs[key] = label_pairs or {} end @@ -77,7 +90,7 @@ function Shared:inc(num, label_pairs) error("Collector increment should be a number") end num = num or 1 - local key = self.make_key(label_pairs) + local key = self.make_key(label_pairs, self.label_keys) local old_value = self.observations[key] or 0 self.observations[key] = old_value + num self.label_pairs[key] = label_pairs or {} @@ -88,7 +101,7 @@ function Shared:dec(num, label_pairs) error("Collector decrement should be a number") end num = num or 1 - local key = self.make_key(label_pairs) + local key = self.make_key(label_pairs, self.label_keys) local old_value = self.observations[key] or 0 self.observations[key] = old_value - num self.label_pairs[key] = label_pairs or {} diff --git a/test/collectors/counter_test.lua b/test/collectors/counter_test.lua index 3692f344..18434b66 100644 --- a/test/collectors/counter_test.lua +++ b/test/collectors/counter_test.lua @@ -101,3 +101,50 @@ g.test_metainfo_immutable = function() metainfo['my_useful_info'] = 'there' t.assert_equals(c.metainfo, {my_useful_info = 'here'}) end + +g.test_counter_with_fixed_labels = function() + local fixed_labels = {'label1', 'label2'} + local counter = metrics.counter('counter_with_labels', nil, {}, fixed_labels) + + counter:inc(1, {label1 = 1, label2 = 'text'}) + utils.assert_observations(counter:collect(), { + {'counter_with_labels', 1, {label1 = 1, label2 = 'text'}}, + }) + + counter:inc(5, {label2 = 'text', label1 = 2}) + utils.assert_observations(counter:collect(), { + {'counter_with_labels', 1, {label1 = 1, label2 = 'text'}}, + {'counter_with_labels', 5, {label1 = 2, label2 = 'text'}}, + }) + + counter:reset({label1 = 1, label2 = 'text'}) + utils.assert_observations(counter:collect(), { + {'counter_with_labels', 0, {label1 = 1, label2 = 'text'}}, + {'counter_with_labels', 5, {label1 = 2, label2 = 'text'}}, + }) + + counter:remove({label1 = 2, label2 = 'text'}) + utils.assert_observations(counter:collect(), { + {'counter_with_labels', 0, {label1 = 1, label2 = 'text'}}, + }) +end + +g.test_counter_missing_label = function() + local fixed_labels = {'label1', 'label2'} + local counter = metrics.counter('counter_with_labels', nil, {}, fixed_labels) + + counter:inc(42, {label1 = 1, label2 = 'text'}) + utils.assert_observations(counter:collect(), { + {'counter_with_labels', 42, {label1 = 1, label2 = 'text'}}, + }) + + local function assert_missing_label_error(fun, ...) + t.assert_error_msg_contains( + "is missing", + fun, counter, ...) + end + + assert_missing_label_error(counter.inc, 1, {label1 = 1}) + assert_missing_label_error(counter.reset, {label2 = 0}) + assert_missing_label_error(counter.remove, {}) +end diff --git a/test/collectors/gauge_test.lua b/test/collectors/gauge_test.lua index cc50914a..cd2d7d18 100644 --- a/test/collectors/gauge_test.lua +++ b/test/collectors/gauge_test.lua @@ -88,3 +88,57 @@ g.test_metainfo_immutable = function() metainfo['my_useful_info'] = 'there' t.assert_equals(c.metainfo, {my_useful_info = 'here'}) end + +g.test_gauge_with_fixed_labels = function() + local fixed_labels = {'label1', 'label2'} + local gauge = metrics.gauge('gauge_with_labels', nil, {}, fixed_labels) + + gauge:set(1, {label1 = 1, label2 = 'text'}) + utils.assert_observations(gauge:collect(), { + {'gauge_with_labels', 1, {label1 = 1, label2 = 'text'}}, + }) + + gauge:set(42, {label2 = 'text', label1 = 100}) + utils.assert_observations(gauge:collect(), { + {'gauge_with_labels', 1, {label1 = 1, label2 = 'text'}}, + {'gauge_with_labels', 42, {label1 = 100, label2 = 'text'}}, + }) + + gauge:inc(5, {label2 = 'text', label1 = 100}) + utils.assert_observations(gauge:collect(), { + {'gauge_with_labels', 1, {label1 = 1, label2 = 'text'}}, + {'gauge_with_labels', 47, {label1 = 100, label2 = 'text'}}, + }) + + gauge:dec(11, {label1 = 1, label2 = 'text'}) + utils.assert_observations(gauge:collect(), { + {'gauge_with_labels', -10, {label1 = 1, label2 = 'text'}}, + {'gauge_with_labels', 47, {label1 = 100, label2 = 'text'}}, + }) + + gauge:remove({label2 = 'text', label1 = 100}) + utils.assert_observations(gauge:collect(), { + {'gauge_with_labels', -10, {label1 = 1, label2 = 'text'}}, + }) +end + +g.test_gauge_missing_label = function() + local fixed_labels = {'label1', 'label2'} + local gauge = metrics.gauge('gauge_with_labels', nil, {}, fixed_labels) + + gauge:set(42, {label1 = 1, label2 = 'text'}) + utils.assert_observations(gauge:collect(), { + {'gauge_with_labels', 42, {label1 = 1, label2 = 'text'}}, + }) + + local function assert_missing_label_error(fun, ...) + t.assert_error_msg_contains( + "is missing", + fun, gauge, ...) + end + + assert_missing_label_error(gauge.inc, 1, {label1 = 1}) + assert_missing_label_error(gauge.dec, 2, {label1 = 1}) + assert_missing_label_error(gauge.set, 42, {label1 = 1}) + assert_missing_label_error(gauge.remove, {}) +end From ad6766aba40d03f2347b214953dcfeeca85cb5e0 Mon Sep 17 00:00:00 2001 From: p0rtale Date: Sun, 29 Jun 2025 23:23:28 +0300 Subject: [PATCH 2/6] Add table preallocation --- metrics/collectors/shared.lua | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/metrics/collectors/shared.lua b/metrics/collectors/shared.lua index 335f6edd..77d521c1 100644 --- a/metrics/collectors/shared.lua +++ b/metrics/collectors/shared.lua @@ -49,21 +49,24 @@ function Shared.make_key(label_pairs, label_keys) return "" end - local parts = {} if label_keys ~= nil then - for _, label_key in ipairs(label_keys) do + local parts = table.new(#label_keys, 0) + for i, label_key in ipairs(label_keys) do local label_value = label_pairs[label_key] if label_value == nil then error(string.format("Label key '%s' is missing", label_key)) end - table.insert(parts, label_value) - end - else - for k, v in pairs(label_pairs) do - table.insert(parts, k .. '\t' .. v) + parts[i] = label_value end - table.sort(parts) + + return table.concat(parts, '\t') + end + + local parts = {} + for k, v in pairs(label_pairs) do + table.insert(parts, k .. '\t' .. v) end + table.sort(parts) return table.concat(parts, '\t') end From 9c14b02e64bc1c8b1350d8680c489677b3133c35 Mon Sep 17 00:00:00 2001 From: p0rtale Date: Sun, 29 Jun 2025 23:24:08 +0300 Subject: [PATCH 3/6] Add docs --- doc/monitoring/api_reference.rst | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/doc/monitoring/api_reference.rst b/doc/monitoring/api_reference.rst index 0d93fb9e..51862743 100644 --- a/doc/monitoring/api_reference.rst +++ b/doc/monitoring/api_reference.rst @@ -33,14 +33,18 @@ currently running processes. Use a :ref:`gauge ` ty The design is based on the `Prometheus counter `__. -.. function:: counter(name [, help, metainfo]) +.. function:: counter(name [, help, metainfo, label_keys]) Register a new counter. :param string name: collector name. Must be unique. :param string help: collector description. :param table metainfo: collector metainfo. + :param table label_keys: predefined label keys to optimize performance. + When specified, only these keys can be used in ``label_pairs``. + :return: A counter object. + :rtype: counter_obj .. class:: counter_obj @@ -102,13 +106,15 @@ it might be used for the values that can go up or down, for example, the number The design is based on the `Prometheus gauge `__. -.. function:: gauge(name [, help, metainfo]) +.. function:: gauge(name [, help, metainfo, label_keys]) Register a new gauge. :param string name: collector name. Must be unique. :param string help: collector description. :param table metainfo: collector metainfo. + :param table label_keys: predefined label keys to optimize performance. + When specified, only these keys can be used in ``label_pairs``. :return: A gauge object. From b982adef80077f6b83b5c4fa54e52ead361f38c9 Mon Sep 17 00:00:00 2001 From: p0rtale Date: Sun, 29 Jun 2025 23:24:32 +0300 Subject: [PATCH 4/6] Add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbe87a1b..b1c6b086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- New optional ``label_keys`` parameter for ``counter()`` and ``gauge()`` metrics. ## [1.3.1] - 2025-02-24 From b67f471e817ee326f2a9dd05fd876358b045ba95 Mon Sep 17 00:00:00 2001 From: p0rtale Date: Tue, 1 Jul 2025 16:34:38 +0300 Subject: [PATCH 5/6] Add label_pairs type check --- metrics/collectors/shared.lua | 6 +++++- test/collectors/counter_test.lua | 4 ++++ test/collectors/gauge_test.lua | 4 ++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/metrics/collectors/shared.lua b/metrics/collectors/shared.lua index 77d521c1..5d5a2b3e 100644 --- a/metrics/collectors/shared.lua +++ b/metrics/collectors/shared.lua @@ -45,11 +45,15 @@ function Shared:set_registry(registry) end function Shared.make_key(label_pairs, label_keys) - if type(label_pairs) ~= 'table' then + if (label_keys == nil) and (type(label_pairs) ~= 'table') then return "" end if label_keys ~= nil then + if type(label_pairs) ~= 'table' then + error("Invalid label_pairs: expected a table when label_keys is provided") + end + local parts = table.new(#label_keys, 0) for i, label_key in ipairs(label_keys) do local label_value = label_pairs[label_key] diff --git a/test/collectors/counter_test.lua b/test/collectors/counter_test.lua index 18434b66..0d0c9722 100644 --- a/test/collectors/counter_test.lua +++ b/test/collectors/counter_test.lua @@ -138,6 +138,10 @@ g.test_counter_missing_label = function() {'counter_with_labels', 42, {label1 = 1, label2 = 'text'}}, }) + t.assert_error_msg_contains( + "Invalid label_pairs: expected a table when label_keys is provided", + counter.inc, counter, 42, 1) + local function assert_missing_label_error(fun, ...) t.assert_error_msg_contains( "is missing", diff --git a/test/collectors/gauge_test.lua b/test/collectors/gauge_test.lua index cd2d7d18..c3febcd1 100644 --- a/test/collectors/gauge_test.lua +++ b/test/collectors/gauge_test.lua @@ -131,6 +131,10 @@ g.test_gauge_missing_label = function() {'gauge_with_labels', 42, {label1 = 1, label2 = 'text'}}, }) + t.assert_error_msg_contains( + "Invalid label_pairs: expected a table when label_keys is provided", + gauge.set, gauge, 42, 'text') + local function assert_missing_label_error(fun, ...) t.assert_error_msg_contains( "is missing", From ded251ca87ad0c77b60d8b88824674e147e17e9f Mon Sep 17 00:00:00 2001 From: p0rtale Date: Tue, 1 Jul 2025 17:07:24 +0300 Subject: [PATCH 6/6] Add check for label count mismatch --- metrics/collectors/shared.lua | 9 +++++++++ test/collectors/counter_test.lua | 10 +++++++--- test/collectors/gauge_test.lua | 12 ++++++++---- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/metrics/collectors/shared.lua b/metrics/collectors/shared.lua index 5d5a2b3e..194f3a44 100644 --- a/metrics/collectors/shared.lua +++ b/metrics/collectors/shared.lua @@ -54,6 +54,15 @@ function Shared.make_key(label_pairs, label_keys) error("Invalid label_pairs: expected a table when label_keys is provided") end + local label_count = 0 + for _ in pairs(label_pairs) do + label_count = label_count + 1 + end + + if #label_keys ~= label_count then + error("Label keys count should match the number of label pairs") + end + local parts = table.new(#label_keys, 0) for i, label_key in ipairs(label_keys) do local label_value = label_pairs[label_key] diff --git a/test/collectors/counter_test.lua b/test/collectors/counter_test.lua index 0d0c9722..9aa4274d 100644 --- a/test/collectors/counter_test.lua +++ b/test/collectors/counter_test.lua @@ -142,13 +142,17 @@ g.test_counter_missing_label = function() "Invalid label_pairs: expected a table when label_keys is provided", counter.inc, counter, 42, 1) + t.assert_error_msg_contains( + "Label keys count should match the number of label pairs", + counter.inc, counter, 42, {label1 = 1, label2 = 'text', label3 = 42}) + local function assert_missing_label_error(fun, ...) t.assert_error_msg_contains( "is missing", fun, counter, ...) end - assert_missing_label_error(counter.inc, 1, {label1 = 1}) - assert_missing_label_error(counter.reset, {label2 = 0}) - assert_missing_label_error(counter.remove, {}) + assert_missing_label_error(counter.inc, 1, {label1 = 1, label3 = 'a'}) + assert_missing_label_error(counter.reset, {label2 = 0, label3 = 'b'}) + assert_missing_label_error(counter.remove, {label2 = 0, label3 = 'b'}) end diff --git a/test/collectors/gauge_test.lua b/test/collectors/gauge_test.lua index c3febcd1..8b97a588 100644 --- a/test/collectors/gauge_test.lua +++ b/test/collectors/gauge_test.lua @@ -135,14 +135,18 @@ g.test_gauge_missing_label = function() "Invalid label_pairs: expected a table when label_keys is provided", gauge.set, gauge, 42, 'text') + t.assert_error_msg_contains( + "Label keys count should match the number of label pairs", + gauge.set, gauge, 42, {label1 = 1, label2 = 'text', label3 = 42}) + local function assert_missing_label_error(fun, ...) t.assert_error_msg_contains( "is missing", fun, gauge, ...) end - assert_missing_label_error(gauge.inc, 1, {label1 = 1}) - assert_missing_label_error(gauge.dec, 2, {label1 = 1}) - assert_missing_label_error(gauge.set, 42, {label1 = 1}) - assert_missing_label_error(gauge.remove, {}) + assert_missing_label_error(gauge.inc, 1, {label1 = 1, label3 = 42}) + assert_missing_label_error(gauge.dec, 2, {label1 = 1, label3 = 42}) + assert_missing_label_error(gauge.set, 42, {label2 = 'text', label3 = 42}) + assert_missing_label_error(gauge.remove, {label2 = 'text', label3 = 42}) end