aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--plugins/mod_http_openmetrics.lua62
-rw-r--r--util/openmetrics.lua79
2 files changed, 141 insertions, 0 deletions
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 <linkmauve@linkmauve.fr>
+-- Copyright (C) 2021 Jonas Schäfer <jonas@zombofant.net>
+--
+-- 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