Python | Web / API:logging のフォーマット

Python Python
スポンサーリンク

概要(loggingのフォーマットは「いつ・どこで・何が起きたか」を一行に凝縮する設計)

ログの価値は、後から原因に直行できること。フォーマットはそのための骨組みです。日付・レベル・発生箇所・メッセージを、一定の書式で出力します。重要なのは「何を含めるか」を決めること(最低限の文脈)、「どこに出すか」で書式を使い分けること(コンソールは短く、ファイルは詳細)、そして「可読性」と「機械可読性」のバランスです。


基本のフォーマット(まずは必要最小限を押さえる)

最短の設定と定番フィールド

import logging

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

logging.info("アプリ起動")
Python
  • asctime: ログ時刻(datefmtで書式変更可)
  • levelname: ログレベル(INFO/ERRORなど)
  • name: ロガー名(getLogger(name)の値)
  • lineno: 行番号(原因特定に有効)
  • message: 実際のログ本文

日付書式の指定(ISO風に揃える)

import logging

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

logging.info("日付を読みやすく統一")
Python
  • datefmt: 出力する日時のフォーマット。運用時は「YYYY-MM-DD HH:MM:SS」が無難。

フォーマッタを使い分ける(出力先ごとに最適化)

コンソールは短く、ファイルは詳細に

import logging

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

# コンソール(短く)
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setFormatter(logging.Formatter("%(levelname)s %(message)s"))

# ファイル(詳細)
fh = logging.FileHandler("app.log", encoding="utf-8")
fh.setLevel(logging.DEBUG)
fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s:%(lineno)d %(message)s"))

logger.addHandler(ch)
logger.addHandler(fh)

logger.info("画面は簡潔、ファイルは詳細")
Python

短い書式は「見やすさ」、詳細書式は「原因特定」を優先します。出力先ごとにフォーマッタを分けるのが基本です。

追加フィールドで文脈を強化する

import logging

fmt = "%(asctime)s [%(levelname)s] %(name)s %(process)d/%(threadName)s %(message)s"
logging.basicConfig(level=logging.INFO, format=fmt)

logging.info("プロセスIDやスレッド名も出す")
Python
  • process/threadName: 並列時に「どこから出たか」を識別可能になります。

変数埋め込みと例外(読みやすく、壊れない形)

f文字列ではなくパラメータで埋め込む(遅延評価で軽量)

import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")

user = "taro"
logging.info("ログイン開始 user=%s", user)  # 推奨:必要時のみ整形が走る
Python
  • 理由: ログが出ないレベルでは整形コストがゼロ。f文字列は常に整形し、無駄が増えます。

例外はスタック込みで一撃(logger.exception)

import logging
logging.basicConfig(level=logging.ERROR, format="%(asctime)s [%(levelname)s] %(message)s")

try:
    1 / 0
except Exception:
    logging.exception("計算に失敗")  # = logging.error("...", exc_info=True)
Python

例外の原因箇所をフォーマットに含めるより、スタックトレースを記録する方が圧倒的に役立ちます。


構造化ログとJSON(機械可読性を上げる)

シンプルなJSON風書式(最初の一歩)

import logging
logging.basicConfig(
    level=logging.INFO,
    format='{"ts":"%(asctime)s","lvl":"%(levelname)s","mod":"%(module)s","line":%(lineno)d,"msg":"%(message)s"}',
    datefmt="%Y-%m-%dT%H:%M:%S"
)

logging.info("JSON風の出力で機械処理しやすくする")
Python

機械可読性が重要(集約や検索)なら、JSON風にするのが近道。専用ライブラリ(例:structlog)も検討できます。

コンテキスト値を追加する(extra/Adapter)

import logging
logger = logging.getLogger("api")

handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s req=%(req_id)s %(message)s"))
logger.addHandler(handler)
logger.setLevel(logging.INFO)

logger.info("開始", extra={"req_id": "R-123"})
logger.info("完了", extra={"req_id": "R-123"})
Python
  • extra: 動的なキー(req_idなど)をメッセージに差し込み、追跡性を高めます。

時刻とタイムゾーン(正確さが原因特定を近づける)

ローカル時刻かUTCかを決める(運用方針)

asctimeはデフォルトでローカル時刻。集約基盤や多地域運用ではUTCが安全です。UTCを強制したい場合は、フォーマッタをサブクラス化して自前で時刻を入れる方法が堅実です。

import logging, datetime

class UTCFormatter(logging.Formatter):
    def formatTime(self, record, datefmt=None):
        dt = datetime.datetime.utcfromtimestamp(record.created)
        return dt.strftime(datefmt or "%Y-%m-%dT%H:%M:%SZ")

fmt = UTCFormatter("%(asctime)s [%(levelname)s] %(message)s")
handler = logging.StreamHandler()
handler.setFormatter(fmt)

logger = logging.getLogger("utc")
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.info("UTCで出す")
Python

実務の型(運用で役立つフォーマットひな形)

ひな形1:画面は短く、ファイルは詳細+ローテーション

import logging
from logging.handlers import RotatingFileHandler

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

# 画面:短く
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setFormatter(logging.Formatter("%(levelname)s %(message)s"))

# ファイル:詳細+サイズ管理
fh = RotatingFileHandler("app.log", maxBytes=5_000_000, backupCount=3, encoding="utf-8")
fh.setLevel(logging.DEBUG)
fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s:%(lineno)d %(message)s"))

logger.addHandler(ch)
logger.addHandler(fh)

logger.info("起動")
logger.debug("詳細(ファイルにのみ)")
Python

ひな形2:APIクライアント向け(URL・遅延・ステータス)

import logging, time, requests

fmt = "%(asctime)s [%(levelname)s] url=%(url)s status=%(status)s latency=%(latency).3fs %(message)s"
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(fmt))

logger = logging.getLogger("client")
logger.addHandler(handler)
logger.setLevel(logging.INFO)

def get(url):
    t0 = time.perf_counter()
    try:
        r = requests.get(url, timeout=5)
        latency = time.perf_counter() - t0
        logger.info("GET完了", extra={"url": url, "status": r.status_code, "latency": latency})
        return r
    except Exception as e:
        latency = time.perf_counter() - t0
        logger.error("GET失敗: %s", e, extra={"url": url, "status": -1, "latency": latency})
        return None

get("https://httpbin.org/get")
Python

ひな形3:テスト時は詳細、CIではINFOに切替

import logging, os

level = logging.DEBUG if os.getenv("APP_DEBUG") == "1" else logging.INFO
logging.basicConfig(level=level, format="%(asctime)s [%(levelname)s] %(name)s:%(lineno)d %(message)s")

logging.debug("ローカルでは詳細を出す")
logging.info("CI/本番ではINFO中心")
Python

まとめ

フォーマットは「原因に直行するための情報設計」。最低限の文脈(時刻・レベル・発生箇所・本文)を揃え、出力先ごとに最適化する。変数埋め込みは遅延評価で軽くし、例外はスタック込みで確実に残す。必要に応じてJSON風に構造化し、extraでリクエストIDや遅延などのコンテキストを加える。時刻はローカルかUTCかを決め、運用に合わせて一貫させる。この型を押さえれば、初心者でも「見やすい・追える・使える」ログが短いコードで設計できます。

タイトルとURLをコピーしました