From d892e7dcbb26936be51187fe7c09a572db8e339a Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Mon, 30 Jan 2023 14:29:42 +0300 Subject: [PATCH] api: introduce metrics.cfg{} This patch introduces `metrics.cfg` as a single entrypoint to configure the module. It covers both `enable_default_metrics` and `set_global_labels` with the same behavior (except for deprecated `include={}` behavior, where it uses enable_v2 approach). It doesn't cover external features like cartridge HTTP setup yet, but it may change in the future. A single entrypoint is a prerequirement for configuring the module with box.cfg after embedding. `metrics.cfg` API is similar to `box.cfg` API (or more like `crud.cfg` API [1] since it has read-only table values). `metrics.cfg` values and effect are preserved between reloads. So if some default metrics were enabled with cfg before the reload, both their collectors and callbacks will remain active (they still be reloaded though). It is planned to replace `cartridge.roles.metrics` configuration code with using metrics.cfg after the deprecated behavior will be dropped. 1. https://github.com/tarantool/crud/commit/6da4f5684a00fe39106ac139538b06821c8c829c Part of tarantool/tarantool#7725 --- CHANGELOG.md | 10 + cartridge/roles/metrics.lua | 2 + doc/monitoring/api_reference.rst | 41 ++-- doc/monitoring/getting_started.rst | 11 +- doc/monitoring/plugins.rst | 4 +- metrics-scm-1.rockspec | 2 + metrics/cfg.lua | 91 +++++++++ metrics/init.lua | 2 + metrics/stash.lua | 48 +++++ test/cfg_test.lua | 185 ++++++++++++++++++ test/entrypoint/srv_basic_pure.lua | 9 + test/integration/cartridge_hotreload_test.lua | 32 +++ test/integration/hotreload_test.lua | 42 +++- test/utils.lua | 6 +- 14 files changed, 450 insertions(+), 35 deletions(-) create mode 100644 metrics/cfg.lua create mode 100644 metrics/stash.lua create mode 100644 test/cfg_test.lua create mode 100755 test/entrypoint/srv_basic_pure.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index fbe6eea3..2618cad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ 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 +- `metrics.cfg{}` -- a single entrypoint to setup the module: + - `include` and `exclude` options with the same effect as in + `enable_default_metrics(include, exclude)` (but its deprecated + features already disabled); + - `labels` options with the same effect as `set_global_labels(labels)`; + - values and effect (like default metrics callbacks) are preserved + between reloads; + - does not deal with external features like cartridge HTTP setup + ### Changed - Setup cartridge hotreload inside the role - Extend `enable_default_metrics()` API: diff --git a/cartridge/roles/metrics.lua b/cartridge/roles/metrics.lua index 04a1c538..85be0e6c 100644 --- a/cartridge/roles/metrics.lua +++ b/cartridge/roles/metrics.lua @@ -2,6 +2,7 @@ local cartridge = require('cartridge') local argparse = require('cartridge.argparse') local hotreload_supported, hotreload = pcall(require, 'cartridge.hotreload') local metrics = require('metrics') +local metrics_stash = require('metrics.stash') local log = require('log') local metrics_vars = require('cartridge.vars').new('metrics_vars') @@ -222,6 +223,7 @@ local function init() if hotreload_supported then hotreload.whitelist_globals({'__metrics_registry'}) + metrics_stash.setup_cartridge_reload() end end diff --git a/doc/monitoring/api_reference.rst b/doc/monitoring/api_reference.rst index 86345915..cbab48ce 100644 --- a/doc/monitoring/api_reference.rst +++ b/doc/monitoring/api_reference.rst @@ -303,20 +303,25 @@ You can also set global labels by calling Metrics functions ----------------- -.. function:: enable_default_metrics([include, exclude]) +.. function:: cfg([config]) + + Entrypoint to setup the module. - Enable Tarantool metric collection. + :param table config: module configuration options: - :param string/table include: ``'all'`` to enable all supported default metrics, - ``'none'`` to disable all default metrics, - table with names of the default metrics to enable a specific set of metrics - (``{}`` is the same as ``'all'`` for backward compatibility). - Default is ``'all'``. + * ``cfg.include`` (string/table, default ``'all'``): ``'all`` to enable all + supported default metrics, ``'none'`` to disable all default metrics, + table with names of the default metrics to enable a specific set of metrics. + * ``cfg.exclude`` (table, default ``{}``): table containing the names of + the default metrics that you want to disable. Has higher priority + than ``cfg.include``. + * ``cfg.labels`` (table, default ``{}``): table containing label names as + string keys, label values as values. - :param table exclude: table containing the names of the default metrics that you want to disable. - It has higher priority than ``include``. Default is ``{}``. + You can work with ``metrics.cfg`` as a table to read values, but you must call + ``metrics.cfg{}`` as a function to update them. - Default metric names: + Supported default metric names (for ``cfg.include`` and ``cfg.exclude`` tables): * ``network`` * ``operations`` @@ -340,12 +345,7 @@ Metrics functions See :ref:`metrics reference ` for details. All metric collectors from the collection have ``metainfo.default = true``. -.. function:: set_global_labels(label_pairs) - - Set the global labels to be added to every observation. - - :param table label_pairs: table containing label names as string keys, - label values as values. + ``cfg.labels`` are the global labels to be added to every observation. Global labels are applied only to metric collection. They have no effect on how observations are stored. @@ -358,6 +358,15 @@ Metrics functions Note that both label names and values in ``label_pairs`` are treated as strings. +.. function:: enable_default_metrics([include, exclude]) + + Same as ``metrics.cfg{include=include, exclude=exclude}``, but ``include={}`` is + treated as ``include='all'`` for backward compatibility. + +.. function:: set_global_labels(label_pairs) + + Same as ``metrics.cfg{labels=label_pairs}``. + .. function:: collect([opts]) Collect observations from each collector. diff --git a/doc/monitoring/getting_started.rst b/doc/monitoring/getting_started.rst index ae216c81..980750ad 100644 --- a/doc/monitoring/getting_started.rst +++ b/doc/monitoring/getting_started.rst @@ -21,17 +21,12 @@ Next, require it in your code: local metrics = require('metrics') -Set a global label for your metrics: +Enable default Tarantool metrics such as network, memory, operations, etc. +You may also set a global label for your metrics: .. code-block:: lua - metrics.set_global_labels({alias = 'alias'}) - -Enable default Tarantool metrics such as network, memory, operations, etc.: - -.. code-block:: lua - - metrics.enable_default_metrics() + metrics.cfg{alias = 'alias'} Initialize the Prometheus exporter or export metrics in another format: diff --git a/doc/monitoring/plugins.rst b/doc/monitoring/plugins.rst index 74932515..8a7dcd6c 100644 --- a/doc/monitoring/plugins.rst +++ b/doc/monitoring/plugins.rst @@ -57,7 +57,7 @@ Sample settings .. code-block:: lua metrics = require('metrics') - metrics.enable_default_metrics() + metrics.cfg{} prometheus = require('metrics.plugins.prometheus') httpd = require('http.server').new('0.0.0.0', 8080) httpd:route( { path = '/metrics' }, prometheus.collect_http) @@ -70,7 +70,7 @@ Sample settings cartridge = require('cartridge') httpd = cartridge.service_get('httpd') metrics = require('metrics') - metrics.enable_default_metrics() + metrics.cfg{} prometheus = require('metrics.plugins.prometheus') httpd:route( { path = '/metrics' }, prometheus.collect_http) diff --git a/metrics-scm-1.rockspec b/metrics-scm-1.rockspec index 0cdf2168..029e6a06 100644 --- a/metrics-scm-1.rockspec +++ b/metrics-scm-1.rockspec @@ -58,6 +58,8 @@ build = { ['metrics.tarantool.luajit'] = 'metrics/tarantool/luajit.lua', ['metrics.tarantool.vinyl'] = 'metrics/tarantool/vinyl.lua', ['metrics.utils'] = 'metrics/utils.lua', + ['metrics.cfg'] = 'metrics/cfg.lua', + ['metrics.stash'] = 'metrics/stash.lua', ['cartridge.roles.metrics'] = 'cartridge/roles/metrics.lua', ['cartridge.health'] = 'cartridge/health.lua', } diff --git a/metrics/cfg.lua b/metrics/cfg.lua new file mode 100644 index 00000000..4a24e7cb --- /dev/null +++ b/metrics/cfg.lua @@ -0,0 +1,91 @@ +-- Based on https://github.com/tarantool/crud/blob/73bf5bf9353f9b9ee69c95bb14c610be8f2daeac/crud/cfg.lua + +local checks = require('checks') + +local metrics_api = require('metrics.api') +local const = require('metrics.const') +local stash = require('metrics.stash') +local metrics_tarantool = require('metrics.tarantool') + +local function set_defaults_if_empty(cfg) + if cfg.include == nil then + cfg.include = const.ALL + end + + if cfg.exclude == nil then + cfg.exclude = {} + end + + if cfg.labels == nil then + cfg.labels = {} + end + + return cfg +end + +local function configure(cfg, opts) + if opts.include == nil then + opts.include = cfg.include + end + + if opts.exclude == nil then + opts.exclude = cfg.exclude + end + + if opts.labels == nil then + opts.labels = cfg.labels + end + + + metrics_tarantool.enable_v2(opts.include, opts.exclude) + metrics_api.set_global_labels(opts.labels) + + rawset(cfg, 'include', opts.include) + rawset(cfg, 'exclude', opts.exclude) + rawset(cfg, 'labels', opts.labels) +end + +local _cfg = set_defaults_if_empty(stash.get(stash.name.cfg)) +local _cfg_internal = stash.get(stash.name.cfg_internal) + +if _cfg_internal.initialized then + configure(_cfg, {}) +end + +local function __call(self, opts) + checks('table', { + include = '?string|table', + exclude = '?table', + labels = '?table', + }) + + opts = table.deepcopy(opts) or {} + + configure(_cfg, opts) + + _cfg_internal.initialized = true + + return self +end + +local function __index(_, key) + if _cfg_internal.initialized then + return _cfg[key] + else + error('Call metrics.cfg{} first') + end +end + +local function __newindex() + error('Use metrics.cfg{} instead') +end + +return { + -- Iterating through `metrics.cfg` with pairs is not supported yet. + cfg = setmetatable({}, { + __index = __index, + __newindex = __newindex, + __call = __call, + __serialize = function() return _cfg end + }), +} diff --git a/metrics/init.lua b/metrics/init.lua index badc15d1..25845b26 100644 --- a/metrics/init.lua +++ b/metrics/init.lua @@ -2,6 +2,7 @@ local api = require('metrics.api') local const = require('metrics.const') +local cfg = require('metrics.cfg') local http_middleware = require('metrics.http_middleware') local tarantool = require('metrics.tarantool') @@ -25,6 +26,7 @@ return { invoke_callbacks = api.invoke_callbacks, set_global_labels = api.set_global_labels, enable_default_metrics = tarantool.enable, + cfg = cfg.cfg, http_middleware = http_middleware, collect = api.collect, VERSION = VERSION, diff --git a/metrics/stash.lua b/metrics/stash.lua new file mode 100644 index 00000000..2f5560e0 --- /dev/null +++ b/metrics/stash.lua @@ -0,0 +1,48 @@ +-- Based on https://github.com/tarantool/crud/blob/73bf5bf9353f9b9ee69c95bb14c610be8f2daeac/crud/common/stash.lua + +local stash = {} + +--- Available stashes list. +-- +-- @tfield string cfg +-- Stash for metrics module configuration. +-- +stash.name = { + cfg = '__metrics_cfg', + cfg_internal = '__metrics_cfg_internal' +} + +--- Setup Tarantool Cartridge reload. +-- +-- @function setup_cartridge_reload +-- +-- @return Returns +-- +function stash.setup_cartridge_reload() + local hotreload = require('cartridge.hotreload') + for _, name in pairs(stash.name) do + hotreload.whitelist_globals({ name }) + end +end + +--- Get a stash instance, initialize if needed. +-- +-- Stashes are persistent to package reload. +-- To use them with Cartridge roles reload, +-- call `stash.setup_cartridge_reload` in role. +-- +-- @function get +-- +-- @string name +-- Stash identifier. Use one from `stash.name` table. +-- +-- @treturn table A stash instance. +-- +function stash.get(name) + local instance = rawget(_G, name) or {} + rawset(_G, name, instance) + + return instance +end + +return stash diff --git a/test/cfg_test.lua b/test/cfg_test.lua new file mode 100644 index 00000000..d0c93cc5 --- /dev/null +++ b/test/cfg_test.lua @@ -0,0 +1,185 @@ +local t = require('luatest') +local group = t.group('cfg') + +local fio = require('fio') + +local metrics = require('metrics') +local utils = require('test.utils') + +local root = fio.dirname(fio.dirname(fio.abspath(package.search('test.helper')))) + +local function create_server(g) + g.tmpdir = fio.tempdir() + g.server = t.Server:new({ + command = fio.pathjoin( + fio.dirname(debug.sourcedir()), + 'test', 'entrypoint', 'srv_basic_pure.lua' + ), + workdir = g.tmpdir, + net_box_port = 3031, + net_box_credentials = {user = 'guest', password = nil}, + env = { + LUA_PATH = root .. '/?.lua;' .. + root .. '/?/init.lua;' .. + root .. '/.rocks/share/tarantool/?.lua' + }, + }) + g.server:start() + t.helpers.retrying({}, function() g.server:connect_net_box() end) +end + +local function clean_server(g) + g.server:stop() + fio.rmtree(g.tmpdir) +end + +group.before_all(utils.init) + +group.before_each(function() + -- Reset to defaults. + metrics.cfg{ + include = 'all', + exclude = {}, + labels = {}, + } +end) + +group.before_test('test_default', create_server) + +group.test_default = function(g) + local cfg = g.server:eval([[ + local metrics = require('metrics') + return metrics.cfg{} + ]]) + + t.assert_equals(cfg, {include = 'all', exclude = {}, labels = {}}) + + local observations = g.server:eval([[ + local metrics = require('metrics') + return metrics.collect{invoke_callbacks = true} + ]]) + + local expected_metrics = { + 'tnt_info_uptime', 'tnt_info_memory_lua', + 'tnt_net_sent_total', 'tnt_slab_arena_used', + } + + for _, expected in ipairs(expected_metrics) do + local obs = utils.find_metric(expected, observations) + t.assert_not_equals(obs, nil, ("metric %q found"):format(expected)) + end +end + +group.after_test('test_default', clean_server) + +group.before_test('test_read_before_init', create_server) + +group.test_read_before_init = function(g) + t.assert_error_msg_contains( + 'Call metrics.cfg{} first', + function() + g.server:eval([[ + local metrics = require('metrics') + return metrics.cfg.include + ]]) + end) +end + +group.after_test('test_read_before_init', clean_server) + +group.test_table_value = function() + metrics.cfg{ + include = {'info'} + } + t.assert_equals(metrics.cfg.include, {'info'}) +end + +group.test_change_value = function() + local cfg = metrics.cfg{ + include = {'info'}, + } + t.assert_equals(cfg['include'], {'info'}) +end + +group.test_table_is_immutable = function() + t.assert_error_msg_contains( + 'Use metrics.cfg{} instead', + function() + metrics.cfg.include = {'info'} + end + ) + + t.assert_error_msg_contains( + 'Use metrics.cfg{} instead', + function() + metrics.cfg.newfield = 'newvalue' + end + ) +end + +group.test_include = function() + metrics.cfg{ + include = {'info'}, + } + + local default_metrics = metrics.collect{invoke_callbacks = true} + local uptime = utils.find_metric('tnt_info_uptime', default_metrics) + t.assert_not_equals(uptime, nil) + local memlua = utils.find_metric('tnt_info_memory_lua', default_metrics) + t.assert_equals(memlua, nil) +end + +group.test_exclude = function() + metrics.cfg{ + exclude = {'memory'}, + } + + local default_metrics = metrics.collect{invoke_callbacks = true} + local uptime = utils.find_metric('tnt_info_uptime', default_metrics) + t.assert_not_equals(uptime, nil) + local memlua = utils.find_metric('tnt_info_memory_lua', default_metrics) + t.assert_equals(memlua, nil) +end + +group.test_include_with_exclude = function() + metrics.cfg{ + include = {'info', 'memory'}, + exclude = {'memory'}, + } + + local default_metrics = metrics.collect{invoke_callbacks = true} + local uptime = utils.find_metric('tnt_info_uptime', default_metrics) + t.assert_not_equals(uptime, nil) + local memlua = utils.find_metric('tnt_info_memory_lua', default_metrics) + t.assert_equals(memlua, nil) +end + +group.test_include_none = function() + metrics.cfg{ + include = 'none', + exclude = {'memory'}, + } + + local default_metrics = metrics.collect{invoke_callbacks = true} + t.assert_equals(default_metrics, {}) +end + +group.test_labels = function() + metrics.cfg{ + labels = {mylabel = 'myvalue'}, + } + + local default_metrics = metrics.collect{invoke_callbacks = true} + local uptime = utils.find_obs('tnt_info_uptime', {mylabel = 'myvalue'}, default_metrics) + t.assert_equals(uptime.label_pairs, {mylabel = 'myvalue'}) + + metrics.cfg{ + labels = {}, + } + + default_metrics = metrics.collect{invoke_callbacks = true} + uptime = utils.find_obs('tnt_info_uptime', {}, default_metrics) + t.assert_equals(uptime.label_pairs, {}) +end + + diff --git a/test/entrypoint/srv_basic_pure.lua b/test/entrypoint/srv_basic_pure.lua new file mode 100755 index 00000000..e4e52d37 --- /dev/null +++ b/test/entrypoint/srv_basic_pure.lua @@ -0,0 +1,9 @@ +#!/usr/bin/env tarantool + +local workdir = os.getenv('TARANTOOL_WORKDIR') +local listen = os.getenv('TARANTOOL_LISTEN') +local log = os.getenv('TARANTOOL_LOG') + +box.cfg({work_dir = workdir, log = log}) +box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true}) +box.cfg({listen = listen}) diff --git a/test/integration/cartridge_hotreload_test.lua b/test/integration/cartridge_hotreload_test.lua index cdabdd92..380b562d 100644 --- a/test/integration/cartridge_hotreload_test.lua +++ b/test/integration/cartridge_hotreload_test.lua @@ -250,3 +250,35 @@ g.test_cartridge_hotreload_reset_callbacks = function() end) t.assert_equals(len_before_hotreload, len_after_hotreload) end + +g.test_cartridge_hotreload_preserves_cfg_state = function() + local main_server = g.cluster:server('main') + local cfg_before_hotreload = main_server:eval([[ + local metrics = require('metrics') + return metrics.cfg{include = {'operations'}} + ]]) + local obs_before_hotreload = main_server:eval([[ + local metrics = require('metrics') + return metrics.collect{invoke_callbacks = true} + ]]) + + reload_roles() + + local cfg_after_hotreload = main_server:eval([[ + local metrics = require('metrics') + return metrics.cfg + ]]) + local obs_after_hotreload = main_server:eval([[ + local metrics = require('metrics') + return metrics.collect{invoke_callbacks = true} + ]]) + + t.assert_equals(cfg_before_hotreload, cfg_after_hotreload, + "cfg values are preserved") + + local op_before = utils.find_obs('tnt_stats_op_total', {operation = 'eval'}, + obs_before_hotreload, t.assert_covers) + local op_after = utils.find_obs('tnt_stats_op_total', {operation = 'eval'}, + obs_after_hotreload, t.assert_covers) + t.assert_gt(op_after.value, op_before.value, "metric callbacks enabled by cfg stay enabled") +end diff --git a/test/integration/hotreload_test.lua b/test/integration/hotreload_test.lua index 0dfe8450..5a4b1144 100644 --- a/test/integration/hotreload_test.lua +++ b/test/integration/hotreload_test.lua @@ -5,6 +5,18 @@ local fio = require('fio') local t = require('luatest') local g = t.group('hotreload') +local utils = require('test.utils') + +local function package_reload() + for k, _ in pairs(package.loaded) do + if k:find('metrics') ~= nil then + package.loaded[k] = nil + end + end + + return require('metrics') +end + g.test_reload = function() local tmpdir = fio.tempdir() if type(box.cfg) == 'function' then @@ -27,13 +39,29 @@ g.test_reload = function() metrics.enable_default_metrics() - for k, _ in pairs(package.loaded) do - if k:find('metrics') ~= nil then - package.loaded[k] = nil - end - end - - metrics = require('metrics') + metrics = package_reload() metrics.enable_default_metrics() end + +g.test_cartridge_hotreload_preserves_cfg_state = function() + local metrics = require('metrics') + + local cfg_before_hotreload = metrics.cfg{include = {'operations'}} + local obs_before_hotreload = metrics.collect{invoke_callbacks = true} + + metrics = package_reload() + + local cfg_after_hotreload = metrics.cfg + box.space._space:select(nil, {limit = 1}) + local obs_after_hotreload = metrics.collect{invoke_callbacks = true} + + t.assert_equals(cfg_before_hotreload, cfg_after_hotreload, + "cfg values are preserved") + + local op_before = utils.find_obs('tnt_stats_op_total', {operation = 'select'}, + obs_before_hotreload, t.assert_covers) + local op_after = utils.find_obs('tnt_stats_op_total', {operation = 'select'}, + obs_after_hotreload, t.assert_covers) + t.assert_gt(op_after.value, op_before.value, "metric callbacks enabled by cfg stay enabled") +end diff --git a/test/utils.lua b/test/utils.lua index 7de9bf8f..0d73927c 100644 --- a/test/utils.lua +++ b/test/utils.lua @@ -22,9 +22,11 @@ function utils.init() } end -function utils.find_obs(metric_name, label_pairs, observations) +function utils.find_obs(metric_name, label_pairs, observations, comparator) + comparator = comparator or t.assert_equals + for _, obs in pairs(observations) do - local same_label_pairs = pcall(t.assert_equals, obs.label_pairs, label_pairs) + local same_label_pairs = pcall(comparator, obs.label_pairs, label_pairs) if obs.metric_name == metric_name and same_label_pairs then return obs end