Pythonのloggingモジュール実践ガイド:printからの卒業

Pythonのloggingモジュールの基本から実践まで解説。ログレベル、フォーマット、ハンドラー、RotatingFileHandler、structlogによる構造化ログまで網羅します。

はじめに

Pythonでのデバッグや本番監視に print を使い続けていませんか?print はシンプルですが、ログレベルの制御、ファイルへの出力、タイムスタンプの付与、本番での無効化が難しいです。

Pythonの標準ライブラリ logging を使うと、これらすべてを宣言的に制御できます。本記事では基礎から実践パターンまでを解説します。

ログレベルの種類

logging には5段階のログレベルがあります。

レベル数値用途
DEBUG10詳細なデバッグ情報(開発時のみ)
INFO20正常系の処理記録
WARNING30予期しない状況だが処理は継続できる
ERROR40処理が失敗した
CRITICAL50システムが続行不可能な致命的エラー

デフォルトのルートロガーは WARNING 以上のみ出力します。

基本的な使い方

basicConfigによる設定

import logging

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s %(levelname)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

logging.debug("デバッグ情報")
logging.info("正常処理")
logging.warning("警告")
logging.error("エラー発生")
logging.critical("致命的エラー")

出力例:

2026-03-12 10:00:00 DEBUG デバッグ情報
2026-03-12 10:00:00 INFO 正常処理
2026-03-12 10:00:00 WARNING 警告
2026-03-12 10:00:00 ERROR エラー発生
2026-03-12 10:00:00 CRITICAL 致命的エラー

モジュールごとのロガー

本番コードでは logging.getLogger(__name__) でモジュール専用のロガーを作ります。どのモジュールからのログかが一目でわかります。

import logging

logger = logging.getLogger(__name__)

def process_data(data):
    logger.info("処理開始: %d件", len(data))
    try:
        result = [x * 2 for x in data]
        logger.debug("処理結果: %s", result)
        return result
    except Exception as e:
        logger.error("処理失敗: %s", e, exc_info=True)
        raise

exc_info=True を指定するとスタックトレースも記録されます。

フォーマットのカスタマイズ

%(...)s スタイルのフォーマット文字列でログの見た目を自由に変えられます。

import logging

formatter = logging.Formatter(
    fmt="%(asctime)s [%(levelname)-8s] %(name)s:%(lineno)d - %(message)s",
    datefmt="%Y-%m-%dT%H:%M:%S",
)

handler = logging.StreamHandler()
handler.setFormatter(formatter)

logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
logger.addHandler(handler)

logger.info("サービス起動")

出力例:

2026-03-12T10:00:00 [INFO    ] myapp:10 - サービス起動

主要なフォーマット変数:

変数内容
%(asctime)sタイムスタンプ
%(levelname)sログレベル名
%(name)sロガー名
%(filename)sファイル名
%(lineno)d行番号
%(funcName)s関数名
%(message)sログメッセージ
%(process)dプロセスID
%(thread)dスレッドID

ハンドラーの種類

ハンドラーはログの出力先を決めます。複数のハンドラーを同時に使えます。

StreamHandler(標準出力/標準エラー)

import logging
import sys

console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)

FileHandler(ファイル出力)

file_handler = logging.FileHandler("app.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)

RotatingFileHandler(ログローテーション)

本番環境ではログが肥大化しないよう、ファイルサイズで自動ローテーションします。

from logging.handlers import RotatingFileHandler

rotating_handler = RotatingFileHandler(
    "app.log",
    maxBytes=10 * 1024 * 1024,  # 10MB
    backupCount=5,               # 最大5世代保持
    encoding="utf-8",
)

app.log が 10MB に達すると app.log.1 にリネームされ、新しい app.log が作られます。

TimedRotatingFileHandler(時刻ローテーション)

日次でローテーションする場合:

from logging.handlers import TimedRotatingFileHandler

timed_handler = TimedRotatingFileHandler(
    "app.log",
    when="midnight",   # 毎日深夜にローテーション
    interval=1,
    backupCount=30,    # 30日分保持
    encoding="utf-8",
)

実践:アプリケーション全体の設定

複数モジュールにまたがるアプリでは、専用の設定関数を用意します。

import logging
import sys
from logging.handlers import RotatingFileHandler


def setup_logging(log_level: str = "INFO", log_file: str = "app.log") -> None:
    """アプリケーション全体のログ設定を初期化する"""
    level = getattr(logging, log_level.upper(), logging.INFO)

    formatter = logging.Formatter(
        fmt="%(asctime)s [%(levelname)-8s] %(name)s - %(message)s",
        datefmt="%Y-%m-%dT%H:%M:%S",
    )

    # コンソール: INFO以上
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setLevel(logging.INFO)
    console_handler.setFormatter(formatter)

    # ファイル: DEBUG以上(ローテーション付き)
    file_handler = RotatingFileHandler(
        log_file,
        maxBytes=10 * 1024 * 1024,
        backupCount=5,
        encoding="utf-8",
    )
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(formatter)

    # ルートロガーに設定
    root_logger = logging.getLogger()
    root_logger.setLevel(level)
    root_logger.addHandler(console_handler)
    root_logger.addHandler(file_handler)


if __name__ == "__main__":
    setup_logging(log_level="DEBUG")

    logger = logging.getLogger(__name__)
    logger.info("アプリケーション起動")
    logger.debug("デバッグ情報(ファイルのみ)")

例外のログ記録

例外処理で logger.exception() を使うと、メッセージとスタックトレースを同時に記録します。

import logging

logger = logging.getLogger(__name__)


def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        logger.exception("ゼロ除算エラー: a=%s, b=%s", a, b)
        return None


divide(10, 0)

出力例:

2026-03-12T10:00:00 [ERROR   ] __main__ - ゼロ除算エラー: a=10, b=0
Traceback (most recent call last):
  File "example.py", line 7, in divide
    return a / b
ZeroDivisionError: division by zero

辞書設定(dictConfig)

大規模アプリでは logging.config.dictConfig を使い、設定を辞書(またはYAML/JSON)で管理します。

import logging
import logging.config

LOGGING_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "standard": {
            "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        },
        "detailed": {
            "format": "%(asctime)s [%(levelname)-8s] %(name)s:%(lineno)d %(funcName)s() - %(message)s",
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "level": "INFO",
            "formatter": "standard",
            "stream": "ext://sys.stdout",
        },
        "file": {
            "class": "logging.handlers.RotatingFileHandler",
            "level": "DEBUG",
            "formatter": "detailed",
            "filename": "app.log",
            "maxBytes": 10485760,
            "backupCount": 5,
            "encoding": "utf-8",
        },
    },
    "loggers": {
        "myapp": {
            "handlers": ["console", "file"],
            "level": "DEBUG",
            "propagate": False,
        },
    },
    "root": {
        "handlers": ["console"],
        "level": "WARNING",
    },
}

logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("myapp")
logger.info("dictConfigで設定したロガー")

構造化ログ(structlog)

JSONログはログ収集基盤(Datadog, CloudWatch, ELK)との連携が容易です。外部ライブラリ structlog を使うと構造化ログを簡単に実装できます。

# pip install structlog
import structlog

structlog.configure(
    processors=[
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.stdlib.add_log_level,
        structlog.processors.JSONRenderer(),
    ],
)

log = structlog.get_logger()
log.info("ユーザーログイン", user_id=42, ip="192.168.1.1")

出力例(JSON):

{
  "timestamp": "2026-03-12T10:00:00Z",
  "level": "info",
  "event": "ユーザーログイン",
  "user_id": 42,
  "ip": "192.168.1.1"
}

structlog なしでJSONログを出す場合、logging.Formatter を継承したカスタムフォーマッターを書く方法もあります。

import json
import logging


class JsonFormatter(logging.Formatter):
    def format(self, record):
        log_data = {
            "timestamp": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }
        if record.exc_info:
            log_data["exception"] = self.formatException(record.exc_info)
        return json.dumps(log_data, ensure_ascii=False)


handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger = logging.getLogger("myapp")
logger.addHandler(handler)
logger.info("JSONフォーマットのログ")

printとloggingの比較

観点printlogging
ログレベル制御不可5段階で制御可
ファイル出力リダイレクトのみFileHandler で簡単
タイムスタンプ手動で追加フォーマッターで自動
スタックトレース手動で tracebackexc_info=True で自動
本番での無効化削除または条件分岐レベル設定で一括制御
ログローテーション不可RotatingFileHandler で対応
構造化ログ不可dictConfig + structlog で対応

関連記事

参考文献