Initial commit

This commit is contained in:
Mikko Ahlroth 2013-07-18 11:53:26 +03:00
commit 75b3296f2d
6 changed files with 802 additions and 0 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright © 2013 Mikko Ahlroth
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

35
README.md Normal file
View file

@ -0,0 +1,35 @@
Sysmon-ws is a simple Python WebSocket server which broadcasts system
information to all listening clients. It also includes a small HTML5 UI which
displays the information in a somewhat sensible manner. I pieced this together
one night when inspiration hit, so the code is written in a quick and dirty
manner.
Sysmon-ws uses 3rd party code:
* AutoBahnPython WebSocketServer from https://github.com/tavendo/AutobahnPython/blob/master/examples/websocket/broadcast/server.py
* jquery-graceful-websocket from https://code.google.com/p/jquery-graceful-websocket/
### Installation
Clone the files to a directory, create a virtualenv if you like and then
install libs with pip:
pip install -r requirements.txt
### Usage
Usage example from my server (I use a virtualenv for it):
[nicd@nytsoi ~]$ cd /srv/sysmon-ws
[nicd@nytsoi sysmon-ws]$ . venv-sysmon-ws/bin/activate
(venv-sysmon-ws)[nicd@nytsoi sysmon-ws]$ python server.py >/dev/null
### License
* Original AutoBahnPython code in server.py is licensed with Apache 2.0, read
server.py for more details.
* jquery-graceful-websocket (ws.js) is licensed with the MIT License.
* My code (index.html and modifications in server.py) is licensed with the
MIT License. See LICENSE for more details.

411
htmlui/index.html Normal file
View file

@ -0,0 +1,411 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>nytsoi.net system monitor</title>
<meta charset="utf-8" />
<style type="text/css">
html, body {
background-color: #123456;
margin: 0px;
padding: 0px;
}
#container {
color: #abcdef;
font-size: 14px;
font-family: monospace;
line-height: 1;
padding: 0px 10px;
margin: 0px auto;
width: 1100px;
}
#container h1, h2, h3, h4, h5, h6 {
padding: 10px;
margin: 0px;
font-weight: normal;
text-align: left;
}
#container h1 {
font-size: 16px;
float: left;
}
#container h2, h3, h4, h5, h6 {
font-size: 14px;
}
#container, #container>div {
float: left;
}
#cpu {
/*width: 48%;*/
clear: left;
}
#mem {
/*width: 48%;*/
}
#virt, #swap {
white-space: pre;
}
#cpu_times, #io {
/*width: 48%;*/
}
.t {
border-top: 1px solid #abcdef;
}
.r {
border-right: 1px solid #abcdef;
}
.b {
border-bottom: 1px solid #abcdef;
}
.l {
border-left: 1px solid #abcdef;
}
.p {
padding: 10px;
}
.np {
padding: 0px;
}
#cpu_time_legend, #cpu_time_values, #cpu_time_bars, #io_legend, #io_values {
white-space: pre;
float: left;
}
.cpu {
white-space: pre;
border-top: 1px solid #abcdef;
}
.cpu:first-of-type {
border-top: none;
}
</style>
</head>
<body>
<div id="container">
<h1><span id="connecting"></span>nytsoi.net system monitor
<span id="version"></span>
| Uptime: <span id="uptime">?</span>
| <span id="users">?</span> users, <span id="processes">?</span> processes
</h1>
<div id="cpu" class="r t l b">
<!--<h2 class="b">CPU</h2>-->
</div>
<div id="mem" class="t r">
<div id="virt" class="p b"></div>
<div id="swap" class="p"></div>
</div>
<div id="cpu_times" class="r t b">
<h2 class="b">CPU times                                                   </h2>
<div id="cpu_time_legend" class="p">user
system
idle
nice
iowait
irq
softirq
steal
guest
guest_nice</div>
<div id="cpu_time_values" class="p"></div>
<div id="cpu_time_bars" class="p"></div>
</div>
<div id="io" class="r b">
<h2 class="b">I/O                                                         </h2>
<div id="io_legend" class="p">Network
Bytes in
Bytes out
Disk
Reads
Writes</div>
<div id="io_values" class="p"></div>
</div>
</div>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
<script src="ws.js"></script>
<script type="text/javascript">
var connect_chars = "·oOo· ";
var connect_index = -1;
var connecting = true;
var im_hiding_behind_my_facebokk = false;
var bar_char = "#";
var cpu_bar_length = 55;
var cpu_bar_height = 15;
var cpu_bars = [];
var cpu_amount = 0;
var cpu_time_bar_length = 40;
var mem_bar_length = 55;
var swap_bar_length = mem_bar_length;
var boot_time = 0;
var version = "";
var io_net_in = 0;
var io_net_out = 0;
var io_disk_in = 0;
var io_disk_out = 0;
String.prototype.repeat = function(num)
{
num = Math.round(num);
if (num <= 0) {
return "";
}
return new Array(num + 1).join(this);
};
function pad(str, length, filler) {
if (typeof(filler) === "undefined") {
filler = " ";
}
str = String(str);
if (str.length >= length) {
return str;
}
else {
return str + filler.repeat(length - str.length);
}
}
function draw_connecting() {
if (connecting) {
++connect_index;
if (connect_index >= connect_chars.length) {
connect_index = 0;
}
$('#connecting').text(connect_chars[connect_index] + " ");
im_hiding_behind_my_facebokk = false;
}
else if (!im_hiding_behind_my_facebokk) {
$('#connecting').hide();
im_hiding_behind_my_facebokk = true;
}
current_time = (new Date).getTime() / 1000;
interval = current_time - boot_time;
days = Math.floor(interval / 86400);
hours = Math.floor((interval - (days * 86400)) / 3600);
minutes = Math.floor((interval - (days * 86400) - (hours * 3600)) / 60);
seconds = Math.floor(interval - (days * 86400) - (hours * 3600) - (minutes * 60));
$('#uptime').text("" + days + "d " + hours + "h " + minutes + "m " + seconds + "s");
}
function bar(percent, max_length) {
var bar_str = bar_char.repeat(max_length * (percent / 100));
return pad(bar_str, max_length);
}
function hb(bytes, round) {
if (typeof(round) === "undefined") {
round = 2;
}
if (bytes <= 0) {
return "0 B";
}
var kilo = 1000;
var prefixes = [
[Math.pow(kilo, 4), 'T'],
[Math.pow(kilo, 3), 'G'],
[Math.pow(kilo, 2), 'M'],
[kilo, 'k']
];
for (var i = 0; i < prefixes.length; ++i) {
if (bytes >= prefixes[i][0]) {
return "" + (bytes / prefixes[i][0]).toFixed(2) + " " + prefixes[i][1] + "B";
}
}
return "" + bytes + " B";
}
function connect() {
return $.gracefulWebSocket("ws://sysmon-ws.nytsoi.net");
}
setInterval(draw_connecting, 1000);
var ws = connect();
ws.onopen = function() {
connecting = false;
};
ws.onclose = function() {
connecting = true;
ws = connect();
};
ws.onerror = ws.onclose;
ws.onmessage = function (event) {
var json = $.parseJSON(event.data);
if (json.boot !== undefined) {
boot_time = json.boot;
cpu_amount = json.cpus;
version = json.version;
$('#version').text(version);
for (var i = 0; i < cpu_amount; ++i) {
cpu_bars.push([]);
new_div = $('<div />');
new_div.addClass("cpu p");
new_div.attr({id: "cpu" + i});
$('#cpu').append(new_div);
}
}
else {
// CPU BARS
var cpu_bar_chars = [];
for (var cpu = 0; cpu < cpu_amount; ++cpu) {
new_val = json.cpu[cpu];
var cbar = cpu_bars[cpu];
var chars = [];
cbar.push(new_val);
if (cbar.length > cpu_bar_length) {
cbar.shift();
}
for (var i = 0; i < cbar.length; ++i) {
chars.push(bar(cbar[i], cpu_bar_height));
}
var output = "";
var legend = "CPU" + cpu + " " + Math.round(new_val) + "%";
// Draw bar chart by transposing
for (var y = 0; y < cpu_bar_height; ++y) {
for (var x = 0; x < cpu_bar_length; ++x) {
// Draw legend
if (y == 0 && x < legend.length) {
output += legend[x];
}
else {
var cell = undefined;
var column = chars[x];
if (column !== undefined) {
cell = column[cpu_bar_height-y-1];
}
if (cell === undefined) {
output += " ";
}
else {
output += cell;
}
}
}
output += "\n";
}
$('#cpu' + cpu).text(output);
}
// CPU TIMES
var ct = json.cpu_times;
$('#cpu_time_values').text("" +
pad(Math.round(ct.user), 3) + "\n" +
pad(Math.round(ct.system), 3) + "\n" +
pad(Math.round(ct.idle), 3) + "\n" +
pad(Math.round(ct.nice), 3) + "\n" +
pad(Math.round(ct.iowait), 3) + "\n" +
pad(Math.round(ct.irq), 3) + "\n" +
pad(Math.round(ct.softirq), 3) + "\n" +
pad(Math.round(ct.steal), 3) + "\n" +
pad(Math.round(ct.guest), 3) + "\n" +
pad(Math.round(ct.guest_nice), 3)
);
$('#cpu_time_bars').text("" +
bar(ct.user, cpu_time_bar_length) + "\n" +
bar(ct.system, cpu_time_bar_length) + "\n" +
bar(ct.idle, cpu_time_bar_length) + "\n" +
bar(ct.nice, cpu_time_bar_length) + "\n" +
bar(ct.iowait, cpu_time_bar_length) + "\n" +
bar(ct.irq, cpu_time_bar_length) + "\n" +
bar(ct.softirq, cpu_time_bar_length) + "\n" +
bar(ct.steal, cpu_time_bar_length) + "\n" +
bar(ct.guest, cpu_time_bar_length) + "\n" +
bar(ct.guest_nice, cpu_time_bar_length)
);
// I/O
var net_in_diff = json.net.bytes_in - io_net_in;
var net_out_diff = json.net.bytes_out - io_net_out;
var disk_in_diff = json.disk.read_count - io_disk_in;
var disk_out_diff = json.disk.write_count - io_disk_out;
io_net_in = json.net.bytes_in;
io_net_out = json.net.bytes_out;
io_disk_in = json.disk.read_count;
io_disk_out = json.disk.write_count;
$('#io_values').text(pad("", 20) + "\n" +
hb(json.net.bytes_in) + " (" + hb(net_in_diff) + ")\n" +
hb(json.net.bytes_out) + " (" + hb(net_out_diff) + ")\n" +
"\n" +
"\n" +
json.disk.read_count + " (" + disk_in_diff + ")\n" +
json.disk.write_count + " (" + disk_out_diff + ")"
);
// MEMORY
var mem_str = "Mem " + bar(json.mem, mem_bar_length);
var swap_str = "Swap " + bar(json.swap, swap_bar_length);
var mem_end = "" + json.mem + " %";
var swap_end = "" + json.swap + " %";
mem_str = mem_str.substr(0, mem_str.length - mem_end.length) + mem_end;
swap_str = swap_str.substr(0, swap_str.length - swap_end.length) + swap_end;
$('#virt').text(mem_str);
$('#swap').text(swap_str);
// MISC
$('#users').text(json.users);
$('#processes').text(json.procs);
}
};
</script>
</body>
</html>

165
htmlui/ws.js Normal file
View file

@ -0,0 +1,165 @@
/**
* WebSocket with graceful degradation - jQuery plugin
* @author David Lindkvist
* @version 0.1
*
* Returns an object implementing the WebSocket API.
*
* If browser supports WebSockets a native WebSocket instance is returned.
* If not, a simulated half-duplex implementation is returned which uses polling
* over HTTP to retrieve new messages
*
* OPTIONS
* -----------------------------------------------------------------------------
*
* {Number} fallbackOpenDelay number of ms to delay simulated open
* event for fallback
* {Number} fallbackPollInterval number of ms between requests for
* fallback polling
* {Object} fallbackPollParams optional params to pass with each poll
* requests
*
* EXAMPLES
* -----------------------------------------------------------------------------
*
* var websocket = $.gracefulWebSocket("ws://127.0.0.1:8080/");
*
* var websocket = $.gracefulWebSocket({
* fallbackPollParams: {
* "latestMessageID": function () {
* return latestMessageID;
* }
* }
* });
*
*/
(function ($) {
$.extend({
gracefulWebSocket: function (url, options) {
// Default properties
this.defaults = {
keepAlive: false, // not implemented - should ping server to keep socket open
autoReconnect: false, // not implemented - should try to reconnect silently if socket is closed
fallback: true, // not implemented - always use HTTP fallback if native browser support is missing
fallbackSendURL: url.replace('ws:', 'http:').replace('wss:', 'https:'),
fallbackSendMethod: 'POST',
fallbackPollURL: url.replace('ws:', 'http:').replace('wss:', 'https:'),
fallbackPollMethod: 'GET',
fallbackOpenDelay: 100, // number of ms to delay simulated open event
fallbackPollInterval: 3000, // number of ms between poll requests
fallbackPollParams: {} // optional params to pass with poll requests
};
// Override defaults with user properties
var opts = $.extend({}, this.defaults, options);
/**
* Creates a fallback object implementing the WebSocket interface
*/
function FallbackSocket() {
// WebSocket interface constants
const CONNECTING = 0;
const OPEN = 1;
const CLOSING = 2;
const CLOSED = 3;
var pollInterval;
var openTimout;
// create WebSocket object
var fws = {
// ready state
readyState: CONNECTING,
bufferedAmount: 0,
send: function (data) {
var success = true;
$.ajax({
async: false, // send synchronously
type: opts.fallbackSendMethod,
url: opts.fallbackSendURL + '?' + $.param( getFallbackParams() ),
data: data,
dataType: 'text',
contentType : "application/x-www-form-urlencoded; charset=utf-8",
success: pollSuccess,
error: function (xhr) {
success = false;
$(fws).triggerHandler('error');
}
});
return success;
},
close: function () {
clearTimeout(openTimout);
clearInterval(pollInterval);
this.readyState = CLOSED;
$(fws).triggerHandler('close');
},
onopen: function () {},
onmessage: function () {},
onerror: function () {},
onclose: function () {},
previousRequest: null,
currentRequest: null
};
function getFallbackParams() {
// update timestamp of previous and current poll request
fws.previousRequest = fws.currentRequest;
fws.currentRequest = new Date().getTime();
// extend default params with plugin options
return $.extend({"previousRequest": fws.previousRequest, "currentRequest": fws.currentRequest}, opts.fallbackPollParams);
}
/**
* @param {Object} data
*/
function pollSuccess(data) {
// trigger onmessage
var messageEvent = {"data" : data};
fws.onmessage(messageEvent);
}
function poll() {
$.ajax({
type: opts.fallbackPollMethod,
url: opts.fallbackPollURL,
dataType: 'text',
data: getFallbackParams(),
success: pollSuccess,
error: function (xhr) {
$(fws).triggerHandler('error');
}
});
}
// simulate open event and start polling
openTimout = setTimeout(function () {
fws.readyState = OPEN;
//fws.currentRequest = new Date().getTime();
$(fws).triggerHandler('open');
poll();
pollInterval = setInterval(poll, opts.fallbackPollInterval);
}, opts.fallbackOpenDelay);
// return socket impl
return fws;
}
// create a new websocket or fallback
var ws = window.WebSocket ? new WebSocket(url) : new FallbackSocket();
$(window).unload(function () { ws.close(); ws = null });
return ws;
}
});
})(jQuery);

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
autobahn==0.5.14
psutil==0.7.1

167
server.py Normal file
View file

@ -0,0 +1,167 @@
###############################################################################
##
## Copyright 2011,2012 Tavendo GmbH
##
## 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.
##
##
## Modified by Mikko Ahlroth, 2013
## Original is part of AutobahnPython and can be found at
## https://github.com/tavendo/AutobahnPython/blob/master/examples/websocket/broadcast/server.py
## Modifications are licensed with the MIT License, see LICENSE for details.
##
###############################################################################
import sys
import json
import psutil
from twisted.internet import reactor
from twisted.python import log
from autobahn.websocket import WebSocketServerFactory, \
WebSocketServerProtocol, \
listenWS
TICK_INTERVAL = 2
CPU_INTERVAL = 1
WEBSOCKET = 'ws://localhost:8081/'
NET_INTERFACE = 'eth0'
INITIAL_INFO = {
'cpus': psutil.NUM_CPUS,
'boot': psutil.get_boot_time(),
'version': '0.1',
}
def get_sys_status():
cpu = psutil.cpu_percent(interval=CPU_INTERVAL, percpu=True)
mem = psutil.virtual_memory().percent
swap = psutil.swap_memory().percent
procs = len(psutil.get_pid_list())
ct = psutil.cpu_times_percent()
cpu_times = {
'user': ct[0],
'system': ct[1],
'idle': ct[2],
'nice': ct[3],
'iowait': ct[4],
'irq': ct[5],
'softirq': ct[6],
'steal': ct[7],
'guest': ct[8],
'guest_nice': ct[9],
}
net = psutil.network_io_counters(pernic=True)[NET_INTERFACE]
net_out = {
'bytes_out': net.bytes_sent,
'bytes_in': net.bytes_recv,
}
disk = psutil.disk_io_counters()
disk_out = {
'read_count': disk.read_count,
'write_count': disk.write_count,
}
users = len(psutil.get_users())
return {
'cpu': cpu,
'mem': mem,
'swap': swap,
'procs': procs,
'cpu_times': cpu_times,
'net': net_out,
'disk': disk_out,
'users': users,
}
class BroadcastServerProtocol(WebSocketServerProtocol):
def onOpen(self):
self.factory.register(self)
def onMessage(self, msg, binary):
pass
def connectionLost(self, reason):
WebSocketServerProtocol.connectionLost(self, reason)
self.factory.unregister(self)
class BroadcastServerFactory(WebSocketServerFactory):
def __init__(self, url, debug=False, debugCodePaths=False):
WebSocketServerFactory.__init__(self, url, debug=debug, debugCodePaths=debugCodePaths)
self.clients = []
self.tick()
def tick(self):
self.broadcast(json.dumps(get_sys_status()))
reactor.callLater(TICK_INTERVAL - CPU_INTERVAL, self.tick)
def register(self, client):
if not client in self.clients:
print "registered client " + client.peerstr
self.clients.append(client)
client.sendMessage(json.dumps(INITIAL_INFO))
def unregister(self, client):
if client in self.clients:
print "unregistered client " + client.peerstr
self.clients.remove(client)
def broadcast(self, msg):
print "broadcasting message '%s' .." % msg
for c in self.clients:
c.sendMessage(msg)
print "message sent to " + c.peerstr
class BroadcastPreparedServerFactory(BroadcastServerFactory):
"""
Functionally same as above, but optimized broadcast using
prepareMessage and sendPreparedMessage.
"""
def broadcast(self, msg):
print "broadcasting prepared message '%s' .." % msg
preparedMsg = self.prepareMessage(msg)
for c in self.clients:
c.sendPreparedMessage(preparedMsg)
print "prepared message sent to " + c.peerstr
if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == 'debug':
log.startLogging(sys.stdout)
debug = True
else:
debug = False
#ServerFactory = BroadcastServerFactory
ServerFactory = BroadcastPreparedServerFactory
factory = ServerFactory(WEBSOCKET,
debug=debug,
debugCodePaths=debug)
factory.protocol = BroadcastServerProtocol
factory.setProtocolOptions(allowHixie76=True)
listenWS(factory)
reactor.run()