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