view plugins/mod_storage_xep0227.lua @ 11523:5f15ab7c6ae5

Statistics: Rewrite statistics backends to use OpenMetrics The metric subsystem of Prosody has had some shortcomings from the perspective of the current state-of-the-art in metric observability. The OpenMetrics standard [0] is a formalization of the data model (and serialization format) of the well-known and widely-used Prometheus [1] software stack. The previous stats subsystem of Prosody did not map well to that format (see e.g. [2] and [3]); the key reason is that it was trying to do too much math on its own ([2]) while lacking first-class support for "families" of metrics ([3]) and structured metric metadata (despite the `extra` argument to metrics, there was no standard way of representing common things like "tags" or "labels"). Even though OpenMetrics has grown from the Prometheus world of monitoring, it maps well to other popular monitoring stacks such as: - InfluxDB (labels can be mapped to tags and fields as necessary) - Carbon/Graphite (labels can be attached to the metric name with dot-separation) - StatsD (see graphite when assuming that graphite is used as backend, which is the default) The util.statsd module has been ported to use the OpenMetrics model as a proof of concept. An implementation which exposes the util.statistics backend data as Prometheus metrics is ready for publishing in prosody-modules (most likely as mod_openmetrics_prometheus to avoid breaking existing 0.11 deployments). At the same time, the previous measure()-based API had one major advantage: It is really simple and easy to use without requiring lots of knowledge about OpenMetrics or similar concepts. For that reason as well as compatibility with existing code, it is preserved and may even be extended in the future. However, code relying on the `stats-updated` event as well as `get_stats` from `statsmanager` will break because the data model has changed completely; in case of `stats-updated`, the code will simply not run (as the event was renamed in order to avoid conflicts); the `get_stats` function has been removed completely (so it will cause a traceback when it is attempted to be used). Note that the measure_*_event methods have been removed from the module API. I was unable to find any uses or documentation and thus deemed they should not be ported. Re-implementation is possible when necessary. [0]: https://openmetrics.io/ [1]: https://prometheus.io/ [2]: #959 [3]: #960
author Jonas Schäfer <jonas@wielicki.name>
date Sun, 18 Apr 2021 11:47:41 +0200
parents 6ff50541d2a6
children f3085620b6ff
line wrap: on
line source


local ipairs, pairs = ipairs, pairs;
local setmetatable = setmetatable;
local tostring = tostring;
local next = next;
local t_remove = table.remove;
local os_remove = os.remove;
local io_open = io.open;

local paths = require"util.paths";
local st = require "util.stanza";
local parse_xml_real = require "util.xml".parse;

local function getXml(user, host)
	local jid = user.."@"..host;
	local path = paths.join(prosody.paths.data, jid..".xml");
	local f = io_open(path);
	if not f then return; end
	local s = f:read("*a");
	f:close();
	return parse_xml_real(s);
end
local function setXml(user, host, xml)
	local jid = user.."@"..host;
	local path = paths.join(prosody.paths.data, jid..".xml");
	local f, err = io_open(path, "w");
	if not f then return f, err; end
	if xml then
		local s = tostring(xml);
		f:write(s);
		f:close();
		return true;
	else
		f:close();
		return os_remove(path);
	end
end
local function getUserElement(xml)
	if xml and xml.name == "server-data" then
		local host = xml.tags[1];
		if host and host.name == "host" then
			local user = host.tags[1];
			if user and user.name == "user" then
				return user;
			end
		end
	end
end
local function createOuterXml(user, host)
	return st.stanza("server-data", {xmlns='urn:xmpp:pie:0'})
		:tag("host", {jid=host})
			:tag("user", {name = user});
end
local function removeFromArray(array, value)
	for i,item in ipairs(array) do
		if item == value then
			t_remove(array, i);
			return;
		end
	end
end
local function removeStanzaChild(s, child)
	removeFromArray(s.tags, child);
	removeFromArray(s, child);
end

local handlers = {};

-- In order to support mod_auth_internal_hashed
local extended = "http://prosody.im/protocol/extended-xep0227\1";

handlers.accounts = {
	get = function(self, user)
		user = getUserElement(getXml(user, self.host));
		if user and user.attr.password then
			return { password = user.attr.password };
		elseif user then
			local data = {};
			for k, v in pairs(user.attr) do
				if k:sub(1, #extended) == extended then
					data[k:sub(#extended+1)] = v;
				end
			end
			return data;
		end
	end;
	set = function(self, user, data)
		if data then
			local xml = getXml(user, self.host);
			if not xml then xml = createOuterXml(user, self.host); end
			local usere = getUserElement(xml);
			for k, v in pairs(data) do
				if k == "password" then
					usere.attr.password = v;
				else
					usere.attr[extended..k] = v;
				end
			end
			return setXml(user, self.host, xml);
		else
			return setXml(user, self.host, nil);
		end
	end;
};
handlers.vcard = {
	get = function(self, user)
		user = getUserElement(getXml(user, self.host));
		if user then
			local vcard = user:get_child("vCard", 'vcard-temp');
			if vcard then
				return st.preserialize(vcard);
			end
		end
	end;
	set = function(self, user, data)
		local xml = getXml(user, self.host);
		local usere = xml and getUserElement(xml);
		if usere then
			local vcard = usere:get_child("vCard", 'vcard-temp');
			if vcard then
				removeStanzaChild(usere, vcard);
			elseif not data then
				return true;
			end
			if data then
				vcard = st.deserialize(data);
				usere:add_child(vcard);
			end
			return setXml(user, self.host, xml);
		end
		return true;
	end;
};
handlers.private = {
	get = function(self, user)
		user = getUserElement(getXml(user, self.host));
		if user then
			local private = user:get_child("query", "jabber:iq:private");
			if private then
				local r = {};
				for _, tag in ipairs(private.tags) do
					r[tag.name..":"..tag.attr.xmlns] = st.preserialize(tag);
				end
				return r;
			end
		end
	end;
	set = function(self, user, data)
		local xml = getXml(user, self.host);
		local usere = xml and getUserElement(xml);
		if usere then
			local private = usere:get_child("query", 'jabber:iq:private');
			if private then removeStanzaChild(usere, private); end
			if data and next(data) ~= nil then
				private = st.stanza("query", {xmlns='jabber:iq:private'});
				for _,tag in pairs(data) do
					private:add_child(st.deserialize(tag));
				end
				usere:add_child(private);
			end
			return setXml(user, self.host, xml);
		end
		return true;
	end;
};

handlers.roster = {
	get = function(self, user)
		user = getUserElement(getXml(user, self.host));
		if user then
			local roster = user:get_child("query", "jabber:iq:roster");
			if roster then
				local r = {
					[false] = {
						version = roster.attr.version;
						pending = {};
					}
				};
				for item in roster:childtags("item") do
					r[item.attr.jid] = {
						jid = item.attr.jid,
						subscription = item.attr.subscription,
						ask = item.attr.ask,
						name = item.attr.name,
						groups = {};
					};
					for group in item:childtags("group") do
						r[item.attr.jid].groups[group:get_text()] = true;
					end
					for pending in user:childtags("presence", "jabber:client") do
						r[false].pending[pending.attr.from] = true;
					end
				end
				return r;
			end
		end
	end;
	set = function(self, user, data)
		local xml = getXml(user, self.host);
		local usere = xml and getUserElement(xml);
		if usere then
			local roster = usere:get_child("query", 'jabber:iq:roster');
			if roster then removeStanzaChild(usere, roster); end
			usere:maptags(function (tag)
				if tag.attr.xmlns == "jabber:client" and tag.name == "presence" and tag.attr.type == "subscribe" then
					return nil;
				end
				return tag;
			end);
			if data and next(data) ~= nil then
				roster = st.stanza("query", {xmlns='jabber:iq:roster'});
				usere:add_child(roster);
				for jid, item in pairs(data) do
					if jid then
						roster:tag("item", {
							jid = jid,
							subscription = item.subscription,
							ask = item.ask,
							name = item.name,
						});
						for group in pairs(item.groups) do
							roster:tag("group"):text(group):up();
						end
						roster:up(); -- move out from item
					else
						roster.attr.version = item.version;
						for pending_jid in pairs(item.pending) do
							usere:add_child(st.presence({ from = pending_jid, type = "subscribe" }));
						end
					end
				end
			end
			return setXml(user, self.host, xml);
		end
		return true;
	end;
};


-----------------------------
local driver = {};

function driver:open(datastore, typ) -- luacheck: ignore 212/self
	if typ and typ ~= "keyval" then return nil, "unsupported-store"; end
	local handler = handlers[datastore];
	if not handler then return nil, "unsupported-datastore"; end
	local instance = setmetatable({ host = module.host; datastore = datastore; }, { __index = handler });
	if instance.init then instance:init(); end
	return instance;
end

module:provides("storage", driver);