--- 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)