comparison mod_statistics/top.lua @ 1072:4dbdb1b465e8

mod_statistics: Initial version, and a rough 'prosodyctl mod_statistics top'
author Matthew Wild <mwild1@gmail.com>
date Sat, 15 Jun 2013 19:08:34 +0100
parents
children fe57e9332e52
comparison
equal deleted inserted replaced
1071:8f59b45fe6a7 1072:4dbdb1b465e8
1 module("prosodytop", package.seeall);
2
3 local array = require "util.array";
4 local it = require "util.iterators";
5 local curses = require "curses";
6 local stats = require "stats".stats;
7 local time = require "socket".gettime;
8
9 local sessions_idle_after = 3;
10 local stanza_names = {"message", "presence", "iq"};
11
12 local top = {};
13 top.__index = top;
14
15 local status_lines = {
16 "Prosody $version - $time up $up_since, $total_users users, $cpu busy";
17 "Connections: $total_c2s c2s, $total_s2sout s2sout, $total_s2sin s2sin, $total_component component";
18 "Memory: $memory_used lua, $memory_process process";
19 "Stanzas in: $message_in_per_second message/s, $presence_in_per_second presence/s, $iq_in_per_second iq/s";
20 "Stanzas out: $message_out_per_second message/s, $presence_out_per_second presence/s, $iq_out_per_second iq/s";
21 };
22
23 function top:draw()
24 self:draw_status();
25 self:draw_column_titles();
26 self:draw_conn_list();
27 self.statuswin:refresh();
28 self.listwin:refresh();
29 --self.infowin:refresh()
30 self.stdscr:move(#status_lines,0)
31 end
32
33 -- Width specified as cols or % of unused space, defaults to
34 -- title width if not specified
35 local conn_list_columns = {
36 { title = "ID", key = "id", width = "8" };
37 { title = "JID", key = "jid", width = "100%" };
38 { title = "STANZAS IN>", key = "total_stanzas_in", align = "right" };
39 { title = "MSG", key = "message_in", align = "right", width = "4" };
40 { title = "PRES", key = "presence_in", align = "right", width = "4" };
41 { title = "IQ", key = "iq_in", align = "right", width = "4" };
42 { title = "STANZAS OUT>", key = "total_stanzas_out", align = "right" };
43 { title = "MSG", key = "message_out", align = "right", width = "4" };
44 { title = "PRES", key = "presence_out", align = "right", width = "4" };
45 { title = "IQ", key = "iq_out", align = "right", width = "4" };
46 { title = "BYTES IN", key = "bytes_in", align = "right" };
47 { title = "BYTES OUT", key = "bytes_out", align = "right" };
48
49 };
50
51 function top:draw_status()
52 for row, line in ipairs(status_lines) do
53 self.statuswin:mvaddstr(row-1, 0, (line:gsub("%$([%w_]+)", self.data)));
54 self.statuswin:clrtoeol();
55 end
56 -- Clear stanza counts
57 for _, stanza_type in ipairs(stanza_names) do
58 self.prosody[stanza_type.."_in_per_second"] = 0;
59 self.prosody[stanza_type.."_out_per_second"] = 0;
60 end
61 end
62
63 local function padright(s, width)
64 return s..string.rep(" ", width-#s);
65 end
66
67 local function padleft(s, width)
68 return string.rep(" ", width-#s)..s;
69 end
70
71 function top:resized()
72 self:recalc_column_widths();
73 --self.stdscr:clear();
74 self:draw();
75 end
76
77 function top:recalc_column_widths()
78 local widths = {};
79 self.column_widths = widths;
80 local total_width = curses.cols()-4;
81 local free_width = total_width;
82 for i = 1, #conn_list_columns do
83 local width = conn_list_columns[i].width or "0";
84 if not(type(width) == "string" and width:sub(-1) == "%") then
85 width = math.max(tonumber(width), #conn_list_columns[i].title+1);
86 widths[i] = width;
87 free_width = free_width - width;
88 end
89 end
90 for i = 1, #conn_list_columns do
91 if not widths[i] then
92 local pc_width = tonumber((conn_list_columns[i].width:gsub("%%$", "")));
93 widths[i] = math.floor(free_width*(pc_width/100));
94 end
95 end
96 return widths;
97 end
98
99 function top:draw_column_titles()
100 local widths = self.column_widths;
101 self.listwin:attron(curses.A_REVERSE);
102 self.listwin:mvaddstr(0, 0, " ");
103 for i, column in ipairs(conn_list_columns) do
104 self.listwin:addstr(padright(column.title, widths[i]));
105 end
106 self.listwin:addstr(" ");
107 self.listwin:attroff(curses.A_REVERSE);
108 end
109
110 local function session_compare(session1, session2)
111 local stats1, stats2 = session1.stats, session2.stats;
112 return (stats1.total_stanzas_in + stats1.total_stanzas_out) >=
113 (stats2.total_stanzas_in + stats2.total_stanzas_out);
114 end
115
116 function top:draw_conn_list()
117 local rows = curses.lines()-(#status_lines+2)-5;
118 local cutoff_time = time() - sessions_idle_after;
119 local widths = self.column_widths;
120 local top_sessions = array.collect(it.values(self.active_sessions)):sort(session_compare);
121 for index = 1, rows do
122 session = top_sessions[index];
123 if session then
124 if session.last_update < cutoff_time then
125 self.active_sessions[session.id] = nil;
126 else
127 local row = {};
128 for i, column in ipairs(conn_list_columns) do
129 local width = widths[i];
130 local v = tostring(session[column.key] or ""):sub(1, width);
131 if #v < width then
132 if column.align == "right" then
133 v = padleft(v, width-1).." ";
134 else
135 v = padright(v, width);
136 end
137 end
138 table.insert(row, v);
139 end
140 self.listwin:mvaddstr(index, 0, " "..table.concat(row));
141 end
142 else
143 -- FIXME: How to clear a line? It's 5am and I don't feel like reading docs.
144 self.listwin:move(index, 0);
145 self.listwin:clrtoeol();
146 end
147 end
148 end
149
150 function top:update_stat(name, value)
151 self.prosody[name] = value;
152 end
153
154 function top:update_session(id, jid, stats)
155 self.active_sessions[id] = stats;
156 stats.id, stats.jid, stats.stats = id, jid, stats;
157 stats.total_bytes = stats.bytes_in + stats.bytes_out;
158 for _, stanza_type in ipairs(stanza_names) do
159 self.prosody[stanza_type.."_in_per_second"] = (self.prosody[stanza_type.."_in_per_second"] or 0) + stats[stanza_type.."_in"];
160 self.prosody[stanza_type.."_out_per_second"] = (self.prosody[stanza_type.."_out_per_second"] or 0) + stats[stanza_type.."_out"];
161 end
162 stats.total_stanzas_in = stats.message_in + stats.presence_in + stats.iq_in;
163 stats.total_stanzas_out = stats.message_out + stats.presence_out + stats.iq_out;
164 stats.last_update = time();
165 end
166
167 function new(base)
168 setmetatable(base, top);
169 base.data = setmetatable({}, {
170 __index = function (t, k)
171 local stat = stats[k];
172 if stat and stat.tostring then
173 if type(stat.tostring) == "function" then
174 return stat.tostring(base.prosody[k]);
175 elseif type(stat.tostring) == "string" then
176 local v = base.prosody[k];
177 if v == nil then
178 return "?";
179 end
180 return (stat.tostring):format(v);
181 end
182 end
183 return base.prosody[k];
184 end;
185 });
186
187 base.active_sessions = {};
188
189 base.statuswin = curses.newwin(#status_lines, 0, 0, 0);
190
191 base.promptwin = curses.newwin(1, 0, #status_lines, 0);
192 base.promptwin:addstr("");
193 base.promptwin:refresh();
194
195 base.listwin = curses.newwin(curses.lines()-(#status_lines+2)-5, 0, #status_lines+1, 0);
196 base.listwin:syncok();
197
198 base.infowin = curses.newwin(5, 0, curses.lines()-5, 0);
199 base.infowin:mvaddstr(1, 1, "Hello world");
200 base.infowin:border(0,0,0,0);
201 base.infowin:syncok();
202 base.infowin:refresh();
203
204 base:resized();
205
206 return base;
207 end
208
209 return _M;