367 lines
11 KiB
Python
367 lines
11 KiB
Python
import datetime
|
|
try:
|
|
import json
|
|
except ImportError:
|
|
import simplejson as json
|
|
|
|
import eventlet
|
|
from eventlet import event
|
|
from eventlet import wsgi
|
|
from eventlet.green import os
|
|
|
|
class Server(object):
|
|
def __init__(self, controller, host, port):
|
|
self.controller = controller
|
|
self.host = host
|
|
self.port = port
|
|
self.status_waiter = None
|
|
self.child_events = {}
|
|
socket = eventlet.listen((host, port))
|
|
wsgi.server(socket, self.application)
|
|
|
|
def get_status_data(self):
|
|
# using a waiter because we only want one child collection ping
|
|
# happening at a time; if there are multiple concurrent status requests,
|
|
# they all simply share the same set of data results
|
|
if self.status_waiter is None:
|
|
self.status_waiter = eventlet.spawn(self._collect_status_data)
|
|
return self.status_waiter.wait()
|
|
|
|
def _collect_status_data(self):
|
|
try:
|
|
now = datetime.datetime.now()
|
|
children = self.controller.children.values()
|
|
status_data = {
|
|
'active_children_count':len([c
|
|
for c in children
|
|
if c.active]),
|
|
'killed_children_count':len([c
|
|
for c in children
|
|
if not c.active]),
|
|
'configured_children_count':self.controller.num_processes,
|
|
'now':now.ctime(),
|
|
'pid':os.getpid(),
|
|
'uptime':format_timedelta(now - self.controller.started_at),
|
|
'started_at':self.controller.started_at.ctime(),
|
|
'config':self.controller.config}
|
|
# fire up a few greenthreads to wait on children's responses
|
|
p = eventlet.GreenPile()
|
|
for child in self.controller.children.values():
|
|
p.spawn(self.collect_child_status, child)
|
|
status_data['children'] = dict([pid_cd for pid_cd in p])
|
|
|
|
# total concurrent connections
|
|
status_data['concurrent_requests'] = sum([
|
|
child.get('concurrent_requests', 0)
|
|
for child in status_data['children'].values()])
|
|
finally:
|
|
# wipe out the waiter so that subsequent requests create new ones
|
|
self.status_waiter = None
|
|
return status_data
|
|
|
|
def collect_child_status(self, child):
|
|
self.child_events[child.pid] = event.Event()
|
|
try:
|
|
try:
|
|
# tell the child to POST its status to us, we handle it in the
|
|
# wsgi application below
|
|
eventlet.hubs.trampoline(child.kill_pipe, write=True)
|
|
os.write(child.kill_pipe, 's')
|
|
t = eventlet.Timeout(1)
|
|
results = self.child_events[child.pid].wait()
|
|
t.cancel()
|
|
except (OSError, IOError), e:
|
|
results = {'error': "%s %s" % (type(e), e)}
|
|
except eventlet.Timeout:
|
|
results = {'error':'Timed out'}
|
|
finally:
|
|
self.child_events.pop(child.pid, None)
|
|
|
|
results.update({
|
|
'pid':child.pid,
|
|
'active':child.active,
|
|
'uptime':format_timedelta(datetime.datetime.now() - child.forked_at),
|
|
'forked_at':child.forked_at.ctime()})
|
|
return child.pid, results
|
|
|
|
def application(self, environ, start_response):
|
|
if environ['REQUEST_METHOD'] == 'GET':
|
|
status_data = self.get_status_data()
|
|
if environ['PATH_INFO'] == '/status':
|
|
start_response('200 OK', [('content-type', 'text/html')])
|
|
return [fill_template(status_data)]
|
|
elif environ['PATH_INFO'] == '/status.json':
|
|
start_response('200 OK', [('content-type', 'application/json')])
|
|
return [json.dumps(status_data, indent=2)]
|
|
|
|
elif environ['REQUEST_METHOD'] == 'POST':
|
|
# it's a client posting its stats to us
|
|
body = environ['wsgi.input'].read()
|
|
child_status = json.loads(body)
|
|
pid = child_status['pid']
|
|
if pid in self.child_events:
|
|
self.child_events[pid].send(child_status)
|
|
start_response('200 OK', [('content-type', 'application/json')])
|
|
else:
|
|
start_response('500 Internal Server Error',
|
|
[('content-type', 'text/plain')])
|
|
print "Don't know about child pid %s" % pid
|
|
return [""]
|
|
|
|
# fallthrough case
|
|
start_response('404 Not Found', [('content-type', 'text/plain')])
|
|
return [""]
|
|
|
|
def format_timedelta(t):
|
|
"""Based on how HAProxy's status page shows dates.
|
|
10d 14h
|
|
3h 20m
|
|
1h 0m
|
|
12m
|
|
15s
|
|
"""
|
|
seconds = t.seconds
|
|
if t.days > 0:
|
|
return "%sd %sh" % (t.days, int(seconds/3600))
|
|
else:
|
|
if seconds > 3600:
|
|
hours = int(seconds/3600)
|
|
seconds -= hours*3600
|
|
return "%sh %sm" % (hours, int(seconds/60))
|
|
else:
|
|
if seconds > 60:
|
|
return "%sm" % int(seconds/60)
|
|
else:
|
|
return "%ss" % seconds
|
|
|
|
class Tag(object):
|
|
"""Yeah, there's a templating DSL in this status module. Deal with it."""
|
|
def __init__(self, name, *children, **attrs):
|
|
self.name = name
|
|
self.attrs = attrs
|
|
self.children = list(children)
|
|
|
|
def __str__(self):
|
|
al = []
|
|
for name, val in self.attrs.iteritems():
|
|
if name == 'cls':
|
|
name = "class"
|
|
if isinstance(val, (list, tuple)):
|
|
val = " ".join(val)
|
|
else:
|
|
val = str(val)
|
|
al.append('%s="%s"' % (name, val))
|
|
if al:
|
|
attrstr = " " + " ".join(al) + " "
|
|
else:
|
|
attrstr = ""
|
|
cl = []
|
|
for child in self.children:
|
|
cl.append(str(child))
|
|
if cl:
|
|
childstr = "\n" + "\n".join(cl) + "\n"
|
|
else:
|
|
childstr = ""
|
|
return "<%s%s>%s</%s>" % (self.name, attrstr, childstr, self.name)
|
|
|
|
def make_tag(name):
|
|
return lambda *c, **a: Tag(name, *c, **a)
|
|
p = make_tag('p')
|
|
div = make_tag('div')
|
|
table = make_tag('table')
|
|
tr = make_tag('tr')
|
|
th = make_tag('th')
|
|
td = make_tag('td')
|
|
h2 = make_tag('h2')
|
|
span = make_tag('span')
|
|
|
|
def fill_template(status_data):
|
|
# controller status
|
|
cont_div = table(id='controller')
|
|
cont_div.children.append(tr(th("PID:", title="Controller Process ID"),
|
|
td(status_data['pid'])))
|
|
cont_div.children.append(tr(th("Uptime:", title="Time since launch"),
|
|
td(status_data['uptime'])))
|
|
cont_div.children.append(tr(th("Host:", title="Host and port server is listening on, all means all interfaces."),
|
|
td("%s:%s" % (status_data['config']['host'] or "all",
|
|
status_data['config']['port']))))
|
|
cont_div.children.append(tr(th("Threads:", title="Threads per child"),
|
|
td(status_data['config']['threadpool_workers'])))
|
|
cont_div = div(cont_div)
|
|
|
|
# children headers and summaries
|
|
child_div = div(h2("Child Processes"))
|
|
count_td = td(status_data['active_children_count'], "/",
|
|
status_data['configured_children_count'])
|
|
if status_data['active_children_count'] < \
|
|
status_data['configured_children_count']:
|
|
count_td.attrs['cls'] = "error"
|
|
count_td.children.append(
|
|
span("(", status_data['killed_children_count'], ")"))
|
|
children_table = table(
|
|
tr(
|
|
th('PID', title="Process ID"),
|
|
th('Active', title="Accepting New Requests"),
|
|
th('Uptime', title="Uptime"),
|
|
th('Concurrent', title="Concurrent Requests")),
|
|
tr(
|
|
td("Total"),
|
|
count_td,
|
|
td(), # no way to "total" uptime
|
|
td(status_data['concurrent_requests'])),
|
|
id="children")
|
|
child_div.children.append(children_table)
|
|
|
|
# children themselves
|
|
odd = True
|
|
for pid in sorted(status_data['children'].keys()):
|
|
child = status_data['children'][pid]
|
|
row = tr(td(pid), cls=['child'])
|
|
if odd:
|
|
row.attrs['cls'].append('odd')
|
|
odd = not odd
|
|
|
|
# active handling
|
|
row.children.append(td({True:'Y', False:'N'}[child['active']]))
|
|
if not child['active']:
|
|
row.attrs['cls'].append('dying')
|
|
|
|
# errors
|
|
if child.get('error'):
|
|
row.attrs['cls'].append('error')
|
|
row.children.append(td(child['error'], colspan=2))
|
|
else:
|
|
# no errors
|
|
row.children.append(td(child['uptime']))
|
|
row.children.append(td(child['concurrent_requests']))
|
|
|
|
children_table.children.append(row)
|
|
|
|
# config dump
|
|
config_div = div(
|
|
h2("Configuration"),
|
|
table(*[tr(th(key), td(status_data['config'][key]))
|
|
for key in sorted(status_data['config'].keys())]),
|
|
id='config')
|
|
|
|
to_format = {'cont_div': cont_div, 'child_div':child_div,
|
|
'config_div':config_div}
|
|
to_format.update(status_data)
|
|
return HTML_SHELL % to_format
|
|
|
|
HTML_SHELL = """
|
|
<!DOCTYPE html>
|
|
<html><head>
|
|
<title>Spawning Status</title>
|
|
<style type="text/css">
|
|
html, p, div, table, h1, h2, input, form {
|
|
margin: 0;
|
|
padding: 0;
|
|
border: 0;
|
|
outline: 0;
|
|
font-size: 12px;
|
|
font-family: Helvetica, Arial, sans-serif;
|
|
vertical-align: baseline;
|
|
}
|
|
body {
|
|
line-height: 1.2;
|
|
color: black;
|
|
background: white;
|
|
margin: 3em;
|
|
}
|
|
table {
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
}
|
|
th, td {
|
|
text-align: center;
|
|
padding: .1em;
|
|
padding-right: .4em;
|
|
}
|
|
#controller td, #controller th {
|
|
text-align: left;
|
|
}
|
|
#config td, #config th {
|
|
text-align: left;
|
|
}
|
|
#children {
|
|
clear: both;
|
|
}
|
|
#options {
|
|
float: right;
|
|
border: 1px solid #dfdfdf;
|
|
padding:.5em;
|
|
}
|
|
h1,h2 {
|
|
margin: .5em;
|
|
margin-left: 0em;
|
|
font-size: 130%%;
|
|
}
|
|
h2 {
|
|
font-size: 115%%;
|
|
}
|
|
tr.odd {
|
|
background: #dfdfdf;
|
|
}
|
|
input {
|
|
border: 1px solid grey;
|
|
}
|
|
#refresh form {
|
|
display: inline;
|
|
}
|
|
tr.child.dying {
|
|
font-style: italic;
|
|
color: #444444;
|
|
}
|
|
.error {
|
|
background: #ff4444;
|
|
}
|
|
|
|
/* Cut out the fat for mobile devices */
|
|
@media screen and (max-width: 400px) {
|
|
body {
|
|
margin-left: .2em;
|
|
margin-right: .2em;
|
|
}
|
|
#options {
|
|
float: none;
|
|
}
|
|
}
|
|
</style>
|
|
</head><body>
|
|
<h1>Spawning Status</h1>
|
|
<div id="options">
|
|
<p>%(now)s</p>
|
|
<div id="refresh">
|
|
<a href="">Refresh</a> (<form>
|
|
<input type="checkbox" /> every
|
|
<input type="text" value="5" size=2 />s
|
|
</form>)
|
|
</div>
|
|
<a href="status.json">JSON</a>
|
|
</div>
|
|
%(cont_div)s
|
|
%(child_div)s
|
|
%(config_div)s
|
|
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
|
|
<script type="text/javascript">
|
|
$(document).ready(function() {
|
|
var timer;
|
|
var arrangeTimeout = function () {
|
|
clearTimeout(timer);
|
|
if($('#refresh input[type=checkbox]').attr('checked')) {
|
|
timer = setTimeout(
|
|
function() {window.location.reload();},
|
|
$('#refresh input[type=text]').val() * 1000);
|
|
}
|
|
if($(this).is('form')) {
|
|
return false;
|
|
}
|
|
};
|
|
$('#refresh input[type=checkbox]').click(arrangeTimeout);
|
|
$('#refresh form').submit(arrangeTimeout).submit();
|
|
});
|
|
</script>
|
|
</body></html>
|
|
"""
|