Software /
code /
prosody-modules
Diff
mod_prometheus/mod_prometheus.lua @ 4555:1e70538e4641
mod_prometheus: Port to new OpenMetrics based statistics module
author | Jonas Schäfer <jonas@wielicki.name> |
---|---|
date | Wed, 28 Apr 2021 08:22:47 +0200 |
parent | 4544:64fa2dd34d43 |
child | 4595:bac3dae031ee |
line wrap: on
line diff
--- a/mod_prometheus/mod_prometheus.lua Wed Apr 28 08:21:54 2021 +0200 +++ b/mod_prometheus/mod_prometheus.lua Wed Apr 28 08:22:47 2021 +0200 @@ -14,13 +14,15 @@ local socket = require "socket"; local statsman = require "core.statsmanager"; local get_stats = statsman.get_stats; +local get_metric_registry = statsman.get_metric_registry; +local collect = statsman.collect; local function escape(text) return text:gsub("\\", "\\\\"):gsub("\"", "\\\""):gsub("\n", "\\n"); end local function escape_name(name) - return name:gsub("[^A-Za-z0-9_]", "_"):gsub("^[^A-Za-z_]", "_%1"); + return name:gsub("/", "__"):gsub("[^A-Za-z0-9_]", "_"):gsub("^[^A-Za-z_]", "_%1"); end local function get_timestamp() @@ -33,6 +35,15 @@ return "# HELP "..escape_name(metric).." "..docstring.."\n"; end +local function repr_unit(metric, unit) + if not unit then + unit = "" + else + unit = unit:gsub("\\", "\\\\"):gsub("\n", "\\n"); + end + return "# UNIT "..escape_name(metric).." "..unit.."\n"; +end + -- local allowed_types = { counter = true, gauge = true, histogram = true, summary = true, untyped = true }; -- local allowed_types = { "counter", "gauge", "histogram", "summary", "untyped" }; local function repr_type(metric, type_) @@ -46,10 +57,18 @@ return key.."=\""..escape(value).."\""; end -local function repr_labels(labels) +local function repr_labels(labelkeys, labelvalues, extra_labels) local values = {} - for key, value in pairs(labels) do - t_insert(values, repr_label(escape_name(key), escape(value))); + if labelkeys then + for i, key in ipairs(labelkeys) do + local value = labelvalues[i] + t_insert(values, repr_label(escape_name(key), escape(value))); + end + end + if extra_labels then + for key, value in pairs(extra_labels) do + t_insert(values, repr_label(escape_name(key), escape(value))); + end end if #values == 0 then return ""; @@ -57,90 +76,128 @@ return "{"..t_concat(values, ",").."}"; end -local function repr_sample(metric, labels, value, timestamp) - return escape_name(metric)..repr_labels(labels).." "..value.." "..timestamp.."\n"; -end - -local allowed_extras = { min = true, max = true, average = true }; -local function insert_extras(data, key, name, timestamp, extra) - if not extra then - return false; - end - local has_extra = false; - for extra_name in pairs(allowed_extras) do - if extra[extra_name] then - local field = { - value = extra[extra_name], - labels = { - ["type"] = name, - field = extra_name, - }, - typ = "gauge"; - timestamp = timestamp, - }; - t_insert(data[key], field); - has_extra = true; - end - end - return has_extra; +local function repr_sample(metric, labelkeys, labelvalues, extra_labels, value) + return escape_name(metric)..repr_labels(labelkeys, labelvalues, extra_labels).." "..string.format("%.17g", value).."\n"; end -local function parse_stats() - local timestamp = tostring(get_timestamp()); - local data = {}; - local stats, changed_only, extras = get_stats(); - for stat, value in pairs(stats) do - -- module:log("debug", "changed_stats[%q] = %s", stat, tostring(value)); - local extra = extras[stat]; - local host, sect, name, typ = stat:match("^/([^/]+)/([^/]+)/(.+):(%a+)$"); - if host == nil then - sect, name, typ = stat:match("^([^.]+)%.(.+):(%a+)$"); - elseif host == "*" then - host = nil; - end - if sect:find("^mod_measure_.") then - sect = sect:sub(13); - elseif sect:find("^mod_statistics_.") then - sect = sect:sub(16); +local get_metrics; +if statsman.get_metric_registry then + module:log("debug", "detected OpenMetrics statsmanager") + -- Prosody 0.12+ with OpenMetrics + function get_metrics(event) + local response = event.response; + response.headers.content_type = "application/openmetrics-text; version=0.0.4"; + + if collect then + -- Ensure to get up-to-date samples when running in manual mode + collect() end - local key = escape_name("prosody_"..sect); - local field = { - value = value, - labels = { ["type"] = name}, - -- TODO: Use the other types where it makes sense. - typ = (typ == "rate" and "counter" or "gauge"), - timestamp = timestamp, - }; - if host then - field.labels.host = host; + local registry = get_metric_registry() + if registry == nil then + response.headers.content_type = "text/plain; charset=utf-8" + response.status_code = 404 + return "No statistics provider configured\n" + end + local answer = {}; + for metric_family_name, metric_family in pairs(registry:get_metric_families()) do + t_insert(answer, repr_help(metric_family_name, metric_family.description)) + t_insert(answer, repr_unit(metric_family_name, metric_family.unit)) + t_insert(answer, repr_type(metric_family_name, metric_family.type_)) + for labelset, metric in metric_family:iter_metrics() do + for suffix, extra_labels, value in metric:iter_samples() do + t_insert(answer, repr_sample(metric_family_name..suffix, metric_family.label_keys, labelset, extra_labels, value)) + end + end end - if data[key] == nil then - data[key] = {}; - end - if not insert_extras(data, key, name, timestamp, extra) then - t_insert(data[key], field); - end + t_insert(answer, "# EOF\n") + return t_concat(answer, ""); end - return data; -end +else + module:log("debug", "detected pre-OpenMetrics statsmanager") + -- Pre-OpenMetrics -local function get_metrics(event) - local response = event.response; - response.headers.content_type = "text/plain; version=0.0.4"; - if statsman.collect then - statsman.collect() + local allowed_extras = { min = true, max = true, average = true }; + local function insert_extras(data, key, name, timestamp, extra) + if not extra then + return false; + end + local has_extra = false; + for extra_name in pairs(allowed_extras) do + if extra[extra_name] then + local field = { + value = extra[extra_name], + labels = { + ["type"] = name, + field = extra_name, + }, + typ = "gauge"; + timestamp = timestamp, + }; + t_insert(data[key], field); + has_extra = true; + end + end + return has_extra; end - local answer = {}; - for key, fields in pairs(parse_stats()) do - t_insert(answer, repr_help(key, "TODO: add a description here.")); - t_insert(answer, repr_type(key, fields[1].typ)); - for _, field in pairs(fields) do - t_insert(answer, repr_sample(key, field.labels, field.value, field.timestamp)); + local function parse_stats() + local timestamp = tostring(get_timestamp()); + local data = {}; + local stats, changed_only, extras = get_stats(); + for stat, value in pairs(stats) do + -- module:log("debug", "changed_stats[%q] = %s", stat, tostring(value)); + local extra = extras[stat]; + local host, sect, name, typ = stat:match("^/([^/]+)/([^/]+)/(.+):(%a+)$"); + if host == nil then + sect, name, typ = stat:match("^([^.]+)%.(.+):(%a+)$"); + elseif host == "*" then + host = nil; + end + if sect:find("^mod_measure_.") then + sect = sect:sub(13); + elseif sect:find("^mod_statistics_.") then + sect = sect:sub(16); + end + + local key = escape_name("prosody_"..sect); + local field = { + value = value, + labels = { ["type"] = name}, + -- TODO: Use the other types where it makes sense. + typ = (typ == "rate" and "counter" or "gauge"), + timestamp = timestamp, + }; + if host then + field.labels.host = host; + end + if data[key] == nil then + data[key] = {}; + end + if not insert_extras(data, key, name, timestamp, extra) then + t_insert(data[key], field); + end end + return data; end - return t_concat(answer, ""); + + function get_metrics(event) + local response = event.response; + response.headers.content_type = "text/plain; version=0.0.4"; + if statsman.collect then + statsman.collect() + end + + local answer = {}; + for key, fields in pairs(parse_stats()) do + t_insert(answer, repr_help(key, "")); + t_insert(answer, repr_type(key, fields[1].typ)); + for _, field in pairs(fields) do + t_insert(answer, repr_sample(key, nil, nil, field.labels, field.value, field.timestamp)); + end + end + return t_concat(answer, ""); + end end function module.add_host(module)