Repository URL to install this package:
|
Version:
0.32.0 ▾
|
import logging
import bugsnag
from django.conf import settings
from pythonjsonlogger import jsonlogger
BUGSNAG_EXTRA = 'extra'
class DockerJsonFormatter(jsonlogger.JsonFormatter):
"""
Implements the standard Docker JSON format and parsed by our
centralized logging infrastructure.
The 'service', 'version' and 'environment' are added to every log
message. The current implementation is tied to Django but it should be easy
to modify to handle any python logging.
"""
def process_log_record(self, log_record):
"""
This method can be overriden to implement custom logic around how
your service is configured.
The log_record might be a ordered dictionary.
"""
# We expect that all hub projects will follow this convention.
log_record['service'] = getattr(settings, 'SERVICE_NAME', 'Unknown Service')
log_record['version'] = getattr(settings, 'VERSION_NUMBER', 'Unknown Version')
log_record['environment'] = getattr(settings, 'RELEASE_STAGE', 'Unknown Environment')
return log_record
def patch_logging_logger():
"""
Patch the logging.Logger to:
1. Fix this Python bug: http://bugs.python.org/issue15541
This function fixes Python's Logger class so that its `exception` method
accepts the same arguments as `error` and other methods.
TODO: https://docker.atlassian.net/browse/HUB-619
remove #1 when we upgrade to python 2.7.9
2. Store the 'extra' dictionary as in in the log record.
This only applies to Logger objects created afterwards.
"""
def exception_with_kwargs(self, msg, *args, **kwargs):
self.error(msg, exc_info=1, *args, **kwargs)
def bugsnag_makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None):
res = self.makeRecord_orig(name, level, fn, lno, msg, args, exc_info, func, extra)
if extra is not None:
res.__dict__[BUGSNAG_EXTRA] = extra
return res
logging.Logger.exception_orig = logging.Logger.exception
logging.Logger.exception = exception_with_kwargs
logging.Logger.makeRecord_orig = logging.Logger.makeRecord
logging.Logger.makeRecord = bugsnag_makeRecord
class BugsnagHandler(logging.Handler, object):
"""
This is mostly copied from bugsnag.
See https://github.com/bugsnag/bugsnag-python/blob/master/bugsnag/handlers.py
SPECIAL BEHAVIOUR
-----------------
If the log record has a 'extra' attribute, it'll be handled specially:
- If extra is not a dictionary, it's ignored.
- If extra has a 'context' key, that value is set as the bugsnag context.
- Other key/value pairs in extra will show up under a "custom data" tab.
Note that our logging.Logger is patched to retain 'extra'.
So here's an example:
LOG.error("Godzilla attack!",
extra={'city': 'SF',
'damage': 'GG Bridge destroyed',
'context': 'Disaster!',
'year': '2015'})
The bugsnag context will show `Disaster!'.
In the "custom data" tab in bugsnag, you'll see:
* city: SF
* damage: GG Bridge destroyed
* year: 2015
"""
def __init__(self, api_key=None, extra_fields={}):
super(BugsnagHandler, self).__init__()
self.api_key = api_key
self.extra_fields = extra_fields
def emit(self, record):
# Severity is not a one-to-one mapping, as there are only
# a fixed number of severity levels available server side
if record.levelname.lower() in ['error', 'critical']:
severity = 'error'
elif record.levelname.lower() in ['warning', ]:
severity = 'warning'
else:
severity = 'info'
# Only extract a few specific fields, as we don't want to
# repeat data already being sent over the wire (such as exc)
record_fields = [
'asctime', 'created', 'levelname', 'levelno', 'msecs',
'name', 'process', 'processName', 'relativeCreated', 'thread',
'threadName', 'message', ]
extra_data = {}
for field in record_fields:
if hasattr(record, field):
extra_data[field] = getattr(record, field)
metadata = {"extra data": extra_data}
_handle_extra_metadata(record, metadata) # Docker special sauce
api_key = self.api_key or bugsnag.configuration.api_key
if record.exc_info:
bugsnag.notify(record.exc_info, severity=severity, meta_data=metadata, api_key=api_key)
else:
# Create exception type dynamically, to prevent bugsnag.handlers
# being prepended to the exception name due to class name
# detection in utils. Because we are messing with the module
# internals, we don't really want to expose this class anywhere
level_name = record.levelname if record.levelname else "Message"
exc_type = type('Log' + level_name, (Exception, ), {})
exc = exc_type(record.getMessage())
exc.__module__ = '__main__'
bugsnag.notify(exc, severity=severity, meta_data=metadata)
def _handle_extra_metadata(record, metadata):
extra = getattr(record, BUGSNAG_EXTRA, None)
if extra is None or not isinstance(extra, dict):
return
context = extra.pop('context', None)
if context is not None:
metadata['context'] = context
metadata.setdefault('custom data', {}).update(extra)