Як додати користувацький loglevel до засобу ведення журналу Python


116

Мені б хотілося, щоб у моєму застосуванні був корівник TRACE (5), оскільки я не думаю, що debug()це достатньо. Крім того log(5, msg), не те, що я хочу. Як я можу додати користувальницький loglevel до реєстратора 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's, що є, IMHO, відмінне рішення, засноване на дуже хорошій пропозиції Еріка С.

Відповіді:


171

@Eric S.

Відповідь Еріка С. є чудовою, але я експериментував, що це завжди спричиняє друк повідомлень, зафіксованих на новому рівні налагодження - незалежно від того, для чого встановлено рівень журналу. Тож якщо ви зробите нове число рівня 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__.pyPython 2.7, це те, що роблять усі стандартні функції журналу (.critical, .debug тощо).

Я, мабуть, не можу розміщувати відповіді на відповіді інших людей через відсутність репутації ... сподіваємось, Ерік оновить свою посаду, якщо побачить це. =)


7
Це краща відповідь, оскільки вона правильно перевіряє рівень журналу.
Полковник Паніка

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)`
Кіт Ганлан

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 теж. Елегантний підхід, і він спрацював чудово. Важлива примітка: робити це потрібно лише один раз, в одному модулі, і це буде працювати для всіх модулів . Вам навіть не потрібно імпортувати модуль "налаштування". Тож киньте це в пакет __init__.pyі будьте щасливі: D
MestreLion

4
@Eric S. Ви повинні поглянути на цей відповідь: stackoverflow.com/a/13638084/600110
Sam Mussmann

1
Я згоден з @SamMussmann. Я пропустив цю відповідь, тому що це була найвища відповідь.
Полковник Паніка

@Eric S. Навіщо потрібні аргументи без *? Якщо я це роблю, я отримую, TypeError: not all arguments converted during string formattingале це добре працює з *. (Python 3.4.3). Це проблема з версією python чи щось мені не вистачає?
Пітер

Ця відповідь для мене не працює. Спроба зробити "logging.debugv" дає помилкуAttributeError: module 'logging' has no attribute 'debugv'
Алекс

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, і ви оціните, що це найкраща відповідь на них усіх!
Серж Стротобандт

Дякую. Я дуже багато працював над тим, як щось подібне спільно, і цей QA був дуже корисним, тому я спробував щось додати.
Божевільний фізик

1
@PeterDolan. Повідомте мене, якщо у вас проблеми з цим. В моєму особистому наборі інструментів у мене є розширена версія, яка дозволяє вам налаштувати, як керувати суперечливими визначеннями рівня. Це придумало для мене один раз, тому що я люблю додавати рівень TRACE, і так це є одним із компонентів сфінкса.
Божевільний фізик

1
Є чи відсутність зірочки перед argsв logForLevelздійсненні навмисного / требуется?
Кріс Л. Барнс

1
@Tunisia. Це ненавмисно. Дякую за улов.
Божевільний фізик

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. Я припускаю, що динамічне підкласифікація використовується в тому випадку, якщо хтось захотів власного логгеру під час використання цієї бібліотеки. Цей 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, lineno, ім'я файлу, ім'я шляху, ...) вказують на функцію налагодження замість фактичного виклику. Це, мабуть, не бажаний результат.
rivy

5
Оскільки коли називати внутрішні методи класу неприпустимими? Тільки тому, що функція визначена поза класом, не означає, що це зовнішній метод.
OozeMeister

3
Цей метод не тільки змінює трасування стека без потреби, але й не перевіряє, чи правильний рівень реєструється.
Божевільний фізик

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

1
Чому метод не використовує внутрішній метод?
Gringo Suave,

9

Мені легше створити новий атрибут для об’єкта реєстратора, який передає функцію log (). Я думаю, що модуль реєстратора забезпечує 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.Дякую, але, як я вже сказав, я не пробував цього. :)
Нуфал Ібрагім

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. Я думаю, що використання лямбда виглядає дещо чистіше, але недолік у тому, що він не може приймати аргументи ключових слів. Я ніколи цього не використовував, так що жодного великого.

  ПРИМІТКА Налаштування: школа закінчується на літо! чувак
  Налаштування FATAL: файл не знайдено.

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?
Павло

Так, я думаю, ви, мабуть, зробили б це. Ця відповідь була адаптована з коду, який вирішує дещо іншу проблему.
відзначено

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

Конфігурація може виглядати так:

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

вихід:

DEBUG: root: y1

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


1
Це не дозволяє вам зробити те, logger.trace('hi')що, на мою думку, головна мета
Ультимація

-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. Ого.
Божевільний фізик

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