はじめに
Pythonにおけるloggingは標準ライブラリとして搭載されており、一般的なログを出力するライブラリではloggingが使用されており、OSのファイルシステムと同じようにグローバルな基盤としての位置づけが与えられている。したがって一般的な使い方では、プログラムの実行において全体を取りまとめるものとして考え、プログラムでは必要性がない限りLoggerを関数やクラスの引数として与える必要はなくグローバル変数として考えるべきものである。またloggingには階層構造が取り入れられており、アプリケーション側(利用する側)とライブラリ側(利用される側)である種の契約をすることにより、依存関係の少ない構築が可能になっている。
概念について
loggingを理解するための概念として以下の項目が挙げられる
LoggerHandlerFormatter-
Loggerにおけるname -
LoggerにおけるPropagate -
Loggerにおけるlevel -
Handlerにおけるlevel -
LoggerにおけるFilter -
HandlerにおけるFilter Logging Levels
Loggerについて
loggingの本体。後述するnameにより階層構造を作られる。0個以上のHandlerを持つ。
Handlerについて
Loggerに取り付けるもの。Loggerが取得した内容をファイルに出力や標準出力や標準エラー出力等に吐き出す動作を行う。
Formatterについて
Handlerに取り付けるもの。Loggerが取得した内容をどう表記するかを示したもの。例えば出力に取得時刻やコードのライン番号等を記載することが可能である。
Loggerにおけるnameについて
一意に識別される名前であり、"."を入れることにより階層構造を含むことが可能である。Noneの場合はrootを指す。"myApp","myApp.b"という名前があれば、root->"myApp"->"b"という上位下位の階層が作られる。
LoggerにおけるPropagateについて
デフォルトはTrueになっていて、Trueの場合は上位の階層のLoggerに取得した内容を伝播させる。デフォルトの状態では最終的にrootのLoggerまで伝播することなる。
LoggerやHandlerにおけるlevelについて
Loggerのlevelは、後述するログレベルに応じて、抽出するものを決定するものである。デフォルトはlogging.NOTSETであり親のLoggerのlevelを設定する。標準的な使い方では、rootのLoggerのlevelが付与されると思ってよい。rootのLoggerは基本的にはlogging.basicConfigで設定するが、デフォルトはlogging.WARNINGとなる。
Handlerのlevelはその対応するLoggerで抽出されたものの中で、設定されたlevelに応じて実際に出力するものを決定する。Handlerにlevelが設定されている理由は、Handler毎に出力する項目を制限させたいといったことをするためである。デフォルトはlogging.NOTSETでこの場合は全部通す。
Logging Levelsについて
| Level | Numeric value | 備考 |
|---|---|---|
| logging.NOTSET | 0 | |
| logging.DEBUG | 10 | |
| logging.INFO | 20 | |
| logging.WARNING | 30 | |
| logging.ERROR | 40 | |
| logging.CRITICAL | 50 |
Logにはレベルが設定されており、重要なものほど数値が高く設定されている。設定したレベル以上のものが抽出あるいは出力される。ただしlogging.NOTSETはLoggerの場合は親が決める。Handlerの場合はそのまま通す。
LoggerにおけるlevelやPropagateと階層構造の関係について
Loggerにおけるlevelと階層構造との関係性については、logging.NOTSETされたものは基本上位層のlevelを継承する。上から降ってくるものが設定される。
LoggerにおけるPropagateと階層構造との関係性については、下位層から上にログを吸い上げる感じになっており、デフォルトではrootのLoggerに情報がすべて集約されるという感じになる。
標準的な使い方
ライブラリ側の書き方
基本的な方針は、Loggerだけを設定し、他は設定しない。
levelも設定しない、PropagateもデフォルトのTrue、Handlerも設定しない。アプリケーション側(使う側)ですべて設定する。
以下のようにそれぞれのファイルごと(正確にはライブラリの書き方によって__init__.pyのみでよいかは違う)にこのようにグローバル変数を設定する。
import logging
logger = logging.getLogger(__name__)
__name__については、例えばurllib3のようなライブラリの場合は、
urllib3
├─ __init__.py
└─ connection.py
__init__.pyの場合はurllib3、connection.pyの場合はurllib3.connectionというようなloggingで設定している階層構造が表示される。
したがって、urllib3ライブラリ全体に対してlevelを設定したい、urllib3ライブラリ全体のログだけをHandlerに設定したい場合は、logging.getLogger("urllib3")に対して設定をアプリケーション側で施せばよい。
アプリケーション側(使う側)の設定
ライブラリ側はロガーだけを設定するという形で、それ以外はアプリケーション側で設定する。
import logging
logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.DEBUG)
logger = logging.getLogger(__name__)
logger.debug('This message should go to the log file')
logger.info('So should this')
logger.warning('And this, too')
logging.basicConfig等でまずはrootのLoggerを設定する。logging.basicConfigではハンドラーを指定しなければStreamHandlerが設定される。必要な場合、Formatterも設定する。
場合によってはlogger = logging.getLogger(__name__)を設定せずに直接logging.debug()等(rootのLoggerに対して出力される)を記載してもよい。アプリケーション側では1ファイルしかないとか、分割されていてもそれほど多くないとか簡単なプログラムなら、直接logging.debug()で良い気がします。
また、logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.DEBUG)の設定だけだと、全体のレベルがlogging.DEBUGとなりライブラリ側もDEBUG以上で出力されてしまうことになる。不要なものが大量に出てしまうということでライブラリ側の出力を表示させたくない場合は別途以下のように高いレベルを設定すればよい。
import logging
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
logging.getLogger("urllib3").setLevel(51)
logging.getLogger("urllib3").setLevel(100)
※Filterでも可能であるが、基本はlevelの設定のみで事足りる。複雑な条件を加えたい場合はFilterで
PythonのloggingにおいてはDI的な注入は不要である
当然、Loggerをグローバル変数として関数やクラスに入れると依存関係になってしまうが、loggingは標準ライブラリとして搭載されている、かつ、一般的なライブラリではloggingを使用しており、階層構造を意識したつくりをしていることが多いので、関数やクラスの引数として入れる発想はほとんどない。依存性を限りなく少なくしたいといった特別な事情がない限りDI的な注入は不要であると考える。
-
loggingが標準ライブラリとして搭載されている -
loggingが一般的なライブラリで利用されている - ライブラリ側が
loggingの方針に沿ってコードを構築されている(ことが多い)
という状態になっているためあえて、関数やクラスの引数として入れる設計にする必要はないわけである。