Source code for gateone.core.log

# -*- coding: utf-8 -*-
#
#       Copyright 2013 Liftoff Software Corporation

# Meta
__license__ = "AGPLv3 or Proprietary (see LICENSE.txt)"
__doc__ = """
.. _log.py:

Logging Module for Gate One
===========================

This module contains a number of pre-defined loggers for use within Gate One:

    ==========  =============================================================
    Name        Description
    ==========  =============================================================
    go_log      Used for logging internal Gate One events.
    auth_log    Used for logging authentication and authorization events.
    msg_log     Used for logging messages sent to/from users.
    ==========  =============================================================

Applications may also use their own loggers for differentiation purposes.  Such
loggers should be prefixed with 'gateone.app.' like so::

    >>> import logging
    >>> logger = logging.getLogger("gateone.app.myapp")

Additional loggers may be defined within a `GOApplication` with additional
prefixing::

    >>> xfer_log = logging.getLogger("gateone.app.myapp.xfer")
    >>> lookup_log = logging.getLogger("gateone.app.myapp.lookup")

.. note::

    This module does not cover session logging within the Terminal application.
    That is a built-in feature of the `termio` module.
"""

import os.path, sys, logging, json
from .utils import mkdir_p
from tornado.options import options
from tornado.log import LogFormatter

LOGS = set() # Holds a list of all our log paths so we can fix permissions
# These should match what's in the syslog module (hopefully not platform-dependent)
FACILITIES = {
    'auth': 32,
    'cron': 72,
    'daemon': 24,
    'kern': 0,
    'local0': 128,
    'local1': 136,
    'local2': 144,
    'local3': 152,
    'local4': 160,
    'local5': 168,
    'local6': 176,
    'local7': 184,
    'lpr': 48,
    'mail': 16,
    'news': 56,
    'syslog': 40,
    'user': 8,
    'uucp': 64
}

# Exceptions
[docs]class UnknownFacility(Exception): """ Raised if `string_to_syslog_facility` is given a string that doesn't match a known syslog facility. """ pass
[docs]class JSONAdapter(logging.LoggerAdapter): """ A `logging.LoggerAdapter` that prepends keyword argument information to log entries. Expects the passed in dict-like object which will be included. """ def process(self, msg, kwargs): extra = self.extra.copy() if 'metadata' in kwargs: extra.update(kwargs.pop('metadata')) if extra: json_data = json.dumps(extra, sort_keys=True, ensure_ascii=False) try: line = u'{json_data} {msg}'.format(json_data=json_data, msg=msg) except UnicodeDecodeError: line = u'{json_data} {msg}'.format( json_data=json_data, msg=repr(msg)) else: line = msg return (line, kwargs)
[docs]def string_to_syslog_facility(facility): """ Given a string (*facility*) such as, "daemon" returns the numeric syslog.LOG_* equivalent. """ if facility.lower() in FACILITIES: return FACILITIES[facility.lower()] else: raise UnknownFacility( "%s does not match a known syslog facility" % repr(facility))
[docs]def go_logger(name, **kwargs): """ Returns a new `logging.Logger` instance using the given *name* pre-configured to match Gate One's usual logging output. The given *name* will automatically be prefixed with 'gateone.' if it is not already. So if *name* is 'app.foo' the `~logging.Logger` would end up named 'gateone.app.foo'. If the given *name* is already prefixed with 'gateone.' it will be left as-is. The log will be saved in the same location as Gate One's configured `log_file_prefix` using the given *name* with the following convention: ``gateone/logs/<modified *name*>.log`` The file name will be modified like so: * It will have the 'gateone' portion removed (since it's redundant). * Dots will be replaced with dashes (-). Examples:: >>> auth_logger = go_logger('gateone.auth.terminal') >>> auth_logger.info('test1') >>> app_logger = go_logger('gateone.app.terminal') >>> app_logger.info('test2') >>> import os >>> os.lisdir('/opt/gateone/logs') ['auth.log', 'auth-terminal.log', 'app-terminal.log' 'webserver.log'] If any *kwargs* are given they will be JSON-encoded and included in the log message after the date/metadata like so:: >>> auth_logger.info('test3', {"user": "bob", "ip": "10.1.1.100"}) [I 130828 15:00:56 app.py:10] {"user": "bob", "ip": "10.1.1.100"} test3 """ logger = logging.getLogger(name) if '--help' in sys.argv: # Skip log file creation if the user is just getting help on the CLI return logger if not options.log_file_prefix or options.logging.upper() == 'NONE': # Logging is disabled but we still have to return the adapter so that # passing metadata to the logger won't throw exceptions return JSONAdapter(logger, kwargs) preserve = None # Save the stdout handler (because it looks nice =) if name == None: # root logger; make sure we save the pretty-printing stdout handler... for handler in logger.handlers: if not isinstance(handler, logging.handlers.RotatingFileHandler): preserve = handler # Remove any existing handlers on the logger logger.handlers = [] if preserve: # Add back the one we preserved (if any) logger.handlers.append(preserve) logger.setLevel(getattr(logging, options.logging.upper())) if options.log_file_prefix: if name: basepath = os.path.split(options.log_file_prefix)[0] filename = name.replace('.', '-') + '.log' path = os.path.join(basepath, filename) else: path = options.log_file_prefix basepath = os.path.split(options.log_file_prefix)[0] if not os.path.isdir(basepath): mkdir_p(basepath) LOGS.add(path) channel = logging.handlers.RotatingFileHandler( filename=path, maxBytes=options.log_file_max_size, backupCount=options.log_file_num_backups) channel.setFormatter(LogFormatter(color=False)) logger.addHandler(channel) logger = JSONAdapter(logger, kwargs) return logger