Skip to content

Commit 1f81509

Browse files
p0rtaleoleg-jukovec
authored andcommitted
Optimize label handling with predefined label keys in counter/gauge
1 parent f68f0db commit 1f81509

File tree

6 files changed

+168
-15
lines changed

6 files changed

+168
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88
### Added
99
- `tnt_cartridge_config_applied` metric.
10+
- New optional ``label_keys`` parameter for ``counter()`` and ``gauge()`` metrics.
1011

1112
## [1.3.1] - 2025-02-24
1213

doc/monitoring/api_reference.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,18 @@ currently running processes. Use a :ref:`gauge <metrics-api_reference-gauge>` ty
3333

3434
The design is based on the `Prometheus counter <https://prometheus.io/docs/concepts/metric_types/#counter>`__.
3535

36-
.. function:: counter(name [, help, metainfo])
36+
.. function:: counter(name [, help, metainfo, label_keys])
3737

3838
Register a new counter.
3939

4040
:param string name: collector name. Must be unique.
4141
:param string help: collector description.
4242
:param table metainfo: collector metainfo.
43+
:param table label_keys: predefined label keys to optimize performance.
44+
When specified, only these keys can be used in ``label_pairs``.
45+
4346
:return: A counter object.
47+
4448
:rtype: counter_obj
4549

4650
.. class:: counter_obj
@@ -102,13 +106,15 @@ it might be used for the values that can go up or down, for example, the number
102106

103107
The design is based on the `Prometheus gauge <https://prometheus.io/docs/concepts/metric_types/#gauge>`__.
104108

105-
.. function:: gauge(name [, help, metainfo])
109+
.. function:: gauge(name [, help, metainfo, label_keys])
106110

107111
Register a new gauge.
108112

109113
:param string name: collector name. Must be unique.
110114
:param string help: collector description.
111115
:param table metainfo: collector metainfo.
116+
:param table label_keys: predefined label keys to optimize performance.
117+
When specified, only these keys can be used in ``label_pairs``.
112118

113119
:return: A gauge object.
114120

metrics/api.lua

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,16 @@ local function clear()
6565
registry:clear()
6666
end
6767

68-
local function counter(name, help, metainfo)
69-
checks('string', '?string', '?table')
68+
local function counter(name, help, metainfo, label_keys)
69+
checks('string', '?string', '?table', '?table')
7070

71-
return registry:find_or_create(Counter, name, help, metainfo)
71+
return registry:find_or_create(Counter, name, help, metainfo, label_keys)
7272
end
7373

74-
local function gauge(name, help, metainfo)
75-
checks('string', '?string', '?table')
74+
local function gauge(name, help, metainfo, label_keys)
75+
checks('string', '?string', '?table', '?table')
7676

77-
return registry:find_or_create(Gauge, name, help, metainfo)
77+
return registry:find_or_create(Gauge, name, help, metainfo, label_keys)
7878
end
7979

8080
local function histogram(name, help, buckets, metainfo)

metrics/collectors/shared.lua

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function Shared:new_class(kind, method_names)
2424
return setmetatable(class, {__index = methods})
2525
end
2626

27-
function Shared:new(name, help, metainfo)
27+
function Shared:new(name, help, metainfo, label_keys)
2828
metainfo = table.copy(metainfo) or {}
2929

3030
if not name then
@@ -35,6 +35,7 @@ function Shared:new(name, help, metainfo)
3535
help = help or "",
3636
observations = {},
3737
label_pairs = {},
38+
label_keys = label_keys,
3839
metainfo = metainfo,
3940
}, self)
4041
end
@@ -43,21 +44,49 @@ function Shared:set_registry(registry)
4344
self.registry = registry
4445
end
4546

46-
function Shared.make_key(label_pairs)
47-
if type(label_pairs) ~= 'table' then
47+
function Shared.make_key(label_pairs, label_keys)
48+
if (label_keys == nil) and (type(label_pairs) ~= 'table') then
4849
return ""
4950
end
51+
52+
if label_keys ~= nil then
53+
if type(label_pairs) ~= 'table' then
54+
error("Invalid label_pairs: expected a table when label_keys is provided")
55+
end
56+
57+
local label_count = 0
58+
for _ in pairs(label_pairs) do
59+
label_count = label_count + 1
60+
end
61+
62+
if #label_keys ~= label_count then
63+
error(("Label keys count (%d) should match the number of label pairs (%d)"):format(#label_keys, label_count))
64+
end
65+
66+
local parts = table.new(#label_keys, 0)
67+
for i, label_key in ipairs(label_keys) do
68+
local label_value = label_pairs[label_key]
69+
if label_value == nil then
70+
error(string.format("Label key '%s' is missing", label_key))
71+
end
72+
parts[i] = label_value
73+
end
74+
75+
return table.concat(parts, '\t')
76+
end
77+
5078
local parts = {}
5179
for k, v in pairs(label_pairs) do
5280
table.insert(parts, k .. '\t' .. v)
5381
end
5482
table.sort(parts)
83+
5584
return table.concat(parts, '\t')
5685
end
5786

5887
function Shared:remove(label_pairs)
5988
assert(label_pairs, 'label pairs is a required parameter')
60-
local key = self.make_key(label_pairs)
89+
local key = self.make_key(label_pairs, self.label_keys)
6190
self.observations[key] = nil
6291
self.label_pairs[key] = nil
6392
end
@@ -67,7 +96,7 @@ function Shared:set(num, label_pairs)
6796
error("Collector set value should be a number")
6897
end
6998
num = num or 0
70-
local key = self.make_key(label_pairs)
99+
local key = self.make_key(label_pairs, self.label_keys)
71100
self.observations[key] = num
72101
self.label_pairs[key] = label_pairs or {}
73102
end
@@ -77,7 +106,7 @@ function Shared:inc(num, label_pairs)
77106
error("Collector increment should be a number")
78107
end
79108
num = num or 1
80-
local key = self.make_key(label_pairs)
109+
local key = self.make_key(label_pairs, self.label_keys)
81110
local old_value = self.observations[key] or 0
82111
self.observations[key] = old_value + num
83112
self.label_pairs[key] = label_pairs or {}
@@ -88,7 +117,7 @@ function Shared:dec(num, label_pairs)
88117
error("Collector decrement should be a number")
89118
end
90119
num = num or 1
91-
local key = self.make_key(label_pairs)
120+
local key = self.make_key(label_pairs, self.label_keys)
92121
local old_value = self.observations[key] or 0
93122
self.observations[key] = old_value - num
94123
self.label_pairs[key] = label_pairs or {}

test/collectors/counter_test.lua

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,58 @@ g.test_metainfo_immutable = function()
101101
metainfo['my_useful_info'] = 'there'
102102
t.assert_equals(c.metainfo, {my_useful_info = 'here'})
103103
end
104+
105+
g.test_counter_with_fixed_labels = function()
106+
local fixed_labels = {'label1', 'label2'}
107+
local counter = metrics.counter('counter_with_labels', nil, {}, fixed_labels)
108+
109+
counter:inc(1, {label1 = 1, label2 = 'text'})
110+
utils.assert_observations(counter:collect(), {
111+
{'counter_with_labels', 1, {label1 = 1, label2 = 'text'}},
112+
})
113+
114+
counter:inc(5, {label2 = 'text', label1 = 2})
115+
utils.assert_observations(counter:collect(), {
116+
{'counter_with_labels', 1, {label1 = 1, label2 = 'text'}},
117+
{'counter_with_labels', 5, {label1 = 2, label2 = 'text'}},
118+
})
119+
120+
counter:reset({label1 = 1, label2 = 'text'})
121+
utils.assert_observations(counter:collect(), {
122+
{'counter_with_labels', 0, {label1 = 1, label2 = 'text'}},
123+
{'counter_with_labels', 5, {label1 = 2, label2 = 'text'}},
124+
})
125+
126+
counter:remove({label1 = 2, label2 = 'text'})
127+
utils.assert_observations(counter:collect(), {
128+
{'counter_with_labels', 0, {label1 = 1, label2 = 'text'}},
129+
})
130+
end
131+
132+
g.test_counter_missing_label = function()
133+
local fixed_labels = {'label1', 'label2'}
134+
local counter = metrics.counter('counter_with_labels', nil, {}, fixed_labels)
135+
136+
counter:inc(42, {label1 = 1, label2 = 'text'})
137+
utils.assert_observations(counter:collect(), {
138+
{'counter_with_labels', 42, {label1 = 1, label2 = 'text'}},
139+
})
140+
141+
t.assert_error_msg_contains(
142+
"Invalid label_pairs: expected a table when label_keys is provided",
143+
counter.inc, counter, 42, 1)
144+
145+
t.assert_error_msg_contains(
146+
"should match the number of label pairs",
147+
counter.inc, counter, 42, {label1 = 1, label2 = 'text', label3 = 42})
148+
149+
local function assert_missing_label_error(fun, ...)
150+
t.assert_error_msg_contains(
151+
"is missing",
152+
fun, counter, ...)
153+
end
154+
155+
assert_missing_label_error(counter.inc, 1, {label1 = 1, label3 = 'a'})
156+
assert_missing_label_error(counter.reset, {label2 = 0, label3 = 'b'})
157+
assert_missing_label_error(counter.remove, {label2 = 0, label3 = 'b'})
158+
end

test/collectors/gauge_test.lua

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,65 @@ g.test_metainfo_immutable = function()
8888
metainfo['my_useful_info'] = 'there'
8989
t.assert_equals(c.metainfo, {my_useful_info = 'here'})
9090
end
91+
92+
g.test_gauge_with_fixed_labels = function()
93+
local fixed_labels = {'label1', 'label2'}
94+
local gauge = metrics.gauge('gauge_with_labels', nil, {}, fixed_labels)
95+
96+
gauge:set(1, {label1 = 1, label2 = 'text'})
97+
utils.assert_observations(gauge:collect(), {
98+
{'gauge_with_labels', 1, {label1 = 1, label2 = 'text'}},
99+
})
100+
101+
gauge:set(42, {label2 = 'text', label1 = 100})
102+
utils.assert_observations(gauge:collect(), {
103+
{'gauge_with_labels', 1, {label1 = 1, label2 = 'text'}},
104+
{'gauge_with_labels', 42, {label1 = 100, label2 = 'text'}},
105+
})
106+
107+
gauge:inc(5, {label2 = 'text', label1 = 100})
108+
utils.assert_observations(gauge:collect(), {
109+
{'gauge_with_labels', 1, {label1 = 1, label2 = 'text'}},
110+
{'gauge_with_labels', 47, {label1 = 100, label2 = 'text'}},
111+
})
112+
113+
gauge:dec(11, {label1 = 1, label2 = 'text'})
114+
utils.assert_observations(gauge:collect(), {
115+
{'gauge_with_labels', -10, {label1 = 1, label2 = 'text'}},
116+
{'gauge_with_labels', 47, {label1 = 100, label2 = 'text'}},
117+
})
118+
119+
gauge:remove({label2 = 'text', label1 = 100})
120+
utils.assert_observations(gauge:collect(), {
121+
{'gauge_with_labels', -10, {label1 = 1, label2 = 'text'}},
122+
})
123+
end
124+
125+
g.test_gauge_missing_label = function()
126+
local fixed_labels = {'label1', 'label2'}
127+
local gauge = metrics.gauge('gauge_with_labels', nil, {}, fixed_labels)
128+
129+
gauge:set(42, {label1 = 1, label2 = 'text'})
130+
utils.assert_observations(gauge:collect(), {
131+
{'gauge_with_labels', 42, {label1 = 1, label2 = 'text'}},
132+
})
133+
134+
t.assert_error_msg_contains(
135+
"Invalid label_pairs: expected a table when label_keys is provided",
136+
gauge.set, gauge, 42, 'text')
137+
138+
t.assert_error_msg_contains(
139+
"should match the number of label pairs",
140+
gauge.set, gauge, 42, {label1 = 1, label2 = 'text', label3 = 42})
141+
142+
local function assert_missing_label_error(fun, ...)
143+
t.assert_error_msg_contains(
144+
"is missing",
145+
fun, gauge, ...)
146+
end
147+
148+
assert_missing_label_error(gauge.inc, 1, {label1 = 1, label3 = 42})
149+
assert_missing_label_error(gauge.dec, 2, {label1 = 1, label3 = 42})
150+
assert_missing_label_error(gauge.set, 42, {label2 = 'text', label3 = 42})
151+
assert_missing_label_error(gauge.remove, {label2 = 'text', label3 = 42})
152+
end

0 commit comments

Comments
 (0)