はじめに
トトロって、サツキがかわいいからメイ探しのお手伝いしたらしい。俺らと同類じゃん! nikkieです。
先日書いたPython自作ライブラリにおけるロギングの積み残しの解消に着手します。
この記事では、ロガーのNOTSET
レベルについて見ていきます。
目次
出発点:自作ライブラリの中でのロギング
ライブラリのコード(src/awesomelib/__init__.py
)
import logging logger = logging.getLogger(__name__) logger.setLevel(logging.WARNING) # <- この記事の主な話題 logger.addHandler(logging.NullHandler()) def awesome() -> None: logger.info("想定通り") logger.warning("ちょっとヤバいよ") print("Awesome ✨")
- ライブラリのロガーの設定
- レベルを
WARNING
に(※この記事の主な話題です) - ハンドラを
NullHandler
に
- レベルを
- ライブラリの中のログ出力
- レベル
WARNING
以上で、ハンドラ呼び出し - しかし、ハンドラは何もしない
- propagateで親のロガーに伝播(ルートロガーまで)
- ライブラリのユーザがルートロガーを設定(例:
logging.basicConfig
)したら、ルートロガーのハンドラによってログ出力される
- ライブラリのユーザがルートロガーを設定(例:
- レベル
ドキュメント「ライブラリのためのロギングの設定」
https://docs.python.org/ja/3/howto/logging.html#configuring-logging-for-a-library
ライブラリにはNullHandler
を設定するよう指南する箇所です。
ここのコードですが、ライブラリのロガーにレベルを設定していません。
import logging logging.getLogger('foo').addHandler(logging.NullHandler())
レベルを設定しないときの動きを理解していきます。
Python 3.11.8で動作を確認しました。
ライブラリのロガーの設定を検証
ライブラリのロガーにレベルやハンドラを設定しない、かつ、ルートロガーも設定しない
ライブラリ
import logging logger = logging.getLogger(__name__) -logger.setLevel(logging.WARNING) -logger.addHandler(logging.NullHandler()) def awesome() -> None: logger.info("想定通り") logger.warning("ちょっとヤバいよ") print("Awesome ✨")
ライブラリを使うコード
from awesomelib import awesome awesome()
出力結果
ちょっとヤバいよ Awesome ✨
この振る舞いは「環境設定が与えられないとどうなるか」で説明されます。
ロギング環境設定を与えられないと、ロギングイベントを出力しなければならないのに、イベントを出力するハンドラが見つからないことがあります。
イベントは、 lastResort に格納された「最終手段ハンドラ」を使用して出力されます。
最終手段ハンドラは
- 標準エラー出力(
sys.stderr
)に書くStreamHandler
(のように動作) - フォーマットは行われない
- -> 「ちょっとヤバいよ」とメッセージだけ出力した
- ハンドラのレベルは
WARNING
- -> INFOは出力されていない
となります。
ライブラリのロガーにレベルのみ設定しない、かつ、ルートロガーも設定しない
ライブラリ
import logging
logger = logging.getLogger(__name__)
-logger.setLevel(logging.WARNING)
logger.addHandler(logging.NullHandler())
def awesome() -> None:
logger.info("想定通り")
logger.warning("ちょっとヤバいよ")
print("Awesome ✨")
ライブラリを使うコード(1つ前と同じ)
from awesomelib import awesome awesome()
出力結果
Awesome ✨
NullHandler
が与えられているので、最終手段ハンドラの出番はないわけですね。
「ライブラリのためのロギングの設定」の以下は、最終手段ハンドラのことを言っていたのか〜
何らかの理由で、ロギング設定がなされていないときにメッセージを表示 させたくない なら、あなたのライブラリのトップレベルロガーに何もしないハンドラを取り付けられます。
ライブラリのロガーにレベルのみ設定しない、かつ、ルートロガーのレベルを設定
ライブラリ(1つ前と同じ)
import logging
logger = logging.getLogger(__name__)
-logger.setLevel(logging.WARNING)
logger.addHandler(logging.NullHandler())
def awesome() -> None:
logger.info("想定通り")
logger.warning("ちょっとヤバいよ")
print("Awesome ✨")
ライブラリを使うコードでは、ルートロガーをWARNINGレベルに設定します
import logging from awesomelib import awesome logging.basicConfig( format="%(asctime)s | %(levelname)s | %(name)s:%(funcName)s:%(lineno)d - %(message)s", ) awesome()
出力結果
2024-08-24 10:36:27,696 | WARNING | awesomelib:awesome:9 - ちょっとヤバいよ Awesome ✨
propagateによりログ出力されていますね。
ここをもう少し見ていきます。
ログレベルNOTSET
と実効レベル
では、ライブラリのロガーのsetLevel()
の有無で何が違うのでしょうか?
setLevel()
を呼び出していないと、レベルはNOTSET
として扱われます。
違いはINFO
レベルのログです。
ライブラリでlogger.setLevel(logging.WARNING)
しているとき、ルートロガーをDEBUG
レベルに指定しても、INFO
レベルのログは出力されません。
2024-08-24 10:37:59,478 | WARNING | awesomelib:awesome:10 - ちょっとヤバいよ Awesome ✨
これは、ライブラリのロガーがWARNING
レベル以上を処理するように設定されているためですね。
INFO
レベル(=WARNING
レベル未満)のログはロガーが処理しません。
ロガーが処理するWARNING
以上がハンドラに渡り、また、親のロガーへもpropagateされます
ライブラリでロガーをsetLevel()
していない(すなわち、NOTSET
レベルのまま)のとき、ルートロガーをDEBUG
レベルに指定すると、INFO
レベルのログが出力されます。
2024-08-24 10:39:00,471 | INFO | awesomelib:awesome:8 - 想定通り 2024-08-24 10:39:00,471 | WARNING | awesomelib:awesome:9 - ちょっとヤバいよ Awesome ✨
ライブラリのロガーのレベルがNOTSET
のとき、どのようにログが処理されているかはドキュメントに記載があります。
https://docs.python.org/ja/3/library/logging.html#logging.Logger.setLevel
「親ロガーに委譲」という用語の意味は、もしロガーのレベルが NOTSET ならば、祖先ロガーの系列の中を NOTSET 以外のレベルの祖先を見つけるかルートに到達するまで辿っていく、ということです。
もし NOTSET 以外のレベルの祖先が見つかったなら、その祖先のレベルが探索を開始したロガーの実効レベルとして扱われ、ログイベントがどのように処理されるかを決めるのに使われます。
ライブラリのロガー
- レベルは
NOTSET
- 2つのレベルで呼び出されている
INFO
WARNING
親ロガー(ここではルートロガー)
- もともと
WARNING
レベル- ライブラリのロガーの実効レベルは
WARNING
となる WARNING
レベルのログのみ処理する
- ライブラリのロガーの実効レベルは
DEBUG
レベルに指定(logging.basicConfig(level=logging.DEBUG)
)- ライブラリのロガーの実効レベルは
DEBUG
となる INFO
レベル・WARNING
レベル、両方を処理する
- ライブラリのロガーの実効レベルは
ロガーのレベルNOTSET
とは、親のレベルを参照ということなのですね。
今回は階層構造が浅いのでルートロガーの設定に応じて、ライブラリのロガーのレベルが変わりました。
a.b
ロガーのように階層を少し深くした場合は、ライブラリのユーザがa
ロガーのレベルを設定することで、a.b
ロガーのレベルを変えることもできますね。
ロガーのレベルNOTSET
が親のレベルを参照というのは、ロガーのgetEffectiveLevel()
メソッドで確認できますね1。
python -i <script.py>
として対話モードに入って
>>> logging.getLogger("awesomelib") # WARNINGはgetEffectiveLevel()の返り値 <Logger awesomelib (WARNING)> >>> logging.getLogger("awesomelib").level 0
ライブラリのロガー自体のレベルはNOTSET
ですが、実効レベルはルートロガー(親)のレベルです
終わりに
公式ドキュメントに書かれた、ライブラリのロガーにNullHandler
を設定するコードで、ロガーのレベルを設定していない点を見てきました。
- ライブラリのロガーは
NOTSET
レベルとなっています - これは親ロガーのレベルを参照して振る舞います(実効レベル)
- ルートロガーの設定に応じて詳細なログ出力をすることが可能です
実はハンドラにもレベルが設定されるのですが、こちらのNOTSET
は直感的でした2。
https://docs.python.org/ja/3/library/logging.html#logging.Handler.setLevel
ハンドラが生成された際、レベルは NOTSET (すべてのメッセージが処理される) に設定されます。
今回のソースコードはこちらです。