Как добавить настраиваемый уровень ведения журнала в средство ведения журнала Python


116

Я бы хотел, чтобы в моем приложении был уровень логирования TRACE (5), поскольку я не думаю, что этого debug()достаточно. Кроме log(5, msg)того, я не хочу. Как я могу добавить настраиваемый уровень ведения журнала в средство ведения журнала Python?

У меня есть mylogger.pyследующий контент:

import logging

@property
def log(obj):
    myLogger = logging.getLogger(obj.__class__.__name__)
    return myLogger

В своем коде я использую его следующим образом:

class ExampleClass(object):
    from mylogger import log

    def __init__(self):
        '''The constructor with the logger'''
        self.log.debug("Init runs")

Сейчас я хочу позвонить self.log.trace("foo bar")

Заранее спасибо за вашу помощь.

Изменить (8 декабря 2016 г.): я изменил принятый ответ на pfa, который, IMHO, является отличным решением, основанным на очень хорошем предложении Эрика С.

Ответы:


172

@ Эрик С.

Ответ Эрика С. превосходен, но я экспериментально узнал, что при этом всегда будут печататься сообщения, зарегистрированные на новом уровне отладки, независимо от того, какой уровень журнала установлен. Поэтому, если вы сделаете новый номер уровня 9, если вы позвоните setLevel(50), сообщения нижнего уровня будут ошибочно распечатаны.

Чтобы этого не произошло, вам понадобится еще одна строка внутри функции «debugv», чтобы проверить, действительно ли включен рассматриваемый уровень ведения журнала.

Исправленный пример, который проверяет, включен ли уровень ведения журнала:

import logging
DEBUG_LEVELV_NUM = 9 
logging.addLevelName(DEBUG_LEVELV_NUM, "DEBUGV")
def debugv(self, message, *args, **kws):
    if self.isEnabledFor(DEBUG_LEVELV_NUM):
        # Yes, logger takes its '*args' as 'args'.
        self._log(DEBUG_LEVELV_NUM, message, args, **kws) 
logging.Logger.debugv = debugv

Если вы посмотрите на код для class Loggerв logging.__init__.pyдля Python 2.7, это то , что все стандартные функции журнала делают (.critical, .debug и т.д.).

Я, по-видимому, не могу публиковать ответы на ответы других из-за отсутствия репутации ... надеюсь, Эрик обновит свой пост, если увидит это. знак равно


7
Это лучший ответ, потому что он правильно проверяет уровень журнала.
Colonel Panic

2
Конечно, гораздо информативнее, чем текущий ответ.
Безумный физик,

4
@pfa Как насчет добавления, logging.DEBUG_LEVEL_NUM = 9чтобы вы могли получить доступ к этому уровню отладки везде, где вы импортируете регистратор в свой код?
edgarstack

4
Определенно вместо этого DEBUG_LEVEL_NUM = 9вам следует определиться logging.DEBUG_LEVEL_NUM = 9. Таким образом, вы сможете использовать так log_instance.setLevel(logging.DEBUG_LEVEL_NUM)же, как вы используете правильное знание logging.DEBUGилиlogging.INFO
maQ

Этот ответ оказался очень полезным. Спасибо pfa и EricS. Я хотел бы предложить, чтобы для полноты картины были включены еще два утверждения: logging.DEBUGV = DEBUG_LEVELV_NUMи logging.__all__ += ['DEBUGV'] Второе не очень важно, но первое необходимо, если у вас есть код, который динамически регулирует уровень ведения журнала, и вы хотите иметь возможность делать что-то вроде if verbose: logger.setLevel(logging.DEBUGV)`
Кейт Hanlan

63

Я взял ответ «не видеть лямбда» и должен был изменить место добавления log_at_my_log_level. Я тоже видел проблему, которую создал Пол: «Я не думаю, что это работает. Вам не нужен логгер в качестве первого аргумента в log_at_my_log_level?» Это сработало для меня

import logging
DEBUG_LEVELV_NUM = 9 
logging.addLevelName(DEBUG_LEVELV_NUM, "DEBUGV")
def debugv(self, message, *args, **kws):
    # Yes, logger takes its '*args' as 'args'.
    self._log(DEBUG_LEVELV_NUM, message, args, **kws) 
logging.Logger.debugv = debugv

7
+1 тоже. Элегантный подход, и он отлично сработал. Важное примечание: вам нужно сделать это только один раз в одном модуле, и это будет работать для всех модулей . Вам даже не нужно импортировать модуль «setup». Так что бросьте это в пакет __init__.pyи будьте счастливы: D
MestreLion

4
@Eric S. Вам стоит взглянуть на этот ответ: stackoverflow.com/a/13638084/600110
Sam Mussmann

1
Я согласен с @SamMussmann. Я пропустил этот ответ, потому что это был самый популярный ответ.
Colonel Panic

@Eric S. Зачем нужны аргументы без *? Если я это сделаю, я получу, TypeError: not all arguments converted during string formattingно он отлично работает с *. (Python 3.4.3). Это проблема с версией Python или что-то, что мне не хватает?
Питер

Этот ответ мне не подходит. Попытка выполнить logging.debugv выдает ошибкуAttributeError: module 'logging' has no attribute 'debugv'
Alex

51

Объединив все существующие ответы с кучей опыта использования, я думаю, что составил список всех вещей, которые необходимо сделать, чтобы обеспечить полное беспрепятственное использование нового уровня. Следующие шаги предполагают, что вы добавляете новый уровень TRACEсо значением logging.DEBUG - 5 == 5:

  1. logging.addLevelName(logging.DEBUG - 5, 'TRACE') необходимо вызвать для внутренней регистрации нового уровня, чтобы на него можно было ссылаться по имени.
  2. Новый уровень должен быть добавлен в качестве атрибута к loggingсебе для последовательности: logging.TRACE = logging.DEBUG - 5.
  3. Вызываемый метод traceнеобходимо добавить в loggingмодуль. Он должен вести себя так же , как debug, infoи т.д.
  4. Вызываемый метод traceнеобходимо добавить к текущему настроенному классу регистратора. Поскольку это не на 100% гарантировано logging.Logger, используйте logging.getLoggerClass()вместо этого.

Все шаги показаны в методе ниже:

def addLoggingLevel(levelName, levelNum, methodName=None):
    """
    Comprehensively adds a new logging level to the `logging` module and the
    currently configured logging class.

    `levelName` becomes an attribute of the `logging` module with the value
    `levelNum`. `methodName` becomes a convenience method for both `logging`
    itself and the class returned by `logging.getLoggerClass()` (usually just
    `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
    used.

    To avoid accidental clobberings of existing attributes, this method will
    raise an `AttributeError` if the level name is already an attribute of the
    `logging` module or if the method name is already present 

    Example
    -------
    >>> addLoggingLevel('TRACE', logging.DEBUG - 5)
    >>> logging.getLogger(__name__).setLevel("TRACE")
    >>> logging.getLogger(__name__).trace('that worked')
    >>> logging.trace('so did this')
    >>> logging.TRACE
    5

    """
    if not methodName:
        methodName = levelName.lower()

    if hasattr(logging, levelName):
       raise AttributeError('{} already defined in logging module'.format(levelName))
    if hasattr(logging, methodName):
       raise AttributeError('{} already defined in logging module'.format(methodName))
    if hasattr(logging.getLoggerClass(), methodName):
       raise AttributeError('{} already defined in logger class'.format(methodName))

    # This method was inspired by the answers to Stack Overflow post
    # http://stackoverflow.com/q/2183233/2988730, especially
    # http://stackoverflow.com/a/13638084/2988730
    def logForLevel(self, message, *args, **kwargs):
        if self.isEnabledFor(levelNum):
            self._log(levelNum, message, args, **kwargs)
    def logToRoot(message, *args, **kwargs):
        logging.log(levelNum, message, *args, **kwargs)

    logging.addLevelName(levelNum, levelName)
    setattr(logging, levelName, levelNum)
    setattr(logging.getLoggerClass(), methodName, logForLevel)
    setattr(logging, methodName, logToRoot)

Отсортируйте ответы по Oldest, и вы убедитесь, что это лучший ответ из всех!
Serge Stroobandt

Спасибо. Я проделал довольно много работы, собирая что-то подобное, и этот QA был очень полезен, поэтому я попытался добавить что-то.
Mad Physicist

1
@PeterDolan. Сообщите мне, если у вас возникнут проблемы с этим. В моем личном наборе инструментов есть расширенная версия, которая позволяет вам настраивать обработку конфликтующих определений уровней. Однажды мне это пришло в голову, потому что мне нравится добавлять уровень TRACE, как и один из компонентов sphinx.
Mad Physicist

1
Является ли отсутствие звездочки перед argsв logForLevelосуществлении намеренного / требуется?
Крис Л. Барнс,

1
@Тунис. Это непреднамеренно. Спасибо за улов.
Безумный физик

40

Этот вопрос довольно старый, но я только что затронул ту же тему и нашел способ, похожий на уже упомянутые, который мне кажется немного чище. Это было протестировано на 3.4, поэтому я не уверен, существуют ли используемые методы в более старых версиях:

from logging import getLoggerClass, addLevelName, setLoggerClass, NOTSET

VERBOSE = 5

class MyLogger(getLoggerClass()):
    def __init__(self, name, level=NOTSET):
        super().__init__(name, level)

        addLevelName(VERBOSE, "VERBOSE")

    def verbose(self, msg, *args, **kwargs):
        if self.isEnabledFor(VERBOSE):
            self._log(VERBOSE, msg, args, **kwargs)

setLoggerClass(MyLogger)

1
Это ИМХО лучший ответ, поскольку он позволяет избежать исправления обезьяны. Что getи что setLoggerClassименно делают и зачем они нужны?
Марко Сулла

3
@MarcoSulla Они задокументированы как часть модуля ведения журнала Python. Я предполагаю, что динамическое подклассирование используется в том случае, если кому-то нужен собственный llogger при использовании этой библиотеки. Этот MyLogger затем станет подклассом моего класса, объединив их.
CrackerJack9,

Это очень похоже на решение, представленное в этом обсуждении, относительно того, нужно ли добавлять TRACEуровень в библиотеку ведения журнала по умолчанию. +1
IMP1

18

Кто начал плохую практику использования внутренних методов ( self._log) и почему каждый ответ основан на этом ?! self.logВместо этого можно было бы использовать питоническое решение, чтобы вам не пришлось возиться с какими-либо внутренними вещами:

import logging

SUBDEBUG = 5
logging.addLevelName(SUBDEBUG, 'SUBDEBUG')

def subdebug(self, message, *args, **kws):
    self.log(SUBDEBUG, message, *args, **kws) 
logging.Logger.subdebug = subdebug

logging.basicConfig()
l = logging.getLogger()
l.setLevel(SUBDEBUG)
l.subdebug('test')
l.setLevel(logging.DEBUG)
l.subdebug('test')

18
Использование _log () вместо log () необходимо, чтобы не вводить дополнительный уровень в стек вызовов. Если используется log (), введение дополнительного кадра стека приводит к тому, что несколько атрибутов LogRecord (funcName, белье, имя файла, путь, ...) указывают на функцию отладки, а не на фактического вызывающего. Скорее всего, это не желаемый результат.
rivy

5
С каких это пор нельзя вызывать собственные внутренние методы класса? То, что функция определена вне класса, не означает, что это внешний метод.
OozeMeister

3
Этот метод не только без необходимости изменяет трассировку стека, но также не проверяет, что правильный уровень регистрируется.
Безумный физик,

Я чувствую, что то, что говорит @schlamar, правильно, но за встречную причину набралось столько же голосов. Так что использовать?
Сумит Мурари

1
Почему бы методу не использовать внутренний метод?
Gringo Suave

9

Мне проще создать новый атрибут для объекта регистратора, который передает функцию log (). Я думаю, что модуль logger предоставляет addLevelName () и log () именно по этой причине. Таким образом, не нужны подклассы или новый метод.

import logging

@property
def log(obj):
    logging.addLevelName(5, 'TRACE')
    myLogger = logging.getLogger(obj.__class__.__name__)
    setattr(myLogger, 'trace', lambda *args: myLogger.log(5, *args))
    return myLogger

сейчас

mylogger.trace('This is a trace message')

должен работать как положено.


Разве это не привело бы к небольшому снижению производительности по сравнению с подклассом? При таком подходе каждый раз, когда кто-то запрашивает регистратор, им придется выполнять вызов setattr. Вероятно, вы бы обернули их вместе в специальный класс, но, тем не менее, этот setattr должен вызываться для каждого созданного регистратора, верно?
Мэтью Лунд,

@Zbigniew ниже указал, что это не сработало, что, я думаю, связано с тем, что вашему регистратору нужно позвонить _log, а не log.
выделен

9

Хотя у нас уже есть много правильных ответов, на мой взгляд, более питоническим является следующее:

import logging

from functools import partial, partialmethod

logging.TRACE = 5
logging.addLevelName(logging.TRACE, 'TRACE')
logging.Logger.trace = partialmethod(logging.Logger.log, logging.TRACE)
logging.trace = partial(logging.log, logging.TRACE)

Если вы хотите использовать mypyв своем коде, рекомендуется добавить, # type: ignoreчтобы подавить предупреждения от добавления атрибута.


1
Выглядит отлично, но последняя строка сбивает с толку. Не должно быть logging.trace = partial(logging.log, logging.TRACE) # type: ignore?
Сергей Нуднов

@SergeyNudnov спасибо, что указал, я исправил. Это была ошибка с моей стороны, я просто скопировал свой код и, видимо, испортил чистку.
DerWeh

8

Я думаю, вам придется Loggerсоздать подкласс класса и добавить вызываемый метод, traceкоторый в основном вызывает Logger.logс уровнем ниже DEBUG. Я не пробовал это, но это то, что указано в документации .


3
И вы, вероятно, захотите заменить, logging.getLoggerчтобы вернуть свой подкласс вместо встроенного класса.
S.Lott

4
@ S.Lott - На самом деле (по крайней мере, с нынешней версией Python, может быть, в 2010 году это было не так) вы должны использовать, setLoggerClass(MyClass)а затем звонить getLogger()как обычно ...
mac

ИМО, это, безусловно, лучший (и самый питонический) ответ, и если бы я мог дать ему несколько +1, я бы это сделал. Его просто выполнить, но пример кода было бы неплохо. :-D
Дуг Р.

@ DougR.Спасибо, но, как я уже сказал, я не пробовал. :)
Noufal Ibrahim

6

Советы по созданию собственного регистратора:

  1. Не использовать _log, использовать log(проверять не обязательно isEnabledFor)
  2. модуль ведения журнала должен быть единственным экземпляром, создающим настраиваемый регистратор, поскольку он выполняет некоторую магию getLogger, поэтому вам нужно будет установить класс черезsetLoggerClass
  3. Вам не нужно определять __init__класс для регистратора, если вы ничего не храните
# Lower than debug which is 10
TRACE = 5
class MyLogger(logging.Logger):
    def trace(self, msg, *args, **kwargs):
        self.log(TRACE, msg, *args, **kwargs)

При вызове этого регистратора используйте, setLoggerClass(MyLogger)чтобы сделать его регистратором по умолчанию изgetLogger

logging.setLoggerClass(MyLogger)
log = logging.getLogger(__name__)
# ...
log.trace("something specific")

Вам нужно setFormatter, setHandlerи setLevel(TRACE)на handlerи на logсам фактически з этой низкого уровня трассировки


3

Это сработало для меня:

import logging
logging.basicConfig(
    format='  %(levelname)-8.8s %(funcName)s: %(message)s',
)
logging.NOTE = 32  # positive yet important
logging.addLevelName(logging.NOTE, 'NOTE')      # new level
logging.addLevelName(logging.CRITICAL, 'FATAL') # rename existing

log = logging.getLogger(__name__)
log.note = lambda msg, *args: log._log(logging.NOTE, msg, args)
log.note('school\'s out for summer! %s', 'dude')
log.fatal('file not found.')

Проблема lambda / funcName исправлена ​​с помощью logger._log, как указал @marqueed. Я думаю, что использование лямбда выглядит немного чище, но недостатком является то, что он не может принимать аргументы ключевых слов. Сам я этим никогда не пользовался, так что ничего страшного.

  ПРИМЕЧАНИЕ: школа на лето! чувак
  ФАТАЛЬНАЯ установка: файл не найден.

2

По моему опыту, это полное решение проблемы оператора ... чтобы не видеть "лямбда" как функцию, в которой отправляется сообщение, идите глубже:

MY_LEVEL_NUM = 25
logging.addLevelName(MY_LEVEL_NUM, "MY_LEVEL_NAME")
def log_at_my_log_level(self, message, *args, **kws):
    # Yes, logger takes its '*args' as 'args'.
    self._log(MY_LEVEL_NUM, message, args, **kws)
logger.log_at_my_log_level = log_at_my_log_level

Я никогда не пробовал работать с автономным классом регистратора, но думаю, что основная идея та же (используйте _log).


Я не думаю, что это работает. Разве вам не нужен loggerпервый аргумент log_at_my_log_level?
Пол

Да, думаю, да. Этот ответ был адаптирован из кода, который решает немного другую проблему.
Marqueed

2

Дополнение к примеру Mad Physicists, чтобы правильно указать имя файла и номер строки:

def logToRoot(message, *args, **kwargs):
    if logging.root.isEnabledFor(levelNum):
        logging.root._log(levelNum, message, args, **kwargs)

1

на основе закрепленного ответа я написал небольшой метод, который автоматически создает новые уровни ведения журнала

def set_custom_logging_levels(config={}):
    """
        Assign custom levels for logging
            config: is a dict, like
            {
                'EVENT_NAME': EVENT_LEVEL_NUM,
            }
        EVENT_LEVEL_NUM can't be like already has logging module
        logging.DEBUG       = 10
        logging.INFO        = 20
        logging.WARNING     = 30
        logging.ERROR       = 40
        logging.CRITICAL    = 50
    """
    assert isinstance(config, dict), "Configuration must be a dict"

    def get_level_func(level_name, level_num):
        def _blank(self, message, *args, **kws):
            if self.isEnabledFor(level_num):
                # Yes, logger takes its '*args' as 'args'.
                self._log(level_num, message, args, **kws) 
        _blank.__name__ = level_name.lower()
        return _blank

    for level_name, level_num in config.items():
        logging.addLevelName(level_num, level_name.upper())
        setattr(logging.Logger, level_name.lower(), get_level_func(level_name, level_num))

config может что-то вроде этого:

new_log_levels = {
    # level_num is in logging.INFO section, that's why it 21, 22, etc..
    "FOO":      21,
    "BAR":      22,
}

0

В качестве альтернативы добавлению дополнительного метода в класс Logger я бы рекомендовал использовать этот Logger.log(level, msg)метод.

import logging

TRACE = 5
logging.addLevelName(TRACE, 'TRACE')
FORMAT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'


logging.basicConfig(format=FORMAT)
l = logging.getLogger()
l.setLevel(TRACE)
l.log(TRACE, 'trace message')
l.setLevel(logging.DEBUG)
l.log(TRACE, 'disabled trace message')

0

Я запутался; с python 3.5, по крайней мере, он просто работает:

import logging


TRACE = 5
"""more detail than debug"""

logging.basicConfig()
logging.addLevelName(TRACE,"TRACE")
logger = logging.getLogger('')
logger.debug("n")
logger.setLevel(logging.DEBUG)
logger.debug("y1")
logger.log(TRACE,"n")
logger.setLevel(TRACE)
logger.log(TRACE,"y2")
    

вывод:

ОТЛАДКА: корень: y1

ТРАССА: корень: y2


1
Это не позволяет вам делать то, logger.trace('hi')что я считаю главной целью
Ultimation

-3

Если кому-то нужен автоматический способ динамического добавления нового уровня ведения журнала в модуль ведения журнала (или его копию), я создал эту функцию, расширив ответ @pfa:

def add_level(log_name,custom_log_module=None,log_num=None,
                log_call=None,
                   lower_than=None, higher_than=None, same_as=None,
              verbose=True):
    '''
    Function to dynamically add a new log level to a given custom logging module.
    <custom_log_module>: the logging module. If not provided, then a copy of
        <logging> module is used
    <log_name>: the logging level name
    <log_num>: the logging level num. If not provided, then function checks
        <lower_than>,<higher_than> and <same_as>, at the order mentioned.
        One of those three parameters must hold a string of an already existent
        logging level name.
    In case a level is overwritten and <verbose> is True, then a message in WARNING
        level of the custom logging module is established.
    '''
    if custom_log_module is None:
        import imp
        custom_log_module = imp.load_module('custom_log_module',
                                            *imp.find_module('logging'))
    log_name = log_name.upper()
    def cust_log(par, message, *args, **kws):
        # Yes, logger takes its '*args' as 'args'.
        if par.isEnabledFor(log_num):
            par._log(log_num, message, args, **kws)
    available_level_nums = [key for key in custom_log_module._levelNames
                            if isinstance(key,int)]

    available_levels = {key:custom_log_module._levelNames[key]
                             for key in custom_log_module._levelNames
                            if isinstance(key,str)}
    if log_num is None:
        try:
            if lower_than is not None:
                log_num = available_levels[lower_than]-1
            elif higher_than is not None:
                log_num = available_levels[higher_than]+1
            elif same_as is not None:
                log_num = available_levels[higher_than]
            else:
                raise Exception('Infomation about the '+
                                'log_num should be provided')
        except KeyError:
            raise Exception('Non existent logging level name')
    if log_num in available_level_nums and verbose:
        custom_log_module.warn('Changing ' +
                                  custom_log_module._levelNames[log_num] +
                                  ' to '+log_name)
    custom_log_module.addLevelName(log_num, log_name)

    if log_call is None:
        log_call = log_name.lower()

    setattr(custom_log_module.Logger, log_call, cust_log)
    return custom_log_module

1
Eval внутри exec. Вот это да.
Mad Physicist

2
..... не знаю, что заставило меня сделать это .... после стольких месяцев я бы с радостью setattr
заменил
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.