閉じる

【Python】loggingの使い方入門|基本設定から実務レベルまで

Pythonでの開発では、ログ出力はデバッグや運用監視に欠かせない要素です。

特にWebアプリやバッチ処理など、後から不具合を追跡する必要があるプログラムでは、標準ライブラリのloggingモジュールを正しく使いこなすことが重要です。

本記事では、printとの違いから始めて、実務で使える設定パターンやフレームワーク連携まで、段階的に解説します。

Pythonのloggingとは

loggingモジュールの概要

Pythonのloggingモジュールは、標準ライブラリとして用意されている汎用的なログ出力フレームワークです。

アプリケーションコードからログを送るロガー(logger)と、ログをどこにどう出力するかを決めるハンドラ(handler)、ログの文字列表現を制御するフォーマッタ(formatter)などで構成されています。

ポイントは、アプリケーション側は「ログを書きたい」という意図だけを記述し、出力先やフォーマットの詳細は設定側に切り離せることです。

これにより、開発環境ではコンソール、本番ではファイルなど、環境に応じた柔軟なログ運用が可能になります。

printとの違いとloggingを使うメリット

Pythonで簡単なデバッグを行う際、ついprintを使ってしまいがちですが、実務レベルのアプリケーションではloggingを使う方が圧倒的に有利です。

printとloggingの主な違いを整理すると次のようになります。

観点printlogging
目的一時的な表示・デバッグ運用を含む継続的なログ管理
ログレベルなしDEBUG〜CRITICALを明示
出力先基本は標準出力のみコンソール、ファイル、メール、外部サービスなど
フォーマットコード側で都度整形設定で一括管理可能
制御コードを書き換えないと制御できない設定ファイルや環境変数で制御しやすい
無効化print削除・コメントアウトが必要ログレベル変更で抑制可能

printを大量に埋め込んだコードは、後から「どこが本当に重要な情報なのか分かりにくくなる」という問題があります。

loggingであればログレベルを使って重要度を明確にしつつ、環境ごとに出力レベルを切り替えることができます。

ログレベル(DEBUG/INFO/WARNING/ERROR/CRITICAL)の意味

loggingには、メッセージの重要度(深刻度)を表すためにログレベルが定義されています。

レベル名数値意味の目安典型的な用途
DEBUG10詳細なデバッグ情報変数の中身、SQL文、詳細なフロー追跡
INFO20通常動作の情報処理開始・終了、設定値のロード結果など
WARNING30注意喚起レベル想定外だが処理可能な事象(リトライ成功など)
ERROR40エラー発生処理を一部中断するエラー(外部API失敗など)
CRITICAL50致命的エラーサービス停止レベルの重大障害

本番運用では、通常INFO以上を出し、障害対応時にはDEBUGまで出すといった運用がよく行われます。

レベルはlogging.DEBUGのような定数で指定できます。

loggingの基本的な使い方

logging.basicConfigの基本設定

最初にloggingを使う場合は、logging.basicConfigを使うのが最も簡単です。

モジュール全体の基本設定を一度に行い、その後はlogging.debug()などの関数でログを出力できます。

最小限のサンプルコード

Python
import logging

# basicConfigで最低限の設定を行う
logging.basicConfig(
    level=logging.INFO,  # INFO以上を出力する
)

# ログ出力の例
logging.debug("これはDEBUGレベルなので表示されません")
logging.info("アプリケーションを開始します")
logging.warning("設定ファイルが見つかりません。デフォルト設定を使用します")
logging.error("データベース接続に失敗しました")
logging.critical("致命的なエラーが発生したため、アプリケーションを終了します")

上記ではlevel=logging.INFOと指定しているため、INFO以上のログだけが表示され、DEBUGログは無視されます。

実行結果(一例)

INFO:root:アプリケーションを開始します
WARNING:root:設定ファイルが見つかりません。デフォルト設定を使用します
ERROR:root:データベース接続に失敗しました
CRITICAL:root:致命的なエラーが発生したため、アプリケーションを終了します

このようにログレベルとメッセージが自動的に付与されて出力されます。

ログレベル(level)とフォーマット(format)の指定

basicConfiglevel引数とformat引数を指定することで、出力するログレベルの下限と、ログメッセージの見た目を制御できます。

レベルとフォーマットを指定した例

Python
import logging

logging.basicConfig(
    level=logging.DEBUG,  # DEBUG以上すべて出力
    format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",  # 日付の形式を指定
)

logging.debug("デバッグ情報です")
logging.info("一般的な情報ログです")
logging.warning("警告レベルのログです")
実行結果
2025-01-01 10:00:00 [DEBUG] root - デバッグ情報です
2025-01-01 10:00:00 [INFO] root - 一般的な情報ログです
2025-01-01 10:00:00 [WARNING] root - 警告レベルのログです

format文字列内の指定フィールドとして、代表的なものを挙げます。

プレースホルダ内容
%(asctime)sログ出力時刻
%(levelname)sログレベル名(DEBUGなど)
%(name)sロガー名
%(message)s実際のログメッセージ
%(filename)sファイル名
%(lineno)d行番号
%(threadName)sスレッド名

運用で何が知りたいかを意識して、必要な項目だけに絞るとログが読みやすくなります。

コンソール(標準出力)へのログ出力

basicConfigだけを使う場合、デフォルトでは標準エラー出力(stderr)にログが出ます。

標準出力(stdout)に出したい場合や、より明示的にコンソール出力を制御したい場合は、StreamHandlerを使います。

Python
import logging
import sys

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

# コンソール出力用ハンドラを作成(標準出力を指定)
console_handler = logging.StreamHandler(stream=sys.stdout)
console_handler.setLevel(logging.INFO)  # コンソールにはINFO以上だけ表示

# フォーマッタを設定
formatter = logging.Formatter(
    "%(asctime)s [%(levelname)s] %(name)s - %(message)s"
)
console_handler.setFormatter(formatter)

# ロガーにハンドラを追加
logger.addHandler(console_handler)

logger.debug("これはコンソールには出ません(DEBUGレベル)")
logger.info("これはコンソールに出力されます(INFOレベル)")
実行結果
2025-01-01 10:00:00 [INFO] __main__ - これはコンソールに出力されます(INFOレベル)

ロガーに対してどのハンドラを付けるかを制御することで、出力先を柔軟に選べるようになります。

ファイルへのログ出力

ログをファイルに出力するには、FileHandlerを利用します。

シンプルな例を示します。

Python
import logging
from logging import FileHandler, Formatter

logger = logging.getLogger("simple_file_logger")
logger.setLevel(logging.DEBUG)

# ファイル出力用ハンドラを作成
file_handler = FileHandler("app.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)

# フォーマッタを設定
formatter = Formatter(
    "%(asctime)s [%(levelname)s] %(name)s - %(message)s"
)
file_handler.setFormatter(formatter)

# ロガーにハンドラを追加
logger.addHandler(file_handler)

logger.info("ファイルにINFOログを書き込みます")
logger.error("ファイルにERRORログを書き込みます")
実行結果
# app.logの中身(一例)
2025-01-01 10:00:00 [INFO] simple_file_logger - ファイルにINFOログを書き込みます
2025-01-01 10:00:00 [ERROR] simple_file_logger - ファイルにERRORログを書き込みます

ファイルログは障害発生時の事後調査で非常に重要な役割を果たすため、本番環境では必ず設定しておくと安心です。

ローテーションログ(RotatingFileHandler)の設定

長期間の運用では、ログファイルが肥大化してディスクを圧迫してしまいます。

これを防ぐためにログローテーションを行います。

PythonのloggingにはRotatingFileHandlerが用意されており、一定サイズを超えたらファイルを切り替えることができます。

Python
import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger("rotating_logger")
logger.setLevel(logging.DEBUG)

# ローテーションするファイルハンドラを作成
rotating_handler = RotatingFileHandler(
    "rotating_app.log",
    maxBytes=1024 * 10,   # 10KBを超えたらローテーション
    backupCount=3,        # 過去3世代分を保持
    encoding="utf-8",
)
rotating_handler.setLevel(logging.INFO)

formatter = logging.Formatter(
    "%(asctime)s [%(levelname)s] %(name)s - %(message)s"
)
rotating_handler.setFormatter(formatter)

logger.addHandler(rotating_handler)

for i in range(100):
    logger.info("ローテーションテストログ %d", i)

上記のコードを実行すると、rotating_app.logが一定サイズに達したタイミングでrotating_app.log.1rotating_app.log.2のようにバックアップファイルが自動生成されます。

実務で使えるloggingの設定パターン

logger/handler/formatterの役割と関係

loggingは大きくlogger / handler / formatterの3つの要素で構成されます。

  1. logger:
    アプリケーションコードからログ出力を受け付ける窓口です。logger.debug()などを呼び出す対象で、名前(例:__name__)を付けて取得します。
  2. handler:
    受け付けたログを「どこに」出力するかを担当します。コンソール、ファイル、メール送信など、用途ごとに複数のハンドラを1つのロガーにぶら下げられます。
  3. formatter:
    ログレコードを「どのような文字列で」出力するかを定義します。時刻・レベル・メッセージなどのフォーマットを書きます。

1つのloggerに複数のhandlerを付けて、同じログを複数の出力先に出す、という設計がloggingの基本パターンです。

名前付きロガー(logging.getLogger)の設計

logging.getLogger(name)で取得するロガーには名前を付けることができます。

この名前はドット区切りの階層構造を持っており、モジュール単位・機能単位でロガーを分割する際に非常に便利です。

Python
import logging

# ルートロガー配下の名前付きロガーを作成
app_logger = logging.getLogger("myapp")
db_logger = logging.getLogger("myapp.database")
web_logger = logging.getLogger("myapp.web")

print(app_logger.name)  # myapp
print(db_logger.name)   # myapp.database
print(web_logger.name)  # myapp.web

運用の観点では、どの部分から出たログかを見分けることが重要です。

名前付きロガーを使うことで、例えばデータベース関連のログだけを詳細に出すといった制御がやりやすくなります。

モジュールごとのロガー分割と階層構造

Pythonの慣習として、各モジュールファイルの先頭でlogger = logging.getLogger(__name__)と書くパターンがよく使われます。

これにより、モジュールのフルパス(パッケージ名.モジュール名)がロガー名となり、階層構造が自然に整います。

Python
# myapp/database.py

import logging

logger = logging.getLogger(__name__)  # logger名は "myapp.database"

def fetch_user(user_id: int):
    logger.debug("ユーザー情報を取得します: user_id=%d", user_id)
    # DBアクセス処理...
    logger.info("ユーザー情報の取得に成功しました: user_id=%d", user_id)
Python
# myapp/web.py

import logging

logger = logging.getLogger(__name__)  # logger名は "myapp.web"

def handle_request(path: str):
    logger.info("リクエストを受信しました: path=%s", path)
    # ルーティング・処理...
    logger.warning("未認証アクセスです: path=%s", path)

アプリケーション全体の設定側でmyapp.databaseだけレベルをDEBUGにし、それ以外はINFOにする、といった部分的な詳細ログも実現できます。

複数ハンドラでコンソールとファイルに同時出力

実務では、コンソールには重要な情報だけ、本番ログファイルには詳細も含めて保存という構成がよく使われます。

その場合、1つのロガーに複数のハンドラを設定します。

Python
import logging
from logging import StreamHandler, FileHandler, Formatter
import sys

logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)  # ベースはDEBUGにしておく

# コンソール(標準出力)用ハンドラ
console_handler = StreamHandler(stream=sys.stdout)
console_handler.setLevel(logging.INFO)  # INFO以上を表示
console_handler.setFormatter(Formatter(
    "%(asctime)s [%(levelname)s] %(name)s - %(message)s"
))

# ファイル用ハンドラ
file_handler = FileHandler("myapp_debug.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)  # DEBUG以上すべてを記録
file_handler.setFormatter(Formatter(
    "%(asctime)s [%(levelname)s] %(name)s - %(filename)s:%(lineno)d - %(message)s"
))

# ロガーに両方のハンドラを追加
logger.addHandler(console_handler)
logger.addHandler(file_handler)

logger.debug("デバッグ用の詳細ログです")
logger.info("通常の情報ログです")
logger.error("エラーログです")

この構成では、DEBUGログはファイルにだけ出力され、INFO以上はコンソールとファイルの両方に出力されます。

開発環境と本番環境でログ設定を切り替える方法

実務で重要になるのが環境ごとのログ設定切り替えです。

開発中はDEBUGログをばんばん出してよいですが、本番環境でDEBUGを出し続けるとパフォーマンスやディスク容量に悪影響があります。

シンプルな方法として、環境変数でモードを切り替える例を示します。

Python
import logging
import os
from logging import StreamHandler, FileHandler, Formatter

ENV = os.getenv("APP_ENV", "dev")  # dev or prod など

logger = logging.getLogger("myapp")

if ENV == "dev":
    logger.setLevel(logging.DEBUG)
    handler = StreamHandler()
    handler.setLevel(logging.DEBUG)
    handler.setFormatter(Formatter(
        "%(asctime)s [%(levelname)s] %(name)s - %(message)s"
    ))
    logger.addHandler(handler)

else:  # prod想定
    logger.setLevel(logging.INFO)
    file_handler = FileHandler("myapp.log", encoding="utf-8")
    file_handler.setLevel(logging.INFO)
    file_handler.setFormatter(Formatter(
        "%(asctime)s [%(levelname)s] %(name)s - %(message)s"
    ))
    logger.addHandler(file_handler)

設定ロジックをコードに直接書きすぎると保守が大変になるため、後述するdictConfigなどの設定ファイル方式に移行していくと運用しやすくなります。

設定ファイル(dictConfig・INI)でloggingを管理する

loggingの設定が増えてくると、コード内にすべて書くのは非効率です。

Python標準ではdictConfigおよびINI形式(fileConfig)での設定読み込みがサポートされています。

ここでは実務で使いやすいdictConfigの例を紹介します。

Python
# logging_config.py
import logging.config

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,

    "formatters": {
        "simple": {
            "format": "%(asctime)s [%(levelname)s] %(name)s - %(message)s"
        },
        "detailed": {
            "format": "%(asctime)s [%(levelname)s] %(name)s "
                      "%(filename)s:%(lineno)d - %(message)s"
        },
    },

    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "level": "INFO",
            "formatter": "simple",
            "stream": "ext://sys.stdout",
        },
        "file": {
            "class": "logging.handlers.RotatingFileHandler",
            "level": "DEBUG",
            "formatter": "detailed",
            "filename": "myapp.log",
            "maxBytes": 1024 * 1024,
            "backupCount": 5,
            "encoding": "utf-8",
        },
    },

    "loggers": {
        "myapp": {
            "level": "DEBUG",
            "handlers": ["console", "file"],
            "propagate": False,
        },
        "myapp.database": {
            "level": "INFO",
            "handlers": ["file"],  # DBはファイルにだけ出す例
            "propagate": False,
        },
    },

    "root": {
        "level": "WARNING",
        "handlers": ["console"],
    },
}
Python
# main.py
import logging
import logging.config
from logging_config import LOGGING

# dictConfigで一括設定
logging.config.dictConfig(LOGGING)

logger = logging.getLogger("myapp")

logger.debug("デバッグログ")
logger.info("情報ログ")
logger.warning("警告ログ")

設定を辞書や外部ファイルに切り出すことで、本番環境だけ設定を差し替えるといった運用が容易になります。

loggingの活用テクニック

スタックトレースと例外ログ

例外発生時にスタックトレース(どの関数をたどってエラーに至ったかの履歴)をログに残すことは、障害解析に極めて重要です。

loggingではlogger.exception()exc_info=Trueで簡単に記録できます。

Python
import logging

logging.basicConfig(
    level=logging.ERROR,
    format="%(asctime)s [%(levelname)s] %(name)s - %(message)s"
)
logger = logging.getLogger(__name__)

def divide(a: int, b: int) -> float:
    return a / b

try:
    divide(10, 0)
except Exception:
    # exception()は現在の例外情報を自動的にログに含める
    logger.exception("ゼロ除算エラーが発生しました")
実行結果
2025-01-01 10:00:00 [ERROR] __main__ - ゼロ除算エラーが発生しました
Traceback (most recent call last):
  File "example.py", line 11, in <module>
    divide(10, 0)
  File "example.py", line 8, in divide
    return a / b
ZeroDivisionError: division by zero

logger.error(…, exc_info=True)のように書いても同様の情報を取得できますが、例外ハンドリングの中ではlogger.exception()が簡潔でおすすめです。

変数やコンテキスト情報をログに埋め込む方法

loggingでは、変数値をメッセージに埋め込むために%sなどのプレースホルダを使うのが基本です。

Python
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s - %(message)s"
)
logger = logging.getLogger(__name__)

user_id = 123
logger.info("ユーザーID=%sの処理を開始します", user_id)

上記のようにf文字列ではなく、引数で値を渡すのがloggingの推奨スタイルです。

この理由は後述のパフォーマンスの項で説明します。

さらに一歩進んで、追加のコンテキスト情報をログに埋め込むにはextra引数やLoggerAdapterを使います。

Python
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s "
           "user_id=%(user_id)s request_id=%(request_id)s - %(message)s"
)
logger = logging.getLogger(__name__)

extra = {"user_id": 123, "request_id": "req-abc-001"}

logger.info("コンテキスト付きログの例です", extra=extra)
実行結果
2025-01-01 10:00:00 [INFO] __main__ user_id=123 request_id=req-abc-001 - コンテキスト付きログの例です

複数のログで同じコンテキストを共有する場合はLoggerAdapterを使うと便利です。

Python
import logging

class RequestAdapter(logging.LoggerAdapter):
    """リクエスト単位でuser_idやrequest_idを付与するアダプタ"""

    def process(self, msg, kwargs):
        # extraにuser_idやrequest_idを自動付加する
        extra = self.extra.copy()
        extra.update(kwargs.get("extra", {}))
        kwargs["extra"] = extra
        return msg, kwargs

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s "
           "user_id=%(user_id)s request_id=%(request_id)s - %(message)s"
)

base_logger = logging.getLogger("myapp")
req_logger = RequestAdapter(base_logger, {
    "user_id": 123,
    "request_id": "req-abc-001",
})

req_logger.info("処理ステップ1開始")
req_logger.info("処理ステップ2完了")

パフォーマンスを意識したログ出力の書き方

ログは多く出力すればよいわけではなく、パフォーマンスへの影響も考慮する必要があります。

特に大量ループ内でのログ出力は要注意です。

良い書き方のポイントは次の通りです。

1つ目は、f文字列などで事前に文字列整形しないことです。

loggingはプレースホルダと引数を受け取ると、ログレベルの判定後に必要な場合だけ文字列整形を行います。

Python
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

value = 42

# 悪い例: f文字列で毎回整形される
logger.debug(f"値は {value} です")  # DEBUGは出力されないが、整形は行われる

# 良い例: プレースホルダ + 引数
logger.debug("値は %d です", value)  # DEBUGが無効なら整形も行われない

2つ目は、logger.isEnabledFor(level)で高コストな処理をガードすることです。

Python
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def heavy_to_string(obj):
    # 重い変換処理を想定
    return str(obj) * 1000

obj = {"key": "value"}

# 直接呼んでしまうと、DEBUGが無効でもheavy_to_stringは実行される
logger.debug("詳細デバッグ: %s", heavy_to_string(obj))

# 良い例: ログが有効なときだけ重い処理を実行
if logger.isEnabledFor(logging.DEBUG):
    logger.debug("詳細デバッグ: %s", heavy_to_string(obj))

このように書くことで、本番環境でDEBUGログを無効にしているときは、不要なオーバーヘッドを避けることができます。

サードパーティライブラリとloggingの連携

多くのサードパーティライブラリもPythonのloggingを利用してログを出力します。

例えばrequestssqlalchemyなどは、独自のロガー名を持っています。

アプリケーション側でこれらのログを制御するには、ライブラリのロガー名に対して設定を行います。

Python
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s - %(message)s"
)

# requestsライブラリのログレベルを制御
requests_logger = logging.getLogger("requests")
requests_logger.setLevel(logging.WARNING)  # WARNING以上だけを出力

# SQLAlchemyのエンジンログをDEBUGに
sqlalchemy_logger = logging.getLogger("sqlalchemy.engine")
sqlalchemy_logger.setLevel(logging.DEBUG)

dictConfigを使う場合は、loggersセクションに"requests""sqlalchemy.engine"などを追加します。

これにより、自分のアプリとライブラリのログを一元管理できるようになります。

DjangoやFlaskでのlogging設定の基本

Webフレームワークも内部でloggingを利用しています。

ここでは代表的なDjangoとFlaskでの基本的な設定方法を簡潔に紹介します。

Djangoのlogging設定例

Djangoではsettings.py内にLOGGING辞書を定義するのが標準です。

中身はほぼdictConfigと同じ形式です。

Python
# settings.py (抜粋)

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,

    "formatters": {
        "simple": {
            "format": "%(asctime)s [%(levelname)s] %(name)s - %(message)s"
        },
    },

    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "level": "INFO",
            "formatter": "simple",
        },
        "file": {
            "class": "logging.FileHandler",
            "level": "INFO",
            "formatter": "simple",
            "filename": "django_app.log",
        },
    },

    "loggers": {
        "django": {
            "handlers": ["console"],
            "level": "INFO",
            "propagate": True,
        },
        "myapp": {
            "handlers": ["console", "file"],
            "level": "DEBUG",
            "propagate": False,
        },
    },
}

Djangoアプリの各モジュールでは、通常どおりlogger = logging.getLogger(__name__)を使い、settingsのLOGGINGに従ってログが出力されます。

Flaskのlogging設定例

Flaskは比較的軽量で、app.loggerというロガーを標準で持っています。

これに対してハンドラやフォーマッタを設定するか、あるいは通常のPython logging設定を使う方法があります。

Python
from flask import Flask
import logging
from logging import StreamHandler, Formatter

app = Flask(__name__)

# Flaskのapp.loggerを取得
logger = app.logger
logger.setLevel(logging.INFO)

# コンソール出力ハンドラを追加
handler = StreamHandler()
handler.setFormatter(Formatter(
    "%(asctime)s [%(levelname)s] %(name)s - %(message)s"
))
logger.addHandler(handler)

@app.route("/")
def index():
    logger.info("インデックスページへアクセスがありました")
    return "Hello, Flask logging!"

このように、フレームワーク固有の設定を理解しつつ、基本はPythonのloggingの考え方を使うことで、Webアプリケーションのログ運用もスムーズになります。

まとめ

Pythonのloggingモジュールは、printよりもはるかに強力かつ柔軟なログ基盤を提供してくれます。

基本的なbasicConfigから始めて、logger/handler/formatterの分離、ログレベルやフォーマットの設計、ローテーションや設定ファイルによる管理へと段階的に発展させていくことで、実務に耐えるログ環境を構築できます。

例外スタックトレースやコンテキスト付きログ、サードパーティやフレームワークとの連携も押さえておけば、障害解析や運用監視の精度が大きく向上します。

まずは小さなスクリプトからloggingを導入し、プロジェクト規模に応じて設定を洗練させていくことをおすすめします。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!