Software /
code /
prosody-modules
File
mod_prometheus/mod_prometheus.lua @ 4646:072d078be095
mod_storage_xmlarchive: Advertise capabilities (none atm)
Both 'total' and 'quota' would be prohibitively expensive for the kind
of archives this module targets as it would require counting items
across the entire archive.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Fri, 13 Aug 2021 01:18:32 +0200 |
parent | 4596:c406e4bf7ee5 |
line wrap: on
line source
-- Log statistics to Prometheus -- -- Copyright (C) 2014 Daurnimator -- Copyright (C) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> -- Copyright (C) 2021 Jonas Schäfer <jonas@zombofant.net> -- -- This module is MIT/X11 licensed. module:set_global(); local tostring = tostring; local t_insert = table.insert; local t_concat = table.concat; 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("/", "__"):gsub("[^A-Za-z0-9_]", "_"):gsub("^[^A-Za-z_]", "_%1"); end local function get_timestamp() -- Using LuaSocket for that because os.time() only has second precision. return math.floor(socket.gettime() * 1000); end local function repr_help(metric, docstring) docstring = docstring:gsub("\\", "\\\\"):gsub("\n", "\\n"); 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_) -- if not allowed_types:contains(type_) then -- return; -- end return "# TYPE "..escape_name(metric).." "..type_.."\n"; end local function repr_label(key, value) return key.."=\""..escape(value).."\""; end local function repr_labels(labelkeys, labelvalues, extra_labels) local values = {} 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 ""; end return "{"..t_concat(values, ",").."}"; end 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 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 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 t_insert(answer, "# EOF\n") return t_concat(answer, ""); end else module:log("debug", "detected pre-OpenMetrics statsmanager") -- Pre-OpenMetrics 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 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 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) module:depends "http"; module:provides("http", { default_path = "metrics"; route = { GET = get_metrics; }; }); end