├── .gitignore ├── README.markdown ├── crash_hound └── __init__.py ├── setup.py └── test └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Monitor anything and get notifications directly on your mobile phone 2 | =========================================== 3 | 4 | Crash Hound lets you script monitoring and lets you receive notifications directly on your mobile phone. 5 | 6 | It works via Tropo and a http://tropo.com acocunt is required. 7 | 8 | For more information check out: 9 | 10 | * http://amix.dk/blog/post/19637#Monitor-anything-and-get-SMS-notifications 11 | * http://amix.dk/blog/post/19625#International-SMS-messaging-The-cheap-way 12 | 13 | To install it do following: 14 | 15 | sudo easy_install crash_hound 16 | 17 | 18 | Examples 19 | ======== 20 | 21 | Example of list wrapper: 22 | 23 | from crash_hound import CrashHound, ReportCrash, CommonChecks, SenderTropo 24 | 25 | def check_fn(): 26 | if 42: 27 | raise ReportCrash('42 is true!') 28 | else: 29 | pass #Ignore 30 | 31 | #--- Configure sender and checker ---------------------------------------------- 32 | crash_sender = SenderTropo('YOUR TROPO.COM API KEY', 33 | '+56 ... YOUR MOBILE NUMBER ...') 34 | 35 | crash_checker = CrashHound(crash_sender) 36 | 37 | crash_checker.register_check('42 Checker', 38 | check_fn, 39 | notify_every=60) 40 | 41 | crash_checker.register_check('Google.com Blah test', 42 | lambda: CommonChecks.website_check('http://google.com/blah'), 43 | notify_every=60) 44 | 45 | crash_checker.run_checks(check_interval=10) 46 | 47 | Example of email notifications using the [SendGrid](http://sendgrid.com) SMTP sever: 48 | 49 | from crash_hound import CrashHound, CommonChecks, SenderMail 50 | 51 | #--- Configure sender and checker ---------------------------------------------- 52 | crash_sender = SenderMail('EMAIL TO', 'EMAIL FROM', 'smtp.sendgrid.net', 'SENDGRID USER', 'SENDGRID PASSWORD') 53 | 54 | crash_checker = CrashHound(crash_sender) 55 | 56 | crash_checker.register_check('Google.com Blah test', 57 | lambda: CommonChecks.website_check('http://google.com/blah'), 58 | notify_every=60) 59 | 60 | crash_checker.run_checks(check_interval=10) 61 | -------------------------------------------------------------------------------- /crash_hound/__init__.py: -------------------------------------------------------------------------------- 1 | import time 2 | import types 3 | import urllib 4 | 5 | from datetime import datetime, timedelta 6 | 7 | 8 | #--- Message senders ---------------------------------------------- 9 | class SenderTropo: 10 | 11 | def __init__(self, api_token, numbers): 12 | self.api_token = api_token 13 | 14 | if type(numbers) != types.ListType: 15 | numbers = [numbers] 16 | 17 | self.numbers = numbers 18 | 19 | def send_notification(self, name, crash_message): 20 | statuses = [] 21 | 22 | for number in self.numbers: 23 | data = urllib.urlencode({ 24 | 'action': 'create', 25 | 'token': self.api_token, 26 | 'numberToDial': number.replace(' ', ''), 27 | 'msg': '%s: %s' % (name, crash_message) 28 | }) 29 | 30 | fp = urllib.urlopen('https://api.tropo.com/1.0/sessions', 31 | data) 32 | 33 | statuses.append( fp.read() ) 34 | 35 | return ' '.join(statuses) 36 | 37 | 38 | class SenderMail: 39 | """Send mail notifications using a specified SMTP server.""" 40 | 41 | def __init__(self, mail_to, mail_from, smtp_host, smtp_user, smtp_password, smtp_port=587, tls=True): 42 | """Construct mail notification sender class. 43 | 44 | `mail_to` the email address (or list of email addresses) to send the notification to. 45 | `mail_from` the email address that the notification should be sent from. 46 | `smtp_host` the IP address or hostname of the SMTP server. 47 | `smtp_user` the user to authenticate the SMTP connection with. 48 | `smtp_password` the password to authenticate the SMTP connection with. 49 | `smtp_port` the port number of the SMTP server (defaults to 587). 50 | `tls` specifies if TLS must be used to encrypt the connection (defaults to True). 51 | """ 52 | if (isinstance(mail_to, str)): 53 | self.mail_to = (mail_to,) 54 | else: 55 | self.mail_to = mail_to 56 | self.mail_from = mail_from 57 | self.smtp_host = smtp_host 58 | self.smtp_user = smtp_user 59 | self.smtp_password = smtp_password 60 | self.smtp_port = smtp_port 61 | self.use_tls = tls 62 | 63 | def send_notification(self, name, crash_message): 64 | """Send an mail notification about a crash event. 65 | 66 | `name` the name of the crash checker that initiated the notificaton. 67 | `crash_message` the message from the crash error. 68 | """ 69 | from smtplib import SMTP 70 | from email.MIMEText import MIMEText 71 | 72 | # build message 73 | message = MIMEText(crash_message, 'plain') 74 | message['Subject'] = name 75 | message['To'] = ','.join(self.mail_to) 76 | message['From'] = self.mail_from 77 | 78 | # connect to SMTP server and send message 79 | connection = SMTP(self.smtp_host, self.smtp_port) 80 | connection.ehlo() 81 | if self.use_tls: 82 | # put SMTP connection into TLS mode so that further commands are encrypted 83 | connection.starttls() 84 | connection.ehlo() 85 | connection.login(self.smtp_user, self.smtp_password) 86 | try: 87 | connection.sendmail(self.mail_from, self.mail_to, message.as_string()) 88 | return True # success 89 | except: 90 | return False # failure 91 | finally: 92 | connection.quit() 93 | 94 | 95 | #--- Exceptions ---------------------------------------------- 96 | class ReportCrash(Exception): 97 | pass 98 | 99 | 100 | 101 | #--- Impl ---------------------------------------------- 102 | class CrashHound: 103 | 104 | def __init__(self, sender): 105 | self.check_functions = {} 106 | self.sender = sender 107 | 108 | #--- Registers ---------------------------------------------- 109 | def register_check(self, name, check_fn, notify_every=240): 110 | """Register a check. 111 | 112 | `check_fn` should throw ReportCrash to indicate that a check has failed. 113 | `notify_every` indicates how often a notification should be sent. 114 | The default is 240 (it will send a notifcation every 4 minutes if the check keeps failing). 115 | 116 | Example of a custom check looks like this:: 117 | 118 | def check_fn(): 119 | if 42: 120 | raise ReportCrash('42 is true!') 121 | 122 | crash_hound.register_check('42 Checker', check_fn) 123 | """ 124 | self.check_functions[name] = { 125 | 'check_fn': check_fn, 126 | 'last_notifcation': None, 127 | 'notify_every': notify_every 128 | } 129 | 130 | def remove_check(self, name): 131 | """Removes a registered check.""" 132 | if name in self.check_functions: 133 | del self.check_functions[name] 134 | 135 | def run_checks(self, check_interval=30): 136 | """Runs checks that are currently registred. 137 | 138 | `check_interval` specifies how often the checks are run. 139 | The default is every 30 seconds. 140 | """ 141 | while True: 142 | for check_name, check_data in self.check_functions.items(): 143 | try: 144 | check_data['check_fn']() 145 | except ReportCrash, e: 146 | if self._should_send_notification(check_data): 147 | self._send_notification(check_name, str(e)) 148 | check_data['last_notifcation'] = datetime.utcnow() 149 | except: 150 | self._send_notification(check_name, 'Crash checker crashed!') 151 | raise 152 | time.sleep(check_interval) 153 | 154 | #--- Private ---------------------------------------------- 155 | def _send_notification(self, name, crash_message): 156 | status = self.sender.send_notification(name, crash_message) 157 | print 'Sending error report [%s] %s' % (name, crash_message) 158 | print 'Status: %s' % status 159 | print '--------' 160 | 161 | def _should_send_notification(self, check_data): 162 | last_notifcation = check_data.get('last_notifcation') 163 | if not last_notifcation: 164 | return True 165 | else: 166 | n_ago = datetime.utcnow() - timedelta(seconds=check_data['notify_every']) 167 | if n_ago > last_notifcation: 168 | return True 169 | else: 170 | return False 171 | 172 | 173 | class CommonChecks: 174 | 175 | @staticmethod 176 | def website_check(url): 177 | code = 'UNKNOWN' 178 | 179 | try: 180 | fp = urllib.urlopen(url) 181 | code = fp.getcode() 182 | except: 183 | pass 184 | 185 | if code not in [200, 302]: 186 | raise ReportCrash('Could not open %s. Error code was %s.' % (url, code)) 187 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2007 Qtrac Ltd. All rights reserved. 3 | # This module is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or (at 6 | # your option) any later version. 7 | 8 | import os 9 | 10 | from setuptools import setup 11 | 12 | setup(name='crash_hound', 13 | version = '2.2', 14 | author="amix", 15 | author_email="amix@amix.dk", 16 | url="http://www.amix.dk/", 17 | classifiers=[ 18 | "Development Status :: 5 - Production/Stable", 19 | "Intended Audience :: Developers", 20 | "License :: OSI Approved :: BSD License", 21 | "Operating System :: OS Independent", 22 | "Programming Language :: Python", 23 | "Topic :: Software Development :: Libraries :: Python Modules", 24 | ], 25 | packages=['crash_hound', 'test'], 26 | platforms=["Any"], 27 | license="BSD", 28 | keywords='tropo crash hound notification sms crash reports monitoring', 29 | description="Monitor anything and get notifications directly on your iPhone", 30 | long_description="""\ 31 | crash_hound 32 | --------------- 33 | 34 | Crash Hound lets you script monitoring and lets you receive notifications directly on your mobile phone. 35 | 36 | It works via Tropo and a http://tropo.com acocunt is required. 37 | 38 | For more information check out: 39 | 40 | * http://amix.dk/blog/post/19637#Monitor-anything-and-get-SMS-notifications 41 | * http://amix.dk/blog/post/19625#International-SMS-messaging-The-cheap-way 42 | 43 | To install it do following: 44 | 45 | sudo easy_install crash_hound 46 | 47 | Examples 48 | ---------- 49 | 50 | Registers:: 51 | 52 | from crash_hound import CrashHound, ReportCrash, CommonChecks, SenderTropo 53 | 54 | def check_fn(): 55 | if 42: 56 | raise ReportCrash('42 is true!') 57 | else: 58 | pass #Ignore 59 | 60 | crash_sender = SenderTropo('YOUR TROPO.COM API KEY', 61 | '+56 ... YOUR MOBILE NUMBER ...') 62 | 63 | crash_checker = CrashHound(crash_sender) 64 | 65 | crash_checker.register_check('42 Checker', 66 | check_fn, 67 | notify_every=60) 68 | 69 | crash_checker.register_check('Google.com Blah test', 70 | lambda: CommonChecks.website_check('http://google.com/blah'), 71 | notify_every=60) 72 | 73 | crash_checker.run_checks(check_interval=10) 74 | 75 | Copyright: 2011 by amix 76 | License: BSD.""") 77 | -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 1 | from crash_hound import CrashHound, ReportCrash, CommonChecks, SenderTropo 2 | 3 | def check_fn(): 4 | if 42: 5 | raise ReportCrash('42 is true!') 6 | else: 7 | pass #Ignore 8 | 9 | 10 | #--- Configure sender and checker ---------------------------------------------- 11 | crash_sender = SenderTropo('YOUR TROPO.COM API KEY', 12 | '+56 ... YOUR MOBILE NUMBER ...') 13 | 14 | crash_checker = CrashHound(crash_sender) 15 | 16 | crash_checker.register_check('42 Checker', 17 | check_fn, 18 | notify_every=60) 19 | 20 | crash_checker.register_check('Google.com Blah test', 21 | lambda: CommonChecks.website_check('http://google.com/blah'), 22 | notify_every=60) 23 | 24 | crash_checker.run_checks(check_interval=10) 25 | --------------------------------------------------------------------------------