Библиотека журнала


Из-за нескольких ограничений модуля лесозаготовки стандартной библиотеке, я написал свой собственный.
Ниже приведены наиболее важные части. Вы можете найти целую библиотеку здесь.
Любая обратная связь приветствуется.

#  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)

Пример использования:

#! /usr/bin/env python3
"""divide.

Usage:
    divide <dividend> <divisor> [options]

Options:
    --help, -h  Show this page.
"""    
from docopt import docopt
from fancylog import LogLevel, LogEntry, Logger


def divide(dividend, divisor):

    try:
        return dividend / divisor
    except ZeroDivisionError:
        raise LogEntry('Cannot divide by zero.', level=LogLevel.ERROR)


def main(options):

    with Logger('Division logger', level=LogLevel.SUCCESS) as logger:
        try:
            dividend = float(options['<dividend>'])
        except ValueError:
            logger.error('Divident is not a float.')
            return

        try:
            divisor = float(options['<divisor>'])
        except ValueError:
            logger.error('Divisor is not a float.')
            return

        logger.success(divide(dividend, divisor))


if __name__ == '__main__':
    main(docopt(__doc__))


Комментарии
1 ответ

Код читает хорошо, у меня только несколько что доставит некоторое неудобство:


  • Использовать значения по умолчанию, где это применимо, вместо None: Это облегчит использование API, так как они могут быть анализировался;

    class LogEntry(Exception):
    """A log entry."""

    def __init__(self, *messages, level=LogLevel.ERROR, sep=' ', color=None):

    class Logger:
    """A logger that can be nested."""

    CHILD_SEP = '→'

    def __init__(self, name, level=LogLevel.INFO, parent=None, file=None):


  • Измените значение параметра вместо reasigning его через тернарный: это сделает его более явно о том, что ты пытаешься сделать;

    def inherit(self, name, level=None, file=None):
    """Returns a new child of this logger."""
    if level is None:
    level = self.level
    if file is None:
    file = self.file
    return self.__class__(name, level=level, parent=self, file=file)

  • Избежать return Noneособенно в последние доннуэте: это просто шум.

Теперь это аккуратный код и все, но это так... весьма ограничены. Две главные проблемы я об этом модуле заключается в том, что:


  1. он только входит в файлов (или аналогичных);

  2. это дает ооочень мало контроля над форматированием сообщений журнала.

Мне иногда приходится отправлять журналы системы демон syslog и я могу сделать это с помощью обработчика, предусмотренных в Python logging модуль, но если я хочу сделать это, используя Ваш, я бы обернуть syslog и ваш модуль сам... же касается и отправки логов по TCP (для прослушивания logstash например).

То же самое идет для форматирования сообщения. Насколько я ненавижу % форматирование введенной logging модуль, это как минимум можно записать в Formatter это format знать и передавать крупные объекты к регистратору, который будет использовать строку формата, чтобы пересечь его и извлекать значения, что мне нужно войти, только если он на самом деле что-то журналы. Используя ваш подход, я могу написать не гибкий __str__ способ на каждый объект я хочу, чтобы войти, так что я могу иметь нужную информацию (что не всегда возможно и не хотел, если я хочу войти несколько разные вещи в нескольких разных местах) или разделить строку формата (плохо для читабельности) и пересекают объект для получения атрибутов на себя, что немного дополнительной нагрузки (некоторые из них могут быть дорогие объекты, даже) если журнал заканчивается выбросить.

Других пропавших вещей включает в себя способность настроить иерархию регистратор хоть файл и получить/создать любой регистратор по имени в любом модуле.

Я тоже не совсем понимаю использование файл contextmanager, особенно LogEntry исключение. Насколько мне интересно создавать специальный регистратор для определенной части кода, почему я хочу использовать LogEntry "исключение":


  1. он глотает фактические ошибки, что делает вызывающий код не зная никаких проблем: если ваш пример кода, чтобы использовать значение, возвращаемое divideон будет иметь трудное время делать так, как None будут возвращены;

  2. он меняет тип исключения предпринимают никаких попыток, чтобы поймать реальных ошибок спорный. Почему бы не сохранить его простым

    except XXX:
    logger.xxx(…)
    return

    что вы используете в своей main? (или даже лучше: except XXX: logger.xxx(…); raise)

    Не можете вы вместо того, чтобы предоставить служебную функцию как current_logger что использует этот контекст менеджер, чтобы возвратить соответствующие один (или создайте корневом регистраторе, если ни один не находится в использовании)? Или что-то похожее на decimal.localcontext если вы предоставляете модуль ведения на уровне функций (debug, success, info...), что относится к текущему регистратор и другие утилиты для выбора регистратора является текущей.


4
ответ дан 19 февраля 2018 в 09:02 Источник Поделиться