/dozer/logview.py

https://bitbucket.org/bbangert/dozer/ · Python · 174 lines · 159 code · 7 blank · 8 comment · 2 complexity · 7fd65c9719dc229d4be31559b4cbacdd MD5 · raw file

  1. import logging
  2. import os
  3. import re
  4. import time
  5. import itertools
  6. import traceback
  7. import sys
  8. from mako.lookup import TemplateLookup
  9. from paste import urlparser
  10. from paste.util.converters import asbool
  11. from webob import Request, Response
  12. from webob import exc
  13. try:
  14. import thread
  15. import threading
  16. except ImportError:
  17. thread = None
  18. here_dir = os.path.dirname(os.path.abspath(__file__))
  19. class Logview(object):
  20. def __init__(self, app, config=None, loglevel='DEBUG', **kwargs):
  21. """Stores logging statements per request, and includes a bar on
  22. the page that shows the logging statements
  23. ''loglevel''
  24. Default log level for messages that should be caught.
  25. Note: the root logger's log level also matters! If you do
  26. logging.getLogger('').setLevel(logging.INFO), no DEBUG messages
  27. will make it to Logview's handler anyway.
  28. Config can contain optional additional loggers and the colors
  29. they should be highlighted (in an ini file)::
  30. logview.sqlalchemy = #ff0000
  31. Or if passing a dict::
  32. app = Logview(app, {'logview.sqlalchemy':'#ff0000'})
  33. """
  34. self.app = app
  35. tmpl_dir = os.path.join(here_dir, 'templates')
  36. self.mako = TemplateLookup(directories=[tmpl_dir])
  37. self.log_colors = {}
  38. for key, val in itertools.chain(config.iteritems(),
  39. kwargs.iteritems()):
  40. if key.startswith('logview.'):
  41. self.log_colors[key[len('logview.'):]] = val
  42. self.traceback_colors = {}
  43. for key, val in itertools.chain(config.iteritems(),
  44. kwargs.iteritems()):
  45. if key.startswith('traceback.'):
  46. self.traceback_colors[key[len('traceback.'):]] = val
  47. self.logger = logging.getLogger(__name__)
  48. self.loglevel = getattr(logging, loglevel)
  49. self.keep_tracebacks = asbool(kwargs.get(
  50. 'keep_tracebacks', config.get(
  51. 'keep_tracebacks', RequestHandler.keep_tracebacks)))
  52. self.keep_tracebacks_limit = int(kwargs.get(
  53. 'keep_tracebacks_limit', config.get(
  54. 'keep_tracebacks_limit', RequestHandler.keep_tracebacks_limit)))
  55. self.skip_first_n_frames = int(kwargs.get(
  56. 'skip_first_n_frames', config.get(
  57. 'skip_first_n_frames', RequestHandler.skip_first_n_frames)))
  58. self.skip_last_n_frames = int(kwargs.get(
  59. 'skip_last_n_frames', config.get(
  60. 'skip_last_n_frames', RequestHandler.skip_last_n_frames)))
  61. reqhandler = RequestHandler()
  62. reqhandler.setLevel(self.loglevel)
  63. reqhandler.keep_tracebacks = self.keep_tracebacks
  64. reqhandler.keep_tracebacks_limit = self.keep_tracebacks_limit
  65. reqhandler.skip_first_n_frames = self.skip_first_n_frames
  66. reqhandler.skip_last_n_frames = self.skip_last_n_frames
  67. logging.getLogger('').addHandler(reqhandler)
  68. self.reqhandler = reqhandler
  69. def __call__(self, environ, start_response):
  70. if thread:
  71. tok = thread.get_ident()
  72. else:
  73. tok = None
  74. req = Request(environ)
  75. start = time.time()
  76. self.logger.log(self.loglevel, 'request started')
  77. response = req.get_response(self.app)
  78. self.logger.log(self.loglevel, 'request finished')
  79. tottime = time.time() - start
  80. reqlogs = self.reqhandler.pop_events(tok)
  81. if 'content-type' in response.headers and \
  82. response.headers['content-type'].startswith('text/html'):
  83. logbar = self.render('/logbar.mako', events=reqlogs,
  84. logcolors=self.log_colors,
  85. traceback_colors=self.traceback_colors,
  86. tottime=tottime, start=start)
  87. logbar = logbar.encode('ascii', 'xmlcharrefreplace')
  88. parts = re.split(r'(<body[^>]*>)', response.body)
  89. # parts = ['preamble', '<body ...>', 'text'] or just ['text']
  90. # we want to insert our logbar after <body> (if it exists) and
  91. # in front of text
  92. response.body = ''.join(parts[:-1] + [logbar] + parts[-1:])
  93. return response(environ, start_response)
  94. def render(self, name, **vars):
  95. tmpl = self.mako.get_template(name)
  96. return tmpl.render(**vars)
  97. class RequestHandler(logging.Handler):
  98. """
  99. A handler class which only records events if its set as active for
  100. a given thread/process (request). Log history per thread must be
  101. removed manually, preferably at the end of the request. A reference
  102. to the RequestHandler instance should be retained for this access.
  103. This handler otherwise works identically to a request-handler,
  104. except that events are logged to specific 'channels' based on
  105. thread id when available.
  106. """
  107. keep_tracebacks = False
  108. keep_tracebacks_limit = 20 # too many of these make things very very slow
  109. skip_first_n_frames = 0
  110. skip_last_n_frames = 6 # number of frames beween logger.log() and our emit()
  111. # determined empirically on Python 2.6
  112. def __init__(self):
  113. """Initialize the handler."""
  114. logging.Handler.__init__(self)
  115. self.buffer = {}
  116. def emit(self, record):
  117. """Emit a record.
  118. Append the record. If shouldFlush() tells us to, call flush() to process
  119. the buffer.
  120. """
  121. self.buffer.setdefault(record.thread, []).append(record)
  122. if self.keep_tracebacks and (not self.keep_tracebacks_limit or
  123. len(self.buffer[record.thread]) < self.keep_tracebacks_limit):
  124. f = sys._getframe(self.skip_last_n_frames)
  125. record.traceback = traceback.format_list(
  126. traceback.extract_stack(f)[self.skip_first_n_frames:])
  127. def pop_events(self, thread_id):
  128. """Return all the events logged for particular thread"""
  129. if thread_id in self.buffer:
  130. return self.buffer.pop(thread_id)
  131. else:
  132. return []
  133. def flush(self):
  134. """Kills all data in the buffer"""
  135. self.buffer = {}
  136. def close(self):
  137. """Close the handler.
  138. This version just flushes and chains to the parent class' close().
  139. """
  140. self.flush()
  141. logging.Handler.close(self)