Pythonのログ機能にカスタムログレベルを追加する方法


116

debug()十分ではないと思うので、アプリケーションにログレベルTRACE(5)を設定します。さらに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")

よろしくお願いします。

編集(2016年12月8日):承認された回答をpfaに変更しました。これは、エリックSからの非常に優れた提案に基づいた優れたソリューションであるIMHOです。

回答:


171

@Eric S.

エリック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 Loggerlogging.__init__.pyのPython 2.7のため、これはすべての標準のログ機能は、(.critical、.DEBUG、など)何をすべきかです。

私はどうやら評判の欠如のために他の人の答えへの返信を投稿することはできません...うまくいけば、エリックがこれを見つけたら彼の投稿を更新するでしょう。=)


7
ログレベルを正しくチェックするため、これはより良い答えです。
大佐パニック

2
確かに現在の答えよりもはるかに有益です。
Mad Physicist

4
@pfa logging.DEBUG_LEVEL_NUM = 9ロガーをコードにインポートするすべての場所でそのデバッグレベルにアクセスできるように、追加についてはどうですか?
edgarstack 2016

4
間違いなくDEBUG_LEVEL_NUM = 9定義する必要がありますlogging.DEBUG_LEVEL_NUM = 9。この方法は、あなたが使用することができますlog_instance.setLevel(logging.DEBUG_LEVEL_NUM)あなたが右のノウハウを使って同じようにlogging.DEBUGlogging.INFO
MAQ

この回答は非常に役に立ちました。pfaとEricSに感謝します。私は完全を期すため、さらに2つの文が含まれていることを提案したい:logging.DEBUGV = DEBUG_LEVELV_NUMlogging.__all__ += ['DEBUGV'] 二つ目はひどく重要ではありませんが、あなたが動的にログ出力レベルを調整任意のコードを持っているし、あなたのような何かを行うことができるようにしたい場合はまず必要であるif verbose: logger.setLevel(logging.DEBUGV)`
キースハンラン2018

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も。エレガントなアプローチで、完璧に機能しました。重要な注意:これを行う必要があるのは1つのモジュールで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'
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.Loggerlogging.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レベルを追加するのが好きで、スフィンクスのコンポーネントの1つを追加したので、それは私に一度思いつきました。
マッド物理学者

1
目の前のアスタリスクの欠如であるargsにおけるlogForLevel実装では、意図的/必要?
Chris L. Barnes

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
これは、サルのパッチを回避するため、私見の最良の答えです。正確に何getsetLoggerClassし、なぜそれらが必要なのですか?
Marco Sulla

3
@MarcoSullaそれらはPythonのロギングモジュールの一部として文書化されています。このライブラリを使用しているときに誰かが自分のlloggerを必要とした場合に、動的サブクラス化が使用されると思います。このMyLoggerは、2つのクラスを組み合わせて、私のクラスのサブクラスになります。
CrackerJack9 2015年

これは、デフォルトのロギングライブラリにレベルを追加するかどうかについてこの説明で示したソリューションと非常に似ていTRACEます。+1
IMP1

18

内部メソッド(self._log)を使用する悪い習慣を始めたのは誰ですか。それぞれの答えがそれに基づいているのはなぜですか。self.log代わりにpythonicソリューションを使用するので、内部のものをいじる必要はありません。

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、filename、pathnameなど)が実際の呼び出し元ではなくデバッグ関数を指すようになります。これはおそらく望ましい結果ではありません。
2014

5
クラス自身の内部メソッドを呼び出すのはいつ許されないのですか?関数がクラスの外部で定義されているからといって、それが外部メソッドであるとは限りません。
OozeMeister 2015年

3
この方法では、スタックトレースが不必要に変更されるだけでなく、正しいレベルがログに記録されているかどうかもチェックされません。
Mad Physicist、

@schlamarの言うことは正しいと思いますが、反対の理由は同じ票数を得ました。だから何を使うのですか?
Sumit Murari 2017年

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
marqueed

9

すでに正しい答えはたくさんありますが、私の意見では次のようにもっとpythonicです。

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
Sergey Nudnov

@SergeyNudnov指摘してくれてありがとう、修正しました。私の側からのミスでした、私は自分のコードからコピーし、明らかに掃除を台無しにしただけです。
DerWeh

8

Loggerクラスをサブクラス化し、trace基本的にはLogger.log未満のレベルで呼び出す呼び出されるメソッドを追加する必要があると思いますDEBUG。私はこれを試していませんが、これはドキュメントが示していることです。


3
またlogging.getLogger、組み込みクラスの代わりにサブクラスを返すように置き換えたいと思うでしょう。
S.Lott

4
@ S.Lott-実際(少なくとも現在のバージョンのPythonでは2010年にはそうではなかったかもしれません)、通常どおり使用setLoggerClass(MyClass)してから呼び出す必要がありgetLogger()ます...
Mac

IMO、これは断然最高の(そして最もPythonicな)答えであり、もし私がそれに複数の+1を与えることができたなら、私はそうしたでしょう。実行は簡単ですが、サンプルコードを使用すると便利です。:-D
Doug R.

@DougR。ありがとうございますが、私が言ったように、私はそれを試していません。:)
Noufal Ibrahim 2018

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

あなたはする必要がありますsetFormattersetHandlersetLevel(TRACE)handlerとでlog実際にこの低レベルのトレースをSEに自分自身


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

@marqueedが指摘したように、lambda / funcNameの問題はlogger._logで修正されています。ラムダを使用すると少し見栄えがよくなると思いますが、欠点はキーワード引数をとることができないことです。自分で使ったことがないので、大したことはありません。

  注:学校は夏休みです!おい
  致命的なセットアップ:ファイルが見つかりません。

2

私の経験では、これはopの問題の完全な解決策です...メッセージが発行される関数として「ラムダ」が表示されないようにするには、さらに深く行きます。

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

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

出力:

DEBUG:root:y1

TRACE:root: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
exec内での評価。ワオ。
Mad Physicist

2
.....私にこれを実行させたsetattr
理由が
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.