summaryrefslogtreecommitdiffstats
path: root/docs/server-status/server-status.lua
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 06:33:50 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 06:33:50 +0000
commitfe39ffb8b90ae4e002ed73fe98617cd590abb467 (patch)
treeb80e5956907d8aeaaffe4e4f0c068c0e6157ce8b /docs/server-status/server-status.lua
parentInitial commit. (diff)
downloadapache2-fe39ffb8b90ae4e002ed73fe98617cd590abb467.tar.xz
apache2-fe39ffb8b90ae4e002ed73fe98617cd590abb467.zip
Adding upstream version 2.4.56.upstream/2.4.56
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'docs/server-status/server-status.lua')
-rw-r--r--docs/server-status/server-status.lua1901
1 files changed, 1901 insertions, 0 deletions
diff --git a/docs/server-status/server-status.lua b/docs/server-status/server-status.lua
new file mode 100644
index 0000000..323a699
--- /dev/null
+++ b/docs/server-status/server-status.lua
@@ -0,0 +1,1901 @@
+--[[
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+]]
+
+--[[ mod_lua implementation of the server-status page ]]
+local ssversion = "0.11" -- version of this script
+local redact_ips = true -- whether to replace the last two bits of every IP with 'x.x'
+local warning_banner = [[
+ <div style="float: left; color: #222; margin-bottom: 8px; margin-top: 24px; text-align: center; width: 200px; font-size: 0.7rem; border: 1px dashed #333; background: #F8C940;">
+ <h3 style="margin: 4px; font-size: 1rem;">Don't be alarmed - this page is here for a reason!</h3>
+ <p style="font-weight: bolder; font-size: 0.8rem;">This is an example server status page for the Apache HTTP Server. Nothing on this server is secret, no URL tokens, no sensitive passwords. Everything served from here is static data.</p>
+ </div>
+]]
+local show_warning = true -- whether to display the above warning/notice on the page
+local show_modules = false -- Whether to list loaded modules or not
+local show_threads = true -- whether to list thread information or not
+
+-- pre-declare some variables defined at the bottom of this script:
+local status_js, status_css, quokka_js
+
+-- quick and dirty JSON conversion
+local function quickJSON(input)
+ if type(input) == "table" then
+ local t = 'array'
+ for k, v in pairs(input) do
+ if type(k) ~= "number" then
+ t = 'hash'
+ break
+ end
+ end
+
+ if t == 'hash' then
+ local out = ""
+ local tbl = {}
+ for k, v in pairs(input) do
+ local kv = ([["%s": %s]]):format(k, quickJSON(v))
+ table.insert(tbl, kv)
+ end
+ return "{" .. table.concat(tbl, ", ") .. "}"
+ else
+ local tbl = {}
+ for k, v in pairs(input) do
+ table.insert(tbl, quickJSON(v))
+ end
+ return "[" .. table.concat(tbl, ", ") .. "]"
+ end
+ elseif type(input) == "string" then
+ return ([["%s"]]):format(input:gsub('"', '\\"'):gsub("[\r\n\t]", " "))
+ elseif type(input) == "number" then
+ return tostring(input)
+ elseif type(input) == "boolean" then
+ return (input and "true" or "false")
+ else
+ return "null"
+ end
+end
+
+-- Module information callback
+local function modInfo(r, modname)
+ if modname then
+ r:puts [[
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ ]]
+ r:puts (status_css)
+ r:puts [[
+ </style>
+ <title>Module information</title>
+ </head>
+
+ <body>
+ ]]
+ r:puts( ("<h3>Details for module %s</h3>\n"):format(r:escape_html(modname)) )
+ -- Queries the server for information about a module
+ local mod = r.module_info(modname)
+ if mod then
+ for k, v in pairs(mod.commands) do
+ -- print out all directives accepted by this module
+ r:puts( ("<b>%s:</b> %s<br>\n"):format(r:escape_html(k), v))
+ end
+ end
+ -- HTML tail
+ r:puts[[
+ </body>
+ </html>
+ ]]
+ end
+end
+
+-- Function for generating server stats
+function getServerState(r, verbose)
+ local state = {}
+
+ state.mpm = {
+ type = "prefork", -- default to prefork until told otherwise
+ threadsPerChild = 1,
+ threaded = false,
+ maxServers = r.mpm_query(12),
+ activeServers = 0
+ }
+ if r.mpm_query(14) == 1 then
+ state.mpm.type = "event" -- this is event mpm
+ elseif r.mpm_query(3) >= 1 then
+ state.mpm.type = "worker" -- it's not event, but it's threaded, we'll assume worker mpm (could be motorz??)
+ elseif r.mpm_query(2) == 1 then
+ state.mpm.type = "winnt" -- it's threaded, but not worker nor event, so it's probably winnt
+ end
+ if state.mpm.type ~= "prefork" then
+ state.mpm.threaded = true -- it's threaded
+ state.mpm.threadsPerChild = r.mpm_query(6) -- get threads per child proc
+ end
+
+ state.processes = {} -- list of child procs
+ state.connections = { -- overall connection info
+ idle = 0,
+ active = 0
+ }
+ -- overall server stats
+ state.server = {
+ connections = 0,
+ bytes = 0,
+ built = r.server_built,
+ localtime = os.time(),
+ uptime = os.time() - r.started,
+ version = r.banner,
+ host = r.server_name,
+ modules = nil,
+ extended = show_threads, -- whether extended status is available or not
+ }
+
+ -- if show_modules is true, add list of modules to the JSON
+ if show_modules then
+ state.server.modules = {}
+ for k, module in pairs(r:loaded_modules()) do
+ table.insert(state.server.modules, module)
+ end
+ end
+
+ -- Fetch process/thread data
+ for i=0,state.mpm.maxServers-1,1 do
+ local server = r.scoreboard_process(r, i);
+ if server then
+ local s = {
+ active = false,
+ pid = nil,
+ bytes = 0,
+ stime = 0,
+ utime = 0,
+ connections = 0,
+ }
+ local tstates = {}
+ if server.pid then
+ state.connections.idle = state.connections.idle + (server.keepalive or 0)
+ s.connections = 0
+ if server.pid > 0 then
+ state.mpm.activeServers = state.mpm.activeServers + 1
+ s.active = true
+ s.pid = server.pid
+ end
+ for j = 0, state.mpm.threadsPerChild-1, 1 do
+ local worker = r.scoreboard_worker(r, i, j)
+ if worker then
+ s.stime = s.stime + (worker.stimes or 0);
+ s.utime = s.utime + (worker.utimes or 0);
+ if verbose and show_threads then
+ s.threads = s.threads or {}
+ table.insert(s.threads, {
+ bytes = worker.bytes_served,
+ thread = ("0x%x"):format(worker.tid),
+ client = redact_ips and (worker.client or "???"):gsub("[a-f0-9]+[.:]+[a-f0-9]+$", "x.x") or worker.client or "???",
+ cost = ((worker.utimes or 0) + (worker.stimes or 0)),
+ count = worker.access_count,
+ vhost = worker.vhost:gsub(":%d+", ""),
+ request = worker.request,
+ last_used = math.floor(worker.last_used/1000000)
+ })
+ end
+ state.server.connections = state.server.connections + worker.access_count
+ s.bytes = s.bytes + worker.bytes_served
+ s.connections = s.connections + worker.access_count
+ if server.pid > 0 then
+ tstates[worker.status] = (tstates[worker.status] or 0) + 1
+ end
+ end
+ end
+ end
+
+ s.workerStates = {
+ keepalive = (server.keepalive > 0) and server.keepalive or tstates[5] or 0,
+ closing = tstates[8] or 0,
+ idle = tstates[2] or 0,
+ writing = tstates[4] or 0,
+ reading = tstates[3] or 0,
+ graceful = tstates[9] or 0
+ }
+ table.insert(state.processes, s)
+ state.server.bytes = state.server.bytes + s.bytes
+ state.connections.active = state.connections.active + (tstates[8] or 0) + (tstates[4] or 0) + (tstates[3] or 0)
+ end
+ end
+ return state
+end
+
+-- Handler function
+function handle(r)
+
+ -- Parse GET data, if any, and set content type
+ local GET = r:parseargs()
+
+ if GET['module'] then
+ modInfo(r, GET['module'])
+ return apache2.OK
+ end
+
+
+ -- If we only need the stats feed, compact it and hand it over
+ if GET['view'] and GET['view'] == "json" then
+ local state = getServerState(r, GET['extended'] == 'true')
+ r.content_type = "application/json"
+ r:puts(quickJSON(state))
+ return apache2.OK
+ end
+
+ if not GET['resource'] then
+
+ local state = getServerState(r, show_threads)
+
+ -- Print out the HTML for the front page
+ r.content_type = "text/html"
+ r:puts ( ([=[
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <!-- Stylesheet -->
+ <link href="?resource=css" rel="stylesheet">
+
+ <!-- JavaScript-->
+ <script type="text/javascript" src="?resource=js"></script>
+
+ <title>Server status for %s</title>
+ </head>
+
+ <body onload="refreshCharts(false);">
+ <div class="wrapper" id="wrapper">
+ <div class="navbarLeft">
+ <img align='absmiddle' src='?resource=feather' width="15" height="30"/>
+ Apache HTTPd
+ </div>
+ <div class="navbarRight">Status for %s on %s</div>
+ <div style="clear: both;"></div>
+ <div class="serverinfo" id="leftpane">
+ <ul id="menubar">
+ <li>
+ <a class="btn active" id="dashboard_button" href="javascript:void(showPanel('dashboard'));">Dashboard</a>
+ </li>
+ <li>
+ <a class="btn" id="misc_button" href="javascript:void(showPanel('misc'));">Server Info</a>
+ </li>
+ <li>
+ <a class="btn" id="threads_button" style="display: none;" href="javascript:void(showPanel('threads'));">Show thread information</a>
+ </li>
+ <li>
+ <a class="btn" id="modules_button" style="display: none;" href="javascript:void(showPanel('modules'));">Show loaded modules</a>
+ </li>
+ </ul>
+
+ <!-- warning --> %s <!-- /warning -->
+
+ </div>
+
+ <!-- dashboard -->
+ <div class="charts" id="dashboard_panel">
+
+ <div class="infobox_wrapper" style="clear: both; width: 100%%;">
+ <div class="infobox_title">Quick Stats</div>
+ <div class="infobox" id="general_stats">
+ </div>
+ </div>
+ <div class="infobox_wrapper" style="width: 100%%;">
+ <div class="infobox_title">Charts</div>
+ <div class="infobox">
+ <!--Div that will hold the pie chart-->
+ <canvas id="actions_div" width="1400" height="400" class="canvas_wide"></canvas>
+ <canvas id="status_div" width=580" height="400" class="canvas_narrow"></canvas>
+ <canvas id="traffic_div" width="1400" height="400" class="canvas_wide"></canvas>
+ <canvas id="idle_div" width="580" height="400" class="canvas_narrow"></canvas>
+ <canvas id="connection_div" width="1400" height="400" class="canvas_wide"></canvas>
+ <canvas id="cpu_div" width="580" height="400" class="canvas_narrow"></canvas>
+ <div style="clear: both"></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- misc server info -->
+ <div class="charts" id="misc_panel" style="display: none;">
+ <div class="infobox_wrapper" style="clear: both; width: 100%%;">
+ <div class="infobox_title">General server information</div>
+ <div class="infobox" style='padding: 16px; width: calc(100%% - 32px);' id="server_breakdown">
+ </div>
+ </div>
+ </div>
+
+ <!-- thread info -->
+ <div class="charts" id="threads_panel" style="display: none;">
+ <div class="infobox_wrapper" style="clear: both; width: 100%%;">
+ <div class="infobox_title">Thread breakdown</div>
+ <div class="infobox" style='padding: 16px; width: calc(100%% - 32px);' id="threads_breakdown">
+ </div>
+ </div>
+ </div>
+
+ <!-- module info -->
+ <div class="charts" id="modules_panel" style="display: none;">
+ <div class="infobox_wrapper" style="clear: both; width: 100%%;">
+ <div class="infobox_title">Modules loaded</div>
+ <div class="infobox" style='padding: 16px; width: calc(100%% - 32px);' id="modules_breakdown">
+ blabla
+ </div>
+ </div>
+ </div>
+
+
+ </div>
+
+
+ ]=]):format(
+ r.server_name,
+ r.banner,
+ r.server_name,
+ show_warning and warning_banner or ""
+ ) );
+ -- HTML tail
+ r:puts[[
+ </body>
+ </html>
+ ]]
+ else
+ -- Resource documents (CSS, JS, PNG)
+ if GET['resource'] == 'js' then
+ r.content_type = "application/javascript"
+ r:puts(quokka_js)
+ r:puts(status_js)
+ elseif GET['resource'] == 'css' then
+ r.content_type = "text/css"
+ r:puts(status_css)
+ elseif GET['resource'] == 'feather' then
+ r.content_type = "image/png"
+ r:write(r:base64_decode('iVBORw0KGgoAAAANSUhEUgAAACUAAABACAYAAACdp77qAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QEWECwoSXwjUAAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAlvSURBVGje7Zl7cFXVFcZ/a50bHhIRAQWpICSEgGKEUKAUgqKDWsBBHBFndKzYKdAWlWkDAlUEkfIogyAUxfqqdYqP1scg2mq1QLCiIC8LhEeCPDQwoWAgBHLvOXv1j3PvJQRQAjfgH90zmXvu3nv2/u73fWutvU/gHLX9C3IBOLCgc9MDz+S+dGB+l6B0Tu7re2d1bgawd0bn5Fw5F4D+uyCXJsNXs//pzi1U5SMg25zgYkYQY4s76ro3H7/2m8R8PRegmgxfTenTnS8R1SIgG0AERAQR2kma/gFgz7Rrah/UvwdfnpCucUR1KVAvLo4hFj4qiNDz6yk56c3Hrqt9UG3aXxbaw/gz0CHebcBhANE4RKW+RrwW50S+yyavtF0P5T7nH6IfxxCVAWlJCUOmVDXsqzVQW+/PAWDXmC53I9wXO0hgQRh8QClQN7G7KKAEiFTWKqiINuTL/Nzmzsk8c4qL4vkV5kRtjXhkiRKYTyyosCBWTix6gIP+odieWgG1eVi30EtzlhNEvfctkItcAC5QjpTI24d3cP2hbRYt24KW7yCtogQvup80d5SSFpO+KN817pray1NbR3Sbqx4jRUE8ANuunlWKWntRQOy4+Wb201bT17xUa8lz833d+4vKG+JRR9Qg/HvGi8gwEUPU4jkqPgZBy2mrI1XXSKl8G+/60UXOl6nmU8fFwPmCxeQFAumf+O58xQWCc4L5ijkmAKzLz0ktqPW39ghliOk0i+nVzhfMBxdjrQukmfn6gxCQ4Pxj4IJA9vlRferw9O5cM3N96kCt+Uk3ct76hPUDe1xvASNCMIKLaWAxPreAvs4H8wXzBRfTquCey5i96sDevdHj1kyJp1b3657uqbdBlFaSyD0ehepZiXj0EQE8IzEW5ibbD35O1oLPv6q+3lkxVdCqF2tv6om/L21YEJVWxxgAF7PnnS95LhaXLaYhg/HxwGd01oLPv9o6ousJ654xUx+37UXPbctZntHrAo3IoUhT57wGRMQDUXtTlXT16EtVdrzEs/tnh5dX9N10b3c6vPhp6kAlTwJZee8BN+Ph6jQzxOMI6h7ROjJL1FCpKhmIx0Y8rqtXP1qa+fyqk1eEswG0PCPvDkNuFgAf9cvwvQa2SOrog64SJBKyg4GYodjbR0t1YRC1uletWHXKdc+IqaVt8vA8GoAsBbokKz4c8RoFz4onw8SjLkrMnPkSUN8CVltMWksailjOl4e/2XXHhg2pAwVQkJE3SFTeqFYvloryDSIDxWGYCRruIl7SU38N6kaH9Fz5qTvV2jWOvmUZvcNfIzqr+pjDppjJQHPgMEElRGRhMrUo5qK8+G2Aagxqaca19C5exrKM3sMNWlcl2rDZgk6oKoIzw6qKYnz648KCxf/pdCMpA3Vt8VKWtO6djsgUA5yBmWAmBzEpFqFXdXeYJebZKudzM8CesrJvP4/V2EyeN8zgYjCEJBMfCfIzi98Fqh9NgM8Cx7O9txeUfZyZR8+igtSAej/jJpRYuqFDwFQAw8WBua0gvSV+KxAST2Bmu0TEU5VGwHcCqpF8Nxb/AyStY4B2C9A4HA+H7gY9YkjjkLtQLhfKiqAtMfaA/0RBZt7pHadPZ9Litv3pv20xvsk4EUHjsikOQ/IV7ylJWtoQXPIuhdm7ecXLBtTEIaedpxZn9WsuTkpUDMzF049txmyeCnMlDiZx0VPMGW6rwGHn3KDrthfsPN29vlO+11vdEuYg5z1sooTSeTgUH53hRGc4BJfsFwzFoQpetiH7agLotOQbvHMRsxoNVMNudxY3sRgBtlPMtTGR+s4szg4IHsdYE4BJNQ3w0zJ66ybaN8BrGIS3RgJTnGmhE69ngEcgHiaKk/g4SoBHgBRGrd6Kf2X2IaVMAQR4XRWrHxaNUCDMPlBkvAAqQhBPAxr3Vdz4T91U/K6r8WX2uya8mjG4rsENAWHUCYpguxH2gFwsOMyMMCrBiZdIDHtx+saZFPtvle/lNkMw1YhDe1jczAGK73Sow5tzzOBKYAlZBRfKO69f8Xu7P7xqQGpB3b39VQInVzu0rksmTN1pKi0c2jiIgwzwsOSzEhibBxS98/iizAHcsOEdUi6fE++2KrkHzP6kovnJs0GyBiaizspA+gPcUvQOKZcvfHfTsI9ZMveUG1IRoO2rMJewt8Wjc8RtxW8WvZlx6xkfs08ANbZF/nHfK6XeD4+SFljola8C0aaGprl46Cc+DXFm3D+46G+vvJZ5O4OK3zpjUCctM4+3ze+LBR+CXZqmXkk9dzRo6Mo9wc0RoYtAL5FE+TUEK4xY5d0rtXNhRummil+W/cXOFNCKNh31OKbym8VZcm4dXmQRGslxCBVaX3wU37n5zqSXQ3CJaHMy+q6ihR12asvmza30nrMBlLRx9Z7JV4zikR2zmdxu9DwxrhWhY/jWJpjfyB00xX4FVgq8fkDS58a0XoM0/IfF7Iox257InZn5gOQXPXlWwE55Snis3ZjOgiwDSxcMM3IFW4WgDm+XYFEPawQ0EXOFmN0wbtusr1PxbuKU0Tdhy4w1TmSTieKQzwLx+gQa0TD0aQlkOmhi8Nrho0c6Hah0JdMyR6XmnWn1jvyMhyJpaXVaTt08eXsgskyQrghLnOlQFTAxxAwxyh3MFyNWt/4FPR7fMnNJKgCNHPngpScwVX60IhCzluPbP7zYiTfQiUYdXomptkiWFVGcajqio0xs6SNbZi55ZciClLAkIrkngLrwokvEx9aZ6UZncplDyn3TSmfS0InGDKIOqXDIQt/k0ke3/P6DCW1/w52vDk8FS8ydO/vvxxl9VPajEQ86RoQ7wZaJ0UOgsQkHwDYolAD+7wonL6+t/1KMHPlg90i1UHRmbJy+edJYgNEdJo5R828DvcSht0wrnLQwMXdc1jimbp1aG7h2nHLk19mPXZ7f/rEXkgGQPTGPc9ROmRLM006B6PtxQMzcPLEgP3viOQF10uR5/1VTEBgL8taTG8YXco7bCUw90OMZ5m74LQFeVnj7/Z604VdOv/IXV86Yeb72P6mnTL0RvvA236d2Z8dJRQCjOs0+L/t71Tuubz9qUCXR3UWlnxSs2HMhsPGcgzqhIJdZ+R0Vh4/eE3+TcP49lZM9tFEMt2/TjpdjXdv+/LzZJ8nU1Vn3IkgGsBZg5bY/ct6j74utL2JYJtjOnHZDz2ugHZ8SjKYYK9ZveeH7kwpy2t2r/L+dvP0P/Tla8usTzhIAAAAASUVORK5CYII='))
+ end
+ end
+ return apache2.OK;
+end
+
+
+------------------------------------
+-- JavaScript and CSS definitions --
+------------------------------------
+
+-- Set up some JavaScripts:
+status_js = [==[
+Number.prototype.pad = function(size) {
+ var str = String(this);
+ while (str.length < size) {
+ str = "0" + str;
+ }
+ return str;
+}
+
+function getAsync(theUrl, xstate, callback) {
+ var xmlHttp = null;
+ if (window.XMLHttpRequest) {
+ xmlHttp = new XMLHttpRequest();
+ } else {
+ xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
+ }
+ xmlHttp.open("GET", theUrl, true);
+ xmlHttp.send(null);
+ xmlHttp.onreadystatechange = function(state) {
+ if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
+ if (callback) {
+ callback(JSON.parse(xmlHttp.responseText));
+ }
+
+ }
+ }
+}
+
+var actionCache = [];
+var connectionCache = [];
+var trafficCache = [];
+var processes = {};
+var lastBytes = 0;
+var lastConnections = 0;
+var negativeBytes = 0; // cache for proc reloads, which skews traffic
+var updateSpeed = 5; // How fast do charts update?
+var maxRecords = 24; // How many records to show per chart
+var cpumax = 1000000; // random cpu max(?)
+
+function refreshCharts(json, state) {
+ if (json && json.processes) {
+
+
+
+ // general server info box
+ var gs = document.getElementById('server_breakdown');
+ gs.innerHTML = "";
+ gs.innerHTML += "<b>Server version: </b>" + json.server.version + "<br/>";
+ gs.innerHTML += "<b>Server built: </b>" + json.server.built + "<br/>";
+ gs.innerHTML += "<b>Server MPM: </b>" + json.mpm.type + " <span id='mpminfo'></span><br/>";
+
+
+ // Get a timestamp
+ var now = new Date();
+ var ts = now.getHours().pad(2) + ":" + now.getMinutes().pad(2) + ":" + now.getSeconds().pad(2);
+
+ var utime = 0;
+ var stime = 0;
+
+ // Construct state based on proc details
+ var state = {
+ timestamp: ts,
+ closing: 0,
+ idle: 0,
+ writing: 0,
+ reading: 0,
+ keepalive: 0,
+ graceful: 0
+ }
+ for (var i in json.processes) {
+ var proc = json.processes[i];
+ if (proc.pid) {
+ state.closing += proc.workerStates.closing||0;
+ state.idle += proc.workerStates.idle||0;
+ state.writing += proc.workerStates.writing||0;
+ state.reading += proc.workerStates.reading||0;
+ state.keepalive += proc.workerStates.keepalive||0;
+ state.graceful += proc.workerStates.graceful||0;
+ utime += proc.utime;
+ stime += proc.stime;
+ }
+ }
+
+ // Push action state entry into action cache with timestamp
+ // Shift if more than 10 entries in cache
+ actionCache.push(state);
+ if (actionCache.length > maxRecords) {
+ actionCache.shift();
+ }
+
+ // construct array for QuokkaLines
+ var arr = [];
+ for (var i in actionCache) {
+ var el = actionCache[i];
+ if (json.mpm.type == 'event') {
+ arr.push([el.timestamp, el.closing, el.idle, el.writing, el.reading, el.graceful]);
+ } else {
+ arr.push([el.timestamp, el.keepalive, el.closing, el.idle, el.writing, el.reading, el.graceful]);
+ }
+ }
+ var states = ['Keepalive', 'Closing', 'Idle', 'Writing', 'Reading', 'Graceful']
+ if (json.mpm.type == 'event') {
+ states.shift();
+ if (document.getElementById('mpminfo')) {
+ document.getElementById('mpminfo').innerHTML = "(" + fn(parseInt(json.connections.idle)) + " connections in idle keepalive)";
+ }
+ }
+ // Draw action chart
+ quokkaLines("actions_div", states, arr, { lastsum: true, hires: true, nosum: true, stack: true, curve: true, title: "Thread states" } );
+
+
+ // Get traffic, figure out how much it was this time (0 if just started!)
+ var bytesThisTurn = 0;
+ var connectionsThisTurn = 0;
+ for (var i in json.processes) {
+ var proc = json.processes[i];
+ var pid = proc.pid
+ // if we haven't seen this proc before, ignore its bytes first time
+ if (!processes[pid]) {
+ processes[pid] = {
+ bytes: proc.bytes,
+ connections: proc.connections,
+ }
+ } else {
+ bytesThisTurn += proc.bytes - processes[pid].bytes;
+ if (pid) {
+ x = proc.connections - processes[pid].connections;
+ connectionsThisTurn += (x > 0) ? x : 0;
+ }
+ processes[pid].bytes = proc.bytes;
+ processes[pid].connections = proc.connections;
+ }
+ }
+
+ if (lastBytes == 0 ) {
+ bytesThisTurn = 0;
+ }
+ lastBytes = 1;
+
+ // Push a new element into cache, prune cache
+ var el = {
+ timestamp: ts,
+ bytes: bytesThisTurn/updateSpeed
+ };
+ trafficCache.push(el);
+ if (trafficCache.length > maxRecords) {
+ trafficCache.shift();
+ }
+
+ // construct array for QuokkaLines
+ arr = [];
+ for (var i in trafficCache) {
+ var el = trafficCache[i];
+ arr.push([el.timestamp, el.bytes]);
+ }
+ // Draw action chart
+ quokkaLines("traffic_div", ['Traffic'], arr, { traffic: true, hires: true, nosum: true, stack: true, curve: true, title: "Traffic per second" } );
+
+
+ // Get connections per second
+ // Push a new element into cache, prune cache
+ var el = {
+ timestamp: ts,
+ connections: (connectionsThisTurn+1)/updateSpeed
+ };
+ connectionCache.push(el);
+ if (connectionCache.length > maxRecords) {
+ connectionCache.shift();
+ }
+
+ // construct array for QuokkaLines
+ arr = [];
+ for (var i in connectionCache) {
+ var el = connectionCache[i];
+ arr.push([el.timestamp, el.connections]);
+ }
+ // Draw connection chart
+ quokkaLines("connection_div", ['Connections/sec'], arr, { traffic: false, hires: true, nosum: true, stack: true, curve: true, title: "Connections per second" } );
+
+
+ // Thread info
+ quokkaCircle("status_div", [
+ { title: 'Active', value: (json.mpm.threadsPerChild*json.mpm.activeServers)},
+ { title: 'Reserve', value: (json.mpm.threadsPerChild*(json.mpm.activeServers-json.mpm.maxServers))}
+ ],
+ { title: "Worker pool", hires: true});
+
+ // Idle vs active connections
+ var idlecons = json.connections.idle;
+ var activecons = json.connections.active;
+ quokkaCircle("idle_div", [
+ { title: 'Idle', value: idlecons},
+ { title: 'Active', value: activecons},
+ ],
+ { hires: true, title: "Idle vs active connections"});
+
+
+ // CPU info
+ while ( (stime+utime) > cpumax ) {
+ cpumax = cpumax * 2;
+ }
+
+ quokkaCircle("cpu_div", [
+ { title: 'Idle', value: (cpumax - stime - utime) / (cpumax/100)},
+ { title: 'System', value: stime/(cpumax/100)},
+ { title: 'User', value: utime/(cpumax/100)}
+ ],
+ { hires: true, title: "CPU usage", pct: true});
+
+
+
+
+
+
+ // General stats infobox
+ var gstats = document.getElementById('general_stats');
+ gstats.innerHTML = ''; // wipe the box
+
+ // Days since restart
+ var u_f = Math.floor(json.server.uptime/8640.0) / 10;
+ var u_d = Math.floor(json.server.uptime/86400);
+ var u_h = Math.floor((json.server.uptime%86400)/3600);
+ var u_m = Math.floor((json.server.uptime%3600)/60);
+ var u_s = Math.floor(json.server.uptime %60);
+ var str = u_d + " day" + (u_d != 1 ? "s, " : ", ") + u_h + " hour" + (u_h != 1 ? "s, " : ", ") + u_m + " minute" + (u_m != 1 ? "s" : "");
+ var ubox = document.createElement('div');
+ ubox.setAttribute("class", "statsbox");
+ ubox.innerHTML = "<span style='font-size: 2rem;'>" + u_f + " days</span><br/><i>since last (re)start.</i><br/><small>" + str;
+ gstats.appendChild(ubox);
+
+
+ // Bytes transferred in total
+ var MB = fnmb(json.server.bytes);
+ var KB = (json.server.bytes > 0) ? fnmb(json.server.bytes/json.server.connections) : 0;
+ var KBs = fnmb(json.server.bytes/json.server.uptime);
+ var mbbox = document.createElement('div');
+ mbbox.setAttribute("class", "statsbox");
+ mbbox.innerHTML = "<span style='font-size: 2rem;'>" + MB + "</span><br/><i>transferred in total.</i><br/><small>" + KBs + "/sec, " + KB + "/request";
+ gstats.appendChild(mbbox);
+
+ // connections in total
+ var cons = fn(json.server.connections);
+ var cps = Math.floor(json.server.connections/json.server.uptime*100)/100;
+ var conbox = document.createElement('div');
+ conbox.setAttribute("class", "statsbox");
+ conbox.innerHTML = "<span style='font-size: 2rem;'>" + cons + " conns</span><br/><i>since server started.</i><br/><small>" + cps + " requests per second";
+ gstats.appendChild(conbox);
+
+ // threads working
+ var tpc = json.mpm.threadsPerChild;
+ var activeThreads = fn(json.mpm.activeServers * json.mpm.threadsPerChild);
+ var maxThreads = json.mpm.maxServers * json.mpm.threadsPerChild;
+ var tbox = document.createElement('div');
+ tbox.setAttribute("class", "statsbox");
+ tbox.innerHTML = "<span style='font-size: 2rem;'>" + activeThreads + " threads</span><br/><i>currently at work (" + json.mpm.activeServers + "x" + tpc+" threads).</i><br/><small>" + maxThreads + " (" + json.mpm.maxServers + "x"+tpc+") threads allowed.";
+ gstats.appendChild(tbox);
+
+
+
+ window.setTimeout(waitTwo, updateSpeed*1000);
+
+ // resize pane
+ document.getElementById('leftpane').style.height = document.getElementById('wrapper').getBoundingClientRect().height + "px";
+
+ // Do we have extended info and module lists??
+ if (json.server.extended) document.getElementById('threads_button').style.display = 'block';
+ if (json.server.modules && json.server.modules.length > 0) {
+ var panel = document.getElementById('modules_breakdown');
+ var list = "<ul>";
+ for (var i in json.server.modules) {
+ var mod = json.server.modules[i];
+ list += "<li>" + mod + "</li>";
+ }
+ list += "</ul>";
+ panel.innerHTML = list;
+
+ document.getElementById('modules_button').style.display = 'block';
+ }
+
+
+ } else if (json === false) {
+ waitTwo();
+ }
+}
+
+function refreshThreads(json, state) {
+ var box = document.getElementById('threads_breakdown');
+ box.innerHTML = "";
+ for (var i in json.processes) {
+ var proc = json.processes[i];
+ var phtml = '<div style="color: #DDF">';
+ if (!proc.active) phtml = '<div title="this process is inactive" style="color: #999;">';
+ phtml += "<h3>Process " + i + ":</h3>";
+ phtml += "<b>PID:</b> " + (proc.pid||"None (not active)") + "<br/>";
+ if (proc.threads && proc.active) {
+ phtml += "<table style='width: 800px; color: #000;'><tr><th>Thread ID</th><th>Access count</th><th>Bytes served</th><th>Last Used</th><th>Last client</th><th>Last request</th></tr>";
+ for (var j in proc.threads) {
+ var thread = proc.threads[j];
+ thread.request = (thread.request||"(Unknown)").replace(/[<>]+/g, "");
+ phtml += "<tr><td>"+thread.thread+"</td><td>"+thread.count+"</td><td>"+thread.bytes+"</td><td>"+thread.last_used+"</td><td>"+thread.client+"</td><td>"+thread.request+"</td></tr>";
+ }
+ phtml += "</table>";
+ } else {
+ phtml += "<p>No thread information available</p>";
+ }
+ phtml += "</div>";
+ box.innerHTML += phtml;
+ }
+}
+
+function waitTwo() {
+ getAsync(location.href + "?view=json&rnd=" + Math.random(), null, refreshCharts)
+}
+
+ function showPanel(what) {
+ var items = ['dashboard','misc','threads','modules'];
+ for (var i in items) {
+ var item = items[i];
+ var btn = document.getElementById(item+'_button');
+ var panel = document.getElementById(item+'_panel');
+ if (item == what) {
+ btn.setAttribute("class", "btn active");
+ panel.style.display = 'block';
+ } else {
+ btn.setAttribute("class", "btn");
+ panel.style.display = 'none';
+ }
+ }
+
+ // special constructors
+ if (what == 'threads') {
+ getAsync(location.href + "?view=json&extended=true&rnd=" + Math.random(), null, refreshThreads)
+ }
+ }
+
+ function fn(num) {
+ num = num + "";
+ num = num.replace(/(\d)(\d{9})$/, '$1,$2');
+ num = num.replace(/(\d)(\d{6})$/, '$1,$2');
+ num = num.replace(/(\d)(\d{3})$/, '$1,$2');
+ return num;
+ }
+
+ function fnmb(num) {
+ var add = "bytes";
+ var dec = "";
+ var mul = 1;
+ if (num > 1024) { add = "KB"; mul= 1024; }
+ if (num > (1024*1024)) { add = "MB"; mul= 1024*1024; }
+ if (num > (1024*1024*1024)) { add = "GB"; mul= 1024*1024*1024; }
+ if (num > (1024*1024*1024*1024)) { add = "TB"; mul= 1024*1024*1024*1024; }
+ num = num / mul;
+ if (add != "bytes") {
+ dec = "." + Math.floor( (num - Math.floor(num)) * 100 );
+ }
+ return ( fn(Math.floor(num)) + dec + " " + add );
+ }
+
+ function sort(a,b){
+ last_col = -1;
+ var sort_reverse = false;
+ var sortWay = a.getAttribute("sort_" + b);
+ if (sortWay && sortWay == "forward") {
+ a.setAttribute("sort_" + b, "reverse");
+ sort_reverse = true;
+ }
+ else {
+ a.setAttribute("sort_" + b, "forward");
+ }
+ var c,d,e,f,g,h,i;
+ c=a.rows.length;
+ if(c<1){ return; }
+ d=a.rows[1].cells.length;
+ e=1;
+ var j=new Array(c);
+ f=0;
+ for(h=e;h<c;h++){
+ var k=new Array(d);
+ for(i=0;i<d;i++){
+ cell_text="";
+ cell_text=a.rows[h].cells[i].textContent;
+ if(cell_text===undefined){cell_text=a.rows[h].cells[i].innerText;}
+ k[i]=cell_text;
+ }
+ j[f++]=k;
+ }
+ var l=false;
+ var m,n;
+ if(b!=lastcol) lastseq="A";
+ else{
+ if(lastseq=="A") lastseq="D";
+ lastseq="A";
+ }
+
+ g=c-1;
+
+ for(h=0;h<g;h++){
+ l=false;
+ for(i=0;i<g-1;i++){
+ m=j[i];
+ n=j[i+1];
+ if(lastseq=="A"){
+ var gt = (m[b]>n[b]) ? true : false;
+ var lt = (m[b]<n[b]) ? true : false;
+ if (n[b].match(/^(\d+)$/)) { gt = parseInt(m[b], 10) > parseInt(n[b], 10) ? true : false; lt = parseInt(m[b], 10) < parseInt(n[b], 10) ? true : false; }
+ if (sort_reverse) {gt = (!gt); lt = (!lt);}
+ if(gt){
+ j[i+1]=m;
+ j[i]=n;
+ l=true;
+ }
+ }
+ else{
+ if(lt){
+ j[i+1]=m;
+ j[i]=n;
+ l=true;
+ }
+ }
+ }
+ if(l===false){
+ break;
+ }
+ }
+ f=e;
+ for(h=0;h<g;h++){
+ m=j[h];
+ for(i=0;i<d;i++){
+ if(a.rows[f].cells[i].innerText!==undefined){
+ a.rows[f].cells[i].innerText=m[i];
+ }
+ else{
+ a.rows[f].cells[i].textContent=m[i];
+ }
+ }
+ f++;
+ }
+ lastcol=b;
+ }
+
+
+ var CPUmax = 1000000;
+
+
+ var showing = false;
+ function showDetails() {
+ for (i=1; i < 1000; i++) {
+ var obj = document.getElementById("srv_" + i);
+ if (obj) {
+ if (showing) { obj.style.display = "none"; }
+ else { obj.style.display = "block"; }
+ }
+ }
+ var link = document.getElementById("show_link");
+ showing = (!showing);
+ if (showing) { link.innerHTML = "Hide thread information"; }
+ else { link.innerHTML = "Show thread information"; }
+ }
+
+ var showing_modules = false;
+ function show_modules() {
+
+ var obj = document.getElementById("modules");
+ if (obj) {
+ if (showing_modules) { obj.style.display = "none"; }
+ else { obj.style.display = "block"; }
+ }
+ var link = document.getElementById("show_modules_link");
+ showing_modules = (!showing_modules);
+ if (showing_modules) { link.innerHTML = "Hide loaded modules"; }
+ else { link.innerHTML = "Show loaded modules"; }
+ }
+]==]
+
+quokka_js = [==[
+/*
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Traffic shaper
+function quokka_fnmb(num) {
+ var add = "b";
+ var dec = "";
+ var mul = 1;
+ if (num > 1024) { add = "KB"; mul= 1024; }
+ if (num > (1024*1024)) { add = "MB"; mul= 1024*1024; }
+ if (num > (1024*1024*1024)) { add = "GB"; mul= 1024*1024*1024; }
+ if (num > (1024*1024*1024*1024)) { add = "TB"; mul= 1024*1024*1024*1024; }
+ num = num / mul;
+ if (add != "b" && num < 10) {
+ dec = "." + Math.floor( (num - Math.floor(num)) * 100 );
+ }
+ return ( Math.floor(num) + dec + " " + add );
+}
+
+// Hue, Saturation and Lightness to Red, Green and Blue:
+function quokka_internal_hsl2rgb (h,s,l)
+{
+ var min, sv, switcher, fract, vsf;
+ h = h % 1;
+ if (s > 1) s = 1;
+ if (l > 1) l = 1;
+ var v = (l <= 0.5) ? (l * (1 + s)) : (l + s - l * s);
+ if (v === 0)
+ return { r: 0, g: 0, b: 0 };
+
+ min = 2 * l - v;
+ sv = (v - min) / v;
+ var sh = (6 * h) % 6;
+ switcher = Math.floor(sh);
+ fract = sh - switcher;
+ vsf = v * sv * fract;
+
+ switch (switcher)
+ {
+ case 0: return { r: v, g: min + vsf, b: min };
+ case 1: return { r: v - vsf, g: v, b: min };
+ case 2: return { r: min, g: v, b: min + vsf };
+ case 3: return { r: min, g: v - vsf, b: v };
+ case 4: return { r: min + vsf, g: min, b: v };
+ case 5: return { r: v, g: min, b: v - vsf };
+ }
+ return {r:0, g:0, b: 0};
+}
+
+// RGB to Hex conversion
+function quokka_internal_rgb2hex(r, g, b) {
+ return "#" + ((1 << 24) + (Math.floor(r) << 16) + (Math.floor(g) << 8) + Math.floor(b)).toString(16).slice(1);
+}
+
+
+// Generate color list used for charts
+var colors = [];
+var rgbs = []
+var numColorRows = 6;
+var numColorColumns = 20;
+for (var x=0;x<numColorRows;x++) {
+ for (var y=0;y<numColorColumns;y++) {
+ var rnd = [[148, 221, 119], [0, 203, 171], [51, 167, 215] , [35, 160, 253], [218, 54, 188], [16, 171, 246], [110, 68, 206], [21, 49, 248], [142, 104, 210]][y]
+ var color = quokka_internal_hsl2rgb(y > 8 ? (Math.random()) : (rnd[0]/255), y > 8 ? (0.75+(y*0.05)) : (rnd[1]/255), y > 8 ? (0.42 + (y*0.05*(x/numColorRows))) : (0.1 + rnd[2]/512));
+
+ // Light (primary) color:
+ var hex = quokka_internal_rgb2hex(color.r*255, color.g*255, color.b*255);
+
+ // Darker variant for gradients:
+ var dhex = quokka_internal_rgb2hex(color.r*131, color.g*131, color.b*131);
+
+ // Medium variant for legends:
+ var mhex = quokka_internal_rgb2hex(color.r*200, color.g*200, color.b*200);
+
+ colors.push([hex, dhex, color, mhex]);
+ }
+}
+
+
+/* Function for drawing pie diagrams
+ * Example usage:
+ * quokkaCircle("canvasName", [ { title: 'ups', value: 30}, { title: 'downs', value: 70} ] );
+ */
+
+function quokkaCircle(id, tags, opts) {
+ // Get Canvas object and context
+ var canvas = document.getElementById(id);
+ var ctx=canvas.getContext("2d");
+
+ // Calculate the total value of the pie
+ var total = 0;
+ var k;
+ for (k in tags) {
+ tags[k].value = Math.abs(tags[k].value);
+ total += tags[k].value;
+ }
+
+
+
+ // Draw the empty pie
+ var begin = 0;
+ var stop = 0;
+ var radius = (canvas.height*0.75)/2;
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.beginPath();
+ ctx.shadowBlur = 6;
+ ctx.shadowOffsetX = 6;
+ ctx.shadowOffsetY = 6;
+ ctx.shadowColor = "#555";
+ ctx.lineWidth = (opts && opts.hires) ? 6 : 2;
+ ctx.strokeStyle = "#222";
+ ctx.arc((canvas.width-140)/2,canvas.height/2,radius, 0, Math.PI * 2);
+ ctx.closePath();
+ ctx.stroke();
+ ctx.fill();
+ ctx.shadowBlur = 0;
+ ctx.shadowOffsetY = 0;
+ ctx.shadowOffsetX = 0;
+
+
+ // Draw a title if set:
+ if (opts && opts.title) {
+ ctx.font= (opts && opts.hires) ? "28px Sans-Serif" : "15px Sans-Serif";
+ ctx.fillStyle = "#000000";
+ ctx.textAlign = "center";
+ ctx.fillText(opts.title,(canvas.width-140)/2, (opts && opts.hires) ? 30:15);
+ ctx.textAlign = "left";
+ }
+
+ ctx.beginPath();
+ var posY = 50;
+ var left = 120 + ((canvas.width-140)/2) + ((opts && opts.hires) ? 40 : 25)
+ for (k in tags) {
+ var val = tags[k].value;
+ stop = stop + (2 * Math.PI * (val / total));
+
+ // Make a pizza slice
+ ctx.beginPath();
+ ctx.lineCap = 'round';
+ ctx.arc((canvas.width-140)/2,canvas.height/2,radius,begin,stop);
+ ctx.lineTo((canvas.width-140)/2,canvas.height/2);
+ ctx.closePath();
+ ctx.lineWidth = 0;
+ ctx.stroke();
+
+ // Add color gradient
+ var grd=ctx.createLinearGradient(0,canvas.height*0.2,0,canvas.height);
+ grd.addColorStop(0,colors[k % colors.length][1]);
+ grd.addColorStop(1,colors[k % colors.length][0]);
+ ctx.fillStyle = grd;
+ ctx.fill();
+ begin = stop;
+
+ // Make color legend
+ ctx.fillRect(left, posY-((opts && opts.hires) ? 15 : 10), (opts && opts.hires) ? 14 : 7, (opts && opts.hires) ? 14 : 7);
+
+ // Add legend text
+ ctx.shadowColor = "rgba(0,0,0,0)"
+ ctx.font= (opts && opts.hires) ? "22px Sans-Serif" : "12px Sans-Serif";
+ ctx.fillStyle = "#000";
+ ctx.fillText(tags[k].title + " (" + Math.floor(val) + (opts && opts.pct ? "%" : "") + ")",left+20,posY);
+
+ posY += (opts && opts.hires) ? 28 : 14;
+ }
+
+}
+
+
+/* Function for drawing line charts
+ * Example usage:
+ * quokkaLines("myCanvas", ['Line a', 'Line b', 'Line c'], [ [x1,a1,b1,c1], [x2,a2,b2,c2], [x3,a3,b3,c3] ], { stacked: true, curve: false, title: "Some title" } );
+ */
+function quokkaLines(id, titles, values, options, sums) {
+ var canvas = document.getElementById(id);
+ var ctx=canvas.getContext("2d");
+ // clear the canvas first
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+
+
+
+ ctx.lineWidth = 0.25;
+ ctx.strokeStyle = "#000000";
+
+ var lwidth = 300;
+ var lheight = 75;
+ wspace = (options && options.hires) ? 110 : 55;
+ var rectwidth = canvas.width - lwidth - wspace;
+ var stack = options ? options.stack : false;
+ var curve = options ? options.curve : false;
+ var title = options ? options.title : null;
+ var spots = options ? options.points : false;
+ var noX = options ? options.nox : false;
+ var verts = options ? options.verts : true;
+ if (noX) {
+ lheight = 0;
+ }
+
+
+ // calc rectwidth if titles are large
+ var nlwidth = 0
+ for (var k in titles) {
+ ctx.font= (options && options.hires) ? "24px Sans-Serif" : "12px Sans-Serif";
+ ctx.fillStyle = "#00000";
+ var x = parseInt(k)
+ if (!noX) {
+ x = x + 1;
+ }
+ var sum = 0
+ for (var y in values) {
+ sum += values[y][x]
+ }
+ var t = titles[k] + (!options.nosum ? " (" + ((sums && sums[k]) ? sums[k] : sum.toFixed(0)) + ")" : "");
+ var w = ctx.measureText(t).width + 48;
+ if (w > lwidth && w > nlwidth) {
+ nlwidth = w
+ }
+ if (nlwidth > 0) {
+ rectwidth -= nlwidth - lwidth
+ lwidth = nlwidth
+ }
+ }
+
+ // Draw a border
+ ctx.lineWidth = 0.5;
+ ctx.strokeRect((wspace*0.75), 30, rectwidth, canvas.height - lheight - 40);
+
+ // Draw a title if set:
+ if (title != null) {
+ ctx.font= (options && options.hires) ? "24px Sans-Serif" : "15px Sans-Serif";
+ ctx.fillStyle = "#00000";
+ ctx.textAlign = "center";
+ ctx.fillText(title,rectwidth/2, 20);
+ }
+
+ // Draw legend
+ ctx.textAlign = "left";
+ var posY = 50;
+ for (var k in titles) {
+ var x = parseInt(k)
+ if (!noX) {
+ x = x + 1;
+ }
+ var sum = 0
+ for (var y in values) {
+ sum += values[y][x]
+ }
+
+ var title = titles[k] + (!options.nosum ? (" (" + ((sums && sums[k]) ? sums[k] : sum.toFixed(0)) + ")") : "");
+ if (options && options.lastsum) {
+ title = titles[k] + " (" + values[values.length-1][x].toFixed(0) + ")";
+ }
+ ctx.fillStyle = colors[k % colors.length][3];
+ ctx.fillRect(wspace + rectwidth + 75 , posY-((options && options.hires) ? 18:9), (options && options.hires) ? 20:10, (options && options.hires) ?20:10);
+
+ // Add legend text
+ ctx.font= (options && options.hires) ? "24px Sans-Serif" : "14px Sans-Serif";
+ ctx.fillStyle = "#00000";
+ ctx.fillText(title,canvas.width - lwidth + ((options && options.hires) ? 100:60), posY);
+
+ posY += (options && options.hires) ? 30:15;
+ }
+
+ // Find max and min
+ var max = null;
+ var min = 0;
+ var stacked = null;
+ for (x in values) {
+ var s = 0;
+ for (y in values[x]) {
+ if (y > 0 || noX) {
+ s += values[x][y];
+ if (max === null || max < values[x][y]) {
+ max = values[x][y];
+ }
+ if (min === null || min > values[x][y]) {
+ min = values[x][y];
+ }
+ }
+ }
+ if (stacked === null || stacked < s) {
+ stacked = s;
+ }
+ }
+ if (min == max) max++;
+ if (stack) {
+ min = 0;
+ max = stacked;
+ }
+
+
+ // Set number of lines to draw and each step
+ var numLines = 5;
+ var step = (max-min) / (numLines+1);
+
+ // Prettify the max value so steps aren't ugly numbers
+ if (step %1 != 0) {
+ step = (Math.round(step+0.5));
+ max = step * (numLines+1);
+ }
+
+ // Draw horizontal lines
+
+ for (x = -1; x <= numLines; x++) {
+ ctx.beginPath();
+ var y = 30 + (((canvas.height-40-lheight) / (numLines+1)) * (x+1));
+ ctx.moveTo(wspace*0.75, y);
+ ctx.lineTo(wspace*0.75 + rectwidth, y);
+ ctx.lineWidth = 0.25;
+ ctx.stroke();
+
+ // Add values
+ ctx.font= (options && options.hires) ? "20px Sans-Serif" : "12px Sans-Serif";
+ ctx.fillStyle = "#000000";
+
+ var val = Math.round( ((max-min) - (step*(x+1))) );
+ if (options && options.traffic) {
+ val = quokka_fnmb(val);
+ }
+ ctx.textAlign = "left";
+ ctx.fillText( val,canvas.width - lwidth - 20, y+8);
+ ctx.textAlign = "right";
+ ctx.fillText( val,wspace-32, y+8);
+ ctx.closePath();
+ }
+
+
+
+ // Draw vertical lines
+ var sx = 1
+ var numLines = values.length-1;
+ var step = (canvas.width - lwidth - wspace*0.75) / values.length;
+ while (step < 24) {
+ step *= 2
+ sx *= 2
+ }
+
+
+ if (verts) {
+ ctx.beginPath();
+ for (var x = 1; x < values.length; x++) {
+ if (x % sx == 0) {
+ var y = (wspace*0.75) + (step * (x/sx));
+ ctx.moveTo(y, 30);
+ ctx.lineTo(y, canvas.height - 10 - lheight);
+ ctx.lineWidth = 0.25;
+ ctx.stroke();
+ }
+ }
+ ctx.closePath();
+ }
+
+
+
+ // Some pre-calculations of steps
+ var step = (rectwidth) / (values.length > 1 ? values.length-1:1);
+
+ // Draw X values if noX isn't set:
+ if (noX != true) {
+ ctx.beginPath();
+ for (var i = 0; i < values.length; i++) {
+ zz = 1
+ var x = (wspace*0.75) + ((step) * i);
+ var y = canvas.height - lheight + 5;
+ if (i % sx == 0) {
+ ctx.translate(x, y);
+ ctx.moveTo(0,0);
+ ctx.lineTo(0,-15);
+ ctx.stroke();
+ ctx.rotate(45*Math.PI/180);
+ ctx.textAlign = "left";
+ var val = values[i][0];
+ if (val.constructor.toString().match("Date()")) {
+ val = val.toDateString();
+ }
+ ctx.fillText(val.toString(), 0, 0);
+ ctx.rotate(-45*Math.PI/180);
+ ctx.translate(-x,-y);
+ }
+ }
+ ctx.closePath();
+
+ }
+
+
+
+
+ // Draw each line
+ var stacks = [];
+ var pstacks = [];
+ for (k in values) { if (k > 0) { stacks[k] = 0; pstacks[k] = canvas.height - 40 - lheight; }}
+
+ for (k in titles) {
+ var maxY = 0, minY = 99999;
+ ctx.beginPath();
+ var color = colors[k % colors.length][0];
+ var f = parseInt(k) + 1;
+ if (noX) {
+ f = parseInt(k);
+ }
+ var value = values[0][f];
+ var step = rectwidth / numLines;
+ var x = (wspace*0.75);
+ var y = (canvas.height - 10 - lheight) - (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
+ var py = y;
+ if (stack) {
+ stacks[0] = stacks[0] ? stacks[0] : 0
+ y -= stacks[0];
+ pstacks[0] = stacks[0];
+ stacks[0] += (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
+ }
+
+ // Draw line
+ ctx.moveTo(x, y);
+ var pvalY = y;
+ var pvalX = x;
+ for (var i in values) {
+ if (i > 0) {
+ x = (wspace*0.75) + (step*i);
+ var f = parseInt(k) + 1;
+ if (noX == true) {
+ f = parseInt(k);
+ }
+ value = values[i][f];
+ y = (canvas.height - 10 - lheight) - (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
+ if (stack) {
+ y -= stacks[i];
+ pstacks[i] = stacks[i];
+ stacks[i] += (((value-min) / (max-min)) * (canvas.height - 40- lheight));
+ }
+ if (y > maxY) maxY = y;
+ if (y < minY) minY = y;
+ // Draw curved lines??
+ /* We'll do: (x1,y1)-----(x1.5,y1)
+ * |
+ * (x1.5,y2)-----(x2,y2)
+ * with a quadratic beizer thingy
+ */
+ if (curve) {
+ ctx.bezierCurveTo((pvalX + x) / 2, pvalY, (pvalX + x) / 2, y, x, y);
+ pvalX = x;
+ pvalY = y;
+ }
+ // Nope, just draw straight lines
+ else {
+ ctx.lineTo(x, y);
+ }
+ if (spots) {
+ ctx.fillStyle = color;
+ ctx.translate(x-2, y-2);
+ ctx.rotate(-45*Math.PI/180);
+ ctx.fillRect(-2,1,4,4);
+ ctx.rotate(45*Math.PI/180);
+ ctx.translate(-x+2, -y+2);
+ }
+ }
+ }
+
+ ctx.lineWidth = 4;
+ ctx.strokeStyle = color;
+ ctx.stroke();
+
+
+ if (minY == maxY) maxY++;
+
+ // Draw stack area
+ if (stack) {
+ ctx.globalAlpha = 0.65;
+ for (i in values) {
+ if (i > 0) {
+ var f = parseInt(k) + 1;
+ if (noX == true) {
+ f = parseInt(k);
+ }
+ x = (wspace*0.75) + (step*i);
+ value = values[i][f];
+ y = (canvas.height - 10 - lheight) - (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
+ y -= stacks[i];
+ }
+ }
+ var pvalY = y;
+ var pvalX = x;
+ if (y > maxY) maxY = y;
+ if (y < minY) minY = y;
+ for (i in values) {
+ var l = values.length - i - 1;
+ x = (wspace*0.75) + (step*l);
+ y = canvas.height - 10 - lheight - pstacks[l];
+ if (y > maxY) maxY = y;
+ if (y < minY) minY = y;
+ if (curve) {
+ ctx.bezierCurveTo((pvalX + x) / 2, pvalY, (pvalX + x) / 2, y, x, y);
+ pvalX = x;
+ pvalY = y;
+ }
+ else {
+ ctx.lineTo(x, y);
+ }
+ }
+ ctx.lineTo((wspace*0.75), py - pstacks[0]);
+ ctx.lineWidth = 0;
+ var grad = ctx.createLinearGradient(0, minY, 0, maxY);
+ grad.addColorStop(0.25, colors[k % colors.length][0])
+ grad.addColorStop(1, colors[k % colors.length][1])
+ ctx.strokeStyle = colors[k % colors.length][0];
+ ctx.fillStyle = grad;
+ ctx.fill();
+ ctx.fillStyle = "#000"
+ ctx.strokeStyle = "#000"
+ ctx.globalAlpha = 1;
+ }
+ ctx.closePath();
+ }
+
+ // draw feather
+ base_image = new Image();
+ base_image.src = 'data:image/png;base64,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';
+ base_image.onload = function(){
+ ctx.globalAlpha = 0.15
+ ctx.drawImage(base_image, (canvas.width/2) - 64 - (lwidth/2), (canvas.height/2) - 128);
+ ctx.globalAlpha = 1
+ }
+}
+
+
+
+/* Function for drawing line charts
+ * Example usage:
+ * quokkaLines("myCanvas", ['Line a', 'Line b', 'Line c'], [ [x1,a1,b1,c1], [x2,a2,b2,c2], [x3,a3,b3,c3] ], { stacked: true, curve: false, title: "Some title" } );
+ */
+function quokkaBars(id, titles, values, options) {
+ var canvas = document.getElementById(id);
+ var ctx=canvas.getContext("2d");
+ // clear the canvas first
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ var lwidth = 150;
+ var lheight = 75;
+ var stack = options ? options.stack : false;
+ var astack = options ? options.astack : false;
+ var curve = options ? options.curve : false;
+ var title = options ? options.title : null;
+ var noX = options ? options.nox : false;
+ var verts = options ? options.verts : true;
+ if (noX) {
+ lheight = 0;
+ }
+
+
+
+ // Draw a border
+ ctx.lineWidth = 0.5;
+ ctx.strokeRect(25, 30, canvas.width - lwidth - 40, canvas.height - lheight - 40);
+
+ // Draw a title if set:
+ if (title != null) {
+ ctx.font="15px Arial";
+ ctx.fillStyle = "#000";
+ ctx.textAlign = "center";
+ ctx.fillText(title,(canvas.width-lwidth)/2, 15);
+ }
+
+ // Draw legend
+ ctx.textAlign = "left";
+ var posY = 50;
+ for (var k in titles) {
+ var x = parseInt(k)
+ if (!noX) {
+ x = x + 1;
+ }
+ var title = titles[k];
+ if (title && title.length > 0) {
+ ctx.fillStyle = colors[k % colors.length][0];
+ ctx.fillRect(canvas.width - lwidth + 20, posY-10, 10, 10);
+
+ // Add legend text
+ ctx.font="12px Arial";
+ ctx.fillStyle = "#000";
+ ctx.fillText(title,canvas.width - lwidth + 40, posY);
+
+ posY += 15;
+ }
+
+
+ }
+
+ // Find max and min
+ var max = null;
+ var min = 0;
+ var stacked = null;
+ for (x in values) {
+ var s = 0;
+ for (y in values[x]) {
+ if (y > 0 || noX) {
+ s += values[x][y];
+ if (max == null || max < values[x][y]) {
+ max = values[x][y];
+ }
+ if (min == null || min > values[x][y]) {
+ min = values[x][y];
+ }
+ }
+ }
+ if (stacked == null || stacked < s) {
+ stacked = s;
+ }
+ }
+ if (min == max) {
+ max++;
+ }
+ if (stack) {
+ min = 0;
+ max = stacked;
+ }
+
+
+ // Set number of lines to draw and each step
+ var numLines = 5;
+ var step = (max-min) / (numLines+1);
+
+ // Prettify the max value so steps aren't ugly numbers
+ if (step %1 != 0) {
+ step = (Math.round(step+0.5));
+ max = step * (numLines+1);
+ }
+
+ // Draw horizontal lines
+ for (x = numLines; x >= 0; x--) {
+
+ var y = 30 + (((canvas.height-40-lheight) / (numLines+1)) * (x+1));
+ ctx.moveTo(25, y);
+ ctx.lineTo(canvas.width - lwidth - 15, y);
+ ctx.lineWidth = 0.25;
+ ctx.stroke();
+
+ // Add values
+ ctx.font="10px Arial";
+ ctx.fillStyle = "#000";
+ ctx.textAlign = "right";
+ ctx.fillText( Math.round( ((max-min) - (step*(x+1))) * 100 ) / 100,canvas.width - lwidth + 12, y-4);
+ ctx.fillText( Math.round( ((max-min) - (step*(x+1))) * 100 ) / 100,20, y-4);
+ }
+
+
+ // Draw vertical lines
+ var sx = 1
+ var numLines = values.length-1;
+ var step = (canvas.width - lwidth - 40) / values.length;
+ while (step < 24) {
+ step *= 2
+ sx *= 2
+ }
+
+
+ if (verts) {
+ ctx.beginPath();
+ for (var x = 1; x < values.length; x++) {
+ if (x % sx == 0) {
+ var y = 35 + (step * (x/sx));
+ ctx.moveTo(y, 30);
+ ctx.lineTo(y, canvas.height - 10 - lheight);
+ ctx.lineWidth = 0.25;
+ ctx.stroke();
+ }
+ }
+ }
+
+
+
+ // Some pre-calculations of steps
+ var step = (canvas.width - lwidth - 48) / values.length;
+ var smallstep = (step / titles.length) - 2;
+
+ // Draw X values if noX isn't set:
+ if (noX != true) {
+ ctx.beginPath();
+ for (var i = 0; i < values.length; i++) {
+ smallstep = (step / (values[i].length-1)) - 2;
+ zz = 1
+ var x = 35 + ((step) * i);
+ var y = canvas.height - lheight + 5;
+ if (i % sx == 0) {
+ ctx.translate(x, y);
+ ctx.moveTo(0,0);
+ ctx.lineTo(0,-15);
+ ctx.stroke();
+ ctx.rotate(45*Math.PI/180);
+ ctx.textAlign = "left";
+ var val = values[i][0];
+ if (val.constructor.toString().match("Date()")) {
+ val = val.toDateString();
+ }
+ ctx.fillText(val.toString(), 0, 0);
+ ctx.rotate(-45*Math.PI/180);
+ ctx.translate(-x,-y);
+ }
+ }
+
+ }
+
+
+
+
+ // Draw each line
+ var stacks = [];
+ var pstacks = [];
+
+ for (k in values) {
+ smallstep = (step / (values[k].length)) - 2;
+ stacks[k] = 0;
+ pstacks[k] = canvas.height - 40 - lheight;
+ var beginX = 0;
+ for (i in values[k]) {
+ if (i > 0 || noX) {
+ var z = parseInt(i);
+ var zz = z;
+ if (!noX) {
+ z = parseInt(i) + 1;
+ zz = z - 2;
+ if (z > values[k].length) {
+ break;
+ }
+ }
+ var value = values[k][i];
+ var title = titles[i];
+ var color = colors[zz % colors.length][1];
+ var fcolor = colors[zz % colors.length][2];
+ if (values[k][2] && values[k][2].toString().match(/^#.+$/)) {
+ color = values[k][2]
+ fcolor = values[k][2]
+ smallstep = (step / (values[k].length-2)) - 2;
+ }
+ var x = ((step) * k) + ((smallstep+2) * zz) + 5;
+ var y = canvas.height - 10 - lheight;
+ var mdiff = (max-min);
+ mdiff = (mdiff == 0) ? 1 : mdiff;
+ var height = ((canvas.height - 40 - lheight) / (mdiff)) * value * -1;
+ var width = smallstep - 2;
+ if (width <= 1) {
+ width = 1
+ }
+ if (stack) {
+ width = step - 10;
+ y -= stacks[k];
+ stacks[k] -= height;
+ x = (step * k) + 4;
+ if (astack) {
+ y = canvas.height - 10 - lheight;
+ }
+ }
+
+
+ // Draw bar
+ ctx.beginPath();
+ ctx.lineWidth = 2;
+ ctx.strokeStyle = color;
+ ctx.strokeRect(27 + x, y, width, height);
+ var alpha = 0.75
+ if (fcolor.r) {
+ ctx.fillStyle = 'rgba('+ [parseInt(fcolor.r*255),parseInt(fcolor.g*255),parseInt(fcolor.b*255),alpha].join(",") + ')';
+ } else {
+ ctx.fillStyle = fcolor;
+ }
+ ctx.fillRect(27 + x, y, width, height);
+
+ }
+ }
+
+
+ }
+}
+
+
+]==]
+
+
+status_css = [[
+ html {
+ font-size: 14px;
+ position: relative;
+ background: #272B30;
+ }
+
+ body {
+ background-color: #272B30;
+ color: #000;
+ margin: 0 auto;
+ min-height: 100%;
+ font-family: Arial, Helvetica, sans-serif;
+ font-weight: normal;
+ }
+
+ .navbarLeft {
+ background: linear-gradient(to bottom, #F8A900 0%,#D88900 100%);
+ width: 200px;
+ height: 30px;
+ padding-top: 2px;
+ font-size: 1.35rem;
+ color: #FFF;
+ border-bottom: 2px solid #000;
+ float: left;
+ text-align: center;
+ }
+
+ .navbarRight {
+ background: linear-gradient(to bottom, #EFEFEF 0%,#EEE 100%);
+ width: calc(100% - 240px);
+ height: 28px;
+ color: #333;
+ border-bottom: 2px solid #000;
+ float: left;
+ font-size: 1.3rem;
+ padding-top: 4px;
+ text-align: left;
+ padding-left: 40px;
+ }
+
+ .wrapper {
+ width: 100%;
+ float: left;
+ background: #33363F;
+ min-height: calc(100% - 80px);
+ position: relative;
+ }
+
+ .serverinfo {
+ float: left;
+ width: 200px;
+ height: calc(100% - 34px);
+ background: #293D4C;
+ }
+
+ .skey {
+ background: rgba(30,30,30,0.3);
+ color: #C6E7FF;
+ font-weight: bold;
+ padding: 2px;
+ }
+
+ .sval {
+ padding: 2px;
+ background: rgba(30,30,30,0.3);
+ color: #FFF;
+ font-size: 0.8rem;
+ border-bottom: 1px solid rgba(200,200,200,0.2);
+ }
+
+ .charts {
+ padding: 0px;
+ width: calc(100% - 220px);
+ max-width: 1000px;
+ min-height: 100%;
+ margin: 0px auto;
+ position: relative;
+ float: left;
+ margin-left: 20px;
+ }
+
+ pre, code {
+ font-family: "Courier New", Courier, monospace;
+ }
+
+ strong {
+ font-weight: bold;
+ }
+
+ q, em, var {
+ font-style: italic;
+ }
+ /* h1 */
+ /* ====================== */
+ h1 {
+ padding: 0.2em;
+ margin: 0;
+ border: 1px solid #405871;
+ background-color: inherit;
+ color: #036;
+ text-decoration: none;
+ font-size: 22px;
+ font-weight: bold;
+ }
+
+ /* h2 */
+ /* ====================== */
+ h2 {
+ padding: 0.2em 0 0.2em 0.7em;
+ margin: 0 0 0.5em 0;
+ text-decoration: none;
+ font-size: 18px;
+ font-weight: bold;
+ text-align: center;
+ }
+
+ #modules {
+ margin-top:20px;
+ display:none;
+ width:400px;
+ }
+
+ .servers {
+
+ width: 1244px;
+ background: #EEE;
+ }
+
+ tr:nth-child(odd) {
+ background: #F6F6F6;
+ }
+ tr:nth-child(even) {
+ background: #EBEBEB;
+ }
+ td {
+ padding: 2px;
+ }
+ table {
+ border: 1px solid #333;
+ padding: 0px;
+ margin: 5px;
+ min-width: 360px;
+ background: #999;
+ font-size: 0.8rem;
+ }
+
+ canvas {
+ background: #FFF;
+ margin: 3px;
+ text-align: center;
+ padding: 2px;
+ border-radius: 10px;
+ border: 1px solid #999;
+ }
+
+ .canvas_wide {
+ position: relative;
+ width: 65%;
+ }
+ .canvas_narrow {
+ position: relative;
+ width: 27%;
+ }
+
+ a {
+ color: #FFA;
+ }
+
+ .statsbox {
+ border-radius: 3px;
+ background: #3C3E47;
+ min-width: 150px;
+ height: 60px;
+ float: left;
+ margin: 15px;
+ padding: 10px;
+ }
+
+ .btn {
+ background: linear-gradient(to bottom, #72ca72 0%,#55bf55 100%);
+ border-radius: 5px;
+ color: #FFF;
+ text-decoration: none;
+ padding-top: 6px;
+ padding-bottom: 6px;
+ padding-left: 3px;
+ padding-right: 3px;
+ font-weight: bold;
+ text-shadow: 1px 1px rgba(0,0,0,0.4);
+ margin: 12px;
+ float: left;
+ clear: none;
+ }
+
+ .infobox_wrapper {
+ float: left;
+ min-width: 200px;
+ margin: 10px;
+ }
+ .infobox_title {
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ background: #FAB227;
+ color: #FFF;
+ border: 2px solid #FAB227;
+ border-bottom: none;
+ font-weight: bold;
+ text-align: center;
+ width: 100%;
+ }
+ .infobox {
+ background: #222222;
+ border: 2px solid #FAB227;
+ border-top: none;
+ color: #EFEFEF;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ float: left;
+ width: 100%;
+
+ }
+
+
+ .serverinfo ul {
+ margin: 0px;
+ padding: 0px;
+ margin-top: 20px;
+ list-style: none;
+ }
+
+ .serverinfo ul li .btn {
+ width: calc(100% - 8px);
+ margin: 0px;
+ border: 0px;
+ border-radius: 0px;
+ padding: 0px;
+ padding-top: 8px;
+ padding-left: 8px;
+ height: 24px;
+ background: rgba(0,0,0,0.2);
+ border-bottom: 1px solid rgba(100,100,100,0.3);
+ }
+
+ .serverinfo ul li:nth-child(1) {
+ border-top: 1px solid rgba(100,100,100,0.3);
+ }
+ .serverinfo ul li .btn.active {
+ background: rgba(30,30,50,0.2);
+ border-left: 4px solid #27FAB2;
+ padding-left: 4px;
+ color: #FFE;
+ }
+
+ .serverinfo ul li .btn:hover {
+ background: rgba(50,50,50,0.15);
+ border-left: 4px solid #FAB227;
+ padding-left: 4px;
+ color: #FFE;
+ }
+]]