diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c73fe0b..b69ae41b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `metrics.collect()`; - tools to compute metrics aggregates: - per second rate for counters; + - min and max for gauges; ### Changed - Setup cartridge hotreload inside the role diff --git a/doc/monitoring/api_reference.rst b/doc/monitoring/api_reference.rst index 602a5091..794bd480 100644 --- a/doc/monitoring/api_reference.rst +++ b/doc/monitoring/api_reference.rst @@ -614,7 +614,9 @@ Metrics functions Supported aggregates: * ``rate`` for counter collectors: per second rate of value change for the last - two observations. + two observations; + * ``min`` for gauge collectors: minimal value for the history of observations; + * ``max`` for gauge collectors: maximal value for the history of observations. :param table output_with_aggregates_prev: a previous result of this method call. Use ``nil`` if this is the first invokation. You may use diff --git a/metrics/aggregates.lua b/metrics/aggregates.lua index 91cea5ba..e562ee06 100644 --- a/metrics/aggregates.lua +++ b/metrics/aggregates.lua @@ -5,6 +5,8 @@ local Gauge = require('metrics.collectors.gauge') local mksec_in_sec = 1e6 local RATE_SUFFIX = 'per_second' +local MIN_SUFFIX = 'min' +local MAX_SUFFIX = 'max' local function compute_rate_value(time_delta, obs_prev, obs) if obs_prev == nil then @@ -70,13 +72,86 @@ local function compute_counter_rate(output_with_aggregates_prev, output, coll_ke return output end +local function compute_extremum_value(obs_prev, obs, method) + if obs_prev == nil then + return { + label_pairs = obs.label_pairs, + value = obs.value, + } + end + + return { + label_pairs = obs.label_pairs, + -- math.min and math.max doesn't work with cdata. + value = method(tonumber(obs_prev.value), tonumber(obs.value)) + } +end + +local function compute_gauge_extremum(output_with_aggregates_prev, output, coll_key, coll_obs, + extremum_method, extremum_suffix, extremum_help_line) + local name = string_utils.build_name(coll_obs.name_prefix, extremum_suffix) + local kind = coll_obs.kind + local registry_key = string_utils.build_registry_key(name, kind) + + if output[registry_key] ~= nil then + -- If, for any reason, registry collision had happenned, + -- we assume that there is already an aggregate metric with the + -- similar meaning. + return output + end + + local known_extremum + if output_with_aggregates_prev[registry_key] then -- previous extremum + known_extremum = output_with_aggregates_prev[registry_key] + elseif output_with_aggregates_prev[coll_key] then -- previous value + known_extremum = output_with_aggregates_prev[coll_key] + else -- only current observation + known_extremum = coll_obs + end + + local values = {} + + for key, obs in pairs(coll_obs.observations['']) do + local obs_prev = known_extremum.observations[''][key] + values[key] = compute_extremum_value(obs_prev, obs, extremum_method) + end + + local metainfo = table.deepcopy(coll_obs.metainfo) + metainfo.aggregate = true + + output[registry_key] = { + name = name, + name_prefix = coll_obs.name_prefix, + help = extremum_help_line .. coll_obs.name, + kind = kind, + metainfo = metainfo, + timestamp = coll_obs.timestamp, + observations = {[''] = values} + } + + return output +end + +local function compute_gauge_min(output_with_aggregates_prev, output, coll_key, coll_obs) + return compute_gauge_extremum(output_with_aggregates_prev, output, coll_key, coll_obs, + math.min, MIN_SUFFIX, "Minimum of ") +end + +local function compute_gauge_max(output_with_aggregates_prev, output, coll_key, coll_obs) + return compute_gauge_extremum(output_with_aggregates_prev, output, coll_key, coll_obs, + math.max, MAX_SUFFIX, "Maximum of ") +end + local default_kind_rules = { [Counter.kind] = { 'rate' }, + [Gauge.kind] = { 'min', 'max' }, } local rule_processors = { rate = compute_counter_rate, + min = compute_gauge_min, + max = compute_gauge_max, } local function compute(output_with_aggregates_prev, output, kind_rules) diff --git a/test/aggregates_test.lua b/test/aggregates_test.lua index 21889c76..6c17d230 100644 --- a/test/aggregates_test.lua +++ b/test/aggregates_test.lua @@ -35,6 +35,36 @@ local function get_counter_example(timestamp, value1, value2) return res end +local function get_gauge_example(timestamp, value1, value2) + local res = { + lj_gc_memorygauge = { + name = 'lj_gc_memory', + name_prefix = 'lj_gc_memory', + kind = 'gauge', + help = 'Memory currently allocated', + metainfo = { default = true }, + timestamp = timestamp, + observations = { [''] = {} } + } + } + + if value1 ~= nil then + res['lj_gc_memorygauge'].observations[''][''] = { + label_pairs = { alias = 'router' }, + value = value1, + } + end + + if value2 ~= nil then + res['lj_gc_memorygauge'].observations['']['source\tvinyl_procedures'] = { + label_pairs = { alias = 'router', source = 'vinyl_procedures' }, + value = value2, + } + end + + return res +end + g.test_unknown_rule = function() local output = get_counter_example(1676364616294847ULL, 14148, 3204) @@ -130,3 +160,148 @@ g.test_counter_rate_disabled = function() t.assert_equals(utils.len(output_with_aggregates_2), 1, "No rate computed due to options") end + +g.test_gauge_extremum_no_previous_data = function() + local output = get_gauge_example(1676364616294847ULL, 2020047, 327203) + + local output_with_aggregates = metrics.compute_aggregates(nil, output) + t.assert_equals(utils.len(output_with_aggregates), 3, + "Min and max computed for a single observation") + + local min_obs = output_with_aggregates['lj_gc_memory_mingauge'] + t.assert_not_equals(min_obs, nil, "min computed") + t.assert_equals(min_obs.name, 'lj_gc_memory_min') + t.assert_equals(min_obs.name_prefix, 'lj_gc_memory') + t.assert_equals(min_obs.kind, 'gauge') + t.assert_equals(min_obs.help, 'Minimum of lj_gc_memory') + t.assert_equals(min_obs.metainfo.default, true) + t.assert_equals(min_obs.metainfo.aggregate, true) + t.assert_equals(min_obs.timestamp, 1676364616294847ULL) + t.assert_equals(min_obs.observations[''][''].label_pairs, { alias = 'router' }) + t.assert_almost_equals(min_obs.observations[''][''].value, 2020047) + t.assert_equals(min_obs.observations['']['source\tvinyl_procedures'].label_pairs, + { alias = 'router', source = 'vinyl_procedures' }) + t.assert_almost_equals(min_obs.observations['']['source\tvinyl_procedures'].value, 327203) + + local max_obs = output_with_aggregates['lj_gc_memory_maxgauge'] + t.assert_not_equals(max_obs, nil, "max computed") + t.assert_equals(max_obs.name, 'lj_gc_memory_max') + t.assert_equals(max_obs.name_prefix, 'lj_gc_memory') + t.assert_equals(max_obs.kind, 'gauge') + t.assert_equals(max_obs.help, 'Maximum of lj_gc_memory') + t.assert_equals(max_obs.metainfo.default, true) + t.assert_equals(max_obs.metainfo.aggregate, true) + t.assert_equals(max_obs.timestamp, 1676364616294847ULL) + t.assert_equals(max_obs.observations[''][''].label_pairs, { alias = 'router' }) + t.assert_almost_equals(max_obs.observations[''][''].value, 2020047) + t.assert_equals(max_obs.observations['']['source\tvinyl_procedures'].label_pairs, + { alias = 'router', source = 'vinyl_procedures' }) + t.assert_almost_equals(max_obs.observations['']['source\tvinyl_procedures'].value, 327203) +end + +g.test_gauge_extremum_prev_aggregates = function() + local output_1 = get_gauge_example(1676364616294847ULL, 2020047, 327203) + local output_2 = get_gauge_example(1676364616294847ULL, 1920047, 429203) + + local output_with_aggregates_1 = metrics.compute_aggregates(nil, output_1) + local output_with_aggregates_2 = metrics.compute_aggregates(output_with_aggregates_1, output_2) + t.assert_equals(utils.len(output_with_aggregates_2), 3, + "Min and max computed for a single observation") + + local min_obs = output_with_aggregates_2['lj_gc_memory_mingauge'] + t.assert_not_equals(min_obs, nil, "min computed") + t.assert_equals(min_obs.name, 'lj_gc_memory_min') + t.assert_equals(min_obs.name_prefix, 'lj_gc_memory') + t.assert_equals(min_obs.kind, 'gauge') + t.assert_equals(min_obs.help, 'Minimum of lj_gc_memory') + t.assert_equals(min_obs.metainfo.default, true) + t.assert_equals(min_obs.metainfo.aggregate, true) + t.assert_equals(min_obs.timestamp, 1676364616294847ULL) + t.assert_equals(min_obs.observations[''][''].label_pairs, { alias = 'router' }) + t.assert_almost_equals(min_obs.observations[''][''].value, 1920047) + t.assert_equals(min_obs.observations['']['source\tvinyl_procedures'].label_pairs, + { alias = 'router', source = 'vinyl_procedures' }) + t.assert_almost_equals(min_obs.observations['']['source\tvinyl_procedures'].value, 327203) + + local max_obs = output_with_aggregates_2['lj_gc_memory_maxgauge'] + t.assert_not_equals(max_obs, nil, "max computed") + t.assert_equals(max_obs.name, 'lj_gc_memory_max') + t.assert_equals(max_obs.name_prefix, 'lj_gc_memory') + t.assert_equals(max_obs.kind, 'gauge') + t.assert_equals(max_obs.help, 'Maximum of lj_gc_memory') + t.assert_equals(max_obs.metainfo.default, true) + t.assert_equals(max_obs.metainfo.aggregate, true) + t.assert_equals(max_obs.timestamp, 1676364616294847ULL) + t.assert_equals(max_obs.observations[''][''].label_pairs, { alias = 'router' }) + t.assert_almost_equals(max_obs.observations[''][''].value, 2020047) + t.assert_equals(max_obs.observations['']['source\tvinyl_procedures'].label_pairs, + { alias = 'router', source = 'vinyl_procedures' }) + t.assert_almost_equals(max_obs.observations['']['source\tvinyl_procedures'].value, 429203) +end + +g.test_gauge_extremum_prev_raw = function() + local output_1 = get_gauge_example(1676364616294847ULL, 2020047, 327203) + local output_2 = get_gauge_example(1676364616294847ULL, 1920047, 429203) + + local output_with_aggregates_2 = metrics.compute_aggregates(output_1, output_2) + t.assert_equals(utils.len(output_with_aggregates_2), 3, + "Min and max computed for a single observation") + + local min_obs = output_with_aggregates_2['lj_gc_memory_mingauge'] + t.assert_not_equals(min_obs, nil, "min computed") + t.assert_equals(min_obs.name, 'lj_gc_memory_min') + t.assert_equals(min_obs.name_prefix, 'lj_gc_memory') + t.assert_equals(min_obs.kind, 'gauge') + t.assert_equals(min_obs.help, 'Minimum of lj_gc_memory') + t.assert_equals(min_obs.metainfo.default, true) + t.assert_equals(min_obs.metainfo.aggregate, true) + t.assert_equals(min_obs.timestamp, 1676364616294847ULL) + t.assert_equals(min_obs.observations[''][''].label_pairs, { alias = 'router' }) + t.assert_almost_equals(min_obs.observations[''][''].value, 1920047) + t.assert_equals(min_obs.observations['']['source\tvinyl_procedures'].label_pairs, + { alias = 'router', source = 'vinyl_procedures' }) + t.assert_almost_equals(min_obs.observations['']['source\tvinyl_procedures'].value, 327203) + + local max_obs = output_with_aggregates_2['lj_gc_memory_maxgauge'] + t.assert_not_equals(max_obs, nil, "max computed") + t.assert_equals(max_obs.name, 'lj_gc_memory_max') + t.assert_equals(max_obs.name_prefix, 'lj_gc_memory') + t.assert_equals(max_obs.kind, 'gauge') + t.assert_equals(max_obs.help, 'Maximum of lj_gc_memory') + t.assert_equals(max_obs.metainfo.default, true) + t.assert_equals(max_obs.metainfo.aggregate, true) + t.assert_equals(max_obs.timestamp, 1676364616294847ULL) + t.assert_equals(max_obs.observations[''][''].label_pairs, { alias = 'router' }) + t.assert_almost_equals(max_obs.observations[''][''].value, 2020047) + t.assert_equals(max_obs.observations['']['source\tvinyl_procedures'].label_pairs, + { alias = 'router', source = 'vinyl_procedures' }) + t.assert_almost_equals(max_obs.observations['']['source\tvinyl_procedures'].value, 429203) +end + +g.test_gauge_extremum_new_label = function() + local output_1 = get_gauge_example(1676364616294847ULL, 2020047, nil) + local output_2 = get_gauge_example(1676364616294847ULL, 1920047, 429203) + + local output_with_aggregates_1 = metrics.compute_aggregates(nil, output_1) + local output_with_aggregates_2 = metrics.compute_aggregates(output_with_aggregates_1, output_2) + t.assert_equals(utils.len(output_with_aggregates_2), 3, + "Min and max computed for a single observation") + + local min_obs = output_with_aggregates_2['lj_gc_memory_mingauge'] + t.assert_almost_equals(min_obs.observations['']['source\tvinyl_procedures'].value, 429203) + + local max_obs = output_with_aggregates_2['lj_gc_memory_maxgauge'] + t.assert_almost_equals(max_obs.observations['']['source\tvinyl_procedures'].value, 429203) +end + +g.test_gauge_min_max_disabled = function() + local output_1 = get_counter_example(1676364616294847ULL, 14148, 3204) + local output_2 = get_counter_example(1676364616294847ULL + 100 * 1e6, 14148 + 200, 3204 + 50) + + local opts = { gauge = {} } + local output_with_aggregates_1 = metrics.compute_aggregates(nil, output_1, opts) + local output_with_aggregates_2 = metrics.compute_aggregates(output_with_aggregates_1, output_2, opts) + + t.assert_equals(utils.len(output_with_aggregates_2), 1, + "No min or max computed due to options") +end