概要(例外ログは「エラーの証拠を残すブラックボックスレコーダー」)
例外ログは、
「エラーが起きた“瞬間の情報”を、その場で消えずに後から見返せるように残しておく仕組み」
です。
プログラムは、一瞬で例外を投げて落ちます。
でも、人間がそれを目で見て気づけるとは限らないし、本番環境ならなおさら見えません。
だから、自動化では
- いつ
- どの処理で
- どんな例外が
- どんな値を持ったまま
発生したのかを、ログとして残しておくことがとても大事です。
ここでは、
printではダメな理由loggingモジュールで例外ログを出す基本パターンlogging.exceptionやlogger.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」する線引きを徹底する。
リトライやタイムアウトと組み合わせて、「何回試してダメだったのか」「どの操作がどれくらい待って落ちたか」を残し、後から改善・チューニングに活かす。
うまく設計された例外ログは、
「完璧に動くプログラム」を作るためではなく、
「いつか壊れたときにも、どう壊れたかが分かるプログラム」を作るためのものです。
