mirror of
https://github.com/sprockets/sprockets.logging.git
synced 2024-11-28 19:29:53 +00:00
177 lines
5.8 KiB
Python
177 lines
5.8 KiB
Python
"""
|
|
Make good log output easier.
|
|
|
|
- :class:`ContextFilter` adds fixed properties to a log record
|
|
- :class:`JSONRequestFormatter` formats log records as JSON output
|
|
- :method:`tornado_log_function` is for use as the
|
|
:class`tornado.web.Application.log_function` in conjunction with
|
|
:class:`JSONRequestFormatter` to output log lines as JSON.
|
|
|
|
"""
|
|
from __future__ import absolute_import
|
|
|
|
from logging import config
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
import traceback
|
|
|
|
try:
|
|
from tornado import escape, log
|
|
except ImportError: # pragma no cover
|
|
escape = None
|
|
log = None
|
|
|
|
version_info = (1, 3, 0)
|
|
__version__ = '.'.join(str(v) for v in version_info)
|
|
|
|
# Shortcut methods and constants to avoid needing to import logging directly
|
|
dictConfig = config.dictConfig
|
|
getLogger = logging.getLogger
|
|
|
|
DEBUG = logging.DEBUG
|
|
INFO = logging.INFO
|
|
WARN = logging.WARN
|
|
WARNING = logging.WARNING
|
|
ERROR = logging.ERROR
|
|
|
|
|
|
class ContextFilter(logging.Filter):
|
|
"""
|
|
Ensures that properties exist on a LogRecord.
|
|
|
|
:param list|None properties: optional list of properties that
|
|
will be added to LogRecord instances if they are missing
|
|
|
|
This filter implementation will ensure that a set of properties
|
|
exists on every log record which means that you can always refer
|
|
to custom properties in a format string. Without this, referring
|
|
to a property that is not explicitly passed in will result in an
|
|
ugly ``KeyError`` exception.
|
|
|
|
"""
|
|
|
|
def __init__(self, name='', properties=None):
|
|
logging.Filter.__init__(self, name)
|
|
self.properties = list(properties) if properties else []
|
|
|
|
def filter(self, record):
|
|
for property_name in self.properties:
|
|
if not hasattr(record, property_name):
|
|
setattr(record, property_name, None)
|
|
return True
|
|
|
|
|
|
class JSONRequestFormatter(logging.Formatter):
|
|
"""Instead of spitting out a "human readable" log line, this outputs
|
|
the log data as JSON.
|
|
|
|
"""
|
|
|
|
def extract_exc_record(self, typ, val, tb):
|
|
"""Create a JSON representation of the traceback given the records
|
|
exc_info
|
|
|
|
:param `Exception` typ: Exception type of the exception being handled
|
|
:param `Exception` instance val: instance of the Exception class
|
|
:param `traceback` tb: traceback object with the call stack
|
|
|
|
:rtype: dict
|
|
|
|
"""
|
|
exc_record = {'type': typ.__name__,
|
|
'message': str(val),
|
|
'stack': []}
|
|
for file_name, line_no, func_name, txt in traceback.extract_tb(tb):
|
|
exc_record['stack'].append({'file': file_name,
|
|
'line': str(line_no),
|
|
'func': func_name,
|
|
'text': txt})
|
|
return exc_record
|
|
|
|
def format(self, record):
|
|
"""Return the log data as JSON
|
|
|
|
:param record logging.LogRecord: The record to format
|
|
:rtype: str
|
|
|
|
"""
|
|
if hasattr(record, 'exc_info'):
|
|
try:
|
|
traceback = self.extract_exc_record(*record.exc_info)
|
|
except:
|
|
traceback = None
|
|
|
|
output = {'name': record.name,
|
|
'module': record.module,
|
|
'message': record.msg % record.args,
|
|
'level': logging.getLevelName(record.levelno),
|
|
'line_number': record.lineno,
|
|
'process': record.processName,
|
|
'timestamp': self.formatTime(record),
|
|
'thread': record.threadName,
|
|
'file': record.filename,
|
|
'request': record.args,
|
|
'traceback': traceback}
|
|
for key, value in list(output.items()):
|
|
if not value:
|
|
del output[key]
|
|
if 'message' in output:
|
|
output.pop('request', None)
|
|
return json.dumps(output)
|
|
|
|
|
|
def tornado_log_function(handler):
|
|
"""Assigned when creating a :py:class:`tornado.web.Application` instance
|
|
by passing the method as the ``log_function`` argument:
|
|
|
|
.. code:: python
|
|
|
|
app = tornado.web.Application([('/', RequestHandler)],
|
|
log_function=tornado_log_function)
|
|
|
|
:type handler: :py:class:`tornado.web.RequestHandler`
|
|
|
|
"""
|
|
status_code = handler.get_status()
|
|
if status_code < 400:
|
|
log_method = log.access_log.info
|
|
elif status_code < 500:
|
|
log_method = log.access_log.warning
|
|
else:
|
|
log_method = log.access_log.error
|
|
correlation_id = (getattr(handler, 'correlation_id', None) or
|
|
handler.request.headers.get('Correlation-ID', None))
|
|
log_method('', {'correlation_id': correlation_id,
|
|
'duration': 1000.0 * handler.request.request_time(),
|
|
'headers': handler.request.headers,
|
|
'method': handler.request.method,
|
|
'path': handler.request.path,
|
|
'protocol': handler.request.protocol,
|
|
'query_args': escape.recursive_unicode(
|
|
handler.request.query_arguments),
|
|
'remote_ip': handler.request.remote_ip,
|
|
'status_code': status_code,
|
|
'environment': os.environ.get('ENVIRONMENT')})
|
|
|
|
|
|
def currentframe():
|
|
"""Return the frame object for the caller's stack frame."""
|
|
try:
|
|
raise Exception
|
|
except:
|
|
traceback = sys.exc_info()[2]
|
|
frame = traceback.tb_frame
|
|
while True:
|
|
if hasattr(frame, 'f_code'):
|
|
filename = frame.f_code.co_filename
|
|
if filename.endswith('logging.py') or \
|
|
filename.endswith('logging/__init__.py'):
|
|
frame = frame.f_back
|
|
continue
|
|
return frame
|
|
return traceback.tb_frame.f_back
|
|
|
|
# Monkey-patch currentframe
|
|
logging.currentframe = currentframe
|