--[[ 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 = [[

Don't be alarmed - this page is here for a reason!

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.

]] 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 [[ Module information ]] r:puts( ("

Details for module %s

\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( ("%s: %s
\n"):format(r:escape_html(k), v)) end end -- HTML tail r:puts[[ ]] 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 ( ([=[ Server status for %s
%s
Quick Stats
Charts
]=]):format( r.server_name, r.banner, r.server_name, show_warning and warning_banner or "" ) ); -- HTML tail r:puts[[ ]] 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('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')) 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 += "Server version: " + json.server.version + "
"; gs.innerHTML += "Server built: " + json.server.built + "
"; gs.innerHTML += "Server MPM: " + json.mpm.type + "
"; // 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 = "" + u_f + " days
since last (re)start.
" + 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 = "" + MB + "
transferred in total.
" + 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 = "" + cons + " conns
since server started.
" + 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 = "" + activeThreads + " threads
currently at work (" + json.mpm.activeServers + "x" + tpc+" threads).
" + 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 = "
    "; for (var i in json.server.modules) { var mod = json.server.modules[i]; list += "
  • " + mod + "
  • "; } list += "
"; 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 = '
'; if (!proc.active) phtml = '
'; phtml += "

Process " + i + ":

"; phtml += "PID: " + (proc.pid||"None (not active)") + "
"; if (proc.threads && proc.active) { phtml += ""; for (var j in proc.threads) { var thread = proc.threads[j]; thread.request = (thread.request||"(Unknown)").replace(/[<>]+/g, ""); phtml += ""; } phtml += "
Thread IDAccess countBytes servedLast UsedLast clientLast request
"+thread.thread+""+thread.count+""+thread.bytes+""+thread.last_used+""+thread.client+""+thread.request+"
"; } else { phtml += "

No thread information available

"; } phtml += "
"; 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;hn[b]) ? true : false; var lt = (m[b] 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 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 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; } ]]