目次
この記事を読むことで得られるメリット
- Pythonのロギングシステムの基本から応用までを体系的に学べる
- 共通の落とし穴を避け、ベストプラクティスに従ったコードが書けるようになる
- プロジェクトで再利用可能なロギング設定の方法を習得できる
- デバッグ効率が向上し、問題発見・解決が容易になる
- 本番環境でのログ管理が適切に行えるようになる
この記事を読むのにかかる時間
約15分
Pythonロギングの基本概念
Pythonのロギングシステムは階層構造を持っており、大きく分けて「ルートロガー」と「名前付きロガー」の2種類がある。
ロギングシステムの階層構造は、Pythonのパッケージ・モジュール構造を反映しており、最も上位に位置するのがルートロガーだ。この階層構造を理解することで、効果的なロギング設計が可能になる。
# ロギングの基本的な階層構造
root (ルートロガー)
├── app
│ ├── app.models
│ └── app.views
└── lib
└── lib.utils
ロガーオブジェクトの正しい使い方
loggingを直接importして使用しない
ロギングを実装する際の最も重要なポイントは、ルートロガーを直接変更しないことだ。
NG例:
import logging
logging.debug('this is bad logging') # ルートロガーを直接使用している
この方法ではルートロガーを直接変更することになり、グローバル変数を直接書き換えるのと同様の問題が発生する。例えば、ログレベルを変更すると、他のモジュールのロギング動作にも影響を与えてしまう。
OK例:
from logging import getLogger
logger = getLogger(__name__) # 現在のモジュール名でロガーを取得
logger.debug('this is good logging')
この方法では、現在のモジュール専用のロガーを取得するため、他のモジュールに影響を与えず、階層構造も適切に維持できる。
現在のモジュール名を使用してロガーオブジェクトを取得する
__name__
を使用することで、ログメッセージにどのモジュールから生成されたかを明示できる。これにより問題発生時の原因特定が容易になる。
ログレベルの理解と活用
Pythonのロギングシステムには5つの標準ログレベルがある:
- DEBUG - 詳細なデバッグ情報
- INFO - 一般的な情報メッセージ
- WARNING - 警告(潜在的な問題)
- ERROR - エラー(機能が動作しない)
- CRITICAL - 重大なエラー(プログラムが動作しない可能性)
ログレベルを適切に設定することで、出力されるログの量と種類を制御できる:
from logging import getLogger, INFO
logger = getLogger(__name__)
logger.setLevel(INFO) # INFO以上のログレベルのみ出力
logger.debug('debug') # 表示されない
logger.info('info') # 表示される
logger.warning('warning') # 表示される
logger.error('error') # 表示される
logger.critical('critical') # 表示される
各ログレベルの詳細な使用シーンについては、Python公式ドキュメントを参照するとよい。
ハンドラーの設定と活用法
ログを実際に出力するためには、ハンドラーの設定が必要だ。主に使用される2種類のハンドラーについて説明する:
- StreamHandler - コンソールにログを出力
- FileHandler - ファイルにログを出力
from logging import getLogger, StreamHandler, FileHandler, Formatter, INFO, ERROR
logger = getLogger(__name__)
logger.setLevel(INFO)
# StreamHandlerの設定
ch = StreamHandler()
ch.setLevel(INFO)
ch_formatter = Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(ch_formatter)
logger.addHandler(ch)
# FileHandlerの設定
fh = FileHandler('log/test.log')
fh.setLevel(ERROR) # ERRORとCRITICALのみファイルに記録
fh_formatter = Formatter('%(asctime)s - %(levelname)s - %(filename)s - %(name)s - %(funcName)s - %(message)s')
fh.setFormatter(fh_formatter)
logger.addHandler(fh)
ロガーとハンドラーのログレベル関係
ロガーとハンドラーのログレベルは独立しており、それぞれが独自のフィルタリングを行う。ただし、重要なポイントとして:
- ロガーは設定されたレベル未満のメッセージをフィルタリングするため、ロガーのレベルがハンドラーのレベルより高い場合、ハンドラーに低レベルのメッセージは届かない
- 各ハンドラーは独自のログレベルとフォーマットを持つことができる
フォーマッターによるログの整形
フォーマッターを使用すると、ログメッセージに時刻、モジュール名、ログレベルなどの情報を追加できる:
from logging import getLogger, StreamHandler, Formatter
logger = getLogger(__name__)
handler = StreamHandler()
format = Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(format)
logger.addHandler(handler)
logger.info('this is formatted log')
一般的なフォーマット指定子には以下のものがある:
%(asctime)s
- 日時%(name)s
- ロガー名%(levelname)s
- ログレベル名%(message)s
- ログメッセージ%(filename)s
- ファイル名%(funcName)s
- 関数名%(lineno)d
- 行番号
設定ファイルを使ったロギング設定
大規模なアプリケーションでは、設定ファイルを使用してロギングの設定を管理すると便利だ。Pythonでは、主に以下の方法がある:
- fileConfig - INIファイルから設定を読み込む
- dictConfig - 辞書から設定を読み込む
fileConfig(INIファイル)
from logging import getLogger
import logging.config
logging.config.fileConfig('logging_settings.ini')
logger = getLogger('root')
logger.info('this is fileConfig from logging_settings.ini')
logging_settings.ini
の内容:
[loggers]
keys=root
[handlers]
keys=newStreamhandler
[formatters]
keys=newFormatter
[logger_root]
level=INFO
handlers=newStreamhandler
[handler_newStreamhandler]
class=StreamHandler
level=DEBUG
formatter=newFormatter
args=(sys.stderr,)
[formatter_newFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
dictConfig(辞書)
from logging import getLogger, DEBUG, INFO
import logging.config
dict_logging_settings = {
'version': 1,
'handlers': {
'newStreamHandler': {
'class': 'logging.StreamHandler',
'formatter': 'newFormatter',
'level': DEBUG
}
},
'formatters': {
'newFormatter': {
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
},
},
'root': {
'handlers': ['newStreamHandler'],
'level': INFO
},
'loggers': {}
}
logging.config.dictConfig(dict_logging_settings)
logger = getLogger('root')
logger.info('this is dictConfig from dict_logging_settings')
Python公式ドキュメントではJSON/YAMLを使用した設定が推奨されている。INIファイルは読み書きが複雑になる傾向があるためだ。JSONはPython標準ライブラリで読み込めるが、YAMLは外部ライブラリが必要となる。
注意点として、設定ファイルのパラメータであるdisable_existing_loggers
の取り扱いに注意が必要だ。
print文ではなくloggerを使うべき理由
1. 情報量が多い
print
はシンプルでデバッグ時に便利だが、本番環境のコードではlogging
モジュールを使用すべきだ。ロギングでは各メッセージに時刻やログレベルなど多くのメタデータを含められる。
2. ログレベルを指定できる
ロギングでは、メッセージの重要度に応じたログレベルを明示的に指定できる。これにより、どのようなメッセージなのかがすぐに判断できるのだ。
3. 出力先の指定が豊富
print
はコンソールにしか出力できないが、ロギングはファイル、コンソール、メールなど様々な出力先に対応している。
4. トレースバックを簡単に含められる
例外発生時にトレースバックを記録する場合:
try:
open('/path/to/does/not/exist', 'rb')
except (SystemExit, KeyboardInterrupt):
raise
except Exception:
logger.error('Failed to open file', exc_info=True) # トレースバック情報が自動的に含まれる
実践的なロギングパターン
大規模なアプリケーションでは、以下のようなベストプラクティスを考慮すべきだ:
- アプリケーション起動時にロギング設定を行う
- 異なる環境(開発、テスト、本番)で異なるロギング設定を使用する
- 重要な操作やエラーは適切なログレベルで記録する
- ログローテーションを設定して、ログファイルのサイズ管理を行う