local config = require "prosody.core.configmanager"; local log = require "prosody.util.logger".init("stats"); local timer = require "prosody.util.timer"; local fire_event = prosody.events.fire_event; local array = require "prosody.util.array"; local timed = require "prosody.util.openmetrics".timed; local stats_interval_config = config.get("*", "statistics_interval"); local stats_interval = tonumber(stats_interval_config); if stats_interval_config and not stats_interval and stats_interval_config ~= "manual" then log("error", "Invalid 'statistics_interval' setting, statistics will be disabled"); end local stats_provider_name; local stats_provider_config = config.get("*", "statistics"); local stats_provider = stats_provider_config; if not stats_provider and stats_interval then stats_provider = "internal"; elseif stats_provider and not stats_interval then stats_interval = 60; end if stats_interval_config == "manual" then stats_interval = nil; end local builtin_providers = { internal = "prosody.util.statistics"; statsd = "prosody.util.statsd"; }; local stats, stats_err = false, nil; if stats_provider then if stats_provider:sub(1,1) == ":" then stats_provider = stats_provider:sub(2); stats_provider_name = "external "..stats_provider; elseif stats_provider then stats_provider_name = "built-in "..stats_provider; stats_provider = builtin_providers[stats_provider]; if not stats_provider then log("error", "Unrecognized statistics provider '%s', statistics will be disabled", stats_provider_config); end end local have_stats_provider, stats_lib = pcall(require, stats_provider); if not have_stats_provider then stats, stats_err = nil, stats_lib; else local stats_config = config.get("*", "statistics_config"); stats, stats_err = stats_lib.new(stats_config); stats_provider_name = stats_lib._NAME or stats_provider_name; end end if stats == nil then log("error", "Error loading statistics provider '%s': %s", stats_provider, stats_err); end local measure, collect, metric, cork, uncork; if stats then 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 name.." "..type_ 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 local mark_collection_start = measure("times", "stats.collection"); local mark_processing_start = measure("times", "stats.processing"); 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.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(); 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, manual_result; end if stats_interval then log("debug", "Statistics enabled using %s provider, collecting every %d seconds", stats_provider_name, stats_interval); timer.add_task(stats_interval, collect); prosody.events.add_handler("server-started", function () collect() end, -1); prosody.events.add_handler("server-stopped", function () collect() end, -1); else log("debug", "Statistics enabled using %s provider, no scheduled collection", stats_provider_name); end else log("debug", "Statistics enabled using %s provider, collection is disabled", stats_provider_name); end 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; if stats_interval_config == "manual" then exported_collect = collect; end return { collect = exported_collect; measure = measure; cork = cork; uncork = uncork; metric = metric; get_metric_registry = function () return stats and stats.metric_registry or nil end; };