Changeset

3125:07a2ba55de4d

mod_prometheus: Add a new statistics export module, for Prometheus.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Thu, 21 Jun 2018 21:37:13 +0200
parents 3124:cabe58ae17c9
children 3126:888375de933c
files mod_prometheus/README.markdown mod_prometheus/mod_prometheus.lua
diffstat 2 files changed, 139 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_prometheus/README.markdown	Thu Jun 21 21:37:13 2018 +0200
@@ -0,0 +1,14 @@
+---
+summary: Implementation of the Prometheus protocol
+...
+
+Summary
+=======
+
+This module implements the Prometheus reporting protocol, allowing you
+to collect statistics directly from Prosody into Prometheus.
+
+See the [Prometheus documentation][prometheusconf] on the format for
+more information.
+
+[prometheusconf]: https://prometheus.io/docs/instrumenting/exposition_formats/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_prometheus/mod_prometheus.lua	Thu Jun 21 21:37:13 2018 +0200
@@ -0,0 +1,125 @@
+-- Log common stats to statsd
+--
+-- Copyright (C) 2014 Daurnimator
+--
+-- This module is MIT/X11 licensed.
+
+module:set_global();
+module:depends "http";
+
+local s_format = string.format;
+local t_insert = table.insert;
+local socket = require "socket";
+local mt = require "util.multitable";
+
+local meta = mt.new(); meta.data = module:shared"meta";
+local data = mt.new(); data.data = module:shared"data";
+
+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");
+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 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(labels)
+	local values = {}
+	for key, value in pairs(labels) do
+		t_insert(values, repr_label(escape_name(key), escape(value)));
+	end
+	if #values == 0 then
+		return "";
+	end
+	return "{"..table.concat(values, ", ").."}";
+end
+
+local function repr_sample(metric, labels, value, timestamp)
+	return escape_name(metric)..repr_labels(labels).." "..value.." "..timestamp.."\n";
+end
+
+module:hook("stats-updated", function (event)
+	local all_stats, this = event.stats_extra;
+	local host, sect, name, typ, key;
+	for stat, value in pairs(event.changed_stats) do
+		this = all_stats[stat];
+		-- module:log("debug", "changed_stats[%q] = %s", stat, tostring(value));
+		host, sect, name, typ = stat:match("^/([^/]+)/([^/]+)/(.+):(%a+)$");
+		if host == nil then
+			sect, name, typ, host = 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
+		key = escape_name(s_format("%s_%s_%s", host or "global", sect, typ));
+
+		if not meta:get(key) then
+			if host then
+				meta:set(key, "", "graph_title", s_format("%s %s on %s", sect, typ, host));
+			else
+				meta:set(key, "", "graph_title", s_format("Global %s %s", sect, typ, host));
+			end
+			meta:set(key, "", "graph_vlabel", this and this.units or typ);
+			meta:set(key, "", "graph_category", sect);
+
+			meta:set(key, name, "label", name);
+		elseif not meta:get(key, name, "label") then
+			meta:set(key, name, "label", name);
+		end
+
+		data:set(key, name, value);
+	end
+end);
+
+local function get_metrics(event)
+	local response = event.response;
+	response.headers.content_type = "text/plain; version=0.4.4";
+
+	local response = {};
+	local timestamp = tostring(get_timestamp());
+	for section, data in pairs(data.data) do
+		for key, value in pairs(data) do
+			local name = section.."_"..key;
+			t_insert(response, repr_help(name, "TODO: add a description here."));
+			t_insert(response, repr_type(name, "gauge"));
+			t_insert(response, repr_sample(name, {}, value, timestamp));
+		end
+	end
+	return table.concat(response, "");
+end
+
+function module.add_host(module)
+	module:provides("http", {
+		default_path = "metrics";
+		route = {
+			GET = get_metrics;
+		};
+	});
+end