Package eggbasket :: Package controllers :: Module errorcatcher
[hide private]

Source Code for Module eggbasket.controllers.errorcatcher

  1  """TurboGears root controller base class catching errors, displaying custom error pages and sending email notifications.""" 
  2  # -*- coding: UTF-8 -*- 
  3  # errorhandling.py 
  4   
  5  import datetime 
  6  import logging 
  7  import socket 
  8  import StringIO 
  9  import traceback 
 10   
 11  import cherrypy 
 12  from turbogears import config, controllers, identity, util 
 13   
 14  try: 
 15      import turbomail 
 16      has_turbomail = True 
 17  except ImportError: 
 18      has_turbomail = False 
 19      from email.mime.multipart import MIMEMultipart 
 20      from email.mime.text import MIMEText 
 21      from email.Utils import formatdate 
 22   
 23  __all__ = ['ErrorCatcher'] 
 24   
 25  log = logging.getLogger("turbogears.controllers") 
 26   
 27   
28 -def format_request_info(req):
29 """Return string with formatted info about important request properties.""" 30 data = [] 31 for key in ['remote_host', 'remote_addr', 'remote_port', 'requestLine', 32 'protocol', 'method', 'query_string', 'browser_url', 'body']: 33 value = getattr(req, key, None) 34 if value: 35 data.append(u'%s: %r' % (key, value)) 36 return u'\n'.join(data)
37
38 -def format_request_headers(req):
39 """Return string with formatted request headers.""" 40 data = [] 41 for key, value in req.header_list: 42 data.append(u'%s: %r' % (key, value)) 43 return u'\n'.join(data)
44
45 -def format_user_info(user):
46 """Return string with formatted info about request user.""" 47 data = [] 48 if user: 49 for key in ['user_id', 'user_name', 'display_name', 'email_address']: 50 value = getattr(user, key, None) 51 if value: 52 data.append(u'%s: %r' % (key, value)) 53 else: 54 data.append(u'Anonymous user') 55 return u'\n'.join(data)
56
57 -class ErrorCatcher(controllers.RootController):
58 """Base class for root controllers catching errors and showing error page. 59 60 To use enable custom error pages, the root controller must subclass the 61 ``ErrorCatcher`` class and the CherryPy filter hook must be enabled by 62 setting ``error_catcher.on = True`` in the deployment configuration. 63 64 When the error catcher is enabled and an HTML error (including an 65 unhandled exception) occurs in the controller, an error page is displayed 66 using a template, whose name is looked up in the ``_error_page_templates`` 67 class attribute by the HTML status code. 68 69 Currently, there are default templates for the status codes 401, 403 and 70 404, called ``401_error``, ``403_error`` and ``404_error`` resp. and 71 ``unhandled_exception`` for all other errors. The templates are searched 72 in the ``templates`` sub-package of the application. 73 74 Also, if ``mail.on`` is ``True`` sends an email to the admin, 75 when an error occurs. No email is sent if the HTML status code is 76 contained in the list set by the option ``error_catcher.no_email_on``. 77 The default is not to send emails for 401, 403 and 404 errors. 78 79 For email sending to work, at least the configuration options 80 'error_catcher.sender_email' and 'error_catcher.admin_email' must be 81 set to valid email addresses. 82 83 See the docstring for the method 'send_exception_email' for more email 84 related configuration information. 85 86 """ 87 _error_codes = { 88 None: u'Unknown Error', 89 400: u'400 - Bad Request', 90 401: u'401 - Unauthorized', 91 403: u'403 - Forbidden', 92 404: u'404 - Not Found', 93 500: u'500 - Internal Server Error', 94 501: u'501 - Not Implemented', 95 502: u'502 - Bad Gateway', 96 } 97 _error_page_templates = { 98 None: '.templates.unhandled_exception', 99 401: '.templates.401_error', 100 403: '.templates.403_error', 101 404: '.templates.404_error', 102 } 103 _error_mail_templates = { 104 None: 'cheetah:.templates.email_unhandled_exception', 105 } 106 admin_group_name = 'admin' 107
108 - def __init__(self, *args, **kw):
109 super(ErrorCatcher, self).__init__(*args, **kw) 110 self.sender_email = config.get('error_catcher.sender_email') 111 self.admin_email = config.get('error_catcher.admin_email') 112 self.smtp_server = config.get('mail.server', 'localhost') 113 self.smtp_username = config.get('mail.username') 114 self.smtp_password = config.get('mail.password') 115 self.no_email_on = config.get('error_catcher.no_email_on', 116 (401, 403, 404))
117
118 - def cp_on_http_error(self, status, message):
119 """Handle HTTP errors by sending an error page and email.""" 120 try: 121 cherrypy._cputil._cp_on_http_error(status, message) 122 error_msg = self._get_error_message(status, message) 123 url = cherrypy.request.requestLine 124 log.exception("CherryPy %s error (%s) for request '%s'", status, 125 error_msg, url) 126 127 # get exception traceback 128 buf = StringIO.StringIO() 129 traceback.print_exc(file=buf) 130 exc_traceback = buf.getvalue() 131 buf.close() 132 133 # get request info 134 req = cherrypy.request 135 data = dict( 136 error_msg = error_msg, 137 header_info = format_request_headers(req), 138 is_admin = identity.in_group(self.admin_group_name), 139 message = message, 140 request_info = format_request_info(req), 141 server = req.headers.get('host', socket.getfqdn()), 142 status = status, 143 url = url, 144 timestamp = datetime.datetime.now(), 145 traceback = exc_traceback, 146 user_info = format_user_info(identity.current.user), 147 ) 148 149 if config.get('mail.on') and status not in self.no_email_on: 150 try: 151 self.send_exception_email(**data) 152 data['email_sent'] = True 153 except Exception, exc: 154 log.exception('Error email failed: %s', exc) 155 data['email_sent'] = False 156 else: 157 data['email_sent'] = False 158 159 self.send_error_page(**data) 160 # don't catch SystemExit 161 except StandardError, exc: 162 log.exception('Error handler failed: %s', exc)
163 164 # Hook in error handler if enabled in configuration 165 if config.get('error_catcher.on', False): 166 _cp_on_http_error = cp_on_http_error 167
168 - def send_error_page(self, **data):
169 """Render error page using template looked up in self._error_templates. 170 """ 171 template = self._error_page_templates.get(data['status'], 172 self._error_page_templates.get(None)) 173 body = self._render_error_template(template, data=data) 174 cherrypy.response.headers['Content-Length'] = len(body) 175 cherrypy.response.body = body
176
177 - def send_exception_email(self, **data):
178 """Send an email with the error info to the admin. 179 180 Uses TurboMail if installed and activated, otherwise tries to send 181 email with the ``smtplib`` module. The SMTP settings can be configured 182 with the following settings: 183 184 ``mail.server`` - Mail server to connect to (default 'localhost'). 185 ``mail.username`` - User name for SMTP authentication. If the value 186 is unset or evaluates to False no SMTP login is 187 performed. 188 ``mail.password`` - Password for SMTP authentication. may be an empty 189 string. 190 191 See also the class docstring for information on setting the 192 sender and recipient address. 193 194 """ 195 if not self.sender_email or not self.admin_email: 196 msg = ('Configuration error: could not send error notification ' 197 'because sender and/or admin email address is not set.') 198 log.exception(msg) 199 raise RuntimeError(msg) 200 201 template = self._error_mail_templates.get(data['status'], 202 self._error_mail_templates.get(None)) 203 subject = '%(status)s ERROR on server %(server)s' % data 204 body = self._render_error_template(template, 'plain', 'text/plain', 205 data) 206 207 if has_turbomail: 208 msg = turbomail.Message( 209 self.sender_email, self.admin_email, subject) 210 msg.plain = body 211 turbomail.enqueue(msg) 212 else: 213 msg = MIMEMultipart() 214 msg['From'] = self.sender_email 215 msg['To'] = self.admin_email 216 msg['Date'] = formatdate(localtime=True) 217 msg['Subject'] = subject 218 msg.attach(MIMEText(body)) 219 self._send_email_by_smtp(self.sender_email, self.admin_email, 220 msg.as_string())
221
222 - def _send_email_by_smtp(self, from_addr, to_addr, message):
223 """Send email via SMTP.""" 224 import smtplib 225 smtp = smtplib.SMTP(self.smtp_server) 226 if self.smtp_username and self.smtp_password is not None: 227 smtp.login(self.smtp_username, self.smtp_passwordd) 228 smtp.sendmail(from_addr, to_addr, message) 229 smtp.close()
230
231 - def _get_error_message(self, status, default=None):
232 """Return string error for HTTP status code.""" 233 return self._error_codes.get(status, default or self._error_codes[None])
234
235 - def _render_error_template(self, template, format='html', 236 content_type='text/html', data={}):
237 if ':' in template: 238 prefix, template = template.split(':', 1) 239 prefix += ':' 240 else: 241 prefix = '' 242 if template.startswith('.'): 243 package = util.get_package_name() 244 else: 245 package = '' 246 template = "%s%s%s" % (prefix, package, template) 247 return controllers._process_output(data, template, format, 248 content_type, None)
249