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>