Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
hub-client / dockerhub / logger.py
Size: Mime:
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)