1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
|
# Copyright (c) 2007-2015 L. C. Rees. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. Neither the name of the Portable Site Information Project nor the names
# of its contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
'''WSGI logging and event reporting middleware.'''
import pdb
import sys
import logging
from cgitb import html
from logging.handlers import HTTPHandler, SysLogHandler
from logging.handlers import TimedRotatingFileHandler, SMTPHandler
__all__ = ['WsgiLog', 'log']
# File rotation constants
BACKUPS = 1
INTERVAL = 'h'
SPACING = 1
# Default logger name (should be changed)
LOGNAME = 'wsgilog.log'
# Default 'environ' entries
CATCHID = 'wsgilog.catch'
LOGGERID = 'wsgilog.logger'
# Current proposed 'environ' key signalling no middleware exception handling
THROWERR = 'x-wsgiorg.throw_errors'
# HTTP error messages
HTTPMSG = '500 Internal Error'
ERRORMSG = 'Server got itself in trouble'
# Default log formats
DATEFORMAT = '%a, %d %b %Y %H:%M:%S'
LOGFORMAT = '%(name)s: %(asctime)s %(levelname)-4s %(message)s'
def _errapp(environ, start_response):
'''Default error handling WSGI application.'''
start_response(HTTPMSG, [('Content-type', 'text/plain')], sys.exc_info())
return [ERRORMSG]
def log(**kw):
'''Decorator for logging middleware.'''
def decorator(application):
return WsgiLog(application, **kw)
return decorator
class LogStdout(object):
'''File-like object for sending stdout output to a logger.'''
def __init__(self, logger, level=logging.DEBUG):
# Set logger level
if level == logging.DEBUG:
self.logger = logger.debug
elif level == logging.CRITICAL:
self.logger = logger.critical
elif level == logging.ERROR:
self.logger = logger.warning
elif level == logging.WARNING:
self.logger = logger.warning
elif level == logging.INFO:
self.logger = logger.info
def write(self, info):
'''Writes non-whitespace strings to logger.'''
if info.lstrip().rstrip() != '':
self.logger(info)
class WsgiLog(object):
'''Class for WSGI logging and event recording middleware.'''
def __init__(self, application, **kw):
self.application = application
# Error handling WSGI app
self._errapp = kw.get('errapp', _errapp)
# Flag controlling logging
self.log = kw.get('log', True)
# Log if set
if self.log:
# Log error message
self.message = kw.get('logmessage', ERRORMSG)
# Individual logger for WSGI app with custom name 'logname'
self.logger = logging.getLogger(kw.get('logname', LOGNAME))
# Set logger level
self.logger.setLevel(kw.get('loglevel', logging.DEBUG))
# Coroutine for setting individual log handlers
def setlog(logger):
logger.setFormatter(logging.Formatter(
# Log entry format
kw.get('logformat', LOGFORMAT),
# Date format
kw.get('datefmt', DATEFORMAT)
))
self.logger.addHandler(logger)
# Log to STDOUT
if 'tostream' in kw:
setlog(logging.StreamHandler())
# Log to a rotating file that with periodic backup deletions
if 'tofile' in kw:
setlog(TimedRotatingFileHandler(
# Log file path
kw.get('file', LOGNAME),
# Interval to backup log file
kw.get('interval', INTERVAL),
# pacing between interval units
kw.get('spacing', SPACING),
# Number of backups to keep
kw.get('backups', BACKUPS)
))
# Send log entries to an email address
if 'toemail' in kw:
setlog(SMTPHandler(
# Mail server
kw.get('mailserver'),
# From email address
kw.get('frommail'),
# To email address
kw.get('toemail'),
# Email subject
kw.get('mailsubject')
))
# Send log entries to a web server
if 'tohttp' in kw:
setlog(HTTPHandler(
# Web server host
kw.get('httphost'),
# Web URL
kw.get('httpurl'),
# HTTP method
kw.get('httpmethod', 'GET')
))
# Log to syslog
if 'tosyslog' in kw:
setlog(SysLogHandler(
# syslog host
kw.get('syshost', ('localhost', 514)),
# syslog user
kw.get('facility', 'LOG_USER')
))
assert self.logger.handlers, 'At least one logging handler needed'
# Redirect STDOUT to the logger
if 'toprint' in kw:
sys.stdout = LogStdout(
self.logger,
# Sets log level STDOUT is displayed under
kw.get('prnlevel', logging.DEBUG)
)
# Flag for turning on PDB in situ
self.debug = kw.get('debug', False)
# Flag for sending HTML-formatted exception tracebacks to the browser
self.tohtml = kw.get('tohtml', False)
# Write HTML-formatted exception tracebacks to a file if provided
self.htmlfile = kw.get('htmlfile')
def __call__(self, environ, start_response):
# Make logger available to other WSGI apps/middlware
if self.log:
environ[LOGGERID] = self.logger
# Make catch method available to other WSGI apps/middleware
environ[CATCHID] = self.catch
# Let exceptions "bubble up" to WSGI server/gateway
if THROWERR in environ:
return self.application(environ, start_response)
# Try application
try:
return self.application(environ, start_response)
# Log and/or report any errors
except:
return self.catch(environ, start_response)
def catch(self, environ, start_response):
'''Exception catcher.'''
# Log exception
if self.log:
self.logger.exception(self.message)
# Debug
if self.debug:
pdb.pm()
# Write HTML-formatted exception tracebacks to a file
if self.htmlfile is not None:
open(self.htmlfile, 'wb').write(html(sys.exc_info()))
# Send HTML-formatted exception tracebacks to the browser
if self.tohtml:
start_response(HTTPMSG, [('Content-type', 'text/html')])
return [html(sys.exc_info())]
# Return error handler
return self._errapp(environ, start_response)
|