cron-tab217
index.yaml
indexes:
# AUTOGENERATED
# This index.yaml is automatically updated whenever the dev_appserver
# detects that a new type of query is run. If you want to manage the
# index.yaml file manually, remove the above marker line (the line
# saying "# AUTOGENERATED"). If you want to manage some indexes
# manually, move them above the marker line. The index.yaml file is
# automatically uploaded to the admin console when you next deploy
# your application using appcfg.py.
app.yaml
# http://code.google.com/appengine/docs/python/config/appconfig.html
application: cron-tab
version: 2
runtime: python
api_version: 1
handlers:
- url: /cron
script: main.py
login: admin
- url: /clear
script: main.py
login: admin
- url: /admin
script: main.py
login: admin
- url: /checkurl
script: main.py
- url: /
script: main.py
- url: /.*
script: main.py
login: required
cron.yaml
cron:
- description: basic activity
url: /cron
schedule: every 5 minutes
- description: daily clearing
url: /clear
schedule: every day 23:56
main.html
<html>
<head><title>Main</title>
{{ style }}
</head>
<body>
{{ header }}
<br><br>
The following features are provided by the tool cron-tab:
<ul>
<li>Perform periodic test of a cron url. Result of the tests can be viewed on the <a href="view">View statistics</a> page.
</li>
<li>Check the status of several links at any time. The status is displayed in the <a href="checkurl">Dashboard</a> page.
</li>
</ul>
Also an <a href="admin">administrator</a> interface is provided to configure properly this tool.
{{ footer }}
</body>
</html>
main.py
#!/usr/bin/env python
#
# http://cron-tab.appspot.com/
# version 2.03 (18-07-2009): templates added!
# version 2.14 (08-09-2009): added graphics and sendmail
#
# author: javalc6
#
# IMPORTANT NOTICE, please read:
#
# This software is licensed under the terms of the GNU GENERAL PUBLIC LICENSE,
# please read the enclosed file license.txt or http://www.gnu.org/licenses/licenses.html
#
# Note that this software is freeware and it is not designed, licensed or intended
# for use in mission critical, life support and military purposes.
#
# The use of this software is at the risk of the user.
import cgi
import time
import datetime
import re
import wsgiref.handlers
import os
from cronDB import *
from google.appengine.api import mail
from google.appengine.api import urlfetch
from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.ext import db
from google.appengine.ext.webapp import template
from urlparse import urlparse
#sendmail as suggested by user lovaxi
def sendmail(status, url):
query = ConfigDB.all()
if query.get() != None:
if query.get().send_mail:
if status == -1:
mail_body = 'web site (%s) down!' % url
elif status == 0:
mail_body = 'web site (%s) status undefined' % url
elif status == 200:
mail_body = 'web site (%s) up and running!' % url
elif status == -2:
mail_body = 'bad url (%s) provided for the cron-tab feature' % url
else:
mail_body = 'web site (%s) down\nstatus: %s' % (url, status)
mail.send_mail(sender = query.get().admin_email,
to = query.get().alert_email,
subject = 'Server status change notification',
body = mail_body)
# style used in html templates
style = '''
<style type="text/css">
<!--
body { background-color: #FFFFEE; color: #000000; font-family: Arial }
td.header { background-color: #F0F000;}
.small { font-size: 8pt; font-style: normal; font-family: Arial, Helvetica; }
table { background-color:#FFFFFF; border-width:3px; border-style:solid; border-color:#006699; }
td { background-color: #DEE3E7; }
th { background-color: #C8D1D7; }
table.menu { background-color:#FFFFFF; border-width:0px; }
-->
</style>
'''
# MainHandler
class MainHandler(webapp.RequestHandler):
def get(self):
template_values = {
'style': style,
'header': header(self),
'footer': footer(self),
}
path = os.path.join(os.path.dirname(__file__), 'main.html')
self.response.out.write(template.render(path, template_values))
# CronHandler
class CronHandler(webapp.RequestHandler):
def get(self):
query = CronUrlDB.all()
if query.get() == None:
return
cronUrlDB = query.get()
timeref = datetime.datetime.now()
oldstatus = cronUrlDB.status
try:
result = urlfetch.fetch(cronUrlDB.url, deadline=10)
cronUrlDB.status = result.status_code
except urlfetch.InvalidURLError:
self.response.out.write('Exception: Not an URL')
cronUrlDB.status = -2 # bad url
return
except urlfetch.DownloadError:
self.response.out.write('Exception: Download error')
cronUrlDB.status = -1 # download error
delay = (datetime.datetime.now() - timeref)
delay_ms = delay.microseconds / 1000.0
if cronUrlDB.counter == 0:
cronUrlDB.date = timeref
cronUrlDB.counter = 1
if (cronUrlDB.status == 200):
cronUrlDB.n_errors = 0
else:
cronUrlDB.n_errors = 1
cronUrlDB.acc_delay = delay_ms
cronUrlDB.min_delay = delay_ms
cronUrlDB.max_delay = delay_ms
cronUrlDB.put()
elif cronUrlDB.counter > 11:
statDB = StatDB()
statDB.date = cronUrlDB.date
statDB.mean_delay = cronUrlDB.acc_delay / cronUrlDB.counter
statDB.min_delay = cronUrlDB.min_delay
statDB.max_delay = cronUrlDB.max_delay
statDB.n_errors = cronUrlDB.n_errors
statDB.put()
cronUrlDB.date = timeref
cronUrlDB.counter = 1
if (cronUrlDB.status == 200):
cronUrlDB.n_errors = 0
else:
cronUrlDB.n_errors = 1
cronUrlDB.acc_delay = delay_ms
cronUrlDB.min_delay = delay_ms
cronUrlDB.max_delay = delay_ms
cronUrlDB.put()
else:
cronUrlDB.counter = cronUrlDB.counter + 1
if (cronUrlDB.status != 200):
cronUrlDB.n_errors = cronUrlDB.n_errors + 1
cronUrlDB.acc_delay = delay_ms + cronUrlDB.acc_delay
if cronUrlDB.min_delay > delay_ms:
cronUrlDB.min_delay = delay_ms
if cronUrlDB.max_delay < delay_ms:
cronUrlDB.max_delay = delay_ms
cronUrlDB.put()
if oldstatus != cronUrlDB.status:
sendmail(cronUrlDB.status, cronUrlDB.url)
# ClearHandler
class ClearHandler(webapp.RequestHandler):
def get(self):
query = StatDB.all()
if query.count() > 0:
statDB = query.get()
acc_delay = statDB.mean_delay
min_delay = statDB.min_delay
max_delay = statDB.max_delay
n_errors = statDB.n_errors
n_items = 1
for statDB in query:
n_errors += statDB.n_errors
n_items +=1
acc_delay += statDB.mean_delay
if min_delay > statDB.min_delay:
min_delay = statDB.min_delay
if max_delay < statDB.max_delay:
max_delay = statDB.max_delay
statDB.delete()
dailyDB = DailyDB()
dailyDB.mean_delay = acc_delay / n_items
dailyDB.min_delay = min_delay
dailyDB.max_delay = max_delay
dailyDB.date = datetime.datetime.now().date()
dailyDB.n_errors = n_errors
dailyDB.put()
# query = db.GqlQuery("SELECT * FROM StatDB")
# query = StatDB.all()
# db.delete(query.fetch(500))
# ViewHandler
class ViewHandler(webapp.RequestHandler):
def get(self):
if self.request.get('action') == 'week':
dailyDBs = db.GqlQuery("SELECT * FROM DailyDB ORDER BY date DESC LIMIT 7")
latency = []
errors = []
for dailyDB in dailyDBs:
latency.append(int(dailyDB.mean_delay))
errors.append(int(dailyDB.n_errors))
latency.reverse()
errors.reverse()
template_values = {
'style': style,
'header': header(self),
'footer': footer(self),
'dailyDBs': dailyDBs,
'latency': latency,
'errors': errors,
}
elif self.request.get('action') == 'month':
dailyDBs = db.GqlQuery("SELECT * FROM DailyDB ORDER BY date DESC LIMIT 30")
latency = []
errors = []
for dailyDB in dailyDBs:
latency.append(int(dailyDB.mean_delay))
errors.append(int(dailyDB.n_errors))
latency.reverse()
errors.reverse()
template_values = {
'style': style,
'header': header(self),
'footer': footer(self),
'dailyDBs': dailyDBs,
'latency': latency,
'errors': errors,
}
else:
statDBs = db.GqlQuery("SELECT * FROM StatDB ORDER BY date DESC LIMIT 12")
latency = []
errors = []
for statDB in statDBs:
latency.append(int(statDB.mean_delay))
errors.append(int(statDB.n_errors))
latency.reverse()
errors.reverse()
template_values = {
'style': style,
'header': header(self),
'footer': footer(self),
'statDBs': statDBs,
'latency': latency,
'errors': errors,
}
path = os.path.join(os.path.dirname(__file__), 'view.html')
self.response.out.write(template.render(path, template_values))
# dashboard
def dashboard(self):
template_values = {
'style': style,
'header': header(self),
'footer': footer(self),
'urlDBs': UrlDB.all(),
}
path = os.path.join(os.path.dirname(__file__), 'checkurl.html')
self.response.out.write(template.render(path, template_values))
#checkurl
class checkurl(webapp.RequestHandler):
def get(self):
value = self.request.get("url")
if value:
urlDB = db.get(value)
url = urlDB.url
else:
dashboard(self)
return
# url = base64.b64decode(self.request.get("url"))
self.response.out.write(check_url(self, url))
# check_url
def check_url(self, url):
timeref = datetime.datetime.now()
try:
result = urlfetch.fetch(url)
except urlfetch.InvalidURLError:
return 'Error: Not an URL'
except urlfetch.DownloadError:
return 'Error: Download error'
if result.status_code == 200:
delay = (datetime.datetime.now() - timeref)
if len(result.content) > 0:
if re.search('</html>', result.content, re.I):
return "OK: %s ms" % (delay.microseconds/1000.0)
else:
return "Error: bad html"
else:
return 'Error: Empty response'
else:
return 'Error: %s' % result.status_code
#admin
class admin(webapp.RequestHandler):
def post(self):
action = self.request.get('action')
result = None
if action == 'addurl2':
if self.request.get('url').startswith('http://'):
urlDB = UrlDB()
urlDB.url = self.request.get('url')
urlDB.put()
result = 'url added'
else:
result = 'url must start with http://'
if action == 'addcronurl2':
if self.request.get('url').startswith('http://'):
query = CronUrlDB.all()
if query.get() == None: # singleton
cronUrlDB = CronUrlDB()
cronUrlDB.url = self.request.get('url')
cronUrlDB.counter = 0 # just defined!
cronUrlDB.status = 0 # undef
cronUrlDB.put()
result = 'cron url added'
else:
result = 'already defined'
else:
result = 'url must start with http://'
if action == 'edit.email':
if self.request.get('admin.email').find('@') != -1:
if self.request.get('alert.email').find('@') != -1:
query = ConfigDB.all()
if query.get() == None: # singleton
configDB = ConfigDB()
else:
configDB = query.get()
configDB.admin_email = self.request.get('admin.email')
configDB.alert_email = self.request.get('alert.email')
configDB.send_mail = (self.request.get('send.email') == 'True')
configDB.put()
result = 'email values saved in configuration'
else:
result = 'alert email must contain @'
else:
result = 'admin email must contain @'
render_admin(self, action, result, None)
def get(self):
action = self.request.get('action')
result = None
if action == 'delete':
mykey = self.request.get('key')
else:
mykey = None
if action == 'delete2':
mykey = self.request.get('key')
entity = db.get(mykey)
if entity:
entity.delete()
result = 'entity deleted'
else:
result = 'nothing to delete'
render_admin(self, action, result, mykey)
# render_admin
def render_admin(self, action, result, mykey):
query = ConfigDB.all()
if query.get() != None:
admin_email = query.get().admin_email
alert_email = query.get().alert_email
send_mail = query.get().send_mail
else:
admin_email = ''
alert_email = ''
template_values = {
'style': style,
'header': header(self),
'footer': footer(self),
'action': action,
'result': result,
'mykey': mykey,
'cronUrlDBs': CronUrlDB.all(),
'urlDBs': UrlDB.all(),
'admin_email': admin_email,
'alert_email': alert_email,
'send_mail': send_mail,
}
path = os.path.join(os.path.dirname(__file__), 'admin.html')
self.response.out.write(template.render(path, template_values))
# header
def header(self):
return "".join(('''<table class="menu" width="100%" cellpadding="0" cellspacing="0"><tr><td align="left">
<a href="view">View statistics</a> | <a href="checkurl">Dashboard</a> | <a href="admin">Admin</a></td>''',
'<td align="right">%s [%s]</td>' % (users.get_current_user(), datetime.datetime.now().strftime("%d %b %y, %H:%M:%S")),
'</tr></table>'
))
# footer
def footer(self):
return '<div align="center" style="font-size: smaller">Powered by <a title="cron-tab@googlecode" href="http://code.google.com/p/cron-tab/">cron-tab</a></div>'
#
# main()
#
def main():
application = webapp.WSGIApplication([('/', MainHandler), ('/view', ViewHandler), ('/cron', CronHandler), ('/clear', ClearHandler), ('/checkurl', checkurl), ('/admin', admin)],
debug=True)
wsgiref.handlers.CGIHandler().run(application)
if __name__ == '__main__':
main()
crronDB.py
#!/usr/bin/env python
#
# http://cron-tab.appspot.com/
# version 1 (07-07-2009): first release
#
# author: javalc6
#
from google.appengine.ext import db
# UrlDB
class UrlDB(db.Model):
url = db.StringProperty()
delay = db.FloatProperty() # last delay
date = db.DateTimeProperty() # last check
n_errors = db.IntegerProperty() # number of errors
# StatDB
class StatDB(db.Model):
mean_delay = db.FloatProperty() # accumulated delay
min_delay = db.FloatProperty() # minimum delay
max_delay = db.FloatProperty() # maximum delay
date = db.DateTimeProperty()
n_errors = db.IntegerProperty() # number of errors
# DailyDB
class DailyDB(db.Model):
mean_delay = db.FloatProperty() # accumulated delay
min_delay = db.FloatProperty() # minimum delay
max_delay = db.FloatProperty() # maximum delay
date = db.DateProperty()
n_errors = db.IntegerProperty() # number of errors
# CronUrlDB
class CronUrlDB(db.Model):
url = db.StringProperty()
counter = db.IntegerProperty()
acc_delay = db.FloatProperty() # accumulated delay
min_delay = db.FloatProperty() # minimum delay
max_delay = db.FloatProperty() # maximum delay
date = db.DateTimeProperty(auto_now_add=True)
n_errors = db.IntegerProperty() # number of errors
status = db.IntegerProperty(0) # 0: undef, 1: ok, -1: error, -2: bad url
# ConfigDB
class ConfigDB(db.Model):
admin_email = db.StringProperty()
alert_email = db.StringProperty()
send_mail = db.BooleanProperty()
view.html
<html>
<head><title>View</title>
{{ style }}
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load('visualization', '1', {packages:['imagesparkline']});
</script>
</head>
<body>
{{ header }}
<br>View: <a href="view">Today</a> | <a href="view?action=week">Week</a> | <a href="view?action=month">30 days</a><br><br>
<script type="text/javascript">
google.setOnLoadCallback(function() {
var data = [['latency','006699', {{latency}}],['errors','ff0000', {{errors}}]];
for(i=0; i < data.length; ++i) {
var key = data[i][0], color = data[i][1], values = data[i][2];
if(values.length > 0) {
var gData = new google.visualization.DataTable();
gData.addColumn("number", data[i][0]);
gData.addRows(values.length);
for(j=0; j < values.length; ++j) gData.setValue(j,0,values[j]);
chart = new google.visualization.ImageSparkLine(document.getElementById('chart_id_'+key));
chart.draw(gData, {color: data[i][1], width:200, height:40, showAxisLines:true, showValueLabels:false, labelPosition:'right'});
}
}
}
);
</script>
<div id="chart_id_latency"></div><div id="chart_id_errors"></div>
{% if statDBs %}
{{ statDBs.count }} items in statDBs
<table>
<tr><th>Time</th><th> </th><th>Min delay (ms)</th><th>Mean delay (ms)</th><th>Max delay (ms)</th><th># Errors</th></tr>
{% for statDB in statDBs %}
<tr>
<td>{{ statDB.date }}</td><td> </td><td>{{ statDB.min_delay }}</td><td>{{ statDB.mean_delay }}</td><td>{{ statDB.max_delay }}</td> <td>{{ statDB.n_errors }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% if dailyDBs %}
{{ dailyDBs.count }} items in dailyDBs
<table>
<tr><th>Time</th><th> </th><th>Min delay (ms)</th><th>Mean delay (ms)</th><th>Max delay (ms)</th><th># Errors</th></tr>
{% for dailyDB in dailyDBs %}
<tr>
<td>{{ dailyDB.date }}</td><td> </td><td>{{ dailyDB.min_delay }}</td><td>{{ dailyDB.mean_delay }}</td><td>{{ dailyDB.max_delay }}</td> <td>{{ dailyDB.n_errors }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
{{ footer }}
</body>
</html>
checkurl.html
<html>
<head><title>Dashboard</title>
{{ style }}
<script type="text/javascript">//<![CDATA[
function update() {
{% for urlDB in urlDBs %}
callRemote({{ forloop.counter0 }}, 'checkurl', 'url={{ urlDB.key }}', '{{ urlDB.key }}');
{% endfor %}
}
var xmlHttpObject = Array();
function callRemote(i, url, params, element_id) {
xmlHttpObject[i] = false;
if (window.XMLHttpRequest) { // Mozilla, Safari,...
xmlHttpObject[i] = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE
try {
xmlHttpObject[i] = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
try {
xmlHttpObject[i] = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {}
}
}
if (xmlHttpObject[i]) {
if (params)
url += "?"+params;
xmlHttpObject[i].onreadystatechange=function () {
stateChanged(i, element_id);
};
xmlHttpObject[i].open('GET', url, true);
xmlHttpObject[i].send(null);
}
}
function stateChanged(k, element_id) {
if (xmlHttpObject[k].readyState == 4) {
if (xmlHttpObject[k].status == 200)
document.getElementById(element_id).innerHTML = xmlHttpObject[k].responseText;
}
}
//]]>
</script>
</head>
<body onLoad='update()'>
{{ header }}
<br>
<table>
{% for urlDB in urlDBs %}
<tr><td>{{ urlDB.url }}</td><td id='{{ urlDB.key }}'></td></tr>
{% endfor %}
</table>
<a href="admin?action=addurl">Add new url</a>
{{ footer }}
</body>
</html>
admin.html
<html>
<head><title>Admin</title>
{{ style }}
</head>
<body>
{{ header }}
{% if result %}
Result: {{ result }}<br><br>
{% endif %}
{% ifequal action 'addurl' %}
<form action='/admin' method='post'>
enter url: <input type='text' name='url' size='40' value='http://'>
<input name='action' type='hidden' value='addurl2'>
<input type='submit' name='submit' value='Add'>
</form>
{% endifequal %}
{% ifequal action 'delete' %}
<form action='admin' method='get'>
<input name='key' type='hidden' value='{{ mykey }}'>
<input name='action' type='hidden' value='delete2'>
<input type='submit' name='submit' value='Confirm delete'>
</form>
{% endifequal %}
{% ifequal action 'addcronurl' %}
<form action='/admin' method='post'>
enter cron url: <input type='text' name='url' size='40' value='http://'>
<input name='action' type='hidden' value='addcronurl2'>
<input type='submit' name='submit' value='Add'>
</form>
{% endifequal %}
{% if not action %}
<br>
{{ urlDBs.count }} items in urlDBs
<table>
<tr><th>Url</th><th> </th><th>Action</th></tr>
{% for urlDB in urlDBs %}
<tr>
<td>{{ urlDB.url }}</td><td> </td><td><a href="admin?action=delete&key={{ urlDB.key }}">delete</a></td>
</tr>
{% endfor %}
<tr>
<td colspan='3'><a href="admin?action=addurl">add new url</a></td>
</tr>
</table>
{% ifequal cronUrlDBs.count 0 %}
<br><br><a href="admin?action=addcronurl">add cron url</a>
{% endifequal %}
<br>
{% ifnotequal cronUrlDBs.count 0 %}
<table>
<tr><th>Cron Url</th><th>Status</th><th>Action</th></tr>
{% for cronUrlDB in cronUrlDBs %}
<tr>
<td>{{ cronUrlDB.url }}</td><td>{{ cronUrlDB.status }}</td><td><a href="admin?action=delete&key={{ cronUrlDB.key }}">delete</a></td>
</tr>
{% endfor %}
</table>
{% endifnotequal %}
<br>
<table>
<form action='/admin' method='post'>
<tr>
<td>enter e-mail of the administrator: </td><td><input type='text' name='admin.email' size='40' value='{{ admin_email }}'></td>
</tr>
<tr>
<td>enter e-mail to receive alerts: </td><td><input type='text' name='alert.email' size='40' value='{{ alert_email }}'></td>
</tr>
<tr><td>send alert mail:
{% if send_mail %}
<input type="checkbox" name="send.email" value="True" checked="checked">
{% else %}
<input type="checkbox" name="send.email" value="True">
{% endif %}
</td><td>
<input name='action' type='hidden' value='edit.email'>
<input type='submit' name='submit' value='Save'></td>
<tr>
</table>
</form>
{% endif %}
{{ footer }}
</body>
</html>