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