Skip to content

Instantly share code, notes, and snippets.

@landonb
Created October 13, 2016 16:45
Show Gist options
  • Save landonb/7b40ca46125b37ed9d663a2f01d354d7 to your computer and use it in GitHub Desktop.
Save landonb/7b40ca46125b37ed9d663a2f01d354d7 to your computer and use it in GitHub Desktop.
# File: logging2.py
# Last Modified: 2016.10.13 /coding: utf-8
# Author: Landon Bouma (landonb @ retrosoft . com)
# Project Pages: https://github.com/landonb/*
# Summary: Python logging wrapper.
# Copyright: © 2008, 2015-2016 Landon Bouma.
# vim:tw=0:ts=4:sw=4:noet
# License: GPLv3. See LICENSE.txt.
# -------------------------------------------------------------------
# Summary: Wrap Python logging facility:
# - Print timestamp.
# - Print facility.
# - Wrap and columnize wide (long) lines.
# - Add convenience fcns., e.g., log.info(), etc.
# See: /usr/lib/python2.7/logging/__init__.py
# /usr/lib/python3.5/logging/__init__.py
# etc.
# Sample usage:
# $ py3
example_usage="""
cat > logging2_test.py << EOF
#!/usr/bin/env python3
import logging
import logging2
logging2.init_logging(
logging.DEBUG,
show_logger_name=True,
show_mod_func_line=False,
)
log = logging.getLogger('filename-or-service')
log.debug('A message')
log.warn('A warning')
log.fatal('Goodbye')
EOF
chmod 775 logging2_test.py
./logging2_test.py
DEBG|2016-Oct-13|Thu|11:42:33|filename-or-service | A message
WARN|2016-Oct-13|Thu|11:42:33|filename-or-service | A warning
CRIT|2016-Oct-13|Thu|11:42:33|filename-or-service | Goodbye
"""
fixme_mod_name="""
cat > logging2_test.py << EOF
#!/usr/bin/env python3
import logging
import logging2
logging2.init_logging(logging.DEBUG)
log = logging.getLogger('filename-or-service')
log.debug('A message')
EOF
chmod 775 logging2_test.py
./logging2_test.py
DEBG|2016-Oct-13|Thu|11:31:26|__init__.debug:1262| A message
FIXME: The logging module name is used (__init__.debug)
i.e., from /usr/lib/python3.4/logging/__init__.py
see: target_frame, below.
"""
import os
import sys
import logging
import logging.handlers
import inspect
#import string
import threading
import traceback
try:
import wx
import wx.lib.newevent
# Create wxPython event type.
wxLogEvent, EVT_WX_LOG_EVENT = wx.lib.newevent.NewEvent()
except ImportError:
pass
__all__ = [
#'My_Logger',
#'My_Handler',
'init_logging',
# SYNC_ME: Log levels.
'log',
'fatal',
'critical',
'error',
'warning',
'notice',
'debug',
'trace',
'verbose1',
'verbose2',
'verbose3',
'verbose4',
'verbose5',
'verbose',
'info',
#
'assert_soft',
]
"""
The Python logging library defines the following levels:
---Level--- ---Value---
CRITICAL 50
FATAL 50 [*aliases CRITICAL]
ERROR 40
WARNING 30 [*also WARN]
INFO 20
DEBUG 10
NOTSET 0
Some other loggers are more expressible and include additional
levels, such as 'notice' and 'verbose'. We add those levels to
the logger, as well as a handful of verbose sublevels. You can
feel comfortable adding lots of logging without worrying about
noise, since it's easily turned off.
---Level--- ---Value---
NOTICE 25
VERBOSE1 9
VERBOSE2 8
VERBOSE3 7
VERBOSE4 6
VERBOSE5 5
VERBOSE 5
# SYNC_ME: Log levels.
---Level--- ---Value---
FATAL 186
CRITICAL 50
FATAL 50 [*aliases CRITICAL]
ERROR 40
WARNING 30 [*also WARN]
NOTICE 25 ** NEW
INFO 20
TRACE 15 ** NEW
DEBUG 10
VERBOSE1 9 ** NEW
VERBOSE2 8 ** NEW
VERBOSE3 7 ** NEW
VERBOSE4 6 ** NEW
VERBOSE5 5 ** NEW
VERBOSE 5 ** NEW
NOTSET 0
Also, this script splits long messages into multiple lines and prefixes
each line except the first with a bunch of spaces, so that messages are
right-justified and don't fill the columns on the left, which are used to
show the timestamp, debug level, and logger name.
E.g.,
12:24:36 DEBUG item_user_access # A one-line message
12:34:56 DEBUG grax.grac_mgr # This is an example of a log message
# that spans two lines.
"""
# ***
# SYNC_ME: Log levels.
FATAL = 186
NOTICE = 25
TRACE = 15
VERBOSE1 = 9
VERBOSE2 = 8
VERBOSE3 = 7
VERBOSE4 = 6
VERBOSE5 = 5
VERBOSE = 5
# Aliases.
CRITICAL = logging.CRITICAL
FATAL = logging.FATAL
ERROR = logging.ERROR
WARNING = logging.WARNING
WARN = logging.WARN
INFO = logging.INFO
DEBUG = logging.DEBUG
NOTSET = logging.NOTSET
# ***
APACHE_REQUEST = None
# ***
def config_line_format(frmat_len, frmat_postfix, line_len=None, add_tid=False):
global msg_continuation_prefix
global line_len_log
global line_len_msg
global include_thread_id
if line_len == 0:
# Most terminals' widths are 80 chars, right?
line_len = 80
msg_continuation_prefix = (' ' * frmat_len) + frmat_postfix
line_len_log = line_len
include_thread_id = add_tid
if line_len_log is not None:
line_len_msg = line_len_log - len(msg_continuation_prefix)
else:
line_len_msg = None
# ***
if sys.version_info.major == 2:
make_string = lambda s: unicode(s)
else:
make_string = lambda s: str(s)
class My_Logger(logging.Logger):
def __init__(self, name, level=logging.NOTSET):
logging.Logger.__init__(self, name, level)
# C.f., e.g., /usr/lib64/python2.7/logging/__init__.py
def _log(self, level, msg, args, exc_info=None, extra=None):
global APACHE_REQUEST
global include_thread_id
global show_logger_name_
global show_mod_func_line_
# For multi-threaded apps, including the thread ID.
if include_thread_id:
# FIXME/MAYBE: Use Formatter's %(thread)s.
if APACHE_REQUEST is not None:
# You'll see the same parent process ID (and it's not 1)
# for all apache request threads (os.getppid()).
# You'll see lots of unique process IDs for each request,
# but some processes appear to handle multiple connections
# per process (os.getpid()).
# You'll find that each thread has a unique identifier. It
# doesn't matter if we use the cooked-in get_ident() value,
# or if we use the object id, id(threading.currentThread()).
msg = '%8d-%3d: %s' % (
threading.currentThread().ident,
int(APACHE_REQUEST.connection.id),
msg,
)
else:
msg = '%8d: %s' % (threading.currentThread().ident, msg,)
#
if False and show_mod_func_line_:
# Grab the frame so we can spit the fcn., file, and line number.
# (We cannot use Formatter's %()s options because wrappered.)
frame = inspect.currentframe().f_back
#frame = frame.f_back
mod = make_string(inspect.getmodulename(frame.f_code.co_filename))
func = frame.f_code.co_name + '()'
linen = make_string(frame.f_lineno)
mod_func_line = '%s.%s:%s' % (mod, func, linen)
##template = string.Template(msg)
##template.substitute({'mod_func_line': mod_func_line,})
#msg = msg.replace('${mod_func_line}', mod_func_line)
msg = '%s # %s' % (mod_func_line, msg,)
logging.Logger._log(self, level, msg, args, exc_info, extra)
# NOTE: Old source used apply, which is deprecated. E.g.,:
# apply(self._log, (NOTICE, msg, args), kwargs)
# The new source uses the extended call syntax instead.
def notice(self, msg, *args, **kwargs):
"""
Log 'msg % args' with severity 'NOTICE'.
To pass exception information, use the keyword argument exc_info with
a true value, e.g.
logger.notice("Houston, we have a %s", "interesting problem", exc_info=1)
"""
global NOTICE
if self.isEnabledFor(NOTICE):
# NOTE: We don't repackages args. I.e., you'll get
# TypeError: _log() takes at least 4 arguments (3 given)
# if you try: self._log(NOTICE, msg, *args, **kwargs)
self._log(NOTICE, msg, args, **kwargs)
def trace(self, msg, *args, **kwargs):
global TRACE
if self.isEnabledFor(TRACE):
self._log(TRACE, msg, args, **kwargs)
def verbose1(self, msg, *args, **kwargs):
global VERBOSE1
if self.isEnabledFor(VERBOSE1):
self._log(VERBOSE1, msg, args, **kwargs)
def verbose2(self, msg, *args, **kwargs):
global VERBOSE2
if self.isEnabledFor(VERBOSE2):
self._log(VERBOSE2, msg, args, **kwargs)
def verbose3(self, msg, *args, **kwargs):
global VERBOSE3
if self.isEnabledFor(VERBOSE3):
self._log(VERBOSE3, msg, args, **kwargs)
def verbose4(self, msg, *args, **kwargs):
global VERBOSE4
if self.isEnabledFor(VERBOSE4):
self._log(VERBOSE4, msg, args, **kwargs)
def verbose5(self, msg, *args, **kwargs):
global VERBOSE5
if self.isEnabledFor(VERBOSE5):
self._log(VERBOSE5, msg, args, **kwargs)
def verbose(self, msg, *args, **kwargs):
"""
Log 'msg % args' with severity 'VERBOSE'.
To pass exception information, use the keyword argument exc_info with
a true value, e.g.
logger.notice("Houston, we have a %s", "loud problem", exc_info=1)
"""
global VERBOSE
# Pre-Python 2.7:
#if self.manager.disable >= VERBOSE:
# return
#if VERBOSE >= self.getEffectiveLevel():
# apply(self._log, (VERBOSE, msg, args), kwargs)
# FIXME: This works in Python 2.7, but hasn't been tested on other
# releases.
if self.isEnabledFor(VERBOSE):
self._log(VERBOSE, msg, args, **kwargs)
# NOTE: This overrides the Python's Logger.fatal which is Logger.critical.
def fatal(self, msg, *args, **kwargs):
global FATAL
if self.isEnabledFor(FATAL):
self._log(FATAL, msg, args, **kwargs)
# ***
class My_StreamHandler(logging.StreamHandler):
def __init__(self):
logging.StreamHandler.__init__(self)
def format(self, record):
return My_Handler.format(self, record)
class My_FileHandler(logging.FileHandler):
def __init__(self, filename, mode='a'):
logging.FileHandler.__init__(self, filename, mode)
def format(self, record):
return My_Handler.format(self, record)
# MAYBE: Mehhhhhhhh. 2016-06-27: Untested/could not get basic wxPython test to work
# (it's my first day using wxPython -- ever! -- so I'll worry/care/
# not care about this later since I got normal console working).
class My_wxPythonHandler(logging.StreamHandler):
def __init__(self, wx_dest=None):
"""
Initialize handler.
@param wx_dest: destination object to which to post event
@type wx_dest: wx.Window
"""
logging.StreamHandler.__init__(self)
self.wx_dest = wx_dest
#self.level = logging.DEBUG
def format(self, record):
return My_Handler.format(self, record)
def flush(self):
"""
does nothing for this handler
"""
pass
def emit(self, record):
"""
Emit a record.
"""
try:
msg = self.format(record)
evt = wxLogEvent(message=msg, levelname=record.levelname)
wx.PostEvent(self.wx_dest, evt)
except (KeyboardInterrupt, SystemExit):
raise
except:
self.handleError(record)
class My_Handler(object):
@staticmethod
def format(handler, record):
"""
Format the specified record. If a formatter is set, use it.
Otherwise, use the default formatter for the module.
"""
global msg_continuation_prefix
global line_len_log
global line_len_msg
if handler.formatter:
fmt = handler.formatter
else:
fmt = logging._defaultFormatter
if False:
# I cannot get inspect to work as expected.
# Grab the frame so we can spit the fcn., file, and line number.
# (We cannot use Formatter's %()s options because wrappered.)
frame = inspect.currentframe().f_back
#frame = frame.f_back
mod = make_string(inspect.getmodulename(frame.f_code.co_filename))
func = frame.f_code.co_name + '()'
linen = make_string(frame.f_lineno)
#mod_func_line = '%s.%s:%s' % (mod, func, linen)
else:
stack = traceback.extract_stack()
target_frame = None
for frame in stack:
# __name__ is, e.g., 'retrosoft.logging2'
pathname = __name__.replace('.', os.path.sep) + '.py'
if frame[0].endswith(pathname):
break
target_frame = frame
if target_frame is not None:
mod = os.path.splitext(os.path.basename(target_frame[0]))[0]
func = target_frame[2]
linen = target_frame[1]
# else, MAYBE: complain?
#
# HACK!!
try:
record.module = mod
record.funcName = func
record.lineno = linen
except NameError:
pass
# Fix problem is message is unicode:
# File "/usr/lib/python2.7/logging/__init__.py", line 467, in format
# s = self._fmt % record.__dict__
# UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 35: ordinal not in range(128)
record.msg = str(record.msg)
msg = fmt.format(record)
if (line_len_log is None) or (msg.find('\n') != -1):
verbatim = True
else:
verbatim = False
#msg = msg % (('\n' if verbatim else ''),)
if verbatim:
# FIXME: This is completely correct. The msg is printed verbatim --
# including newlines -- but the first part of the message still
# follows the date and names; ideally, there'd be a newline after the
# date and names, but before the message. Well, not when line_len_log
# is None, but when the message already has newlines.
formatted = '%s' % msg.strip()
else:
first = True
multi_line = []
while len(msg) > 0:
# BUG nnnn: Only split (insert newlines) on whitespace, otherwise
# if you are searching a trace your keyword may miss a hit.
if not first:
snip = msg_continuation_prefix
snip += msg[0:line_len_msg]
msg = msg[line_len_msg:]
else:
snip = msg[0:line_len_log]
msg = msg[line_len_log:]
first = False
# FIXME: Newlines mess up formatting. If there's a newline in snip,
# you should back up to the newline and put the remainder
# back in msg.
snip += '\n'
multi_line.append(snip)
multi_line[-1] = multi_line[-1].strip('\n')
formatted = ''.join(multi_line)
return formatted
# ***
logging_inited = False
logging_handlers = []
root_logger = None
def init_logging(
log_level=logging.INFO,
log_fname=None,
log_frmat=None,
log_dfmat=None,
log_to_file=False,
log_to_console=False,
log_to_wx=False,
log_frmat_len=0,
#log_frmat_postfix='#',
log_frmat_postfix='| ',
log_line_len=None,
add_thread_id=False,
show_logger_name=False,
show_mod_func_line=False,
):
global logging_inited
if not logging_inited:
init_logging_impl(
log_level,
log_fname,
log_frmat,
log_dfmat,
log_to_file,
log_to_console,
log_to_wx,
log_frmat_len,
log_frmat_postfix,
log_line_len,
add_thread_id,
show_logger_name,
show_mod_func_line,
)
logging_inited = True
# else, MAYBE: complain? warn-tell user?
def init_logging_impl(
log_level,
log_fname,
log_frmat,
log_dfmat,
log_to_file,
log_to_console,
log_to_wx,
log_frmat_len,
log_frmat_postfix,
log_line_len,
add_thread_id,
show_logger_name,
show_mod_func_line,
):
global include_thread_id
global show_logger_name_
global show_mod_func_line_
config_line_format(
log_frmat_len,
log_frmat_postfix,
log_line_len,
add_thread_id,
)
if (not show_logger_name) and (not show_mod_func_line):
show_mod_func_line = True
show_logger_name_ = show_logger_name
show_mod_func_line_ = show_mod_func_line
if not log_frmat:
# See class Formatter in /usr/lib/python2.7/logging/__init__.py for options.
#
# NOTE: Skipping:
#
# %(asctime)s -- Textual time when the LogRecord was created
# See: log_dfmat (next!)
# %(created)s -- time.time() when LogRecord was created
# %(msecs)s -- silly. just the msec. portion of %(created)s
# %(relativeCreated)s -- since start of logger;
# could be interesting for niche applications
#
# %(module)s -- We cannot use Formatter's special variables to
# %(funcName)s -- show the fcn. name and line, e.g.,
# %(lineno)s -- %(module)s.%(funcName)s:%(lineno)s
# -- because that's us! E.g.,
# -- logging2.notice:199
# so we'll override log() and mess around there.
#
log_frmat = (
#'%%(asctime)s %%(levelname)-4s %s%s%s %%(message)s'
'%%(levelname)-4s|%%(asctime)s|%s%s%s%%(message)s'
% (
'%(name)-11s ' if show_logger_name_ else '',
#'%(name)-11s|' if show_logger_name_ else '',
#'${mod_func_line} ' if show_mod_func_line_ else '',
#'%(module)s.%(funcName)s:%(lineno)s ' if show_mod_func_line_ else '',
'%(module)s.%(funcName)s:%(lineno)s' if show_mod_func_line_ else '',
log_frmat_postfix,
)
)
# FIXME: UNTESTED:
if include_thread_id:
# FIXME: 2016-06-27: this code is Untested.
# FIXME: See My_Logger._log, which maybe doesn't need to use threading.
log_frmat = '%%(thread)8d %s' % (log_frmat,)
if not log_dfmat:
# See strftime() for the meaning of these directives.
# Too loquacious:
# log_dfmat = '%a, %d %b %Y %H:%M:%S'
# E.g., "Mon, 27 Jun 2016 20:39:21"
log_dfmat = '%Y-%b-%d|%a|%H:%M:%S'
#logging.basicConfig(
# level=log_level,
# filename=log_fname,
# format=log_frmat,
# datefmt=log_dfmat)
formatter = logging.Formatter(log_frmat, log_dfmat)
logging.setLoggerClass(My_Logger)
global root_logger
root_logger = logging.getLogger('')
root_logger.setLevel(log_level)
# SYNC_ME: Log levels.
logging.addLevelName( FATAL, 'FATL') # 186
logging.addLevelName(logging.CRITICAL, 'CRIT') # 50
#logging.addLevelName(logging.FATAL, 'CRIT') # 50 [*aliases CRITICAL]
logging.addLevelName(logging.ERROR, 'ERRR') # 40
logging.addLevelName(logging.WARNING, 'WARN') # 30
logging.addLevelName( NOTICE, 'NTCE') # 25
logging.addLevelName(logging.INFO, 'INFO') # 20
logging.addLevelName( TRACE, 'TRCE') # 15
logging.addLevelName(logging.DEBUG, 'DEBG') # 10
logging.addLevelName( VERBOSE1, 'VRB1') # 09
logging.addLevelName( VERBOSE2, 'VRB2') # 08
logging.addLevelName( VERBOSE3, 'VRB3') # 07
logging.addLevelName( VERBOSE4, 'VRB4') # 06
logging.addLevelName( VERBOSE5, 'VRB5') # 05
logging.addLevelName( VERBOSE, 'VRBS') # 05
global logging_handlers
if (True
and not log_to_file
and not log_to_console
and not log_to_wx
):
log_to_console = True
if log_to_file:
assert(log_fname)
logging_handlers.append(My_FileHandler(log_fname))
if log_to_console:
logging_handlers.append(My_StreamHandler())
if log_to_wx:
logging_handlers.append(My_wxPythonHandler())
for handler in logging_handlers:
handler.setLevel(log_level)
handler.setFormatter(formatter)
root_logger.addHandler(handler)
def setLevel(log_level):
global root_logger
root_logger.setLevel(log_level)
global logging_handlers
for handler in logging_handlers:
handler.setLevel(level=log_level)
# ***
# SYNC_ME: Log levels.
def log(*args, **kwargs):
# NOTE: The unnamed '' logger is the root logger, not My_Logger.
logging.getLogger('%').info(*args, **kwargs)
def fatal(*args, **kwargs):
logging.getLogger('%').fatal(*args, **kwargs)
def critical(*args, **kwargs):
logging.getLogger('%').critical(*args, **kwargs)
def error(*args, **kwargs):
logging.getLogger('%').error(*args, **kwargs)
def warning(*args, **kwargs):
logging.getLogger('%').warning(*args, **kwargs)
def notice(*args, **kwargs):
logging.getLogger('%').notice(*args, **kwargs)
def debug(*args, **kwargs):
logging.getLogger('%').debug(*args, **kwargs)
def trace(*args, **kwargs):
logging.getLogger('%').trace(*args, **kwargs)
def verbose1(*args, **kwargs):
logging.getLogger('%').verbose1(*args, **kwargs)
def verbose2(*args, **kwargs):
logging.getLogger('%').verbose2(*args, **kwargs)
def verbose3(*args, **kwargs):
logging.getLogger('%').verbose3(*args, **kwargs)
def verbose4(*args, **kwargs):
logging.getLogger('%').verbose4(*args, **kwargs)
def verbose5(*args, **kwargs):
logging.getLogger('%').verbose5(*args, **kwargs)
def verbose(*args, **kwargs):
logging.getLogger('%').verbose(*args, **kwargs)
def info(*args, **kwargs):
logging.getLogger('%').info(*args, **kwargs)
# ***
# A soft assert complains about non-exception-worthy unexpected states.
# DEV-TIPs: Enable breakpoints easily with assert_soft(False) by setting
# ON_ASSERT_TRACE=True environment variable.
ON_ASSERT_TRACE = False
#ON_ASSERT_TRACE = True
def assert_soft(condition, *args, **kwargs):
# MAYBE: There's a way to print out the source code for a failed condition
# and also the value(s) that were used, but [lb] doesn't know it and I don't
# want to spend time searching. See py.test's asserts for an example.
# MAYBE: Use logcheck and email the developers when this happens.
if not condition:
if not args:
kwargs.setdefault('msg', 'Assertion failed.')
fatal(*args, **kwargs)
do_break = False
if ON_ASSERT_TRACE:
do_break = True
else:
try:
do_break = os.environ['ON_ASSERT_TRACE']
except KeyError:
pass
if do_break:
if threading.current_thread().name == 'MainThread':
import pdb; pdb.set_trace()
else:
import pydevd; pydevd.settrace()
# ***
# To initialize the logger when this module is loaded, uncomment the following:
# init_logging()
# Note: Usually, you'll deliberately call init_logging from your own code
# so you can set the logfile, console width, and default log level.
if __name__ == '__main__':
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment