From fa22d40ba35519453adbfb3fa900035f30e7c1d6 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 24 Nov 2021 16:03:05 +0000 Subject: mod_http_openmetrics: Imported from prosody-modules mod_prometheus @df2246b15075 This version has several changes from the earlier mod_prometheus: - Conversion of metrics into the text-based OpenMetrics format is moved to util.openmetrics - Support for IP-based access control - Compatibility with earlier Prosody versions removed --- plugins/mod_http_openmetrics.lua | 62 +++++++++++++++++++++++++++++++ util/openmetrics.lua | 79 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 plugins/mod_http_openmetrics.lua diff --git a/plugins/mod_http_openmetrics.lua b/plugins/mod_http_openmetrics.lua new file mode 100644 index 00000000..78cd6fd4 --- /dev/null +++ b/plugins/mod_http_openmetrics.lua @@ -0,0 +1,62 @@ +-- Export statistics in OpenMetrics format +-- +-- Copyright (C) 2014 Daurnimator +-- Copyright (C) 2018 Emmanuel Gil Peyrot +-- Copyright (C) 2021 Jonas Schäfer +-- +-- This module is MIT/X11 licensed. + +module:set_global(); + +local statsman = require "core.statsmanager"; +local ip = require "util.ip"; + +local get_metric_registry = statsman.get_metric_registry; +local collect = statsman.collect; + +local get_metrics; + +local permitted_ips = module:get_option_set("openmetrics_allow_ips", { "::1", "127.0.0.1" }); +local permitted_cidr = module:get_option_string("openmetrics_allow_cidr"); + +local function is_permitted(request) + local ip_raw = request.ip; + if permitted_ips:contains(ip_raw) or + (permitted_cidr and ip.match(ip.new_ip(ip_raw), ip.parse_cidr(permitted_cidr))) then + return true; + end + return false; +end + +function get_metrics(event) + if not is_permitted(event.request) then + return 403; -- Forbidden + end + + 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 + + return registry:render(); +end + +function module.add_host(module) + module:depends "http"; + module:provides("http", { + default_path = "metrics"; + route = { + GET = get_metrics; + }; + }); +end diff --git a/util/openmetrics.lua b/util/openmetrics.lua index a3ef827b..2fb8b967 100644 --- a/util/openmetrics.lua +++ b/util/openmetrics.lua @@ -25,6 +25,7 @@ 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; +local t_concat, t_insert = table.concat, table.insert; local t_pack, t_unpack = require "util.table".pack, table.unpack or unpack; --luacheck: ignore 113/unpack -- BEGIN of Utility: "metric proxy" @@ -52,6 +53,68 @@ end -- END of Utility: "metric proxy" +-- BEGIN Rendering helper functions (internal) + +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 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 + +-- END Rendering helper functions (internal) + local function render_histogram_le(v) if v == 1/0 then -- I-D-00: 4.1.2.2.1: @@ -286,6 +349,22 @@ function metric_registry_mt:get_metric_families() return self.families end +function metric_registry_mt:render() + local answer = {}; + for metric_family_name, metric_family in pairs(self: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 + -- END of MetricRegistry implementation -- BEGIN of general helpers for implementing high-level APIs on top of OpenMetrics -- cgit v1.2.3