Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add an efficient labels serializer #474

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions metrics/api.lua
Original file line number Diff line number Diff line change
@@ -125,6 +125,68 @@ local function set_global_labels(label_pairs)
registry:set_labels(label_pairs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, squash your commits into the single one with a name like api: support custom labels serialization

end

--- Prepares a serializer for label pairs with given keys.
---
--- `make_key`, which is used during every metric-related operation, is not very efficient itself.
--- To mitigate it, one could add his own serialization implementation.
--- It is done via passing `__metrics_make_key` callback to the label pairs table.
---
--- This function gives you ready-to-use serializer, so you don't have to create one yourself.
---
--- BEWARE! If keys of the `label_pairs` somehow change between serialization turns, it would raise error mostlikely.
--- Therefore, it's important to understand full scope of needed fields. For instance, for histogram:observe,
--- an additional label 'le' is always needed.
---
--- @class LabelsSerializer
--- @field wrap function(label_pairs: table): table Wraps given `label_pairs` with an efficient serialization.
--- @field serialize function(label_pairs: table): string Serialize given `label_pairs` into the key.
--- Exposed so you can write your own serializers on top of it.
---
--- @param labels_keys string[] Label keys for the further use.
--- @return LabelsSerializer
local function labels_serializer(labels_keys)
table.sort(labels_keys)

-- used to protect label_pairs from altering.
local keys_index = {}
for _, key in ipairs(labels_keys) do
keys_index[key] = true
end

local function serialize(label_pairs)
local result = ""
for idx, label in ipairs(labels_keys) do
if idx ~= 1 then
result = result .. '\t'
end
result = result .. label .. '\t' .. label_pairs[label]
end
return result
end

local pairs_metatable = {
__index = {
__metrics_make_key = function(self)
return serialize(self)
end
},
-- It protects pairs from being altered with unexpected labels.
__newindex = function(table, key, value)
if not keys_index[key] then
error(('Label "%s" is unexpected'):format(key), 2)
end
table[key] = value
end
}

return {
wrap = function(label_pairs)
return setmetatable(label_pairs, pairs_metatable)
end,
serialize = serialize
}
end

return {
registry = registry,
collectors = collectors,
@@ -140,4 +202,5 @@ return {
unregister_callback = unregister_callback,
invoke_callbacks = invoke_callbacks,
set_global_labels = set_global_labels,
labels_serializer = labels_serializer
}
4 changes: 4 additions & 0 deletions metrics/collectors/shared.lua
Original file line number Diff line number Diff line change
@@ -47,6 +47,10 @@ function Shared.make_key(label_pairs)
if type(label_pairs) ~= 'table' then
return ""
end
-- `label_pairs` provides its own serialization scheme, it must be used instead of default one.
if label_pairs.__metrics_make_key then
return label_pairs:__metrics_make_key()
end
local parts = {}
for k, v in pairs(label_pairs) do
table.insert(parts, k .. '\t' .. v)
1 change: 1 addition & 0 deletions metrics/init.lua
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ return setmetatable({
unregister_callback = api.unregister_callback,
invoke_callbacks = api.invoke_callbacks,
set_global_labels = api.set_global_labels,
labels_serializer = api.labels_serializer,
enable_default_metrics = tarantool.enable,
cfg = cfg.cfg,
http_middleware = http_middleware,
27 changes: 27 additions & 0 deletions test/metrics_test.lua
Original file line number Diff line number Diff line change
@@ -269,3 +269,30 @@ g.test_deprecated_version = function(cg)
"use require%('metrics'%)._VERSION instead."
t.assert_not_equals(cg.server:grep_log(warn), nil)
end

g.test_labels_serializer_consistent = function()
local shared = require("metrics.collectors.shared")

local label_pairs = {
abc = 123,
cba = "123",
cda = 0,
eda = -1,
acb = 456
}
local label_keys = {}
for key, _ in pairs(label_pairs) do
table.insert(label_keys, key)
end

local serializer = metrics.labels_serializer(label_keys)
local actual = serializer.serialize(label_pairs)
local wrapped = serializer.wrap(table.copy(label_pairs))

t.assert_equals(actual, shared.make_key(label_pairs))
t.assert_equals(actual, shared.make_key(wrapped))
t.assert_not_equals(wrapped.__metrics_make_key, nil)

-- trying to set unexpected label.
t.assert_error_msg_contains('Label "new_label" is unexpected', function() wrapped.new_label = "123456" end)
end