Python | 自動化:例外ログ

Python
スポンサーリンク

概要(例外ログは「エラーの証拠を残すブラックボックスレコーダー」)

例外ログは、

「エラーが起きた“瞬間の情報”を、その場で消えずに後から見返せるように残しておく仕組み」

です。

プログラムは、一瞬で例外を投げて落ちます。
でも、人間がそれを目で見て気づけるとは限らないし、本番環境ならなおさら見えません。

だから、自動化では

  • いつ
  • どの処理で
  • どんな例外が
  • どんな値を持ったまま

発生したのかを、ログとして残しておくことがとても大事です。

ここでは、

  • print ではダメな理由
  • logging モジュールで例外ログを出す基本パターン
  • logging.exceptionlogger.exception の使いどころ
  • 「なにを書いておくと後から助かるか」という中身の設計

を、初心者向けにじっくりかみ砕いていきます。


print と logging の決定的な違い(「その場しのぎ」か「記録」か)

print は「今目の前の人にしか届かない」

小さなスクリプトを書き始めたとき、エラーが起きたら

print("エラーが起きました")
Python

みたいに書きたくなりますよね。

でも、これには致命的な弱点があります。

  • その場で画面を見ていないと、メッセージが消えて終わり
  • いつ・どんな状況で出たものかが分からない
  • 実行環境が違うと、そもそも画面がない(cron、バッチ、サーバーなど)

「今ここで自分が動かしているだけ」の世界では print でも何とかなりますが、
自動化を本気で回し始めるとすぐに限界がきます。

logging は「時刻付きで証拠を残す仕組み」

Python 標準の logging モジュールは、

  • いつ(日時)
  • どこから(モジュール名・関数名)
  • どれくらい重要なメッセージか(レベル)

を含めて、ファイルやコンソールに「記録として残す」ための仕組みです。

特に例外ログでは、

  • スタックトレース(どの行で例外が投げられたか)
  • 例外メッセージ(具体的な原因)

を丸ごと記録してくれます。

これがあると、「朝来たらバッチが落ちていた」みたいな状況でも、
後からログを見て原因を追いかけられます。


logging の基本セットアップ(ここをテンプレ化する)

一番シンプルなファイル出力設定

まずは、例外ログの土台になる「ログファイル出力」を整えます。

import logging
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent
LOG_FILE = BASE_DIR / "app.log"

def setup_logging():
    logging.basicConfig(
        filename=LOG_FILE,
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(name)s - %(message)s"
    )
Python

これをスクリプトの最初のほうで一度だけ呼びます。

if __name__ == "__main__":
    setup_logging()
    # ここからメイン処理
Python

ポイントを整理します。

filename
ログを書き出すファイルを決めます。相対パスではなく、BASE_DIR からの絶対パスを使うのがおすすめです(どこから実行しても迷子にならない)。

level
どのレベル以上のログを記録するか。
INFO にしておけば、INFO・WARNING・ERROR・CRITICAL が記録されます(DEBUG は切り捨て)。

format
ログ1行にどんな情報を含めるか。
%(asctime)s で日時、%(levelname)s でレベル、%(name)s でロガー名、%(message)s でメッセージ本体です。

これで、logging.info("〇〇") などと書くだけで、log ファイルに時刻付きで記録されるようになります。


例外をログに残す基本パターン(try/except+logging)

例外を握りつぶさずに「記録してから上に投げる」

例外ログでよくやりがちなのが、こういう書き方です。

try:
    do_something()
except Exception as e:
    print("エラー:", e)
Python

この書き方は、一見「エラーを捕まえていて良さそう」に見えますが、
実際には、

  • スタックトレース(どこで起きたか)が消えてしまう
  • ログファイルにも残らない
  • 上位に例外が伝わらないので、エラーなのに正常っぽく見えてしまう

という危険なコードです。

例外ログをきちんと書くときは、

  • ログとして記録する
  • 必要なら後続を止める(例外を再度投げる)

という2ステップを意識します。

logging.exception を使ったパターン(トップレベルの例外)

最も簡単で強力なのが、logging.exception です。
これは、except 節の中で呼ぶと、「メッセージ+スタックトレース」を丸ごとログに残してくれます。

import logging

def main():
    # メイン処理
    1 / 0  # わざと例外

if __name__ == "__main__":
    setup_logging()
    try:
        main()
    except Exception:
        logging.exception("メイン処理で予期しない例外が発生しました")
        raise
Python

ここで大事なのは3つです。

1つ目:except Exception: で広く捕まえつつ、中で logging.exception を呼んでいること。
メッセージと一緒に、スタックトレースがログファイルに残ります。

2つ目:ログを出したあとに raise していること。
例外を握りつぶさずに、上位(=最上位)まで伝えています。
これによって、「プロセスはちゃんと異常終了するし、ログも残る」という状態になります。

3つ目:トップレベル(if __name__ == "__main__": の中)でこのパターンを使うこと。
メイン処理のどこで何が起きても、最終的にはここで記録されます。

これを「アプリケーションの最後の砦」として入れておくと、
想定外の例外も逃さずログに残せます。


各処理単位での例外ログ(文脈情報を一緒に書く)

「何をやっている最中のエラーか」が分かるようにする

ただスタックトレースだけあっても、

「そのとき何をしようとしていたのか」

が分からないと、調査がつらくなります。

そこで、処理単位(ファイル1つ、ユーザー1人分、API1回分など)で try/except を入れて、
「どの対象でエラーが起きたか」をログに含めるのが大事です。

import logging
from pathlib import Path

logger = logging.getLogger(__name__)

def process_file(path: Path):
    logger.info(f"ファイル処理開始: {path}")
    try:
        text = path.read_text(encoding="utf-8")
        # ここに何らかの処理
    except Exception:
        logger.exception(f"ファイル処理中にエラー: {path}")
        raise
    else:
        logger.info(f"ファイル処理完了: {path}")
Python

ここでのポイントは、

  • ロガーを logging.getLogger(__name__) で取っている(モジュール単位のロガー)
  • 「開始」と「完了」に INFO ログを出している
  • 例外が起きたときに logger.exception で、対象ファイルパスを含めてログに出している

という3点です。

こうしておくと、ログには例えばこんな流れが出ます。

  • ファイル処理開始: data/input/a.csv
  • ファイル処理開始: data/input/b.csv
  • ファイル処理中にエラー: data/input/b.csv(スタックトレース)
  • ファイル処理開始: data/input/c.csv
  • ファイル処理完了: data/input/c.csv

「どのファイルで落ちたか」が一目で分かります。

これは自動化バッチで非常に効いてくるパターンです。


実務で本当に効く「例外ログの中身」の設計

エラー+対象+パラメータを一緒に残す

例外ログに何を書くかで、後からの調査難易度が大きく変わります。

最低限押さえておくと助かる情報は、

  • エラーが起きた対象(ファイル名、URL、ユーザーIDなど)
  • 関連するパラメータ(開始日・終了日・件数など)
  • どの処理ステップか(例:前処理中・DB書き込み中・API呼び出し中)

です。

例えば Web API 呼び出しなら、

def fetch_user(user_id: int):
    url = f"https://api.example.com/users/{user_id}"
    try:
        resp = requests.get(url, timeout=5)
        resp.raise_for_status()
        return resp.json()
    except Exception:
        logger.exception(f"ユーザー情報取得エラー: user_id={user_id}, url={url}")
        raise
Python

こうしておけば、

  • user_id
  • url
  • スタックトレース

が一緒にログに残ります。

後からログだけを見て、「どのユーザーの、どのAPIで、どんなエラーになったか」が分かる。
ここまで来れば、再現したり、データを補正したりという対処がしやすくなります。

例外ログを「握りつぶさない」勇気

初心者がやりがちなのは、

try:
    ...
except Exception as e:
    logger.error(f"エラー: {e}")
    # 何もせず return
Python

のように「エラーを書いて終わり」にしてしまうことです。

これをやると、

  • プログラム全体としては“成功した体”になってしまう
  • 上位の処理は「全部終わった」と勘違いする
  • 実は一部or全部失敗しているのに気づきにくくなる

という怖い状態になります。

原則として、

  • 「この場で復旧できない例外」は、ログを書いたうえで raise して上に投げる
  • 「この場で安全にスキップできる例外」だけを握りつぶして続行する

という線引きを意識してください。

例えば、

1ファイルの処理に失敗しても、他のファイル処理は続けてよい
→ ファイルごとの try/except でログだけ残して次へ

全体の前提が崩れるような致命的エラー
→ ログを出してから raise してバッチ全体を止める

という感じです。


例外ログとリトライ・タイムアウトとの組み合わせ

「何回リトライして、どこで諦めたか」を残す

リトライ処理を入れたとき、例外ログには

  • 何回目のリトライで失敗したか
  • どの例外が繰り返し出ているか
  • 最終的に諦めたか、成功したか

を残しておくと、運用・改善に役立ちます。

例えばこんな感じです。

def fetch_with_retry(url, retries=3, timeout_sec=5.0):
    for attempt in range(1, retries + 1):
        try:
            resp = requests.get(url, timeout=timeout_sec)
            resp.raise_for_status()
            return resp
        except (requests.Timeout, requests.ConnectionError) as e:
            logger.warning(f"[{attempt}/{retries}] 一時的エラー: {e}, url={url}")
            if attempt == retries:
                logger.error(f"リトライ上限に達したため諦めます: url={url}")
                raise
Python

こうしておけば、

  • たまに1回目だけ失敗して2回目で成功している
  • 最近は3回目まで行くことが増えている
  • 特定の URL だけ毎回上限までリトライしている

といった状況がログから読み取れるようになります。

タイムアウトと例外ログ

タイムアウト処理を入れたときも、
「どの操作が、何秒のタイムアウトで落ちたか」を記録しておくことが大事です。

try:
    resp = requests.get(url, timeout=timeout_sec)
    resp.raise_for_status()
except requests.Timeout:
    logger.error(f"タイムアウト: url={url}, timeout={timeout_sec}")
    raise
Python

これがあると、後から

  • timeout_sec の値が現実に合っているか
  • そもそも API 側のレスポンス遅延が増えていないか

を検証できます。


まとめ(例外ログは「壊れ方を可視化する」のが仕事)

自動化における例外ログを、改めて整理します。

print ではなく logging を使い、ファイルに時刻付きで例外やメッセージを残す。
トップレベル(if __name__ == "__main__": の周辺)で logging.exception を使い、「最後の砦」として全ての予期せぬ例外を記録する。
処理単位(ファイル1つ、ユーザー1人分、API1回分)で try/except+logger.exception を使い、「どの対象で何が起きたか」をログに含める。
例外をログに書いたからといって握りつぶさず、「復旧できないものは再 raise」する線引きを徹底する。
リトライやタイムアウトと組み合わせて、「何回試してダメだったのか」「どの操作がどれくらい待って落ちたか」を残し、後から改善・チューニングに活かす。

うまく設計された例外ログは、
「完璧に動くプログラム」を作るためではなく、
「いつか壊れたときにも、どう壊れたかが分かるプログラム」を作るためのものです。

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