Due to several limitations of the standard library’s logging module, I wrote my own.
Below are the most important parts. You can find the whole library here.
Any feedback is welcome.
# fancylog - A library for human readable logging. # # Copyright (C) 2017 HOMEINFO - Digitale Informationssysteme GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. """A library for beautiful, readble logging.""" from datetime import datetime from enum import Enum from sys import stdout, stderr from threading import Thread from time import sleep from traceback import format_exc from blessings import Terminal __all__ = [ 'logging', 'LogLevel', 'LogEntry', 'Logger', 'TTYAnimation', 'LoggingClass'] TERMINAL = Terminal() def logging(name=None, level=None, parent=None, file=None): """Decorator to attach a logger to the respective class.""" def wrap(obj): """Attaches the logger to the respective class.""" logger_name = obj.__name__ if name is None else name obj.logger = Logger(logger_name, level=level, parent=parent, file=file) return obj return wrap class LogLevel(Enum): """Logging levels.""" DEBUG = (10, '🔧', TERMINAL.bold) SUCCESS = (20, '✓', TERMINAL.green) INFO = (30, 'ℹ', TERMINAL.blue) WARNING = (70, '⚠', TERMINAL.yellow) ERROR = (80, '✗', TERMINAL.red) CRITICAL = (90, '☢', lambda string: TERMINAL.bold(TERMINAL.red(string))) FAILURE = (100, '☠', lambda string: TERMINAL.bold(TERMINAL.magenta( string))) def __init__(self, ident, symbol, format_): """Sets the identifier, symbol and color.""" self.ident = ident self.symbol = symbol self.format = format_ def __int__(self): """Returns the identifier.""" return self.ident def __str__(self): """Returns the colored symbol.""" return self.format(self.symbol) def __eq__(self, other): try: return int(self) == int(other) except TypeError: return NotImplemented def __gt__(self, other): try: return int(self) > int(other) except TypeError: return NotImplemented def __ge__(self, other): return self > other or self == other def __lt__(self, other): try: return int(self) < int(other) except TypeError: return NotImplemented def __le__(self, other): return self < other or self == other def __hash__(self): return hash((self.__class__, self.ident)) @property def erroneous(self): """A log level is considered erroneous if it's identifier > 50.""" return self.ident > 50 class LogEntry(Exception): """A log entry.""" def __init__(self, *messages, level=LogLevel.ERROR, sep=None, color=None): """Sets the log level and the respective message(s).""" super().__init__() self.messages = messages self.level = level self.sep = ' ' if sep is None else sep self.color = color self.timestamp = datetime.now() def __hash__(self): """Returns a unique hash.""" return hash(self._hash_tuple) @property def _hash_tuple(self): """Returns the tuple from which to create a unique hash.""" return (self.__class__, self.level, self.messages, self.timestamp) @property def message(self): """Returns the message elements joint by the selected separator.""" return self.sep.join(str(message) for message in self.messages) @property def text(self): """Returns the formatted message text.""" if self.color is not None: return self.color(self.message) return self.message class Logger: """A logger that can be nested.""" CHILD_SEP = '→' def __init__(self, name, level=None, parent=None, file=None): """Sets the logger's name, log level, parent logger, log entry template and file. """ self.name = name self.level = level or LogLevel.INFO self.parent = parent self.file = file self.template = '{1.level} {0}: {1.text}' def __str__(self): """Returns the logger's nested path as a string.""" return self.CHILD_SEP.join(logger.name for logger in self.path) def __hash__(self): """Returns a unique hash.""" return hash(self.__class__, self.name) def __enter__(self): """Returns itself.""" return self def __exit__(self, _, value, __): """Logs risen log entries.""" if isinstance(value, LogEntry): self.log_entry(value) return True return None @property def root(self): """Determines whether the logger is at the root level.""" return self.parent is None @property def path(self): """Yields the logger's path.""" if not self.root: yield from self.parent.path yield self @property def layer(self): """Returns the layer of the logger.""" return 0 if self.root else self.parent.layer + 1 def log_entry(self, log_entry): """Logs a log entry.""" if log_entry.level >= self.level: if self.file is None: file = stderr if log_entry.level.erroneous else stdout else: file = self.file print(self.template.format(self, log_entry), file=file, flush=True) return log_entry def inherit(self, name, level=None, file=None): """Returns a new child of this logger.""" level = self.level if level is None else level file = self.file if file is None else file return self.__class__(name, level=level, parent=self, file=file) def log(self, level, *messages, sep=None, color=None): """Logs messages of a certain log level.""" log_entry = LogEntry(*messages, level=level, sep=sep, color=color) return self.log_entry(log_entry) def debug(self, *messages, sep=None, color=None): """Logs debug messages, defaulting to a stack trace.""" if not messages: messages = ('Stacktrace:', format_exc()) if sep is None: sep = '\n' return self.log(LogLevel.DEBUG, *messages, sep=sep, color=color) def success(self, *messages, sep=None, color=None): """Logs success messages.""" return self.log(LogLevel.SUCCESS, *messages, sep=sep, color=color) def info(self, *messages, sep=None, color=None): """Logs info messages.""" return self.log(LogLevel.INFO, *messages, sep=sep, color=color) def warning(self, *messages, sep=None, color=None): """Logs warning messages.""" return self.log(LogLevel.WARNING, *messages, sep=sep, color=color) def error(self, *messages, sep=None, color=None): """Logs error messages.""" return self.log(LogLevel.ERROR, *messages, sep=sep, color=color) def critical(self, *messages, sep=None, color=None): """Logs critical messages.""" return self.log(LogLevel.CRITICAL, *messages, sep=sep, color=color) def failure(self, *messages, sep=None, color=None): """Logs failure messages.""" return self.log(LogLevel.FAILURE, *messages, sep=sep, color=color)