# -*- coding: utf-8 -*-
"""Provides a drop in solution for logging json messages in a standard format.
An example of what a json log entry will look like::
{
"server_hostname": "localhost",
"event_timestamp": "2015-10-31T01:01:01.42Z",
"event_name": "foo.event",
"log_level": "INFO",
"application_name": "fizzbuzz",
"fields": {
"custom-field": 42,
"other-custom-thing": "fizzle"
}
}
server_hostname, event_timestamp, event_name, log_level,
application_name, and fields will be present in every log entry.
"""
import os
import json
import socket
import logging
import logging.handlers
import traceback
import datetime
import functools
__author__ = 'zeeto.io'
__version__ = '0.1.3'
default_date_fmt = '%Y-%m-%dT%H:%M:%S.%fZ'
level_map = {
'emergency': ('critical', 'EMERGENCY'),
'alert': ('info', 'ALERT'),
'notice': ('info', 'NOTICE'),
'info': ('info', 'INFO'),
'warning': ('warning', 'WARNING'),
'error': ('error', 'ERROR'),
'critical': ('critical', 'CRITICAL'),
'debug': ('debug', 'DEBUG')
}
"""check out the level_map"""
def _default_json_default(obj):
""" Coerce everything to strings.
All objects representing time get output according to default_date_fmt.
"""
if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)):
return obj.strftime(default_date_fmt)
else:
return str(obj)
[docs]class LogTest(object):
"""Utility class to help testing applications that rely on pyzlog.
Intended to be a mixin for your test classes. If you don't pass path
and target to all the helper methods, be sure to set path and target
properties on your class.
"""
[docs] def remove_log(self, path=None, target=None):
"""remove the specified log file.
Generally called in setUp and tearDown methods to ensure
isolation. If path or target are not specified, will default to
path and target properties on the object.
:param path: path to find the log file
:param target: name of the log file
:type path: string
:type target: string
"""
path = path if path is not None else self.path
target = target if target is not None else self.target
try:
os.remove(os.path.abspath(os.path.join(path, target)))
except OSError:
pass
[docs] def get_log_messages(self, path=None, target=None):
"""fetch all log entries in the given file
Intended to be used to assert that the expected entries were
written out to the correct log file. If path or target are not
specified, will default to path and target properties on the
object.
:param path: path to find the log file
:param target: name of the log file
:type path: string
:type target: string
"""
path = path if path is not None else self.path
target = target if target is not None else self.target
with open(os.path.abspath(os.path.join(path, target))) as f:
return f.readlines()
[docs] def init_logs(self, path=None, target=None, level=None,
server_hostname=None, extra=None):
"""Simple canned way to initialize pyzlog.
Initialize pyslog for tests. If path or target are not
specified, will default to path and target properties on the
object. leve will default to logging.DEBUG, server_hostname
defaults to localhost, and extra defaults to {'extra': None}
:param path: path to find the log file
:param target: name of the log file
:param level: log level for this instance
:param server_hostname: hostname to put in each entry
:param extra: whitelist/defaults of extra fields to add to each entry
:type path: string
:type target: string
:type level: int
:type server_hostname: string
:type extra: dict
"""
path = path if path is not None else self.path
target = target if target is not None else self.target
level = level if level is not None else logging.DEBUG
server_hostname = (server_hostname if server_hostname is not None
else 'localhost')
extra = extra if extra is not None else {'extra': None}
init_logs(path=path, target=target, level=level,
server_hostname=server_hostname, fields=extra)
[docs]def init_logs(path=None,
target=None,
logger_name='root',
level=logging.DEBUG,
maxBytes=1*1024*1024,
backupCount=5,
application_name='default',
server_hostname=None,
fields=None):
"""Initialize the zlogger.
Sets up a rotating file handler to the specified path and file with
the given size and backup count limits, sets the default
application_name, server_hostname, and default/whitelist fields.
:param path: path to write the log file
:param target: name of the log file
:param logger_name: name of the logger (defaults to root)
:param level: log level for this logger (defaults to logging.DEBUG)
:param maxBytes: size of the file before rotation (default 1MB)
:param application_name: app name to add to each log entry
:param server_hostname: hostname to add to each log entry
:param fields: default/whitelist fields.
:type path: string
:type target: string
:type logger_name: string
:type level: int
:type maxBytes: int
:type backupCount: int
:type application_name: string
:type server_hostname: string
:type fields: dict
"""
log_file = os.path.abspath(
os.path.join(path, target))
logger = logging.getLogger(logger_name)
logger.setLevel(level)
handler = logging.handlers.RotatingFileHandler(
log_file, maxBytes=maxBytes, backupCount=backupCount)
handler.setLevel(level)
handler.setFormatter(
JsonFormatter(
application_name=application_name,
server_hostname=server_hostname,
fields=fields))
logger.addHandler(handler)
def _log(logger_name='root', event_name=None,
_type=None, exc_info=False, extra=None):
extra = extra.copy() if extra else {}
method, log_level = level_map.get(_type, ('info', 'INFO'))
extra.update(event_name=event_name, log_level=log_level)
getattr(logging.getLogger(logger_name), method)(
'', exc_info=exc_info, extra=extra)
def _log_fn(exc_info=False):
def wrap(logfunc):
@functools.wraps(logfunc)
def wrapped(*args, **kwargs):
kwargs.update(_type=logfunc.__name__, exc_info=exc_info)
return _log(*args, **kwargs)
return wrapped
return wrap
@_log_fn()
[docs]def emergency(**kwargs):
"""log with pyzlog level EMERGENCY"""
pass
@_log_fn()
[docs]def alert(**kwargs):
"""log with pyzlog level ALERT"""
pass
@_log_fn()
[docs]def notice(**kwargs):
"""log with pyzlog level NOTICE"""
pass
@_log_fn()
[docs]def info(**kwargs):
"""log with pyzlog level INFO"""
pass
@_log_fn()
[docs]def warning(**kwargs):
"""log with pyzlog level WARNING"""
pass
@_log_fn(exc_info=True)
[docs]def error(**kwargs):
"""log with pyzlog level ERROR
exception info is added if it exists
"""
pass
@_log_fn()
[docs]def critical(**kwargs):
"""log with pyzlog level CRITICAL"""
pass
@_log_fn()
[docs]def debug(**kwargs):
"""log with pyzlog level DEBUG"""
pass