view mod_statistics/top.lib.lua @ 6403:13c61a068c71

mod_pubsub_serverinfo: Include hostname in default pubsub node name This change ensures that every host publishes to a unique node by default, which is important when publishing info about multiple hosts. The old default node name was "serverinfo" The new default node name is in the format "serverinfo/example.com" where example.com is automatically replaced by the publishing host's name. This affects existing deployments in the following ways: - If you already configured pubsub_serverinfo_node explicitly in your config, then nothing will change, your current configuration will still be used. - If you did not specify it in your config (i.e. used the default) then the module will switch to publishing to the new node. If your configuration is correct this shouldn't be noticed, because the new node will automatically be created and configured by the module. If you want, you can clean up the old 'serverinfo' node using: prosodyctl shell pubsub delete_node pubsub.example.com serverinfo
author Matthew Wild <mwild1@gmail.com>
date Wed, 11 Feb 2026 09:40:30 +0000
parents 94868c5217a2
children
line wrap: on
line source

local array = require "util.array";
local it = require "util.iterators";
local curses = require "curses";
local stats = module:require "stats".stats;
local time = require "util.time".now;

local sessions_idle_after = 60;
local stanza_names = {"message", "presence", "iq"};

local top = {};
top.__index = top;

local status_lines = {
	"Prosody $version - $time up $up_since, $total_users users, $cpu busy";
	"Connections: $total_c2s c2s, $total_s2sout s2sout, $total_s2sin s2sin, $total_component component";
	"Memory: $memory_lua lua, $memory_allocated process ($memory_used in use)";
	"Stanzas in: $message_in_per_second message/s, $presence_in_per_second presence/s, $iq_in_per_second iq/s";
	"Stanzas out: $message_out_per_second message/s, $presence_out_per_second presence/s, $iq_out_per_second iq/s";
};

function top:draw()
	self:draw_status();
	self:draw_column_titles();
	self:draw_conn_list();
	self.statuswin:refresh();
	self.listwin:refresh();
	--self.infowin:refresh()
	self.stdscr:move(#status_lines,0)
end

-- Width specified as cols or % of unused space, defaults to
-- title width if not specified
local conn_list_columns = {
	{ title = "ID", key = "id", width = "8" };
	{ title = "JID", key = "jid", width = "100%" };
	{ title = "STANZAS IN>", key = "total_stanzas_in", align = "right" };
	{ title = "MSG", key = "message_in", align = "right", width = "4" };
	{ title = "PRES", key = "presence_in", align = "right", width = "4" };
	{ title = "IQ", key = "iq_in", align = "right", width = "4" };
	{ title = "STANZAS OUT>", key = "total_stanzas_out", align = "right" };
	{ title = "MSG", key = "message_out", align = "right", width = "4" };
	{ title = "PRES", key = "presence_out", align = "right", width = "4" };
	{ title = "IQ", key = "iq_out", align = "right", width = "4" };
	{ title = "BYTES IN", key = "bytes_in", align = "right" };
	{ title = "BYTES OUT", key = "bytes_out", align = "right" };

};

function top:draw_status()
	for row, line in ipairs(status_lines) do
		self.statuswin:mvaddstr(row-1, 0, (line:gsub("%$([%w_]+)", self.data)));
		self.statuswin:clrtoeol();
	end
	-- Clear stanza counts
	for _, stanza_type in ipairs(stanza_names) do
		self.prosody[stanza_type.."_in_per_second"] = 0;
		self.prosody[stanza_type.."_out_per_second"] = 0;
	end
end

local function padright(s, width)
	return s..string.rep(" ", width-#s);
end

local function padleft(s, width)
	return string.rep(" ", width-#s)..s;
end

function top:resized()
	self:recalc_column_widths();
	--self.stdscr:clear();
	self:draw();
end

function top:recalc_column_widths()
	local widths = {};
	self.column_widths = widths;
	local total_width = curses.cols()-4;
	local free_width = total_width;
	for i = 1, #conn_list_columns do
		local width = conn_list_columns[i].width or "0";
		if not(type(width) == "string" and width:sub(-1) == "%") then
			width = math.max(tonumber(width), #conn_list_columns[i].title+1);
			widths[i] = width;
			free_width = free_width - width;
		end
	end
	for i = 1, #conn_list_columns do
		if not widths[i] then
			local pc_width = tonumber((conn_list_columns[i].width:gsub("%%$", "")));
			widths[i] = math.floor(free_width*(pc_width/100));
		end
	end
	return widths;
end

function top:draw_column_titles()
	local widths = self.column_widths;
	self.listwin:attron(curses.A_REVERSE);
	self.listwin:mvaddstr(0, 0, "  ");
	for i, column in ipairs(conn_list_columns) do
		self.listwin:addstr(padright(column.title, widths[i]));
	end
	self.listwin:addstr("  ");
	self.listwin:attroff(curses.A_REVERSE);
end

local function session_compare(session1, session2)
	local stats1, stats2 = session1.stats, session2.stats;
	return (stats1.total_stanzas_in + stats1.total_stanzas_out) >
		(stats2.total_stanzas_in + stats2.total_stanzas_out);
end

function top:draw_conn_list()
	local rows = curses.lines()-(#status_lines+2)-5;
	local cutoff_time = time() - sessions_idle_after;
	local widths = self.column_widths;
	local top_sessions = array.collect(it.values(self.active_sessions)):sort(session_compare);
	for index = 1, rows do
		local session = top_sessions[index];
		if session then
			if session.last_update < cutoff_time then
				self.active_sessions[session.id] = nil;
			else
				local row = {};
				for i, column in ipairs(conn_list_columns) do
					local width = widths[i];
					local v = tostring(session[column.key] or ""):sub(1, width);
					if #v < width then
						if column.align == "right" then
							v = padleft(v, width-1).." ";
						else
							v = padright(v, width);
						end
					end
					table.insert(row, v);
				end
				if session.updated then
					self.listwin:attron(curses.A_BOLD);
				end
				self.listwin:mvaddstr(index, 0, "  "..table.concat(row));
				if session.updated then
					session.updated = false;
					self.listwin:attroff(curses.A_BOLD);
				end
			end
		else
			-- FIXME: How to clear a line? It's 5am and I don't feel like reading docs.
			self.listwin:move(index, 0);
			self.listwin:clrtoeol();
		end
	end
end

function top:update_stat(name, value)
	self.prosody[name] = value;
end

function top:update_session(id, jid, stats)
	self.active_sessions[id] = stats;
	stats.id, stats.jid, stats.stats = id, jid, stats;
	stats.total_bytes = stats.bytes_in + stats.bytes_out;
	for _, stanza_type in ipairs(stanza_names) do
		self.prosody[stanza_type.."_in_per_second"] = (self.prosody[stanza_type.."_in_per_second"] or 0) + stats[stanza_type.."_in"];
		self.prosody[stanza_type.."_out_per_second"] = (self.prosody[stanza_type.."_out_per_second"] or 0) + stats[stanza_type.."_out"];
	end
	stats.total_stanzas_in = stats.message_in + stats.presence_in + stats.iq_in;
	stats.total_stanzas_out = stats.message_out + stats.presence_out + stats.iq_out;
	stats.last_update = time();
	stats.updated = true;
end

local function new(base)
	setmetatable(base, top);
	base.data = setmetatable({}, {
		__index = function (t, k)
			local stat = stats[k];
			if stat and stat.tostring then
				if type(stat.tostring) == "function" then
					return stat.tostring(base.prosody[k]);
				elseif type(stat.tostring) == "string" then
					local v = base.prosody[k];
					if v == nil then
						return "?";
					end
					return (stat.tostring):format(v);
				end
			end
			return base.prosody[k];
		end;
	});

	base.active_sessions = {};

	base.statuswin = curses.newwin(#status_lines, 0, 0, 0);

	base.promptwin = curses.newwin(1, 0, #status_lines, 0);
	base.promptwin:addstr("");
	base.promptwin:refresh();

	base.listwin = curses.newwin(curses.lines()-(#status_lines+2)-5, 0, #status_lines+1, 0);
	base.listwin:syncok();

	base.infowin = curses.newwin(5, 0, curses.lines()-5, 0);
	base.infowin:mvaddstr(1, 1, "Hello world");
	base.infowin:border(0,0,0,0);
	base.infowin:syncok();
	base.infowin:refresh();

	base:resized();

	return base;
end

return { new = new };