2026年7月17日
2026年7月17日
Pythonのロギング設定(loggingモジュール)|ファイル出力・ローテーション・dictConfigの基本
Python でログを出すなら、print() ではなく標準ライブラリの logging モジュールを使うのが基本です。各モジュールで logger = logging.getLogger(__name__) を作ってログを出し、出力先や書式の設定はプログラムの入口で一度だけ行う、という役割分担にすると、後から出力先を変えたりレベルを調整したりしやすくなります。この記事では、レベルの考え方、ファイル出力とローテーション、dictConfig による設定、そして「ログが出ない」「二重に出る」といったよくあるつまずきの原因と対処を説明します。
print ではなく logging を使う理由
print() で十分に見える場面でも、運用を始めると次のような情報が欲しくなります。
- いつ起きたのか(日時)
- どのくらい重要なのか(情報・警告・エラー)
- どのモジュールから出たのか
- 画面だけでなくファイルにも残したい、古いログは自動で消したい
logging はこれらを設定だけで切り替えられます。コードの中では「何を、どの重要度で記録するか」だけを書き、「どこに、どんな形式で出すか」は設定側に任せられるのが大きな利点です。
基本の書き方
ロガーを作ってログを出す
import logging
logger = logging.getLogger(__name__)
def process(item_id: int) -> None:
logger.debug("処理開始 id=%s", item_id)
if item_id < 0:
logger.warning("不正なIDを受け取りました id=%s", item_id)
return
logger.info("処理完了 id=%s", item_id)
if __name__ == "__main__":
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
process(1)
process(-1)
getLogger(__name__) とすると、ロガー名がモジュール名(例:myapp.db)になります。ログを見たときにどのモジュールから出たかが分かり、後述のようにモジュール単位でレベルを変えることもできます。
メッセージの組み立ては、f 文字列ではなく logger.info("処理完了 id=%s", item_id) のように引数で渡す書き方が推奨されています。こうすると、そのレベルのログが出力されない設定のときには文字列の組み立て自体が省略されます。
ログレベルの使い分け
レベルは低いほうから DEBUG、INFO、WARNING、ERROR、CRITICAL の5段階です。設定したレベル以上のものだけが出力されます。
- DEBUG:開発中の調査用。変数の中身や分岐の通過など
- INFO:正常な処理の節目。起動、ジョブの開始と完了など
- WARNING:処理は続けられるが注意が必要なこと。再試行した、設定が既定値で補われた、など
- ERROR:その処理が失敗したこと
- CRITICAL:プログラム全体が続けられないほどの問題
本番では INFO 以上、調査時だけ DEBUG に下げる、という運用が一般的です。何も設定しないと、ルートロガーのレベルは WARNING なので、INFO や DEBUG は表示されません。
例外を記録する
except の中では logger.exception() を使うと、メッセージと一緒にトレースバック(エラーの発生箇所の履歴)が記録されます。レベルは ERROR です。
try:
result = 10 / 0
except ZeroDivisionError:
logger.exception("計算に失敗しました")
logger.error("失敗: %s", e) のように例外のメッセージだけを残すと、どこで起きたのかが分からず、後で調査に困ります。エラーの原因の読み方や例外処理の設計については、別記事の「Pythonのエラーハンドリングとデバッグ方法」で詳しく扱っています。
ファイルに出力してローテーションする
ファイルに書き続けるとディスクを圧迫するため、サイズや日付で区切って古いものを削除する「ローテーション」を設定します。標準ライブラリに2種類のハンドラーがあります。
import logging
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
# サイズで区切る: 10MB ごとに切り替え、5世代まで残す
size_handler = RotatingFileHandler(
"app.log", maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8"
)
size_handler.setFormatter(formatter)
# 日付で区切る: 毎日0時に切り替え、14日分残す
time_handler = TimedRotatingFileHandler(
"app-daily.log", when="midnight", backupCount=14, encoding="utf-8"
)
time_handler.setFormatter(formatter)
root = logging.getLogger()
root.setLevel(logging.INFO)
root.addHandler(size_handler)
実際にはどちらか一方を選んで使います。encoding="utf-8" を指定しておかないと、Windows などで日本語が文字化けしたり、書き込み時にエラーになったりすることがあります。
注意したいのは、複数のプロセスから同じログファイルにこれらのハンドラーで書き込むと、ローテーションの際にログが欠けたり壊れたりする点です。Web アプリを複数のワーカープロセスで動かしている場合などが該当します。対策としては次のような方法があります。
- Python 側ではローテーションせず、OS の logrotate に任せる。この場合 Python 側は
WatchedFileHandlerを使うと、logrotate によるファイルの差し替えを検知して開き直してくれます - ファイルではなく標準出力に出し、systemd の journal や Docker のログ機能に集める
QueueHandlerとQueueListenerを使い、書き込みを1か所にまとめる
サーバーで動かすプログラムなら、標準出力に出して収集はインフラ側に任せる形が、設定がシンプルで扱いやすいです。
dictConfig で設定をまとめる
ハンドラーが増えてくると、コードで1つずつ組み立てるより、辞書で設定をまとめて渡すほうが見通しがよくなります。
import logging
import logging.config
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s %(levelname)s %(name)s: %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "standard",
"level": "INFO",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "standard",
"filename": "app.log",
"maxBytes": 10485760,
"backupCount": 5,
"encoding": "utf-8",
},
},
"loggers": {
"urllib3": {"level": "WARNING"}, # 外部ライブラリの細かいログを抑える
},
"root": {
"level": "DEBUG",
"handlers": ["console", "file"],
},
}
logging.config.dictConfig(LOGGING)
logging.getLogger(__name__).info("設定を読み込みました")
この設定では、ルートロガーは DEBUG 以上を受け取り、画面(console)には INFO 以上、ファイルには DEBUG 以上を出します。ロガーとハンドラーの両方にレベルがあり、両方の条件を満たしたものだけが出力される、という関係を押さえておくと設定を読み解きやすくなります。
"disable_existing_loggers": False は重要です。既定値の True のままだと、dictConfig を呼ぶ前に作られていたロガー(モジュールの先頭で getLogger(__name__) したものなど)が無効になり、ログが出なくなることがあります。
設定を JSON や YAML のファイルに分けて、読み込んだ辞書を dictConfig() に渡す形にすると、コードを変えずに環境ごとの設定を切り替えられます。
よくあるつまずきと対処
ログが何も出ない
原因として多い順に確認します。
- レベルが足りていない:何も設定していないと WARNING 以上しか出ません。
basicConfig(level=logging.INFO)などで設定します - basicConfig が効いていない:
basicConfig()は、ルートロガーにすでにハンドラーがあると何もしません。インポートしたライブラリが先に設定している場合などに起こります。Python 3.8 以降ではbasicConfig(..., force=True)で既存の設定を置き換えられます - 既存のロガーが無効化されている:
dictConfigやfileConfigのdisable_existing_loggersを確認します
同じログが2回出る
ロガーは名前の階層(myapp.db の親は myapp、その親はルート)に沿って、親のハンドラーにもログを渡します(propagate)。子のロガーと親のロガーの両方にハンドラーを付けると、同じログが二重に出ます。ハンドラーはルートロガー(または最上位のアプリ用ロガー)にだけ付けるのが基本です。どうしても子にハンドラーを付ける場合は、その子のロガーで propagate = False にします。
関数の中で毎回 addHandler() を呼んでいて、呼ぶたびにハンドラーが増えている、というのもよくある原因です。設定は起動時に一度だけ行います。
ライブラリを作るとき
自分で作ったライブラリの中では、basicConfig() やハンドラーの追加をしないのが作法です。使う側のアプリの設定を上書きしてしまうためです。ライブラリ側では getLogger(__name__) でログを出すだけにし、必要ならパッケージの最上位で logging.getLogger(__name__).addHandler(logging.NullHandler()) を設定しておきます。
ログに残してはいけないもの
ログは、障害の調査のために多くの人が見たり、外部のログ収集サービスに送られたりします。パスワード、API キーやトークン、クレジットカード番号、個人を特定できる情報は記録しないようにします。リクエストの内容をまるごとログに出すと、意図せずこれらが含まれることがあるため、出力する項目を選ぶか、該当する値を伏せ字にする処理(logging.Filter を使った置き換えなど)を入れます。
なお、logging モジュールの基本的な使い方は長く変わっていませんが、細かな機能はバージョンごとに追加されています。この記事の内容は Python 3.8 以降を前提にしています(2026年10月現在)。
まとめ
- 各モジュールで
getLogger(__name__)を使い、出力先と書式の設定は入口で一度だけ行う - 何も設定しないと WARNING 以上しか出ない。例外は
logger.exception()でトレースバックごと残す - ファイル出力は
RotatingFileHandler/TimedRotatingFileHandlerでローテーション。複数プロセスなら logrotate や標準出力への集約を検討する - 設定が増えたら
dictConfigにまとめ、disable_existing_loggersは False にする - 二重出力は propagate とハンドラーの重複追加が原因。秘密情報や個人情報はログに出さない