nikkie-ftnextの日記

イベントレポートや読書メモを発信

Pythonで標準ライブラリloggingを使って自作ライブラリの中でロギングしたい未来の私へ

はじめに

久美子部長と滝先生の誕生日に挟まれた今日8/22は、タモリの誕生日。おめでとうございます。
nikkieです。

Pythonの標準ライブラリloggingのgetLogger()関数を使って得たロガーの扱い方の自分用まとめです。
前提として、他のユーザによって使われるライブラリを開発しており、ライブラリの中でのロギングを書こうとしています。

目次

結論:getLogger()で得たロガーにNullHandlerを設定する

ロガーを仕込むライブラリ
src/awesomelib/__init__.py

import logging

logger = logging.getLogger(__name__)
logger.setLevel(logging.WARNING)
logger.addHandler(logging.NullHandler())


def awesome() -> None:
    logger.warning("ちょっとヤバいよ")
    print("Awesome ✨")

ライブラリを使うコード

import logging

from awesomelib import awesome

logging.basicConfig(
    format="%(asctime)s | %(levelname)s | %(name)s:%(funcName)s:%(lineno)d - %(message)s",
)

awesome()

出力結果

2024-08-22 23:40:05,108 | WARNING | awesomelib:awesome:9 - ちょっとヤバいよ
Awesome ✨

※ただし執筆時点で詰めきれていない積み残しあり(変更履歴が分かるようにアップデートします)

(2023/08/26 追記)
ライブラリのロガーはsetLevelせず、NOTSETレベルのまま使うのがよいように思われます(「ライブラリのためのロギングの設定」のコードにより近づきます)
自作のPythonライブラリでloggingを使うとき、ロガーにsetLevelしない? ログレベルNOTSETのロガーの動きを理解する - nikkie-ftnextの日記
(2023/08/26 追記終わり)

コード解説

まとめ

上記のコードに込めた意図です

  • ライブラリは子のロガーを設定して使う
    • logging.getLogger(__name__)
    • 子のロガーがログをハンドラに送るlevelを設定
    • ハンドラは何もしない
  • ライブラリのユーザはルートロガーを設定して使う
    • logging.basicConfig()
    • ルートロガーが出力するハンドラとそのフォーマットを指定した(標準出力)

ライブラリのロガー

ドキュメントの以下に従っています。
「ライブラリのためのロギングの設定」
https://docs.python.org/ja/3/howto/logging.html#configuring-logging-for-a-library

何らかの理由で、ロギング設定がなされていないときにメッセージを表示 させたくない なら、あなたのライブラリのトップレベルロガーに何もしないハンドラを取り付けられます。

logging.getLogger(__name__)logging.getLogger("awesomelib")なので1、ライブラリのトップレベルロガーです。
ここに何もしないロガーNullHandler()を設定しています。

ロガーのレベルをWARNINGとしました。
WARNING以上のレベルのログがハンドラに渡ります。

ここでハンドラは何もしないので、WARNING以上のレベルのログは何もされず、どこにも出力されません。

  • WARNING以上のレベルのログ:ロガーが処理してハンドラが呼び出されるが、ハンドラの処理ではどこにも出力しない
  • WARNING未満のレベルのログ:ロガーは処理しない(そもそもハンドラは呼ばれない)

よって、どのようなログも(WARNING以上は処理されるのですが)出力されることはありません。

ライブラリのユーザのロガー

ルートロガーを設定します。

ルートロガーは全てのロガーの親です。
すなわち、ライブラリのロガーの親でもあります。

ロガーの親子関係には、propagate(伝播)という仕組みがあります。
子のロガーが処理するログは、親のロガーにも(そのまた親にも)伝播します。
ルートロガーはすべての親なので、ライブラリのロガーが処理するログはルートロガーにも届きます。

上記のlogging.basicConfig()では、ルートロガーのレベルをWARNINGとし、さらに、標準出力するハンドラを設定しました。
参考:Pythonのloggingモジュールのドキュメントの「もっとも単純な例」を説明する 〜logging.warningの裏側で起こっていること〜 - nikkie-ftnextの日記
これにより

  • ライブラリのロガーがWARNING以上のレベルのログを処理する
    • (ライブラリのロガーのハンドラは何もしない)
  • 親であるルートロガーも同じログを処理する
    • ハンドラが標準出力に出力2

結果として、ライブラリのロガーのハンドラは出力しませんが、propagateのおかげでルートロガーのハンドラが出力します!

同じ内容を扱った過去記事(「ロガーの階層関係をもとに、ここまでのロギングを理解する」など参照)

propagateを含め、ロガーの親子関係について書いた過去記事
(※本記事で扱っていないフィルタという概念も登場します)

積み残し事項

終わりに

現時点の私が考える、Pythonで書いたライブラリの中のロギングのコードでした。
ロガーの親子関係(propagate)を利用し、子ロガーは何もしないハンドラを設定。
親ロガーへの伝播を利用して、ルートロガーのハンドラでログを処理させるのがよいと考えています。

ここまで理解するのに私は全然スムーズにいかなかったので、今後見返して出発点とするための記事でした。

PyCon mini Shizuokaの発表準備でもあります。

変更履歴

  • 2024/08/26
    • 積み残し「ロガーやハンドラのlevelをNOTSETに指定」解消
      • 合わせてライブラリのロガーはsetLevelしないことを結論に追加
    • 積み残し「ライブラリでNullHandlerを設定していない場合」解消

  1. 「モジュールの名前」参照 https://docs.python.org/ja/3/reference/datamodel.html#modules
  2. 途中で出した「もっとも単純な例」の記事より、レベルがNOTSETです(一つの記事にまとめきれなかったので落ち穂拾い記事を書きます)