view mod_statistics/top.lib.lua @ 5585:5b316088bef5

mod_rest: Use logger of HTTP request in trunk In Prosody trunk rev c975dafa4303 each HTTP request gained its own log sink, to make it easy to log things related to each request and group those messages. Especially where async is used, spreading the request and response apart as mod_rest does with iq stanzas, this grouped logging should help find related messages.
author Kim Alvefur <zash@zash.se>
date Fri, 07 Jul 2023 00:10:37 +0200
parents 66bda434d476
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 "socket".gettime;

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 };