【初心者向け】Python logging モジュールの使い方解説

はい、承知いたしました。Pythonのloggingモジュールの初心者向け詳細解説記事を、約5000語のボリュームで記述します。記事の内容を直接表示します。


【初心者向け】Python logging モジュールの使い方を徹底解説!ログの基本から実践まで

はじめに:なぜログが必要なのか? printデバッグからの脱却

Pythonでプログラムを書いているとき、処理の流れを確認したり、変数の値がおかしくないか調べたりするために、多くの人がまず思いつくのがprint()関数を使うことでしょう。

“`python
def divide(a, b):
print(f”divide関数が呼び出されました。a={a}, b={b}”) # デバッグ用print
if b == 0:
print(“エラー: ゼロで割ろうとしました!”) # エラー通知用print
return None
result = a / b
print(f”計算結果: {result}”) # 結果表示用print
return result

関数呼び出し

print(“— ゼロで割る場合 —“)
divide(10, 0)

print(“\n— 正常な場合 —“)
divide(10, 2)
“`

このように、print()は手軽で直感的です。しかし、プログラムが複雑になり、規模が大きくなるにつれて、print()デバッグには多くの問題点が出てきます。

print()デバッグの問題点

  1. 情報の取捨選択が難しい: プログラム中にたくさんのprint()を書くと、実行時には大量のテキストが出力されます。この中から必要な情報(エラーメッセージ、特定の処理通過ログなど)だけを探し出すのが非常に困難になります。
  2. 出力先の変更が面倒: 開発中はコンソールに出力すれば良いですが、運用段階ではログをファイルに保存したり、場合によってはネットワーク経由で別の場所に送ったりしたいことがあります。print()だけを使っていると、これらの出力先を変更するために、プログラム中のすべてのprint()を書き換えるか、リダイレクトなどのOSの機能を使う必要があり、非常に手間がかかります。
  3. 重要度によるフィルタリングができない: print()はすべて同じように出力されます。「これは絶対に見落としてはいけないエラー」「これは開発中にだけ見たい詳細情報」「これはアプリケーションの正常な動作を示す情報」といった重要度(ログレベル)に応じて、表示するかしないかを切り替えることができません。
  4. タイムスタンプや情報源の記録が自動化されていない: いつ、プログラムのどこ(どのファイル、どの関数、何行目)でそのメッセージが出力されたのかといった情報が、print()だけでは自動的に記録されません。これらの情報がないと、後からログを見返して原因を特定するのが難しくなります。毎回手動でタイムスタンプや関数名などを埋め込むのは現実的ではありません。
  5. 運用環境での扱い: 実際にユーザーに使われる運用環境では、開発時のような大量の詳細なログは不要なことが多いです。しかし、何か問題が発生した際には、原因究明のために詳細なログが必要になることもあります。print()だけでは、開発用と運用用でログの出し分けをするのが非常に困難です。
  6. 標準エラー出力への切り替え: エラーメッセージは標準エラー出力(stderr)に出力するのが一般的ですが、print()はデフォルトでは標準出力(stdout)に出力します。出力先を分けるには、print("...", file=sys.stderr)のように記述する必要があり、少し手間がかかります。

これらの問題を解決し、より効率的かつ柔軟にプログラムの情報を記録・管理するために設計されたのが、Python標準ライブラリに含まれる loggingモジュール です。

loggingモジュールのメリット

loggingモジュールを使うことで、print()デバッグの多くの問題を克服できます。

  • ログレベルによるフィルタリング: メッセージに重要度(レベル)を設定し、設定されたレベル以上のメッセージだけを出力するように制御できます。
  • 柔軟な出力先: コンソール、ファイル、ネットワークなど、複数の場所に同時にログを出力できます。出力先の設定変更も容易です。
  • 豊富な情報付与: タイムスタンプ、ログレベル、ロガー名(メッセージの発生源)、ファイル名、行番号、プロセスID、スレッドIDなどを自動的にメッセージに含めることができます。
  • フォーマットのカスタマイズ: ログメッセージの表示形式(タイムスタンプをどのように表示するか、どの情報をどの順番で表示するかなど)を自由に定義できます。
  • モジュールごとのロギング: プログラムの各モジュールやクラスごとに独立したロガーを持つことができ、管理が容易になります。
  • 設定の集中管理: プログラムのコードを書き換えることなく、設定ファイルなどを使ってロギングの挙動を変更できます。
  • 例外情報の出力: 例外発生時に、トレースバック情報を含んだログを簡単に出力できます。

loggingモジュールは、本格的なアプリケーション開発や、長期的に運用されるシステムでは必須と言える機能です。最初は少し難しく感じるかもしれませんが、基本を覚えれば非常に強力なツールとなります。

この記事では、Pythonのloggingモジュールを初めて使う方に向けて、その基本的な考え方から、実際の使い方、そして少し踏み込んだ応用例までを、体系的かつ詳細に解説していきます。

さあ、printデバッグから卒業し、loggingを使ったスマートなログ管理の世界へ踏み出しましょう!

loggingモジュールの基本概念:ログを構成する要素

loggingモジュールを理解するためには、いくつかの重要な構成要素を知る必要があります。これらはログの生成、処理、出力の各段階を担います。主な構成要素は以下の4つです。

  1. ロガー (Logger): ログメッセージの発信元。ログメッセージを受け取り、それを処理するためにハンドラーに渡す役割を持ちます。
  2. ハンドラー (Handler): ロガーから渡されたログメッセージを受け取り、指定された出力先(コンソール、ファイルなど)に実際に出力する役割を持ちます。
  3. フォーマッター (Formatter): ログメッセージの出力形式を定義します。タイムスタンプ、ログレベル、メッセージ本文などをどのような順序で、どのような形式で表示するかを指定します。
  4. ログレベル (Log Level): ログメッセージの重要度を示します。ロガーやハンドラーは、設定されたレベル以上のメッセージだけを処理・出力するようにフィルタリングできます。

これらの要素が連携して、ログメッセージのライフサイクルを管理します。

1. ロガー (Logger)

ロガーは、ログメッセージを発信する主体です。プログラムの特定の部分(例えば、特定の関数、クラス、またはモジュール全体)に関連付けて作成することが多いです。

ロガーの役割:

  • ログメッセージ(例: “ユーザーAがログインしました”)を受け取る。
  • そのメッセージにログレベル(例: INFO)を付与する。
  • 自身のログレベル設定に基づき、メッセージを処理するかどうかを判断する。
  • 処理対象と判断したメッセージを、自身に紐づけられたハンドラーに渡す。

ロガーの名前空間と階層構造:

ロガーは名前を持ち、ドット(.)で区切られた階層構造を作ることができます。例えば、myappという名前のロガーがあったとき、その子ロガーとしてmyapp.networkmyapp.databaseといった名前のロガーを作成できます。

  • myapp (親ロガー)
    • myapp.network (子ロガー)
    • myapp.database (子ロガー)

この階層構造は重要で、子ロガーが処理したメッセージは、特に設定しない限り親ロガーにも「伝播 (propagate)」していきます。これにより、上位のロガー(例えばmyapp)で一括してログを処理・出力するといった設定が容易になります。

ルートロガー (Root Logger):

すべてのロガーの最上位に位置するのが「ルートロガー」です。名前を持たない(または空の名前を持つ)特別なロガーです。Pythonのloggingモジュールをimportすると、デフォルトでルートロガーが作成されます。後述する簡単な設定方法であるlogging.basicConfig()は、このルートロガーを設定します。

ロガーの取得方法:

ロガーを取得するには、logging.getLogger()関数を使います。

“`python
import logging

ロガー名無しで取得 -> ルートロガーを取得

root_logger = logging.getLogger()
print(f”ルートロガーの名前: {root_logger.name}”) # 出力: ルートロガーの名前: root

名前を指定してロガーを取得

同じ名前で複数回呼び出しても、常に同じロガーオブジェクトが返されます

my_app_logger = logging.getLogger(“myapp”)
print(f”取得したロガーの名前: {my_app_logger.name}”) # 出力: 取得したロガーの名前: myapp

階層的な名前を持つロガーを取得

network_logger = logging.getLogger(“myapp.network”)
print(f”取得したロガーの名前: {network_logger.name}”) # 出力: 取得したロガーの名前: myapp.network

通常はモジュール名を使ってロガーを作成します

name はそのモジュール自体の名前になります

module_logger = logging.getLogger(name)
print(f”モジュール名を使ったロガーの名前: {module_logger.name}”) # 例: 実行ファイルなら ‘main‘, それ以外のファイルならファイル名
“`

通常、各モジュールではlogging.getLogger(__name__)としてそのモジュール名のロガーを取得するのがベストプラクティスです。これにより、どのモジュールからログが出力されたのかがロガー名によって識別できるようになります。

2. ハンドラー (Handler)

ハンドラーは、ロガーから受け取ったログレコード(ログメッセージとその付帯情報)を、実際に出力する役割を担います。ログを「どこに」「どのように」送るかを決定します。

ハンドラーの役割:

  • ロガーからログレコードを受け取る。
  • 自身のログレベル設定に基づき、そのログレコードを処理するかどうかを判断する。
  • 処理対象と判断したログレコードに、設定されたフォーマッターを適用して出力形式を整える。
  • 整えられたログメッセージを、自身の担当する出力先(コンソール、ファイルなど)に書き出す。

主なハンドラーの種類:

  • StreamHandler: 標準出力(stdout)または標準エラー出力(stderr)にログを出力します。開発中によく使われます。
  • FileHandler: 指定されたファイルにログを出力します。運用環境でログを保存する際によく使われます。
  • RotatingFileHandler: ファイルにログを出力しますが、ファイルサイズが一定以上になったり、一定件数を超えたりすると、新しいファイルを作成してログを書き込みます(ログローテーション)。古いログファイルはバックアップとして保持できます。ログファイルが肥大化しすぎるのを防ぐために重要です。
  • TimedRotatingFileHandler: ファイルにログを出力しますが、一定期間(毎日、毎週など)ごとに新しいファイルを作成してログを書き込みます。これもログローテーションの一種です。日付や時間に基づいてファイルを分割したい場合に便利です。
  • SMTPHandler: 指定したメールアドレスにログを送信します。特に重要なエラーが発生した際に通知するために使われることがあります。
  • SocketHandler/DatagramHandler: ネットワークソケット経由でログを送信します。ログ収集サーバーなどにログを集約する場合に使われます。
  • NullHandler: 何もしないハンドラーです。ライブラリなどでデフォルトのハンドラーとして設定しておくと、アプリケーション側でロギングを設定しない場合に、ライブラリからのログが出力されてしまうのを防ぐことができます。

ロガーへのハンドラーの追加:

ロガーは複数のハンドラーを持つことができます。これにより、例えばコンソールにも出力しつつ、ファイルにも保存するといったことが同時に行えます。

“`python
import logging
import sys

ロガーを取得

logger = logging.getLogger(“my_app_with_handlers”)
logger.setLevel(logging.DEBUG) # このロガーで処理する最低レベルを設定

StreamHandlerを作成 (コンソールに出力)

console_handler = logging.StreamHandler(sys.stdout) # 標準出力へのハンドラー
console_handler.setLevel(logging.INFO) # このハンドラーで出力する最低レベルを設定

FileHandlerを作成 (ファイルに出力)

file_handler = logging.FileHandler(“app.log”) # app.logというファイルに出力
file_handler.setLevel(logging.DEBUG) # このハンドラーで出力する最低レベルを設定

ロガーにハンドラーを追加

logger.addHandler(console_handler)
logger.addHandler(file_handler)

ログを出力

logger.debug(“これはDEBUGレベルのメッセージです”) # file_handlerにだけ出力される (console_handlerはINFO以上のため)
logger.info(“これはINFOレベルのメッセージです”) # 両方のハンドラーに出力される
logger.warning(“これはWARNINGレベルのメッセージです”) # 両方のハンドラーに出力される
“`

3. フォーマッター (Formatter)

フォーマッターは、ログレコード(ログレベル、メッセージ本文、タイムスタンプなどの情報が含まれるオブジェクト)を、人間が読みやすい文字列形式に変換する役割を担います。

フォーマッターの役割:

  • ログレコードから必要な情報(タイムスタンプ、レベル、ロガー名、メッセージ、発生ファイル、行番号など)を取り出す。
  • 定義された形式(フォーマット文字列)に従って、これらの情報を並べ、整形する。

フォーマット文字列:

フォーマッターを作成する際には、出力したい情報の種類とその表示順序を定義する「フォーマット文字列」を指定します。これは、%記号を使ったプレースホルダーを含む文字列です。

よく使われるプレースホルダーの例:

  • %(asctime)s: ログ発生時刻 (デフォルト形式: YYYY-MM-DD HH:MM:SS,sss)
  • %(name)s: ロガー名
  • %(levelname)s: ログレベル名 (DEBUG, INFO, WARNING, ERROR, CRITICAL)
  • %(message)s: ログメッセージ本体
  • %(pathname)s: ログ発生元のファイルのパス名
  • %(filename)s: ログ発生元のファイル名
  • %(lineno)d: ログ発生元の行番号
  • %(funcName)s: ログ発生元の関数名
  • %(process)d: プロセスID
  • %(thread)d: スレッドID

フォーマッターの作成とハンドラーへの設定:

フォーマッターはlogging.Formatter()クラスのインスタンスとして作成し、その後、そのフォーマッターを特定のハンドラーに設定します。

“`python
import logging
import sys

フォーマッターを作成

日時 – ロガー名 – レベル名 – メッセージ の形式

formatter = logging.Formatter(‘%(asctime)s – %(name)s – %(levelname)s – %(message)s’)

別形式のフォーマッター例: より詳細な情報を含める

formatter_detailed = logging.Formatter(‘%(asctime)s – %(name)s – %(levelname)s – %(filename)s:%(lineno)d – %(message)s’)

StreamHandlerを作成

console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.DEBUG) # ハンドラーのレベルを設定

ハンドラーにフォーマッターを設定

console_handler.setFormatter(formatter)

ロガーを取得し、ハンドラーを追加

logger = logging.getLogger(“my_app_with_formatter”)
logger.setLevel(logging.DEBUG) # ロガーのレベルを設定
logger.addHandler(console_handler)

ログを出力

logger.debug(“デバッグメッセージ”)
logger.info(“情報メッセージ”)
logger.warning(“警告メッセージ”)

実行結果の例:

2023-10-27 10:30:00,123 – my_app_with_formatter – DEBUG – デバッグメッセージ

2023-10-27 10:30:00,124 – my_app_with_formatter – INFO – 情報メッセージ

2023-10-27 10:30:00,125 – my_app_with_formatter – WARNING – 警告メッセージ

“`

ハンドラーにフォーマッターが設定されていない場合、デフォルトのフォーマット(通常はレベル名:ロガー名:メッセージ)が使われますが、ほとんどの場合、独自のフォーマッターを設定して見やすい形式にします。

4. ログレベル (Log Level)

ログレベルは、ログメッセージの重要度や詳細度を示すタグのようなものです。loggingモジュールでは、以下の標準的なログレベルが定義されており、それぞれに数値が割り当てられています(数値が大きいほど重要度が高い)。

レベル名 数値 説明 主な用途
CRITICAL 50 システムの根幹に関わる、非常に深刻なエラー。プログラムの続行が危ぶまれる。 アプリケーションのクラッシュ、データベース接続断など、即座に対応が必要な問題
ERROR 40 プログラムの機能が損なわれる可能性のある深刻な問題。ただし、必ずしもシステム全体が停止するわけではない。 想定外の例外発生、必須リソースの不足、不正な入力による処理失敗など
WARNING 30 潜在的な問題や、将来エラーを引き起こす可能性のある状況。予期しない事態だが、プログラムは現在のところ正常に動作している。 設定ファイルの警告、ディスク容量の低下、非推奨の機能使用など
INFO 20 アプリケーションの正常な動作を示す、一般的で有用な情報。 プログラムの開始/終了、主要な処理の完了、ユーザーログイン/ログアウトなど
DEBUG 10 デバッグ目的の詳細な情報。開発中にプログラムの内部状態を追跡するために使う。運用環境では通常出力しない。 変数の値、関数の呼び出し、詳細な処理ステップ、外部システムとの通信内容など
NOTSET 0 レベルが設定されていない状態。

レベルによるフィルタリング:

ロガーとハンドラーは、それぞれ自身が処理・出力するメッセージの最低ログレベルを設定できます。

  • ロガーのレベル: ロガーが受け取ったメッセージのうち、ロガー自身のレベル以上のものだけがハンドラーに渡されます。ロガーのレベルは、そのロガーから発信されるメッセージの全体的な詳細度を制御します。
  • ハンドラーのレベル: ハンドラーがロガーから受け取ったメッセージのうち、ハンドラー自身のレベル以上のものだけが実際に出力されます。ハンドラーのレベルは、特定の出力先(コンソールやファイルなど)にどれくらいの詳細度のメッセージを出すかを制御します。

この二重のレベル設定により、例えば以下のような設定が可能です。

  • 開発中は、ロガーのレベルをDEBUGにしてすべてのメッセージをロガーに渡し、コンソールに出力するハンドラーのレベルもDEBUGにして、詳細なデバッグログを確認する。
  • 運用中は、ロガーのレベルはDEBUGのままでも良いが(必要ならいつでも詳細にできる)、コンソールに出力するハンドラーのレベルはWARNINGにして最低限の警告・エラーだけを表示し、ファイルに出力するハンドラーのレベルはINFOにして重要なイベントも記録するといった制御ができます。

ロガーやハンドラーにレベルを設定しない場合、デフォルトのレベル(通常はWARNINGまたはNOTSET)が適用されます。子ロガーは親ロガーからレベル設定を継承することもできます(明示的に設定しない場合)。

レベルの設定方法:

setLevel()メソッドを使ってレベルを設定します。引数にはlogging.DEBUG, logging.INFOなどの定数を使います。

“`python
import logging

logger = logging.getLogger(“my_app_levels”)

ロガーのレベルをDEBUGに設定 (DEBUG以上のメッセージを処理対象とする)

logger.setLevel(logging.DEBUG)

StreamHandlerを作成

console_handler = logging.StreamHandler()

ハンドラーのレベルをINFOに設定 (INFO以上のメッセージを出力対象とする)

console_handler.setLevel(logging.INFO)

ロガーにハンドラーを追加

logger.addHandler(console_handler)

各レベルでログを出力

logger.debug(“これはDEBUGメッセージです。”) # ロガーは処理するが、ハンドラーのレベルがINFOなので出力されない
logger.info(“これはINFOメッセージです。”) # ロガー、ハンドラーともレベル条件を満たすので出力される
logger.warning(“これはWARNINGメッセージです。”) # 出力される
logger.error(“これはERRORメッセージです。”) # 出力される
logger.critical(“これはCRITICALメッセージです。”) # 出力される

実行結果の例:

これはINFOメッセージです。

これはWARNINGメッセージです。

これはERRORメッセージです。

これはCRITICALメッセージです。

“`

このように、ロガーのレベルとハンドラーのレベルを組み合わせることで、柔軟なログ出力制御が可能になります。

基本的な使い方:最も簡単なログ出力

loggingモジュールを使う最も簡単な方法は、設定をほとんど行わずに、ルートロガーを通じてログを出力することです。これは、簡単なスクリプトや、とりあえずログを出力してみたい場合に便利です。

logging.basicConfig()を使った設定:

logging.basicConfig()関数は、ルートロガーに対して基本的な設定を一度に行うための便利な方法です。この関数を呼び出すことで、ロガーのレベル、出力先のハンドラー、フォーマットなどをまとめて設定できます。

注意点として、basicConfig()は一度呼び出されると、それ以降の呼び出しは無視されます。これは、既にルートロガーにハンドラーが追加されているかなどを内部的にチェックしているためです。したがって、通常はスクリプトの最初や、アプリケーションの初期化部分で一度だけ呼び出します。

basicConfig()の主な引数:

  • level: ルートロガーのレベルを設定します。logging.DEBUG, logging.INFOなどを指定します。
  • format: ログメッセージのフォーマット文字列を指定します。
  • filename: ログの出力先ファイル名を指定します。指定しない場合は標準エラー出力(stderr)に出力されます。
  • filemode: filenameを指定した場合のファイルオープンモードを指定します(例: 'w'で上書き、'a'で追記)。デフォルトは'a'です。
  • stream: ログの出力先ストリームを指定します(例: sys.stdout, sys.stderr)。filenamestreamは同時に指定できません。

簡単な例:

“`python
import logging

basicConfigを使ってルートロガーを設定

レベルをINFOに設定し、標準エラー出力にデフォルトフォーマットで出力

logging.basicConfig(level=logging.INFO)

ルートロガーを使ってログを出力 (getLogger()に名前を指定しないか、logging.関数名()を使う)

logging.debug(“これは表示されません (basicConfigでレベルをINFOに設定したため)”)
logging.info(“これは情報メッセージです”)
logging.warning(“これは警告メッセージです”)
logging.error(“これはエラーメッセージです”)
logging.critical(“これは重大なエラーメッセージです”)

def process_data(data):
logging.debug(f”process_dataが呼ばれました。データ: {data}”) # このレベルでは出力されない
if not data:
logging.warning(“データが空です”) # 出力される
return None
# … 処理 …
logging.info(“データ処理が完了しました”) # 出力される
return “processed_” + data

process_data(“”)
process_data(“sample data”)

実行結果の例(タイムスタンプやロガー名はデフォルトフォーマットによる):

INFO:root:これは情報メッセージです

WARNING:root:これは警告メッセージです

ERROR:root:これはエラーメッセージです

CRITICAL:root:これは重大なエラーメッセージです

WARNING:root:データが空です

INFO:root:データ処理が完了しました

“`

この例では、basicConfigでルートロガーのレベルをINFOに設定したため、debugレベルのメッセージは出力されません。info以上のメッセージが、デフォルトのフォーマットで標準エラー出力(多くの場合コンソール)に表示されます。

ファイルに出力する場合:

“`python
import logging

basicConfigを使ってルートロガーを設定

レベルをDEBUGに、出力先を’app.log’ファイルに設定

logging.basicConfig(level=logging.DEBUG, filename=’app.log’, filemode=’a’,
format=’%(asctime)s – %(name)s – %(levelname)s – %(message)s’)

ルートロガーを使ってログを出力

logging.debug(“これはデバッグメッセージです”)
logging.info(“これは情報メッセージです”)
logging.warning(“これは警告メッセージです”)

この場合、コンソールには何も出力されません。

ログは ‘app.log’ ファイルに追記されます。

“`

この例では、filename引数を使ってログの出力先をファイルに設定し、format引数で出力形式を指定しています。filemode='a'とすることで、スクリプトを実行するたびにログがファイルに追記されていきます。level=logging.DEBUGとしたことで、すべてのレベルのメッセージがファイルに記録されます。

basicConfigの限界:

basicConfigは手軽ですが、ルートロガーしか設定できない、複数のハンドラーを設定できない、複雑なフィルターをかけられないなど、機能に限界があります。より柔軟な設定が必要な場合は、ロガー、ハンドラー、フォーマッターを個別に作成・設定する必要があります。しかし、簡単なスクリプトや、loggingモジュールを試しに使ってみる際には、basicConfigは非常に役立ちます。

より詳細な設定:ロガー、ハンドラー、フォーマッターを組み合わせて使う

basicConfigだけでは実現できない、より柔軟なロギング設定を行うには、ロガー、ハンドラー、フォーマッターを個別に作成し、それらを組み合わせて設定します。これがloggingモジュールの真価を発揮する使い方です。

設定のステップ:

  1. ロガーを取得: logging.getLogger()で名前を指定してロガーを取得します。通常はモジュールごとに取得します。
  2. ロガーのレベルを設定: logger.setLevel()で、そのロガーで扱う最低レベルを設定します。
  3. ハンドラーを作成: logging.StreamHandler()logging.FileHandler()などのクラスを使ってハンドラーを作成します。必要に応じて出力先を指定します。
  4. ハンドラーのレベルを設定: handler.setLevel()で、そのハンドラーで出力する最低レベルを設定します。
  5. フォーマッターを作成: logging.Formatter()でフォーマット文字列を指定してフォーマッターを作成します。
  6. ハンドラーにフォーマッターを設定: handler.setFormatter()で、作成したフォーマッターをハンドラーに関連付けます。
  7. ロガーにハンドラーを追加: logger.addHandler()で、作成・設定済みのハンドラーをロガーに関連付けます。これで、そのロガーが受け取ったメッセージは、関連付けられたハンドラーを通じて出力されるようになります。

例:コンソールとファイルに異なるレベルで同時出力

開発時には詳細なログをコンソールで確認しつつ、運用時にはエラーログだけをファイルに記録しておきたい、といった要望はよくあります。これは、複数のハンドラーを使って実現できます。

“`python
import logging
import sys

1. ロガーを取得

logger = logging.getLogger(“my_multi_handler_app”)

2. ロガーのレベルを設定 (DEBUG以上のメッセージをロガーで受け付ける)

logger.setLevel(logging.DEBUG)

ハンドラーに設定するフォーマッターを作成

formatter = logging.Formatter(‘%(asctime)s – %(name)s – %(levelname)s – %(message)s’)

3. StreamHandlerを作成 (コンソール出力用)

console_handler = logging.StreamHandler(sys.stdout)

4. ハンドラーのレベルを設定 (コンソールにはINFO以上のメッセージを出力)

console_handler.setLevel(logging.INFO)

6. ハンドラーにフォーマッターを設定

console_handler.setFormatter(formatter)

3. FileHandlerを作成 (ファイル出力用)

file_handler = logging.FileHandler(“error.log”)

4. ハンドラーのレベルを設定 (ファイルにはERROR以上のメッセージを出力)

file_handler.setLevel(logging.ERROR)

6. ハンドラーにフォーマッターを設定 (同じフォーマッターを使い回してもOK)

file_handler.setFormatter(formatter)

7. ロガーにハンドラーを追加

logger.addHandler(console_handler)
logger.addHandler(file_handler)

ログを出力してみる

logger.debug(“これはデバッグメッセージです。”) # ロガーは受け付けるが、どちらのハンドラーもレベルが低いので出力されない
logger.info(“これは情報メッセージです。”) # ロガー、console_handlerは受け付けるが、file_handlerはレベルが低いので console_handlerにだけ出力
logger.warning(“これは警告メッセージです。”) # ロガー、console_handlerは受け付けるが、file_handlerはレベルが低いので console_handlerにだけ出力
logger.error(“これはエラーメッセージです。”) # ロガー、console_handler、file_handlerすべてが受け付けるので両方に出力
logger.critical(“これは重大なエラーメッセージです。”) # ロガー、console_handler、file_handlerすべてが受け付けるので両方に出力

実行結果:

コンソール出力 (sys.stdout):

2023-10-27 10:30:00,130 – my_multi_handler_app – INFO – これは情報メッセージです。

2023-10-27 10:30:00,131 – my_multi_handler_app – WARNING – これは警告メッセージです。

2023-10-27 10:30:00,132 – my_multi_handler_app – ERROR – これはエラーメッセージです。

2023-10-27 10:30:00,133 – my_multi_handler_app – CRITICAL – これは重大なエラーメッセージです。

error.log ファイル内容:

2023-10-27 10:30:00,132 – my_multi_handler_app – ERROR – これはエラーメッセージです。

2023-10-27 10:30:00,133 – my_multi_handler_app – CRITICAL – これは重大なエラーメッセージです。

“`

このように、ロガーとハンドラーそれぞれにレベルを設定し、複数のハンドラーをロガーに追加することで、ログメッセージをレベルに応じて異なる出力先に振り分けることができます。

ロガーの名前空間と伝播 (Propagation):

ロガーは名前によって階層構造を形成します。例えば、myapp, myapp.network, myapp.databaseのような関係です。子ロガーは、明示的に設定されていない場合、親ロガーの設定(特にハンドラー)を継承します。これを「伝播 (propagation)」と呼びます。

メッセージがあるロガーでログとして記録されると、そのロガーに紐づけられたハンドラーに渡されるだけでなく、親ロガーにも伝播されます(ロガーのpropagate属性がTrueの場合 – デフォルトはTrue)。親ロガーも同様に自身のハンドラーに渡し、さらにその親ロガーに伝播…と、ルートロガーまで処理が伝わっていきます。

“`python
import logging
import sys

ルートロガーに設定を追加 (basicConfigは使わずに手動でハンドラーを追加する例)

ルートロガーを取得

root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG) # ルートロガーのレベルは低くしておくのが一般的

ルートロガー用のコンソールハンドラー (INFO以上を出力)

root_console_handler = logging.StreamHandler(sys.stdout)
root_console_handler.setLevel(logging.INFO)
root_console_formatter = logging.Formatter(‘[%(name)s] %(levelname)s: %(message)s’)
root_console_handler.setFormatter(root_console_formatter)
root_logger.addHandler(root_console_handler)

子ロガーを作成

child_logger = logging.getLogger(“myapp.network”)

child_logger.setLevel(logging.DEBUG) # 子ロガーにレベルを設定しない場合、親(root)のレベル(DEBUG)を継承する

子ロガーに専用のハンドラーを追加しない場合…

child_logger.addHandler(…) # ここではハンドラーを追加しない

子ロガーからログを出力

child_logger.debug(“これは子ロガーからのDEBUGメッセージです。”) # ルートロガーのレベル(DEBUG)より高いか? YES。
# ルートロガーのconsole_handlerのレベル(INFO)より高いか? NO。 -> 出力されない

child_logger.info(“これは子ロガーからのINFOメッセージです。”) # ルートロガーのレベル(DEBUG)より高いか? YES。
# ルートロガーのconsole_handlerのレベル(INFO)より高いか? YES。 -> ルートロガーのハンドラーによって出力される

ルートロガーからログを出力

root_logger.warning(“これはルートロガーからのWARNINGメッセージです。”) # ルートロガーのレベル(DEBUG)より高いか? YES。
# ルートロガーのconsole_handlerのレベル(INFO)より高いか? YES。 -> ルートロガーのハンドラーによって出力される

実行結果:

[myapp.network] INFO: これは子ロガーからのINFOメッセージです。

[root] WARNING: これはルートロガーからのWARNINGメッセージです。

“`

この例では、child_loggerにはハンドラーを追加していませんが、親であるroot_loggerに設定されたハンドラーを通じてログが出力されています。これは伝播によるものです。

伝播を止めたい場合は、ロガーのpropagate属性をFalseに設定します。

“`python
import logging
import sys

ルートロガーにハンドラーを設定 (前の例と同じ)

root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
root_console_handler = logging.StreamHandler(sys.stdout)
root_console_handler.setLevel(logging.INFO)
root_console_formatter = logging.Formatter(‘[ROOT] %(levelname)s: %(message)s’)
root_console_handler.setFormatter(root_console_formatter)
root_logger.addHandler(root_console_handler)

子ロガーを作成

child_logger = logging.getLogger(“myapp.database”)
child_logger.setLevel(logging.DEBUG) # 子ロガー自身のレベルも設定

子ロガー専用のハンドラーを作成 (DEBUG以上をファイルに出力)

child_file_handler = logging.FileHandler(“database.log”)
child_file_handler.setLevel(logging.DEBUG)
child_file_formatter = logging.Formatter(‘%(asctime)s – %(name)s – %(levelname)s – %(message)s’)
child_file_handler.setFormatter(child_file_formatter)
child_logger.addHandler(child_file_handler)

子ロガーからのメッセージが親に伝播しないように設定

child_logger.propagate = False

子ロガーからログを出力

child_logger.debug(“DB接続情報 (子ロガー DEBUG)”) # 子ロガーのfile_handlerにだけ出力される
child_logger.info(“ユーザー取得完了 (子ロガー INFO)”) # 子ロガーのfile_handlerにだけ出力される

ルートロガーからログを出力

root_logger.warning(“何かしらのシステム警告 (ルートロガー WARNING)”) # ルートロガーのconsole_handlerにだけ出力される

実行結果:

コンソール出力 (sys.stdout):

[ROOT] WARNING: 何かしらのシステム警告 (ルートロガー WARNING)

database.log ファイル内容:

2023-10-27 10:30:00,140 – myapp.database – DEBUG – DB接続情報 (子ロガー DEBUG)

2023-10-27 10:30:00,141 – myapp.database – INFO – ユーザー取得完了 (子ロガー INFO)

“`

この例では、child_logger.propagate = Falseと設定したため、child_loggerから発信されたメッセージは、その親であるroot_loggerには伝播せず、子ロガー自身に設定されたchild_file_handlerにだけ処理されます。これにより、特定のモジュールからのログだけを独立して扱いたい場合に便利です。

ロガーの階層構造と伝播は、大規模なアプリケーションでログ設定を体系的に行う上で非常に重要な概念です。

実践的な使い方:例外処理、変数、設定ファイルなど

ここからは、実際のアプリケーション開発で役立つloggingモジュールの実践的な使い方を見ていきましょう。

例外情報のログ出力 (logger.exception())

プログラム実行中に例外(エラー)が発生した場合、そのトレースバック情報も含めてログに記録することは、原因究明のために非常に重要です。loggingモジュールは、例外情報を簡単に出力するための専用のメソッドや機能を提供しています。

logger.exception()メソッド:

これはlogger.error()logger.critical()の特殊な形式で、例外ハンドラー(exceptブロック)の中で呼び出されることを想定しています。呼び出されると、通常のログメッセージに加えて、現在処理中の例外に関するトレースバック情報を自動的にログに含めて出力します。

“`python
import logging
import sys

basicConfigでルートロガーを設定

logging.basicConfig(level=logging.ERROR,
format=’%(asctime)s – %(name)s – %(levelname)s – %(message)s’)

例外発生元の情報も欲しい場合はフォーマットに %(pathname)s:%(lineno)d などを追加

logging.basicConfig(level=logging.ERROR,

format=’%(asctime)s – %(name)s – %(levelname)s – %(pathname)s:%(lineno)d – %(message)s’)

def divide_numbers(a, b):
try:
result = a / b
logging.info(“除算が成功しました”) # ERRORレベルより低いので出力されない
return result
except ZeroDivisionError:
# 例外発生時にlogger.exception()を呼び出す
logging.exception(“ゼロによる除算エラーが発生しました”) # ERRORレベル以上のメッセージとして扱われる

print(“— ゼロで割る場合 —“)
divide_numbers(10, 0)

print(“\n— 正常な場合 —“)

basicConfigでルートロガーをERRORレベルに設定しているので、infoやdebugは出力されない

DEBUG/INFOレベルも出力したい場合は、basicConfigのlevelを調整してください

divide_numbers(10, 2)

実行結果の例:

— ゼロで割る場合 —

2023-10-27 10:30:00,150 – root – ERROR – ゼロによる除算エラーが発生しました

Traceback (most recent call last):

File “your_script_name.py”, line 30, in divide_numbers # ファイル名や行番号は環境によって異なる

result = a / b

ZeroDivisionError: division by zero

— 正常な場合 —

“`

logger.exception()は、例外が発生した直後のexceptブロック内で使うのが最も効果的です。自動的にトレースバックを含めてくれるため、非常に便利です。これは内部的にはlogger.error(..., exc_info=True)と同じ動作をします。exc_info=Trueは、他のレベルのログメソッド(logger.debug(), logger.info()など)でも例外情報を付加するために使用できますが、logger.exception()errorレベル固定かつexc_info=Trueがデフォルトなので、例外発生時のログ記録に特化しています。

変数の値をログに含める

デバッグ時などには、特定の変数の値をログに出力したいことがよくあります。ログメッセージに変数の値を含める方法はいくつかあります。

  1. f-string (フォーマット済み文字列リテラル): Python 3.6以降で利用可能な、最も推奨される方法です。直感的で読みやすいです。

    python
    user_id = 123
    status = "completed"
    logging.info(f"ユーザーID: {user_id} の処理がステータス: {status} で完了しました")

  2. %演算子 (旧スタイル): C言語スタイルのprintfライクな書式指定です。ログメソッドの第2引数以降にタプルで変数を渡します。

    python
    item_count = 50
    price = 1000
    logging.debug("アイテム数: %d, 合計金額: %.2f" % (item_count, price))

    このスタイルは、ログメッセージの文字列補間をログの記録処理まで遅延させるという利点があります。つまり、もしログレベルが低くてそのメッセージが出力されない場合、文字列補間(%演算子による処理)自体が行われないため、わずかですがパフォーマンス上の利点があります。f-stringはログメソッドが呼ばれる前に文字列が完成してしまうため、レベルに関わらず文字列補間のコストが発生します。

  3. .format()メソッド: 文字列のformat()メソッドを使う方法です。

    python
    version = "1.0.0"
    logging.info("アプリケーション バージョン: {}".format(version))

    これもf-stringと同様に、ログメソッドが呼ばれる前に文字列が完成します。

特別な理由(パフォーマンス重視で古いPythonバージョンもサポートする必要があるなど)がない限り、現代のPythonではf-stringを使うのが最も一般的で分かりやすいでしょう。

設定ファイルを読み込む (logging.config)

プログラムのコード中にロギング設定を直接書き込むのではなく、外部の設定ファイル(INI形式、YAML、JSONなど)から設定を読み込むようにすると、ログの挙動を変更するためにコードを書き換える必要がなくなり、運用が非常に楽になります。

logging.configモジュールは、このための機能を提供します。主に以下の2つの方法があります。

  1. logging.config.fileConfig(): INI形式の設定ファイルを読み込みます。標準的な古い方法ですが、機能に一部制限があります(例: 既存のロガーを無効化しにくい)。
  2. logging.config.dictConfig(): 辞書形式(Python辞書、またはYAML/JSONなどで表現された辞書構造)の設定を読み込みます。より新しく、柔軟で推奨される方法です。

ここでは、より柔軟なdictConfig()を使った設定方法を中心に解説します。

dictConfig()を使った設定:

dictConfig()は、ロギング設定をPythonの辞書形式で定義し、それを読み込ませます。この辞書は、YAMLやJSONファイルとして記述し、プログラムで読み込んでからdictConfig()に渡すこともよく行われます。

設定辞書の基本的な構造は以下のようになります。

python
{
'version': 1, # 設定スキーマのバージョン (現在は1のみ)
'disable_existing_loggers': False, # 既存のロガーを無効にするか (デフォルト: True)
'formatters': { # フォーマッターの定義
'simpleFormatter': {
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
},
'detailedFormatter': {
'format': '%(asctime)s - %(name)s - %(levelname)s - %(pathname)s:%(lineno)d - %(message)s'
},
},
'handlers': { # ハンドラーの定義
'consoleHandler': {
'class': 'logging.StreamHandler',
'level': 'INFO', # ここでは文字列でレベルを指定
'formatter': 'simpleFormatter', # 使用するフォーマッターの名前
'stream': 'ext://sys.stdout' # 標準出力への指定方法
},
'fileHandler': {
'class': 'logging.FileHandler',
'level': 'DEBUG',
'formatter': 'detailedFormatter',
'filename': 'app.log',
'mode': 'a'
},
'errorHandler': {
'class': 'logging.FileHandler',
'level': 'ERROR',
'formatter': 'simpleFormatter',
'filename': 'errors.log'
}
},
'loggers': { # 特定の名前を持つロガーの設定
'myapp': { # ロガー名 'myapp'
'level': 'DEBUG',
'handlers': ['consoleHandler', 'fileHandler'], # 使用するハンドラーの名前リスト
'propagate': False # 親への伝播を止めるかどうか
},
'myapp.database': { # ロガー名 'myapp.database'
'level': 'INFO',
'handlers': ['fileHandler'],
'propagate': True # 親(myapp)に伝播させる
}
},
'root': { # ルートロガーの設定
'level': 'WARNING', # ルートロガーのレベル
'handlers': ['errorHandler'] # ルートロガーが使うハンドラー
}
}

この辞書構造では、formatters, handlers, loggers, rootといったキーの下に、それぞれ具体的な設定を記述します。handlersloggersの定義内で、他のセクションで定義されたフォーマッターやハンドラーをその名前で参照します。

これをPythonコードから読み込んで適用する例:

“`python
import logging
import logging.config
import yaml # PyYAMLライブラリが必要 (pip install PyYAML)
import json # 標準ライブラリ

設定辞書を直接定義するか、ファイルから読み込む

ここではYAMLファイルを想定 (app_logging_config.yaml という名前で保存)

— app_logging_config.yaml の内容例 —

version: 1

disable_existing_loggers: False

formatters:

simpleFormatter:

format: ‘%(asctime)s – %(name)s – %(levelname)s – %(message)s’

handlers:

consoleHandler:

class: logging.StreamHandler

level: INFO

formatter: simpleFormatter

stream: ext://sys.stdout

fileHandler:

class: logging.FileHandler

level: DEBUG

formatter: simpleFormatter

filename: app.log

mode: ‘a’

loggers:

myapp:

level: DEBUG

handlers: [‘consoleHandler’, ‘fileHandler’]

propagate: False

root:

level: WARNING

handlers: [‘consoleHandler’] # rootはconsoleHandlerだけ使う例

—————————————

try:
# YAMLファイルから設定を読み込む
with open(‘app_logging_config.yaml’, ‘r’) as f:
config = yaml.safe_load(f)
logging.config.dictConfig(config)
print(“ログ設定をファイルから読み込みました。”) # これはloggingではないprint

except FileNotFoundError:
print(“設定ファイルが見つかりませんでした。デフォルト設定を使用します。”)
# 設定ファイルが見つからない場合のフォールバックとしてbasicConfigを使うなども考えられる
logging.basicConfig(level=logging.WARNING, format=’%(asctime)s – %(name)s – %(levelname)s – %(message)s’)

設定ファイルで定義したロガーを取得

logger_app = logging.getLogger(“myapp”)
logger_db = logging.getLogger(“myapp.database”) # 設定ファイルには定義してないが、親(myapp)から設定を継承できる

ログを出力してみる

logger_app.debug(“App DEBUG message (myapp)”) # ‘myapp’ロガーのレベルはDEBUG, handlers=[‘consoleHandler’, ‘fileHandler’]
# consoleHandlerのレベルはINFOなので、consoleには出ない
# fileHandlerのレベルはDEBUGなので、app.logに出る
logger_app.info(“App INFO message (myapp)”) # ‘myapp’ロガー、consoleHandler、fileHandlerのレベルより高いので両方に出る
logger_app.error(“App ERROR message (myapp)”) # ‘myapp’ロガーのpropagateはFalseなので、rootロガーには伝播しない

logger_db.info(“DB INFO message (myapp.database)”) # ‘myapp.database’ロガーは設定ファイルに定義がないため、親である’myapp’の設定を継承…と思いきや
# dictConfigのdisable_existing_loggers=Falseの場合、既存のロガー設定は維持され、新しい設定が追加されます。
# ここでmyapp.databaseをdictConfigで定義していない場合、myapp.databaseは親であるmyappのpropagate=Falseの影響を受けません。(伝播はmyappまでで止まる)
# また、レベルもデフォルト(WARNING)となります。
# -> 実際には、もし設定ファイルにmyapp.databaseがない場合、WARNING以上のメッセージがrootに伝播してerrorHandlerで処理される

dictConfigでmyapp.databaseを定義に追加した場合の挙動

logger_db.info(“DB INFO message (myapp.database)”) # ‘myapp.database’ロガーのレベルはINFO, handlers=[‘fileHandler’], propagate=True

                                                # fileHandlerのレベルはDEBUGなので、app.logに出る
                                                # propagate=Trueなので、親である'myapp'に伝播。
                                                # 'myapp'のレベルはDEBUG、handlers=['consoleHandler', 'fileHandler']
                                                # consoleHandlerのレベルはINFO、fileHandlerのレベルはDEBUG
                                                # -> consoleHandlerとfileHandlerの両方で処理される(myappからのメッセージと同様)
                                                # さらに'myapp'からrootに伝播...と思いきや、'myapp'のpropagate=Falseなので伝播はそこで止まる

rootロガーからのログ

root_logger = logging.getLogger() # 名前を指定しないとルートロガー
root_logger.warning(“Root WARNING message”) # rootロガーのレベルはWARNING, handlers=[‘consoleHandler’]
# consoleHandlerのレベルはINFOなので、consoleに出る

実行結果の確認:

コンソール出力 (sys.stdout):

ログ設定をファイルから読み込みました。

2023-10-27 10:30:00,160 – myapp – INFO – App INFO message (myapp)

2023-10-27 10:30:00,161 – myapp – ERROR – App ERROR message (myapp)

2023-10-27 10:30:00,162 – root – WARNING – Root WARNING message

app.log ファイル内容:

2023-10-27 10:30:00,155 – myapp – DEBUG – App DEBUG message (myapp)

2023-10-27 10:30:00,160 – myapp – INFO – App INFO message (myapp)

2023-10-27 10:30:00,161 – myapp – ERROR – App ERROR message (myapp)

errors.log ファイル内容: (この例の設定では root が errorHandler を使うようになっている場合)

2023-10-27 10:30:00,162 – root – WARNING – Root WARNING message # rootのWARNレベルがERROR_handlerのWARNINGレベル以上なので出力

rootロガーのレベルをWARNINGに設定し、errorHandlerのレベルをERRORにした場合、WARNINGメッセージは出力されません。

errors.logファイルにはrootロガーのERROR/CRITICALメッセージだけが出力されます。

例の設定ではrootは’consoleHandler’を使うように定義しているのでerrors.logには何も出ません。

“`

dictConfigを使うと、ロガー、ハンドラー、フォーマッターの関係性やレベル設定などを一覧でき、管理が容易になります。特にYAMLやJSON形式で設定ファイルを作成すれば、コードからロギング設定を分離できるため、環境ごとの設定変更なども容易になります。

disable_existing_loggers: Trueを設定すると、dictConfigが適用される前に存在していたすべてのロガー(ルートロガーを含む)に設定されていたハンドラーが無効化されます。これにより、設定ファイルに記述した内容だけが有効になるため、予期しない二重ログ出力などを防ぐことができます。通常はこのオプションをTrueにしておくのが安全です。

異なるモジュールでのロギング

実際のアプリケーションは複数のPythonファイル(モジュール)で構成されます。各モジュールでロギングを行う場合も、logging.getLogger(__name__)を使って、モジュール名に対応したロガーを取得するのがベストプラクティスです。

例:

main.py

“`python

main.py

import logging
import logging.config
import my_module
import another_module

設定ファイルからロギング設定を読み込む (例としてdictConfigを使う)

ここでは、前のセクションのapp_logging_config.yamlを想定

try:
with open(‘app_logging_config.yaml’, ‘r’) as f:
config = yaml.safe_load(f) # yamlライブラリが必要
logging.config.dictConfig(config)
print(“ログ設定をファイルから読み込みました。”)
except FileNotFoundError:
print(“設定ファイル app_logging_config.yaml が見つかりませんでした。”)
# フォールバックとして基本的なコンソール出力を設定
logging.basicConfig(level=logging.INFO, format=’%(asctime)s – %(name)s – %(levelname)s – %(message)s’)

メインモジュールのロガーを取得

logger = logging.getLogger(name) # ロガー名は ‘main‘ になる

logger.info(“アプリケーションを開始します。”) # ‘main‘ ロガーからのメッセージ

my_module.do_something() # my_module内のログが出力される
another_module.do_something_else() # another_module内のログが出力される

logger.info(“アプリケーションを終了します。”)
“`

my_module.py

“`python

my_module.py

import logging

このモジュールのロガーを取得

logger = logging.getLogger(name) # ロガー名は ‘my_module’ になる

def do_something():
logger.debug(“my_module.do_something 関数が呼び出されました。”)
# … 処理 …
logger.info(“my_module.do_something 処理完了。”)
“`

another_module.py

“`python

another_module.py

import logging

このモジュールのロガーを取得

logger = logging.getLogger(name) # ロガー名は ‘another_module’ になる

def do_something_else():
logger.warning(“another_module.do_something_else で警告発生。”)
# … 処理 …
try:
result = 1 / 0
except ZeroDivisionError:
logger.exception(“another_module.do_something_else で例外発生。”) # トレースバック付きで出力
“`

この構成では、各モジュールがlogging.getLogger(__name__)で独自のロガーを取得します。これらのロガーは、ドットで区切られた階層構造を形成します(例: __main__, my_module, another_module)。main.pydictConfigを使って全体の設定を読み込めば、それぞれのロガーからのメッセージが、設定に従って処理・出力されます。

例えば、app_logging_config.yamlmyappロガーを設定している場合、my_moduleanother_moduleのロガーはmyappロガーの子孫ではないため、直接はその設定を継承しません。もしモジュール名のロガー(例: my_module)も設定ファイルで制御したい場合は、設定ファイルのloggersセクションにその名前を追加する必要があります。または、親ロガーの名前を適切に設計する必要があります(例: myapp.my_module, myapp.another_moduleのように名前を付ける)。

最も簡単な方法は、設定ファイルでルートロガーまたはトップレベルのロガー(例: myapp)に必要なハンドラーとレベルを設定し、各モジュールで取得したロガーがその設定を伝播によって継承するようにすることです(ただし、myappのように特定のロガーにpropagate: Falseを設定している場合は、そこまでの伝播になります)。

高度なトピック(初心者向けに触れる程度)

loggingモジュールには、さらに高度なカスタマイズや機能がありますが、初心者向けの記事としては概要に触れる程度に留めます。

フィルター (Filter)

ハンドラーやロガーに設定されたレベルによるフィルタリングよりも、さらに細かい条件でログメッセージをフィルタリングしたい場合があります。例えば、「特定のユーザーに関するメッセージだけを抽出したい」「特定のエラーコードを持つメッセージだけを出力したい」といった場合です。

このような要件にはフィルターを使用します。フィルターはlogging.Filterクラスを継承して独自のクラスを作成するか、既存のフィルタークラスを使います。フィルターはlogger.addFilter()handler.addFilter()メソッドでロガーやハンドラーに追加します。フィルターが追加されたロガー/ハンドラーは、メッセージを処理する前にすべてのフィルターにメッセージを渡し、すべてのフィルターがTrueを返した場合にのみそのメッセージを処理します。

“`python
import logging

例: 特定の名前空間のメッセージだけを許可するフィルター

class NamespaceFilter(logging.Filter):
def init(self, name=”):
super().init(name)

def filter(self, record):
    # record.name はロガー名
    # self.name はフィルターに設定されたロガー名のプレフィックス
    # ロガー名がフィルター名で始まる場合にTrueを返す
    return record.name.startswith(self.name)

設定例(コードで直接設定)

logger = logging.getLogger(“my_app.data”)
logger.setLevel(logging.DEBUG)

handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)

my_app 名前空間のメッセージだけを通すフィルターを追加

my_app_filter = NamespaceFilter(“my_app”)
handler.addFilter(my_app_filter)

logger.addHandler(handler)

ログ出力

logging.getLogger(“my_app.data”).info(“Data processing message”) # my_app.data は my_app で始まる -> フィルター通過
logging.getLogger(“other_app”).info(“Other app message”) # other_app は my_app で始まらない -> フィルターでブロックされる

実行結果:

Data processing message

“`

フィルターは非常に強力ですが、使いこなすには少し慣れが必要です。まずはレベルによるフィルタリングをしっかり理解するのがおすすめです。

カスタム要素 (Custom Components)

必要であれば、独自のロガー、ハンドラー、フォーマッターを作成することも可能です。

  • カスタムハンドラー: 例えば、ログをSMSで送信したり、独自のキューシステムに格納したり、特定のデータベースに保存したりする場合など、標準のハンドラーでは対応できない出力先にログを送りたい場合に独自のハンドラーを作成します。logging.Handlerクラスを継承してemit(record)メソッドなどを実装します。
  • カスタムフォーマッター: 標準のフォーマット文字列では表現できない、非常に特殊な形式でログを出力したい場合に独自のフォーマッターを作成します。logging.Formatterクラスを継承してformat(record)メソッドを実装します。
  • カスタムロガー: 標準のロガーの挙動を変更したい場合に作成しますが、これは比較的まれです。logging.Loggerクラスを継承します。

これらは高度なカスタマイズであり、多くの場合、標準の機能や既存のライブラリ(例: ログ収集システムへの送信ライブラリ)で十分です。

スレッドセーフティ

loggingモジュールは、標準でスレッドセーフに設計されています。複数のスレッドから同時に同じロガーやハンドラーにログを書き込んでも、データが壊れたり、ログメッセージが混ざり合ったりする心配は基本的にありません。これは、マルチスレッドアプリケーションでログを扱う上で非常に重要な特徴です。

非同期ロギング (QueueHandlerなど)

パフォーマンスが非常に重要で、ログの書き込み処理がアプリケーションの応答性を低下させる可能性がある場合、ログの処理を別スレッドや別プロセスに任せる非同期ロギングという手法が使われることがあります。logging.handlers.QueueHandlerlogging.handlers.QueueListenerといったクラスを使うことで、ログメッセージをキューに入れて、キューから取り出したメッセージを別のハンドラーで処理するという仕組みを構築できます。これにより、ログ出力の呼び出し元はキューに入れるだけで済むため、処理の待ち時間が短縮されます。

ログの設計と運用

loggingモジュールを使いこなせるようになったら、次に考えるべきは「どのようなログを、どのように記録・管理するか」というログの設計と運用です。

どのような情報をログに含めるべきか

  • タイムスタンプ: いつイベントが発生したかを知るために必須です。フォーマッターで正確な時刻(ミリ秒まで含むと良い)とタイムゾーン情報を含めるように設定しましょう。
  • ログレベル: メッセージの重要度を示します。フィルタリングに不可欠です。
  • ロガー名: メッセージがプログラムのどの部分(どのモジュール、どの機能)から出力されたかを示します。__name__を使うことで自動化できます。
  • メッセージ本体: 具体的に何が起こったのかを示すテキストです。変数情報など、状況を把握するために必要な情報を含めましょう。
  • 発生元情報: ファイル名、行番号、関数名など。エラー発生箇所を特定するのに非常に役立ちます。フォーマッターで%(pathname)s:%(lineno)d%(filename)s:%(lineno)dを含めるのがおすすめです。
  • プロセス/スレッドID: マルチプロセスやマルチスレッドのアプリケーションでは、どのプロセス/スレッドでログが出力されたかを識別するために重要です。%(process)d, %(thread)dを使います。
  • コンテキスト情報: アプリケーションによっては、リクエストID、ユーザーID、セッションIDなど、特定の処理に関連するユニークな識別子をログに含めると、後から関連するログをたどるのが容易になります。これは、ログメッセージ本体に含めるか、logging.Logger.with_context()(Python 3.10+)やlogging.set_context()(非公式な方法が多い)といった高度な機能を使って実現できます。

含めるべきでない情報:

  • 個人情報: パスワード、クレジットカード番号、住所、電話番号など、個人を特定できる情報や機密情報はログに含めるべきではありません。セキュリティやプライバシー保護の観点から極めて重要です。
  • 過度に詳細な情報: DEBUGレベルの情報でも、無限ループで大量に出力されるようなログは、ファイルサイズを肥大化させ、パフォーマンスにも影響を与える可能性があります。必要最小限の情報に絞り込むことも重要です。

ログレベルの適切な使い分け

ログレベルを適切に使い分けることで、運用時のログの量や、問題発生時の情報の探しやすさが大きく変わります。

  • DEBUG: 開発中や問題調査時にのみ使用する、非常に詳細な情報。変数の値の変化、関数の細かいステップ、外部システムとの通信データの詳細など。
  • INFO: アプリケーションの正常な動作を示す主要なイベント。サービスの開始/停止、主要な機能の実行、ユーザーの重要な操作(ログイン、登録など)。運用時に「何が起こっているか」を把握するために見るレベル。
  • WARNING: 将来的に問題を引き起こす可能性があるが、現時点では処理が続行できている状況。設定の不備、非推奨機能の使用、リソースの枯渇の兆候など。「おかしいな?」と感じたらまず確認するレベル。
  • ERROR: プログラムの機能が損なわれた、回復不能ではないが何らかの問題が発生した状況。不正な入力による処理中断、期待したリソースが見つからない、外部サービスとの連携失敗など。「何か問題が起こった」ことを示すレベル。try...exceptブロックで補足したエラーなどを記録するのに適しています。
  • CRITICAL: システムの継続が不可能、または非常に危険な状態。データベース接続の喪失、重要な外部サービスの停止、アプリケーション全体のクラッシュなど。「すぐにシステム管理者が対応すべき」レベル。

運用環境では、通常INFOレベル以上のログをファイルに記録し、ERRORCRITICALレベルのログは特別なファイルに分けたり、監視システムに通知したりするといった設定が考えられます。

ログローテーションの重要性

ファイルにログを書き出す場合、何も対策しないとログファイルは際限なく大きくなり、ディスク容量を圧迫したり、ファイルを開くのが困難になったりします。これを防ぐためにログローテーションが必要です。

logging.handlers.RotatingFileHandlerlogging.handlers.TimedRotatingFileHandlerを使うことで、以下の条件で自動的にログファイルを切り替えることができます。

  • RotatingFileHandler: ファイルサイズが指定した上限に達したら切り替え。古いファイルは指定した数だけ.1, .2などの番号を付けて保存される。
  • TimedRotatingFileHandler: 指定した期間(日、週、時など)が経過したら切り替え。古いファイルは日付や時刻を含む名前で保存される。

どちらのハンドラーも、一定数以上のバックアップファイルは自動的に削除してくれる機能があります。これにより、ログファイルによってディスクがいっぱいになるのを防ぎつつ、一定期間のログを遡って確認できるようになります。

ログ分析ツールとの連携

本格的なシステム運用では、ログは単にファイルに保存するだけでなく、集中型のログ収集・分析システム(例: Elasticsearch/Logstash/Kibana (ELKスタック), Splunk, Loki, Datadogなど)に集約して管理することが一般的です。これらのシステムにログを送るための専用ハンドラーやライブラリも存在します。

集中管理することで、複数のサーバーのログを横断的に検索したり、ログからメトリクスを生成してシステムの状態を監視したり、異常を検知してアラートを発したりといった高度なログ活用が可能になります。loggingモジュールの柔軟なハンドラー機構は、このような外部システムとの連携も容易にします。

まとめ:loggingを使いこなす次のステップ

この記事では、Pythonのloggingモジュールの基本的な概念から、basicConfigを使った簡単な使い方、ロガー、ハンドラー、フォーマッターを組み合わせる柔軟な設定方法、そして例外処理や設定ファイルを使った実践的な使い方までを詳細に解説しました。

loggingモジュールを使うメリットの再確認:

  • ログレベルによる重要度に応じたフィルタリング
  • コンソール、ファイルなど多様な出力先への対応
  • タイムスタンプ、発生元などの豊富な情報を自動で付加
  • 出力形式の自由なカスタマイズ
  • モジュールごとのロギングと階層構造による管理
  • 設定ファイルによるコードからの設定分離
  • 例外情報の容易な出力
  • マルチスレッド環境での安全性

これらの機能により、loggingモジュールはprintデバッグの限界をはるかに超えた、強力で柔軟なログ管理機能を提供します。

logging習得のステップ:

  1. まずはbasicConfigを使って、簡単なスクリプトでログレベルを変えたり、ファイルに出力したりして試してみましょう。
  2. 次に、ロガー、ハンドラー、フォーマッターを個別に作成・設定する方法を学び、コンソールとファイルに異なるレベルで同時出力する設定などを試してみてください。
  3. 複数のモジュールでlogging.getLogger(__name__)を使ってロガーを取得し、メインのスクリプトで設定を一元管理する方法を身につけましょう。
  4. 例外発生時にlogger.exception()を使うことを習慣にしましょう。
  5. アプリケーションが大きくなってきたら、dictConfigを使って設定をコードから分離することを検討しましょう。

次へのステップ:

この記事で解説した内容はloggingモジュールのごく一部に過ぎません。さらに深く学びたい場合は、以下のリソースを参照してください。

これらの公式ドキュメントは非常に価値がありますが、最初はこの記事のような解説記事で全体像を掴むのがおすすめです。

loggingモジュールを使いこなすことは、Pythonを使った本格的なアプリケーション開発において必須のスキルです。最初は少し複雑に感じるかもしれませんが、実際に手を動かして様々な設定を試してみることで、徐々に理解が深まります。

この記事が、あなたがloggingモジュールを使い始めるための一助となれば幸いです。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール