aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.luacheckrc4
-rw-r--r--core/moduleapi.lua45
-rw-r--r--core/statsmanager.lua204
-rw-r--r--plugins/mod_admin_shell.lua420
-rw-r--r--util/openmetrics.lua308
-rw-r--r--util/statistics.lua326
-rw-r--r--util/statsd.lua297
7 files changed, 1110 insertions, 494 deletions
diff --git a/.luacheckrc b/.luacheckrc
index 43a508f8..680fb017 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -80,9 +80,7 @@ files["plugins/"] = {
"module.log",
"module.log_status",
"module.measure",
- "module.measure_event",
- "module.measure_global_event",
- "module.measure_object_event",
+ "module.metric",
"module.open_store",
"module.provides",
"module.remove_item",
diff --git a/core/moduleapi.lua b/core/moduleapi.lua
index 82002737..59417027 100644
--- a/core/moduleapi.lua
+++ b/core/moduleapi.lua
@@ -510,26 +510,33 @@ end
function api:measure(name, stat_type, conf)
local measure = require "core.statsmanager".measure;
- return measure(stat_type, "/"..self.host.."/mod_"..self.name.."/"..name, conf);
-end
-
-function api:measure_object_event(events_object, event_name, stat_name)
- local m = self:measure(stat_name or event_name, "times");
- local function handler(handlers, _event_name, _event_data)
- local finished = m();
- local ret = handlers(_event_name, _event_data);
- finished();
- return ret;
+ local fixed_label_key, fixed_label_value
+ if self.host ~= "*" then
+ fixed_label_key = "host"
+ fixed_label_value = self.host
end
- return self:hook_object_event(events_object, event_name, handler);
-end
-
-function api:measure_event(event_name, stat_name)
- return self:measure_object_event((hosts[self.host] or prosody).events.wrappers, event_name, stat_name);
-end
-
-function api:measure_global_event(event_name, stat_name)
- return self:measure_object_event(prosody.events.wrappers, event_name, stat_name);
+ -- new_legacy_metric takes care of scoping for us, as it does not accept
+ -- an array of labels
+ -- the prosody_ prefix is automatically added by statsmanager for legacy
+ -- metrics.
+ return measure(stat_type, "mod_"..self.name.."/"..name, conf, fixed_label_key, fixed_label_value)
+end
+
+function api:metric(type_, name, unit, description, label_keys, conf)
+ local metric = require "core.statsmanager".metric;
+ local is_scoped = self.host ~= "*"
+ if is_scoped then
+ -- prepend `host` label to label keys if this is not a global module
+ local orig_labels = label_keys
+ label_keys = array { "host" }
+ label_keys:append(orig_labels)
+ end
+ local mf = metric(type_, "prosody_mod_"..self.name.."/"..name, unit, description, label_keys, conf)
+ if is_scoped then
+ -- make sure to scope the returned metric family to the current host
+ return mf:with_partial_label(self.host)
+ end
+ return mf
end
local status_priorities = { error = 3, warn = 2, info = 1, core = 0 };
diff --git a/core/statsmanager.lua b/core/statsmanager.lua
index bca7510f..8323c160 100644
--- a/core/statsmanager.lua
+++ b/core/statsmanager.lua
@@ -3,6 +3,8 @@ local config = require "core.configmanager";
local log = require "util.logger".init("stats");
local timer = require "util.timer";
local fire_event = prosody.events.fire_event;
+local array = require "util.array";
+local timed = require "util.openmetrics".timed;
local stats_interval_config = config.get("*", "statistics_interval");
local stats_interval = tonumber(stats_interval_config);
@@ -57,15 +59,149 @@ if stats == nil then
log("error", "Error loading statistics provider '%s': %s", stats_provider, stats_err);
end
-local measure, collect;
-local latest_stats = {};
-local changed_stats = {};
-local stats_extra = {};
+local measure, collect, metric, cork, uncork;
if stats then
- function measure(type, name, conf)
- local f = assert(stats[type], "unknown stat type: "..type);
- return f(name, conf);
+ function metric(type_, name, unit, description, labels, extra)
+ local registry = stats.metric_registry
+ local f = assert(registry[type_], "unknown metric family type: "..type_);
+ return f(registry, name, unit or "", description or "", labels, extra);
+ end
+
+ local function new_legacy_metric(stat_type, name, unit, description, fixed_label_key, fixed_label_value, extra)
+ local label_keys = array()
+ local conf = extra or {}
+ if fixed_label_key then
+ label_keys:push(fixed_label_key)
+ end
+ unit = unit or ""
+ local mf = metric(stat_type, "prosody_" .. name, unit, description, label_keys, conf);
+ if fixed_label_key then
+ mf = mf:with_partial_label(fixed_label_value)
+ end
+ return mf:with_labels()
+ end
+
+ local function unwrap_legacy_extra(extra, type_, name, unit)
+ local description = extra and extra.description or "Legacy "..type_.." metric "..name
+ unit = extra and extra.unit or unit
+ return description, unit
+ end
+
+ -- These wrappers provide the pre-OpenMetrics interface of statsmanager
+ -- and moduleapi (module:measure).
+ local legacy_metric_wrappers = {
+ amount = function(name, fixed_label_key, fixed_label_value, extra)
+ local initial = 0
+ if type(extra) == "number" then
+ initial = extra
+ else
+ initial = extra and extra.initial or initial
+ end
+ local description, unit = unwrap_legacy_extra(extra, "amount", name)
+
+ local m = new_legacy_metric("gauge", name, unit, description, fixed_label_key, fixed_label_value)
+ m:set(initial or 0)
+ return function(v)
+ m:set(v)
+ end
+ end;
+
+ counter = function(name, fixed_label_key, fixed_label_value, extra)
+ if type(extra) == "number" then
+ -- previous versions of the API allowed passing an initial
+ -- value here; we do not allow that anymore, it is not a thing
+ -- which makes sense with counters
+ extra = nil
+ end
+
+ local description, unit = unwrap_legacy_extra(extra, "counter", name)
+
+ local m = new_legacy_metric("counter", name, unit, description, fixed_label_key, fixed_label_value)
+ m:set(0)
+ return function(v)
+ m:add(v)
+ end
+ end;
+
+ rate = function(name, fixed_label_key, fixed_label_value, extra)
+ if type(extra) == "number" then
+ -- previous versions of the API allowed passing an initial
+ -- value here; we do not allow that anymore, it is not a thing
+ -- which makes sense with counters
+ extra = nil
+ end
+
+ local description, unit = unwrap_legacy_extra(extra, "counter", name)
+
+ local m = new_legacy_metric("counter", name, unit, description, fixed_label_key, fixed_label_value)
+ m:set(0)
+ return function()
+ m:add(1)
+ end
+ end;
+
+ times = function(name, fixed_label_key, fixed_label_value, extra)
+ local conf = {}
+ if extra and extra.buckets then
+ conf.buckets = extra.buckets
+ else
+ conf.buckets = { 0.001, 0.01, 0.1, 1.0, 10.0, 100.0 }
+ end
+ local description, _ = unwrap_legacy_extra(extra, "times", name)
+
+ local m = new_legacy_metric("histogram", name, "seconds", description, fixed_label_key, fixed_label_value, conf)
+ return function()
+ return timed(m)
+ end
+ end;
+
+ sizes = function(name, fixed_label_key, fixed_label_value, extra)
+ local conf = {}
+ if extra and extra.buckets then
+ conf.buckets = extra.buckets
+ else
+ conf.buckets = { 1024, 4096, 32768, 131072, 1048576, 4194304, 33554432, 134217728, 1073741824 }
+ end
+ local description, _ = unwrap_legacy_extra(extra, "sizes", name)
+
+ local m = new_legacy_metric("histogram", name, "bytes", description, fixed_label_key, fixed_label_value, conf)
+ return function(v)
+ m:sample(v)
+ end
+ end;
+
+ distribution = function(name, fixed_label_key, fixed_label_value, extra)
+ if type(extra) == "string" then
+ -- compat with previous API
+ extra = { unit = extra }
+ end
+ local description, unit = unwrap_legacy_extra(extra, "distribution", name, "")
+ local m = new_legacy_metric("summary", name, unit, description, fixed_label_key, fixed_label_value)
+ return function(v)
+ m:sample(v)
+ end
+ end;
+ };
+
+ -- Argument order switched here to support the legacy statsmanager.measure
+ -- interface.
+ function measure(stat_type, name, extra, fixed_label_key, fixed_label_value)
+ local wrapper = assert(legacy_metric_wrappers[stat_type], "unknown legacy metric type "..stat_type)
+ return wrapper(name, fixed_label_key, fixed_label_value, extra)
+ end
+
+ if stats.cork then
+ function cork()
+ return stats:cork()
+ end
+
+ function uncork()
+ return stats:uncork()
+ end
+ else
+ function cork() end
+ function uncork() end
end
if stats_interval or stats_interval_config == "manual" then
@@ -76,27 +212,23 @@ if stats then
function collect()
local mark_collection_done = mark_collection_start();
fire_event("stats-update");
+ -- ensure that the backend is uncorked, in case it got stuck at
+ -- some point, to avoid infinite resource use
+ uncork()
mark_collection_done();
+ local manual_result = nil
- if stats.get_stats then
+ if stats.metric_registry then
+ -- only if supported by the backend, we fire the event which
+ -- provides the current metric values
local mark_processing_done = mark_processing_start();
- changed_stats, stats_extra = {}, {};
- for stat_name, getter in pairs(stats.get_stats()) do
- -- luacheck: ignore 211/type
- local type, value, extra = getter();
- local old_value = latest_stats[stat_name];
- latest_stats[stat_name] = value;
- if value ~= old_value then
- changed_stats[stat_name] = value;
- end
- if extra then
- stats_extra[stat_name] = extra;
- end
- end
- fire_event("stats-updated", { stats = latest_stats, changed_stats = changed_stats, stats_extra = stats_extra });
+ local metric_registry = stats.metric_registry;
+ fire_event("openmetrics-updated", { metric_registry = metric_registry })
mark_processing_done();
+ manual_result = metric_registry;
end
- return stats_interval;
+
+ return stats_interval, manual_result;
end
if stats_interval then
log("debug", "Statistics enabled using %s provider, collecting every %d seconds", stats_provider_name, stats_interval);
@@ -112,6 +244,22 @@ if stats then
else
log("debug", "Statistics disabled");
function measure() return measure; end
+
+ local dummy_mt = {}
+ function dummy_mt.__newindex()
+ end
+ function dummy_mt:__index()
+ return self
+ end
+ function dummy_mt:__call()
+ return self
+ end
+ local dummy = {}
+ setmetatable(dummy, dummy_mt)
+
+ function metric() return dummy; end
+ function cork() end
+ function uncork() end
end
local exported_collect = nil;
@@ -122,10 +270,10 @@ end
return {
collect = exported_collect;
measure = measure;
- get_stats = function ()
- return latest_stats, changed_stats, stats_extra;
- end;
- get = function (name)
- return latest_stats[name], stats_extra[name];
+ cork = cork;
+ uncork = uncork;
+ metric = metric;
+ get_metric_registry = function ()
+ return stats and stats.metric_registry or nil
end;
};
diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua
index 7c2003e0..2e24bdf2 100644
--- a/plugins/mod_admin_shell.lua
+++ b/plugins/mod_admin_shell.lua
@@ -36,6 +36,9 @@ local serialization = require "util.serialization";
local serialize_config = serialization.new ({ fatal = false, unquoted = true});
local time = require "util.time";
+local t_insert = table.insert;
+local t_concat = table.concat;
+
local format_number = require "util.human.units".format;
local format_table = require "util.human.io".table;
@@ -1342,187 +1345,112 @@ local short_units = {
bytes = "B",
};
-local function format_stat(type, unit, value, ref_value)
- ref_value = ref_value or value;
- --do return tostring(value) end
- if not unit then
- if type == "duration" then
- unit = "seconds"
- elseif type == "size" then
- unit = "bytes";
- elseif type == "rate" then
- unit = " events/sec"
- if ref_value < 0.9 then
- unit = "events/min"
- value = value*60;
- if ref_value < 0.6/60 then
- unit = "events/h"
- value = value*60;
- end
+local stats_methods = {};
+
+function stats_methods:render_single_fancy_histogram_ex(print, prefix, metric_family, metric, cumulative)
+ local creation_timestamp, sum, count
+ local buckets = {}
+ local prev_bucket_count = 0
+ for suffix, extra_labels, value in metric:iter_samples() do
+ if suffix == "_created" then
+ creation_timestamp = value
+ elseif suffix == "_sum" then
+ sum = value
+ elseif suffix == "_count" then
+ count = value
+ else
+ local bucket_threshold = extra_labels["le"]
+ local bucket_count
+ if cumulative then
+ bucket_count = value
+ else
+ bucket_count = value - prev_bucket_count
+ prev_bucket_count = value
+ end
+ if bucket_threshold == "+Inf" then
+ t_insert(buckets, {threshold = 1/0, count = bucket_count})
+ elseif bucket_threshold ~= nil then
+ t_insert(buckets, {threshold = tonumber(bucket_threshold), count = bucket_count})
end
- return ("%.3g %s"):format(value, unit);
end
end
- return format_number(value, short_units[unit] or unit or "", unit == "bytes" and 'b' or nil);
-end
-local stats_methods = {};
-function stats_methods:bounds(_lower, _upper)
- for _, stat_info in ipairs(self) do
- local data = stat_info[4];
- if data then
- local lower = _lower or data.min;
- local upper = _upper or data.max;
- local new_data = {
- min = lower;
- max = upper;
- samples = {};
- sample_count = 0;
- count = data.count;
- units = data.units;
- };
- local sum = 0;
- for _, v in ipairs(data.samples) do
- if v > upper then
- break;
- elseif v>=lower then
- table.insert(new_data.samples, v);
- sum = sum + v;
- end
- end
- new_data.sample_count = #new_data.samples;
- stat_info[4] = new_data;
- stat_info[3] = sum/new_data.sample_count;
+ if #buckets == 0 or not creation_timestamp or not sum or not count then
+ print("[no data or not a histogram]")
+ return false
+ end
+
+ local graph_width, graph_height, wscale = #buckets, 10, 1;
+ if graph_width < 8 then
+ wscale = 8
+ elseif graph_width < 16 then
+ wscale = 4
+ elseif graph_width < 32 then
+ wscale = 2
+ end
+ local eighth_chars = " ▁▂▃▄▅▆▇█";
+
+ local max_bin_samples = 0
+ for _, bucket in ipairs(buckets) do
+ if bucket.count > max_bin_samples then
+ max_bin_samples = bucket.count
end
end
- return self;
-end
-function stats_methods:trim(lower, upper)
- upper = upper or (100-lower);
- local statistics = require "util.statistics";
- for _, stat_info in ipairs(self) do
- -- Strip outliers
- local data = stat_info[4];
- if data then
- local new_data = {
- min = statistics.get_percentile(data, lower);
- max = statistics.get_percentile(data, upper);
- samples = {};
- sample_count = 0;
- count = data.count;
- units = data.units;
- };
- local sum = 0;
- for _, v in ipairs(data.samples) do
- if v > new_data.max then
- break;
- elseif v>=new_data.min then
- table.insert(new_data.samples, v);
- sum = sum + v;
- end
+ print("");
+ print(prefix)
+ print(("_"):rep(graph_width*wscale).." "..max_bin_samples);
+ for row = graph_height, 1, -1 do
+ local row_chars = {};
+ local min_eighths, max_eighths = 8, 0;
+ for i = 1, #buckets do
+ local char_eighths = math.ceil(math.max(math.min((graph_height/(max_bin_samples/buckets[i].count))-(row-1), 1), 0)*8);
+ if char_eighths < min_eighths then
+ min_eighths = char_eighths;
+ end
+ if char_eighths > max_eighths then
+ max_eighths = char_eighths;
+ end
+ if char_eighths == 0 then
+ row_chars[i] = ("-"):rep(wscale);
+ else
+ local char = eighth_chars:sub(char_eighths*3+1, char_eighths*3+3);
+ row_chars[i] = char:rep(wscale);
end
- new_data.sample_count = #new_data.samples;
- stat_info[4] = new_data;
- stat_info[3] = sum/new_data.sample_count;
end
+ print(table.concat(row_chars).."|- "..string.format("%.8g", math.ceil((max_bin_samples/graph_height)*(row-0.5))));
end
- return self;
-end
-function stats_methods:max(upper)
- return self:bounds(nil, upper);
+ local legend_pat = string.format("%%%d.%dg", wscale-1, wscale-1)
+ local row = {}
+ for i = 1, #buckets do
+ local threshold = buckets[i].threshold
+ t_insert(row, legend_pat:format(threshold))
+ end
+ t_insert(row, " " .. metric_family.unit)
+ print(t_concat(row, "/"))
+
+ return true
end
-function stats_methods:min(lower)
- return self:bounds(lower, nil);
+function stats_methods:render_single_fancy_histogram(print, prefix, metric_family, metric)
+ return self:render_single_fancy_histogram_ex(print, prefix, metric_family, metric, false)
end
-function stats_methods:summary()
- local statistics = require "util.statistics";
- for _, stat_info in ipairs(self) do
- local type, value, data = stat_info[2], stat_info[3], stat_info[4];
- if data and data.samples then
- table.insert(stat_info.output, string.format("Count: %d (%d captured)",
- data.count,
- data.sample_count
- ));
- table.insert(stat_info.output, string.format("Min: %s Mean: %s Max: %s",
- format_stat(type, data.units, data.min),
- format_stat(type, data.units, value),
- format_stat(type, data.units, data.max)
- ));
- table.insert(stat_info.output, string.format("Q1: %s Median: %s Q3: %s",
- format_stat(type, data.units, statistics.get_percentile(data, 25)),
- format_stat(type, data.units, statistics.get_percentile(data, 50)),
- format_stat(type, data.units, statistics.get_percentile(data, 75))
- ));
- end
- end
- return self;
+function stats_methods:render_single_fancy_histogram_cf(print, prefix, metric_family, metric)
+ -- cf = cumulative frequency
+ return self:render_single_fancy_histogram_ex(print, prefix, metric_family, metric, true)
end
function stats_methods:cfgraph()
for _, stat_info in ipairs(self) do
- local name, type, value, data = unpack(stat_info, 1, 4); -- luacheck: ignore 211
+ local family_name, metric_family = unpack(stat_info, 1, 2)
local function print(s)
table.insert(stat_info.output, s);
end
- if data and data.sample_count and data.sample_count > 0 then
- local raw_histogram = require "util.statistics".get_histogram(data);
-
- local graph_width, graph_height = 50, 10;
- local eighth_chars = " ▁▂▃▄▅▆▇█";
-
- local range = data.max - data.min;
-
- if range > 0 then
- local x_scaling = #raw_histogram/graph_width;
- local histogram = {};
- for i = 1, graph_width do
- histogram[i] = math.max(raw_histogram[i*x_scaling-1] or 0, raw_histogram[i*x_scaling] or 0);
- end
-
- print("");
- print(("_"):rep(52)..format_stat(type, data.units, data.max));
- for row = graph_height, 1, -1 do
- local row_chars = {};
- local min_eighths, max_eighths = 8, 0;
- for i = 1, #histogram do
- local char_eighths = math.ceil(math.max(math.min((graph_height/(data.max/histogram[i]))-(row-1), 1), 0)*8);
- if char_eighths < min_eighths then
- min_eighths = char_eighths;
- end
- if char_eighths > max_eighths then
- max_eighths = char_eighths;
- end
- if char_eighths == 0 then
- row_chars[i] = "-";
- else
- local char = eighth_chars:sub(char_eighths*3+1, char_eighths*3+3);
- row_chars[i] = char;
- end
- end
- print(table.concat(row_chars).."|-"..format_stat(type, data.units, data.max/(graph_height/(row-0.5))));
- end
- print(("\\ "):rep(11));
- local x_labels = {};
- for i = 1, 11 do
- local s = ("%-4s"):format((i-1)*10);
- if #s > 4 then
- s = s:sub(1, 3).."…";
- end
- x_labels[i] = s;
- end
- print(" "..table.concat(x_labels, " "));
- local units = "%";
- local margin = math.floor((graph_width-#units)/2);
- print((" "):rep(margin)..units);
- else
- print("[range too small to graph]");
- end
- print("");
+ if not self:render_family(print, family_name, metric_family, self.render_single_fancy_histogram_cf) then
+ return self
end
end
return self;
@@ -1530,81 +1458,90 @@ end
function stats_methods:histogram()
for _, stat_info in ipairs(self) do
- local name, type, value, data = unpack(stat_info, 1, 4); -- luacheck: ignore 211
+ local family_name, metric_family = unpack(stat_info, 1, 2)
local function print(s)
table.insert(stat_info.output, s);
end
- if not data then
- print("[no data]");
- return self;
- elseif not data.sample_count then
- print("[not a sampled metric type]");
- return self;
+ if not self:render_family(print, family_name, metric_family, self.render_single_fancy_histogram) then
+ return self
end
+ end
+ return self;
+end
- local graph_width, graph_height = 50, 10;
- local eighth_chars = " ▁▂▃▄▅▆▇█";
-
- local range = data.max - data.min;
+function stats_methods:render_single_counter(print, prefix, metric_family, metric)
+ local created_timestamp, current_value
+ for suffix, _, value in metric:iter_samples() do
+ if suffix == "_created" then
+ created_timestamp = value
+ elseif suffix == "_total" then
+ current_value = value
+ end
+ end
+ if current_value and created_timestamp then
+ local base_unit = short_units[metric_family.unit] or metric_family.unit
+ local unit = base_unit .. "/s"
+ local factor = 1
+ if base_unit == "s" then
+ -- be smart!
+ unit = "%"
+ factor = 100
+ elseif base_unit == "" then
+ unit = "events/s"
+ end
+ print(("%-50s %s"):format(prefix, format_number(factor * current_value / (self.now - created_timestamp), unit.." [avg]")));
+ end
+end
- if range > 0 then
- local n_buckets = graph_width;
+function stats_methods:render_single_gauge(print, prefix, metric_family, metric)
+ local current_value
+ for _, _, value in metric:iter_samples() do
+ current_value = value
+ end
+ if current_value then
+ local unit = short_units[metric_family.unit] or metric_family.unit
+ print(("%-50s %s"):format(prefix, format_number(current_value, unit)));
+ end
+end
- local histogram = {};
- for i = 1, n_buckets do
- histogram[i] = 0;
- end
- local max_bin_samples = 0;
- for _, d in ipairs(data.samples) do
- local bucket = math.floor(1+(n_buckets-1)/(range/(d-data.min)));
- histogram[bucket] = histogram[bucket] + 1;
- if histogram[bucket] > max_bin_samples then
- max_bin_samples = histogram[bucket];
- end
- end
+function stats_methods:render_single_summary(print, prefix, metric_family, metric)
+ local sum, count
+ for suffix, _, value in metric:iter_samples() do
+ if suffix == "_sum" then
+ sum = value
+ elseif suffix == "_count" then
+ count = value
+ end
+ end
+ if sum and count then
+ local unit = short_units[metric_family.unit] or metric_family.unit
+ if count == 0 then
+ print(("%-50s %s"):format(prefix, "no obs."));
+ else
+ print(("%-50s %s"):format(prefix, format_number(sum / count, unit.."/event [avg]")));
+ end
+ end
+end
- print("");
- print(("_"):rep(52)..max_bin_samples);
- for row = graph_height, 1, -1 do
- local row_chars = {};
- local min_eighths, max_eighths = 8, 0;
- for i = 1, #histogram do
- local char_eighths = math.ceil(math.max(math.min((graph_height/(max_bin_samples/histogram[i]))-(row-1), 1), 0)*8);
- if char_eighths < min_eighths then
- min_eighths = char_eighths;
- end
- if char_eighths > max_eighths then
- max_eighths = char_eighths;
- end
- if char_eighths == 0 then
- row_chars[i] = "-";
- else
- local char = eighth_chars:sub(char_eighths*3+1, char_eighths*3+3);
- row_chars[i] = char;
- end
- end
- print(table.concat(row_chars).."|-"..math.ceil((max_bin_samples/graph_height)*(row-0.5)));
- end
- print(("\\ "):rep(11));
- local x_labels = {};
- for i = 1, 11 do
- local s = ("%-4s"):format(format_stat(type, data.units, data.min+range*i/11, data.min):match("^%S+"));
- if #s > 4 then
- s = s:sub(1, 3).."…";
- end
- x_labels[i] = s;
+function stats_methods:render_family(print, family_name, metric_family, render_func)
+ local labelkeys = metric_family.label_keys
+ if #labelkeys > 0 then
+ print(family_name)
+ for labelset, metric in metric_family:iter_metrics() do
+ local labels = {}
+ for i, k in ipairs(labelkeys) do
+ local v = labelset[i]
+ t_insert(labels, ("%s=%s"):format(k, v))
end
- print(" "..table.concat(x_labels, " "));
- local units = format_stat(type, data.units, data.min):match("%s+(.+)$") or data.units or "";
- local margin = math.floor((graph_width-#units)/2);
- print((" "):rep(margin)..units);
- else
- print("[range too small to graph]");
+ local prefix = " "..t_concat(labels, " ")
+ render_func(self, print, prefix, metric_family, metric)
+ end
+ else
+ for _, metric in metric_family:iter_metrics() do
+ render_func(self, print, family_name, metric_family, metric)
end
- print("");
end
- return self;
end
local function stats_tostring(stats)
@@ -1618,7 +1555,14 @@ local function stats_tostring(stats)
end
print("");
else
- print(("%-50s %s"):format(stat_info[1], format_stat(stat_info[2], (stat_info[4] or {}).units, stat_info[3])));
+ local metric_family = stat_info[2]
+ if metric_family.type_ == "counter" then
+ stats:render_family(print, stat_info[1], metric_family, stats.render_single_counter)
+ elseif metric_family.type_ == "gauge" or metric_family.type_ == "unknown" then
+ stats:render_family(print, stat_info[1], metric_family, stats.render_single_gauge)
+ elseif metric_family.type_ == "summary" or metric_family.type_ == "histogram" then
+ stats:render_family(print, stat_info[1], metric_family, stats.render_single_summary)
+ end
end
end
return #stats.." statistics displayed";
@@ -1626,23 +1570,29 @@ end
local stats_mt = {__index = stats_methods, __tostring = stats_tostring }
local function new_stats_context(self)
- return setmetatable({ session = self.session, stats = true }, stats_mt);
+ -- TODO: instead of now(), it might be better to take the time of the last
+ -- interval, if the statistics backend is set to use periodic collection
+ -- Otherwise we get strange stuff like average cpu usage decreasing until
+ -- the next sample and so on.
+ return setmetatable({ session = self.session, stats = true, now = time.now() }, stats_mt);
end
-function def_env.stats:show(filter)
- -- luacheck: ignore 211/changed
- local stats, changed, extra = require "core.statsmanager".get_stats();
- local available, displayed = 0, 0;
+function def_env.stats:show(name_filter)
+ local statsman = require "core.statsmanager"
+ local collect = statsman.collect
+ if collect then
+ -- force collection if in manual mode
+ collect()
+ end
+ local metric_registry = statsman.get_metric_registry();
local displayed_stats = new_stats_context(self);
- for name, value in iterators.sorted_pairs(stats) do
- available = available + 1;
- if not filter or name:match(filter) then
- displayed = displayed + 1;
- local type = name:match(":(%a+)$");
+ for family_name, metric_family in iterators.sorted_pairs(metric_registry:get_metric_families()) do
+ if not name_filter or family_name:match(name_filter) then
table.insert(displayed_stats, {
- name, type, value, extra[name];
- output = {};
- });
+ family_name,
+ metric_family,
+ output = {}
+ })
end
end
return displayed_stats;
diff --git a/util/openmetrics.lua b/util/openmetrics.lua
new file mode 100644
index 00000000..299b36c7
--- /dev/null
+++ b/util/openmetrics.lua
@@ -0,0 +1,308 @@
+--[[
+This module implements a subset of the OpenMetrics Internet Draft version 00.
+
+URL: https://tools.ietf.org/html/draft-richih-opsawg-openmetrics-00
+
+The following metric types are supported:
+
+- Counter
+- Gauge
+- Histogram
+- Summary
+
+It is used by util.statsd and util.statistics to provite the OpenMetrics API.
+
+To understand what this module is about, it is useful to familiarize oneself
+with the terms MetricFamily, Metric, LabelSet, Label and MetricPoint as
+defined in the I-D linked above.
+--]]
+-- metric constructor interface:
+-- metric_ctor(..., family_name, labels, extra)
+
+local time = require "util.time".now;
+local select = select;
+local array = require "util.array";
+local log = require "util.logger".init("util.openmetrics");
+local new_multitable = require "util.multitable".new;
+local iter_multitable = require "util.multitable".iter;
+
+-- BEGIN of Utility: "metric proxy"
+-- This allows to wrap a MetricFamily in a proxy which only provides the
+-- `with_labels` and `with_partial_label` methods. This allows to pre-set one
+-- or more labels on a metric family. This is used in particular via
+-- `with_partial_label` by the moduleapi in order to pre-set the `host` label
+-- on metrics created in non-global modules.
+local metric_proxy_mt = {}
+metric_proxy_mt.__index = metric_proxy_mt
+
+local function new_metric_proxy(metric_family, with_labels_proxy_fun)
+ return {
+ _family = metric_family,
+ with_labels = function(self, ...)
+ return with_labels_proxy_fun(self._family, ...)
+ end;
+ with_partial_label = function(self, label)
+ return new_metric_proxy(self._family, function(family, ...)
+ return family:with_labels(label, ...)
+ end)
+ end
+ }
+end
+
+-- END of Utility: "metric proxy"
+
+local function render_histogram_le(v)
+ if v == 1/0 then
+ -- I-D-00: 4.1.2.2.1:
+ -- Exposers MUST produce output for positive infinity as +Inf.
+ return "+Inf"
+ end
+
+ return string.format("%g", v)
+end
+
+-- BEGIN of generic MetricFamily implementation
+
+local metric_family_mt = {}
+metric_family_mt.__index = metric_family_mt
+
+local function histogram_metric_ctor(orig_ctor, buckets)
+ return function(family_name, labels, extra)
+ return orig_ctor(buckets, family_name, labels, extra)
+ end
+end
+
+local function new_metric_family(backend, type_, family_name, unit, description, label_keys, extra)
+ local metric_ctor = assert(backend[type_], "statistics backend does not support "..type_.." metrics families")
+ local labels = label_keys or {}
+ local user_labels = #labels
+ if type_ == "histogram" then
+ local buckets = extra and extra.buckets
+ if not buckets then
+ error("no buckets given for histogram metric")
+ end
+ buckets = array(buckets)
+ buckets:push(1/0) -- must have +inf bucket
+
+ metric_ctor = histogram_metric_ctor(metric_ctor, buckets)
+ end
+
+ local data
+ if #labels == 0 then
+ data = metric_ctor(family_name, nil, extra)
+ else
+ data = new_multitable()
+ end
+
+ local mf = {
+ family_name = family_name,
+ data = data,
+ type_ = type_,
+ unit = unit,
+ description = description,
+ user_labels = user_labels,
+ label_keys = labels,
+ extra = extra,
+ _metric_ctor = metric_ctor,
+ }
+ setmetatable(mf, metric_family_mt);
+ return mf
+end
+
+function metric_family_mt:new_metric(labels)
+ return self._metric_ctor(self.family_name, labels, self.extra)
+end
+
+function metric_family_mt:clear()
+ for _, metric in self:iter_metrics() do
+ metric:reset()
+ end
+end
+
+function metric_family_mt:with_labels(...)
+ local count = select('#', ...)
+ if count ~= self.user_labels then
+ error("number of labels passed to with_labels does not match number of label keys")
+ end
+ if count == 0 then
+ return self.data
+ end
+ local metric = self.data:get(...)
+ if not metric then
+ local values = table.pack(...)
+ metric = self:new_metric(values)
+ values[values.n+1] = metric
+ self.data:set(table.unpack(values, 1, values.n+1))
+ end
+ return metric
+end
+
+function metric_family_mt:with_partial_label(label)
+ return new_metric_proxy(self, function (family, ...)
+ return family:with_labels(label, ...)
+ end)
+end
+
+function metric_family_mt:iter_metrics()
+ if #self.label_keys == 0 then
+ local done = false
+ return function()
+ if done then
+ return nil
+ end
+ done = true
+ return {}, self.data
+ end
+ end
+ local searchkeys = {};
+ local nlabels = #self.label_keys
+ for i=1,nlabels do
+ searchkeys[i] = nil;
+ end
+ local it, state = iter_multitable(self.data, table.unpack(searchkeys, 1, nlabels))
+ return function(_s)
+ local label_values = table.pack(it(_s))
+ if label_values.n == 0 then
+ return nil, nil
+ end
+ local metric = label_values[label_values.n]
+ label_values[label_values.n] = nil
+ label_values.n = label_values.n - 1
+ return label_values, metric
+ end, state
+end
+
+-- END of generic MetricFamily implementation
+
+-- BEGIN of MetricRegistry implementation
+
+
+-- Helper to test whether two metrics are "equal".
+local function equal_metric_family(mf1, mf2)
+ if mf1.type_ ~= mf2.type_ then
+ return false
+ end
+ if #mf1.label_keys ~= #mf2.label_keys then
+ return false
+ end
+ -- Ignoring unit here because in general it'll be part of the name anyway
+ -- So either the unit was moved into/out of the name (which is a valid)
+ -- thing to do on an upgrade or we would expect not to see any conflicts
+ -- anyway.
+ --[[
+ if mf1.unit ~= mf2.unit then
+ return false
+ end
+ ]]
+ for i, key in ipairs(mf1.label_keys) do
+ if key ~= mf2.label_keys[i] then
+ return false
+ end
+ end
+ return true
+end
+
+-- If the unit is not empty, add it to the full name as per the I-D spec.
+local function compose_name(name, unit)
+ local full_name = name
+ if unit and unit ~= "" then
+ full_name = full_name .. "_" .. unit
+ end
+ -- TODO: prohibit certain suffixes used by metrics if where they may cause
+ -- conflicts
+ return full_name
+end
+
+local metric_registry_mt = {}
+metric_registry_mt.__index = metric_registry_mt
+
+local function new_metric_registry(backend)
+ local reg = {
+ families = {},
+ backend = backend,
+ }
+ setmetatable(reg, metric_registry_mt)
+ return reg
+end
+
+function metric_registry_mt:register_metric_family(name, metric_family)
+ local existing = self.families[name];
+ if existing then
+ if not equal_metric_family(metric_family, existing) then
+ -- We could either be strict about this, or replace the
+ -- existing metric family with the new one.
+ -- Being strict is nice to avoid programming errors /
+ -- conflicts, but causes issues when a new version of a module
+ -- is loaded.
+ --
+ -- We will thus assume that the new metric is the correct one;
+ -- That is probably OK because unless you're reaching down into
+ -- the util.openmetrics or core.statsmanager API, your metric
+ -- name is going to be scoped to `prosody_mod_$modulename`
+ -- anyway and the damage is thus controlled.
+ --
+ -- To make debugging such issues easier, we still log.
+ log("debug", "replacing incompatible existing metric family %s", name)
+ -- Below is the code to be strict.
+ --error("conflicting declarations for metric family "..name)
+ else
+ return existing
+ end
+ end
+ self.families[name] = metric_family
+ return metric_family
+end
+
+function metric_registry_mt:gauge(name, unit, description, labels, extra)
+ name = compose_name(name, unit)
+ local mf = new_metric_family(self.backend, "gauge", name, unit, description, labels, extra)
+ mf = self:register_metric_family(name, mf)
+ return mf
+end
+
+function metric_registry_mt:counter(name, unit, description, labels, extra)
+ name = compose_name(name, unit)
+ local mf = new_metric_family(self.backend, "counter", name, unit, description, labels, extra)
+ mf = self:register_metric_family(name, mf)
+ return mf
+end
+
+function metric_registry_mt:histogram(name, unit, description, labels, extra)
+ name = compose_name(name, unit)
+ local mf = new_metric_family(self.backend, "histogram", name, unit, description, labels, extra)
+ mf = self:register_metric_family(name, mf)
+ return mf
+end
+
+function metric_registry_mt:summary(name, unit, description, labels, extra)
+ name = compose_name(name, unit)
+ local mf = new_metric_family(self.backend, "summary", name, unit, description, labels, extra)
+ mf = self:register_metric_family(name, mf)
+ return mf
+end
+
+function metric_registry_mt:get_metric_families()
+ return self.families
+end
+
+-- END of MetricRegistry implementation
+
+-- BEGIN of general helpers for implementing high-level APIs on top of OpenMetrics
+
+local function timed(metric)
+ local t0 = time()
+ local submitter = assert(metric.sample or metric.set, "metric type cannot be used with timed()")
+ return function()
+ local t1 = time()
+ submitter(metric, t1-t0)
+ end
+end
+
+-- END of general helpers
+
+return {
+ new_metric_proxy = new_metric_proxy;
+ new_metric_registry = new_metric_registry;
+ render_histogram_le = render_histogram_le;
+ timed = timed;
+}
diff --git a/util/statistics.lua b/util/statistics.lua
index db608217..a8401168 100644
--- a/util/statistics.lua
+++ b/util/statistics.lua
@@ -1,171 +1,191 @@
-local t_sort = table.sort
-local m_floor = math.floor;
local time = require "util.time".now;
+local new_metric_registry = require "util.openmetrics".new_metric_registry;
+local render_histogram_le = require "util.openmetrics".render_histogram_le;
-local function nop_function() end
+-- BEGIN of Metric implementations
-local function percentile(arr, length, pc)
- local n = pc/100 * (length + 1);
- local k, d = m_floor(n), n%1;
- if k == 0 then
- return arr[1] or 0;
- elseif k >= length then
- return arr[length];
- end
- return arr[k] + d*(arr[k+1] - arr[k]);
+-- Gauges
+local gauge_metric_mt = {}
+gauge_metric_mt.__index = gauge_metric_mt
+
+local function new_gauge_metric()
+ local metric = { value = 0 }
+ setmetatable(metric, gauge_metric_mt)
+ return metric
+end
+
+function gauge_metric_mt:set(value)
+ self.value = value
+end
+
+function gauge_metric_mt:add(delta)
+ self.value = self.value + delta
end
-local function new_registry(config)
- config = config or {};
- local duration_sample_interval = config.duration_sample_interval or 5;
- local duration_max_samples = config.duration_max_stored_samples or 5000;
+function gauge_metric_mt:reset()
+ self.value = 0
+end
- local function get_distribution_stats(events, n_actual_events, since, new_time, units)
- local n_stored_events = #events;
- t_sort(events);
- local sum = 0;
- for i = 1, n_stored_events do
- sum = sum + events[i];
+function gauge_metric_mt:iter_samples()
+ local done = false
+ return function(_s)
+ if done then
+ return nil, true
end
+ done = true
+ return "", nil, _s.value
+ end, self
+end
- return {
- samples = events;
- sample_count = n_stored_events;
- count = n_actual_events,
- rate = n_actual_events/(new_time-since);
- average = n_stored_events > 0 and sum/n_stored_events or 0,
- min = events[1] or 0,
- max = events[n_stored_events] or 0,
- units = units,
- };
- end
+-- Counters
+local counter_metric_mt = {}
+counter_metric_mt.__index = counter_metric_mt
+
+local function new_counter_metric()
+ local metric = {
+ _created = time(),
+ value = 0,
+ }
+ setmetatable(metric, counter_metric_mt)
+ return metric
+end
+function counter_metric_mt:set(value)
+ self.value = value
+end
- local registry = {};
- local methods;
- methods = {
- amount = function (name, conf)
- local v = conf and conf.initial or 0;
- registry[name..":amount"] = function ()
- return "amount", v, conf;
- end
- return function (new_v) v = new_v; end
- end;
- counter = function (name, conf)
- local v = conf and conf.initial or 0;
- registry[name..":amount"] = function ()
- return "amount", v, conf;
- end
- return function (delta)
- v = v + delta;
- end;
- end;
- rate = function (name, conf)
- local since, n, total = time(), 0, 0;
- registry[name..":rate"] = function ()
- total = total + n;
- local t = time();
- local stats = {
- rate = n/(t-since);
- count = n;
- total = total;
- units = conf and conf.units;
- type = conf and conf.type;
- };
- since, n = t, 0;
- return "rate", stats.rate, stats;
- end;
- return function ()
- n = n + 1;
- end;
- end;
- distribution = function (name, conf)
- local units = conf and conf.units;
- local type = conf and conf.type or "distribution";
- local events, last_event = {}, 0;
- local n_actual_events = 0;
- local since = time();
-
- registry[name..":"..type] = function ()
- local new_time = time();
- local stats = get_distribution_stats(events, n_actual_events, since, new_time, units);
- events, last_event = {}, 0;
- n_actual_events = 0;
- since = new_time;
- return type, stats.average, stats;
- end;
-
- return function (value)
- n_actual_events = n_actual_events + 1;
- if n_actual_events%duration_sample_interval == 1 then
- last_event = (last_event%duration_max_samples) + 1;
- events[last_event] = value;
- end
- end;
- end;
- sizes = function (name, conf)
- conf = conf or { units = "bytes", type = "size" }
- return methods.distribution(name, conf);
- end;
- times = function (name, conf)
- local units = conf and conf.units or "seconds";
- local events, last_event = {}, 0;
- local n_actual_events = 0;
- local since = time();
-
- registry[name..":duration"] = function ()
- local new_time = time();
- local stats = get_distribution_stats(events, n_actual_events, since, new_time, units);
- events, last_event = {}, 0;
- n_actual_events = 0;
- since = new_time;
- return "duration", stats.average, stats;
- end;
-
- return function ()
- n_actual_events = n_actual_events + 1;
- if n_actual_events%duration_sample_interval ~= 1 then
- return nop_function;
- end
-
- local start_time = time();
- return function ()
- local end_time = time();
- local duration = end_time - start_time;
- last_event = (last_event%duration_max_samples) + 1;
- events[last_event] = duration;
- end
- end;
- end;
-
- get_stats = function ()
- return registry;
- end;
- };
- return methods;
+function counter_metric_mt:add(value)
+ self.value = (self.value or 0) + value
end
-return {
- new = new_registry;
- get_histogram = function (duration, n_buckets)
- n_buckets = n_buckets or 100;
- local events, n_events = duration.samples, duration.sample_count;
- if not (events and n_events) then
- return nil, "not a valid distribution stat";
+function counter_metric_mt:iter_samples()
+ local step = 0
+ return function(_s)
+ step = step + 1
+ if step == 1 then
+ return "_created", nil, _s._created
+ elseif step == 2 then
+ return "_total", nil, _s.value
+ else
+ return nil, nil, true
+ end
+ end, self
+end
+
+function counter_metric_mt:reset()
+ self.value = 0
+end
+
+-- Histograms
+local histogram_metric_mt = {}
+histogram_metric_mt.__index = histogram_metric_mt
+
+local function new_histogram_metric(buckets)
+ local metric = {
+ _created = time(),
+ _sum = 0,
+ _count = 0,
+ }
+ -- the order of buckets matters unfortunately, so we cannot directly use
+ -- the thresholds as table keys
+ for i, threshold in ipairs(buckets) do
+ metric[i] = {
+ threshold = threshold,
+ threshold_s = render_histogram_le(threshold),
+ count = 0
+ }
+ end
+ setmetatable(metric, histogram_metric_mt)
+ return metric
+end
+
+function histogram_metric_mt:sample(value)
+ -- According to the I-D, values must be part of all buckets
+ for i, bucket in pairs(self) do
+ if "number" == type(i) and bucket.threshold > value then
+ bucket.count = bucket.count + 1
end
- local histogram = {};
+ end
+ self._sum = self._sum + value
+ self._count = self._count + 1
+end
- for i = 1, 100, 100/n_buckets do
- histogram[i] = percentile(events, n_events, i);
+function histogram_metric_mt:iter_samples()
+ local key = nil
+ return function (_s)
+ local data
+ key, data = next(_s, key)
+ if key == "_created" or key == "_sum" or key == "_count" then
+ return key, nil, data
+ elseif key ~= nil then
+ return "_bucket", {["le"] = data.threshold_s}, data.count
+ else
+ return nil, nil, nil
end
- return histogram;
- end;
+ end, self
+end
- get_percentile = function (duration, pc)
- local events, n_events = duration.samples, duration.sample_count;
- if not (events and n_events) then
- return nil, "not a valid distribution stat";
+function histogram_metric_mt:reset()
+ self._created = time()
+ self._count = 0
+ self._sum = 0
+ for i, bucket in pairs(self) do
+ if "number" == type(i) then
+ bucket.count = 0
end
- return percentile(events, n_events, pc);
- end;
+ end
+end
+
+-- Summary
+local summary_metric_mt = {}
+summary_metric_mt.__index = summary_metric_mt
+
+local function new_summary_metric()
+ -- quantiles are not supported yet
+ local metric = {
+ _created = time(),
+ _sum = 0,
+ _count = 0,
+ }
+ setmetatable(metric, summary_metric_mt)
+ return metric
+end
+
+function summary_metric_mt:sample(value)
+ self._sum = self._sum + value
+ self._count = self._count + 1
+end
+
+function summary_metric_mt:iter_samples()
+ local key = nil
+ return function (_s)
+ local data
+ key, data = next(_s, key)
+ return key, nil, data
+ end, self
+end
+
+function summary_metric_mt:reset()
+ self._created = time()
+ self._count = 0
+ self._sum = 0
+end
+
+local pull_backend = {
+ gauge = new_gauge_metric,
+ counter = new_counter_metric,
+ histogram = new_histogram_metric,
+ summary = new_summary_metric,
+}
+
+-- END of Metric implementations
+
+local function new()
+ return {
+ metric_registry = new_metric_registry(pull_backend),
+ }
+end
+
+return {
+ new = new;
}
diff --git a/util/statsd.lua b/util/statsd.lua
index 8f6151c6..25e03e38 100644
--- a/util/statsd.lua
+++ b/util/statsd.lua
@@ -1,82 +1,267 @@
local socket = require "socket";
+local time = require "util.time".now;
+local array = require "util.array";
+local t_concat = table.concat;
-local time = require "util.time".now
+local new_metric_registry = require "util.openmetrics".new_metric_registry;
+local render_histogram_le = require "util.openmetrics".render_histogram_le;
-local function new(config)
- if not config or not config.statsd_server then
- return nil, "No statsd server specified in the config, please see https://prosody.im/doc/statistics";
+-- BEGIN of Metric implementations
+
+-- Gauges
+local gauge_metric_mt = {}
+gauge_metric_mt.__index = gauge_metric_mt
+
+local function new_gauge_metric(full_name, impl)
+ local metric = {
+ _full_name = full_name;
+ _impl = impl;
+ value = 0;
+ }
+ setmetatable(metric, gauge_metric_mt)
+ return metric
+end
+
+function gauge_metric_mt:set(value)
+ self.value = value
+ self._impl:push_gauge(self._full_name, value)
+end
+
+function gauge_metric_mt:add(delta)
+ self.value = self.value + delta
+ self._impl:push_gauge(self._full_name, self.value)
+end
+
+function gauge_metric_mt:reset()
+ self.value = 0
+ self._impl:push_gauge(self._full_name, 0)
+end
+
+function gauge_metric_mt.iter_samples()
+ -- statsd backend does not support iteration.
+ return function()
+ return nil
end
+end
- local sock = socket.udp();
- sock:setpeername(config.statsd_server, config.statsd_port or 8125);
+-- Counters
+local counter_metric_mt = {}
+counter_metric_mt.__index = counter_metric_mt
- local prefix = (config.prefix or "prosody")..".";
+local function new_counter_metric(full_name, impl)
+ local metric = {
+ _full_name = full_name,
+ _impl = impl,
+ value = 0,
+ }
+ setmetatable(metric, counter_metric_mt)
+ return metric
+end
+
+function counter_metric_mt:set(value)
+ local delta = value - self.value
+ self.value = value
+ self._impl:push_counter_delta(self._full_name, delta)
+end
- local function send_metric(s)
- return sock:send(prefix..s);
+function counter_metric_mt:add(value)
+ self.value = (self.value or 0) + value
+ self._impl:push_counter_delta(self._full_name, value)
+end
+
+function counter_metric_mt.iter_samples()
+ -- statsd backend does not support iteration.
+ return function()
+ return nil
+ end
+end
+
+function counter_metric_mt:reset()
+ self.value = 0
+end
+
+-- Histograms
+local histogram_metric_mt = {}
+histogram_metric_mt.__index = histogram_metric_mt
+
+local function new_histogram_metric(buckets, full_name, impl)
+ -- NOTE: even though the more or less proprietrary dogstatsd has its own
+ -- histogram implementation, we push the individual buckets in this statsd
+ -- backend for both consistency and compatibility across statsd
+ -- implementations.
+ local metric = {
+ _sum_name = full_name..".sum",
+ _count_name = full_name..".count",
+ _impl = impl,
+ _created = time(),
+ _sum = 0,
+ _count = 0,
+ }
+ -- the order of buckets matters unfortunately, so we cannot directly use
+ -- the thresholds as table keys
+ for i, threshold in ipairs(buckets) do
+ local threshold_s = render_histogram_le(threshold)
+ metric[i] = {
+ threshold = threshold,
+ threshold_s = threshold_s,
+ count = 0,
+ _full_name = full_name..".bucket."..(threshold_s:gsub("%.", "_")),
+ }
end
+ setmetatable(metric, histogram_metric_mt)
+ return metric
+end
- local function send_gauge(name, amount, relative)
- local s_amount = tostring(amount);
- if relative and amount > 0 then
- s_amount = "+"..s_amount;
+function histogram_metric_mt:sample(value)
+ -- According to the I-D, values must be part of all buckets
+ for i, bucket in pairs(self) do
+ if "number" == type(i) and bucket.threshold > value then
+ bucket.count = bucket.count + 1
+ self._impl:push_counter_delta(bucket._full_name, 1)
end
- return send_metric(name..":"..s_amount.."|g");
end
+ self._sum = self._sum + value
+ self._count = self._count + 1
+ self._impl:push_gauge(self._sum_name, self._sum)
+ self._impl:push_counter_delta(self._count_name, 1)
+end
- local function send_counter(name, amount)
- return send_metric(name..":"..tostring(amount).."|c");
+function histogram_metric_mt.iter_samples()
+ -- statsd backend does not support iteration.
+ return function()
+ return nil
end
+end
- local function send_duration(name, duration)
- return send_metric(name..":"..tostring(duration).."|ms");
+function histogram_metric_mt:reset()
+ self._created = time()
+ self._count = 0
+ self._sum = 0
+ for i, bucket in pairs(self) do
+ if "number" == type(i) then
+ bucket.count = 0
+ end
end
+ self._impl:push_gauge(self._sum_name, self._sum)
+end
+
+-- Summaries
+local summary_metric_mt = {}
+summary_metric_mt.__index = summary_metric_mt
+
+local function new_summary_metric(full_name, impl)
+ local metric = {
+ _sum_name = full_name..".sum",
+ _count_name = full_name..".count",
+ _impl = impl,
+ }
+ setmetatable(metric, summary_metric_mt)
+ return metric
+end
+
+function summary_metric_mt:sample(value)
+ self._impl:push_counter_delta(self._sum_name, value)
+ self._impl:push_counter_delta(self._count_name, 1)
+end
- local function send_histogram_sample(name, sample)
- return send_metric(name..":"..tostring(sample).."|h");
+function summary_metric_mt.iter_samples()
+ -- statsd backend does not support iteration.
+ return function()
+ return nil
end
+end
- local methods;
- methods = {
- amount = function (name, conf)
- if conf and conf.initial then
- send_gauge(name, conf.initial);
- end
- return function (new_v) send_gauge(name, new_v); end
- end;
- counter = function (name, conf) --luacheck: ignore 212/conf
- return function (delta)
- send_gauge(name, delta, true);
- end;
- end;
- rate = function (name)
- return function ()
- send_counter(name, 1);
- end;
+function summary_metric_mt.reset()
+end
+
+-- BEGIN of statsd client implementation
+
+local statsd_mt = {}
+statsd_mt.__index = statsd_mt
+
+function statsd_mt:cork()
+ self.corked = true
+ self.cork_buffer = self.cork_buffer or {}
+end
+
+function statsd_mt:uncork()
+ self.corked = false
+ self:_flush_cork_buffer()
+end
+
+function statsd_mt:_flush_cork_buffer()
+ local buffer = self.cork_buffer
+ for metric_name, value in pairs(buffer) do
+ self:_send_gauge(metric_name, value)
+ buffer[metric_name] = nil
+ end
+end
+
+function statsd_mt:push_gauge(metric_name, value)
+ if self.corked then
+ self.cork_buffer[metric_name] = value
+ else
+ self:_send_gauge(metric_name, value)
+ end
+end
+
+function statsd_mt:_send_gauge(metric_name, value)
+ self:_send(self.prefix..metric_name..":"..tostring(value).."|g")
+end
+
+function statsd_mt:push_counter_delta(metric_name, delta)
+ self:_send(self.prefix..metric_name..":"..tostring(delta).."|c")
+end
+
+function statsd_mt:_send(s)
+ return self.sock:send(s)
+end
+
+-- END of statsd client implementation
+
+local function build_metric_name(family_name, labels)
+ local parts = array { family_name }
+ if labels then
+ parts:append(labels)
+ end
+ return t_concat(parts, "/"):gsub("%.", "_"):gsub("/", ".")
+end
+
+local function new(config)
+ if not config or not config.statsd_server then
+ return nil, "No statsd server specified in the config, please see https://prosody.im/doc/statistics";
+ end
+
+ local sock = socket.udp();
+ sock:setpeername(config.statsd_server, config.statsd_port or 8125);
+
+ local prefix = (config.prefix or "prosody")..".";
+
+ local impl = {
+ metric_registry = nil;
+ sock = sock;
+ prefix = prefix;
+ };
+ setmetatable(impl, statsd_mt)
+
+ local backend = {
+ gauge = function(family_name, labels)
+ return new_gauge_metric(build_metric_name(family_name, labels), impl)
end;
- distribution = function (name, conf) --luacheck: ignore 212/conf
- return function (value)
- send_histogram_sample(name, value);
- end;
+ counter = function(family_name, labels)
+ return new_counter_metric(build_metric_name(family_name, labels), impl)
end;
- sizes = function (name)
- name = name.."_size";
- return function (value)
- send_histogram_sample(name, value);
- end;
+ histogram = function(buckets, family_name, labels)
+ return new_histogram_metric(buckets, build_metric_name(family_name, labels), impl)
end;
- times = function (name)
- return function ()
- local start_time = time();
- return function ()
- local end_time = time();
- local duration = end_time - start_time;
- send_duration(name, duration*1000);
- end
- end;
+ summary = function(family_name, labels, extra)
+ return new_summary_metric(build_metric_name(family_name, labels), impl, extra)
end;
};
- return methods;
+
+ impl.metric_registry = new_metric_registry(backend);
+
+ return impl;
end
return {