Pythonの自動化スクリプトにログを残す|loggingで実行時刻・処理件数・エラーを記録する

小さなPythonスクリプトは動く。でも、それを自動で走らせたあと、「今朝は動いたのか」「何件処理したのか」「失敗したなら、なぜか」が残っていない——そんなところで困っていませんか。

print で画面に出した内容は、自分がその場にいるときは役立ちます。一方で、夜のうちに自動実行されたスクリプトの結果を、朝になってからたどりたいときは、日時と重要度が付いた記録がファイルに残っていると安心です。

この記事では、Python標準ライブラリの logging(プログラムの動きを記録するしくみ)を使って、実行時刻・処理件数・エラーの詳細を app.log に残す基本を、順番に見ていきます。私がお伝えしたい到達点はひとつです。自分の自動化スクリプトに数行足して、あとから app.log を開けば「いつ・何が・なぜ失敗したか」を確認できるようになること。

使うのは標準ライブラリだけなので、追加のパッケージインストールは不要です。この記事の実行例は、Debian GNU/Linux 13 のシステム付属 Python 3.13.5 で確認しています。Windows や macOS での実機確認は、この記事では行っていません。

💡 この記事でわかること

  • logging.basicConfig で、日時とレベル(重要度)つきの記録をファイルへ残す最小の書き方
  • print と logging の使い分け(print でもリダイレクトすればファイル保存はできる/logging も設定しないと日時やファイル保存は付かない)
  • DEBUG / INFO / WARNING / ERROR の出し分け
  • 例外(エラーで処理が中断する出来事)の詳細(トレースバック)の残し方
  • ファイルは動かさず、CSVの件数だけを記録する練習用スクリプト
  • あとから tail や grep で確認する見方と、自動実行向けの注意

🔰 この記事の前提

まずは結論:数行で app.log に残す

先に完成形の最小セットを見ましょう。まず、練習用のフォルダを作って移動します。

  • 1
  • 2
mkdir -p ~/logdemo && cd ~/logdemo
ls -A

ls -A(. で始まる隠しファイルも表示します)で何か表示されたら、そのフォルダはすでに使われています。中身を上書きしないように、~/logdemo2 など別の名前にして読み替えてください。この記事のファイルは、すべてこのフォルダに保存して、ここで実行します。

次の内容を step2.py として保存します。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
# ファイル出力。basicConfig に filename を渡すとファイルだけに出る(画面には出ない)
import logging
from pathlib import Path

LOG_FILE = Path(__file__).resolve().parent / "app.log"   # スクリプト基準の絶対パス
logging.basicConfig(
    filename=LOG_FILE,
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    encoding="utf-8",
)
logging.info("開始します")
logging.warning("日本語の警告メッセージ")
logging.info("終了します")

basicConfig は、ログの基本設定をまとめて行う関数です。ここで大事な指定は次の5つです。

  • filename:書き先のファイル。上ではスクリプトと同じフォルダの app.log を、絶対パス(いちばん上から書いた場所)で指定しています
  • level:この重要度以上を残す。INFO にすると、日常の経過も残ります
  • format:1行の形。asctime が日時、levelname が重要度、message が本文です
  • datefmt:日時の見た目。上の指定だと 2026-11-08 19:04:46 のようになります
  • encoding="utf-8":日本語が文字化けしないようにする指定です

同じフォルダで python3 step2.py を2回実行し、cat app.log で中身を見ると、次のようになりました。画面には何も出ず、終了コードは 0 です(終了コードは、直前のコマンドが成功したら 0、失敗したら 0 以外になる数字です。実行した直後に echo $? と打つと確認できます)。2回分が追記されています(filename を指定したときの既定は追記モードです)。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
2026-11-08 19:04:46 INFO 開始します
2026-11-08 19:04:46 WARNING 日本語の警告メッセージ
2026-11-08 19:04:46 INFO 終了します
2026-11-08 19:04:46 INFO 開始します
2026-11-08 19:04:46 WARNING 日本語の警告メッセージ
2026-11-08 19:04:46 INFO 終了します
実際のターミナルで step2.py を実行し、app.log を表示した画面。撮影ではダミーのログを空にしてから個別に実行しています(通常は追記されます)

「画面に何も出ないのに成功している」のが、最初につまずきやすいところです。filename を指定しただけだと、ログはファイルへ行き、画面には出ません。中身の意味は、このあと順番に見ていきましょう。

print と logging、どう使い分ける?

「記録を残したいなら、全部 logging に置き換えなければ」と思う必要はありません。公式の Logging HOWTO でも、コマンドラインスクリプトのふつうの画面表示には print() が向いていて、運用中の出来事(あとから状態や障害を追う記録)にはロガーの info() などが向く、と整理されています。

確認してみましょう。

  • 1
python3 -c 'print("start")'

出力は次のとおりです。日時もレベルも付きません。

  • 1
start

ただし、「ファイルに残せるかどうか」で切り分けると、話がずれます。print の結果も、シェルのリダイレクト(画面に出た内容をファイルへ流すこと)を使えばファイルに保存できます。日時やレベルが自動で付くわけではない、という点がポイントです。自動実行のときに画面出力を丸ごと残す書き方は、毎朝の作業をPCを触らず動かしたい|cron入門(Pythonを定時実行) で扱っています。

いっぽう logging も、設定を書かないと日時は付かず、ファイルにも残りません。次の step1_noconfig.py は basicConfig を書いていません。

  • 1
  • 2
  • 3
  • 4
  • 5
# basicConfig を書かない場合:既定は WARNING 以上だけが標準エラーに出る
import logging
logging.debug("d")
logging.info("i")
logging.warning("w")

実行すると、次の1行だけでした。

  • 1
WARNING:root:w

既定の重要度は WARNING 以上なので、DEBUG と INFO は出ません。出た行にはレベル(WARNING)とロガー名(何も指定しないときの root)は付きますが、日時はなく、ファイルにも残っていません。日時やファイル保存が欲しいときは、最初に見た basicConfig の指定が必要です。

私は次のように使い分けています。

  • 今この場で目で見たい、短い確認:print
  • 自動実行のあとから読みたい記録(開始、件数、警告、失敗理由):logging
printもloggingも、画面にもファイルにも出せる。printの日時は自分で付け、loggingは日時・重要度・出力先を設定で決める

レベル(重要度)を使い分ける

logging では、出来事の重大さに名前が付いています。公式ドキュメントでは、低い方から次の順です。

レベル いつ使うか(公式ドキュメントの趣旨)
DEBUG 問題を調べるとき向けの、詳しい情報
INFO 想定どおり進んだことの確認(開始、処理件数、終了など)
WARNING 想定外だが、処理自体は続けられるできごと(空ファイルをスキップするなど)
ERROR ある処理が実行できない、より重い問題
CRITICAL プログラムの継続が難しい重大なエラー

basicConfig の level に指定した値以上だけが残ります。次の step1.py では、しきい値を INFO にしています。

  • 1
  • 2
  • 3
  • 4
  • 5
import logging
logging.basicConfig(level=logging.INFO)
logging.debug("d")
logging.info("i")
logging.warning("w")

実行結果は次のとおりです。DEBUG は出ず、INFO と WARNING が出ました。

  • 1
  • 2
INFO:root:i
WARNING:root:w

書式を指定していないので、日時は付かず、既定の「レベル:ロガー名:メッセージ」になっています。

この2行は標準エラー(エラーメッセージ専用の出口。ふだんは画面に出ます)へ出ていました。同じスクリプトを python3 step1.py 2>/dev/null として標準エラーを捨てると、画面には何も出ず、終了コードは 0 でした。あとからファイルへ残す設定と混同しないよう、「今は画面用の確認をしている」と分けて考えると分かりやすいです。

ファイルに日時つきで残す

自動実行向けには、画面だけでなくファイルへ残します。再び step2.py です。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
# ファイル出力。basicConfig に filename を渡すとファイルだけに出る(画面には出ない)
import logging
from pathlib import Path

LOG_FILE = Path(__file__).resolve().parent / "app.log"   # スクリプト基準の絶対パス
logging.basicConfig(
    filename=LOG_FILE,
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    encoding="utf-8",
)
logging.info("開始します")
logging.warning("日本語の警告メッセージ")
logging.info("終了します")

確認できたことは、次の4点です。

  1. filename を指定しただけだと、画面(標準出力にも標準エラーにも)には何も出ない
  2. app.log には、日時・レベル・メッセージが1行ずつ残る
  3. 2回実行すると追記される(既定のモードは "a")
  4. encoding="utf-8" を付けたこの例では、日本語は文字化けしなかった

Path(__file__).resolve().parent / "app.log" は、「このスクリプトがあるフォルダの app.log」という意味です。あとで見る相対パス(今いる場所からの近道の書き方)と違い、どこから実行しても書き先がずれにくい書き方です。

画面とファイルの両方へ同時に出す方法もありますが、この記事では確認していないので扱いません。まずはファイルへ残すことを確実にしましょう。

💬 うまく動かないときの相談例(秘密情報は貼らない)

画面に何も出なくて困ったときは、次のようにAIへ聞いてみてください。パスワード、APIキー、トークン、メールアドレス、ログの全文、ホームディレクトリのフルパス(ユーザー名を含むことがあります)は貼らないでください。

  • 1
  • 2
  • 3
  • 4
Pythonのloggingで、basicConfigにfilenameを指定してapp.logへ書き出しています。
実行すると終了コードは0ですが、画面に何も出ません。
知りたいこと: filenameを指定したとき、ログは画面とファイルのどちらに出ますか。
設定の要点(秘密情報は含めていません): level=INFO、formatにasctimeとlevelnameとmessageを指定。

例外の詳細(トレースバック)を残す

自動実行でいちばん知りたいのは、失敗した理由です。例外が起きたとき、logger.exception を使うと、ERROR レベルのメッセージに加えてトレースバック(例外が起きるまでの呼び出しの道筋)も残ります。

公式ドキュメントでも、Logger.exception() は例外ハンドラ(except 節)の中からのみ呼ぶべき、と書かれています。except の外で呼ぶ書き方は、この記事では使いません。

次の step3_exception.py は、わざと int("abc") を失敗させ、同じ失敗を logger.exception と logger.error で記録しています。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
# logger.exception は except ブロックの中でだけ使う(ERROR レベル+トレースバック)
import logging
from pathlib import Path

logging.basicConfig(
    filename=Path(__file__).resolve().parent / "app.log",
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    encoding="utf-8",
)
logger = logging.getLogger(__name__)

try:
    int("abc")
except ValueError:
    logger.exception("変換に失敗")          # トレースバックも残る

try:
    int("abc")
except ValueError:
    logger.error("変換に失敗(errorのみ)")   # 比較:トレースバックは残らない
# ここでは例外を握りつぶして続行している(例示のため)。
# 本番で処理を止めたいなら except 内で記録したあと raise する。

logging.getLogger(__name__) は、今のモジュール名つきのロガー(ログを書き出す窓口)を取る定番の書き方です。このファイルを直接実行すると、ロガー名は __main__ になります。

app.log には、先ほどの step2.py の追記に続いて、次の行が残りました。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
2026-11-08 19:04:46 ERROR __main__ 変換に失敗
Traceback (most recent call last):
  File "/tmp/lvlog-pl05asdv/home/logdemo/step3_exception.py", line 15, in <module>
    int("abc")
    ~~~^^^^^^^
ValueError: invalid literal for int() with base 10: 'abc'
2026-11-08 19:04:46 ERROR __main__ 変換に失敗(errorのみ)

logger.exception のあとは、トレースバックと ValueError: invalid literal for int() with base 10: 'abc' が残っています。logger.error だけだと、メッセージの1行だけで、トレースバックはありません。

実際のターミナル:logger.exception はトレースバックつき、logger.error は1行だけ残る。比較のために例外を記録したあとも続ける例で、失敗を隠す本番向けの書き方ではありません。撮影ではダミーのログを空にして個別に実行しています

パスは環境ごとに違います。上のフルパスは検証環境のものです。自分の環境では、自分のスクリプトの場所がそのまま出ます。ユーザー名を含むパスになることもあるので、ログを人に見せる前には中身を確かめましょう。

もうひとつ、本番向けの注意があります。この例は比較のために、例外を記録したあと握りつぶして続行しています。失敗を隠したまま「成功したように」進むと、自動実行の点検が難しくなります。記録したあと処理を止めたいときは、except の中でログを残してから raise して、例外を再び伝える方法があります。次の実践例では、想定外の例外を記録したあと、終了コード 1 で終了します。

実践:ファイルは動かさず、CSVを数えて記録する

ここまでの部品を、1本の練習用スクリプトにまとめます。ファイル名は organize.py ですが、ダウンロードフォルダを整理する記事のような「移動」はしません。inbox にある *.csv を数え、0バイトのものは警告してスキップし、件数を記録するだけの検査スクリプトです。削除も書き換えもしません。

まず、練習用のダミーファイルを用意します。実在の書類や個人情報は使わないでください。

  • 1
  • 2
  • 3
  • 4
  • 5
cd ~/logdemo && mkdir -p inbox
printf 'a\n' > inbox/report_2026-11-01.csv
printf 'b\n' > inbox/report_2026-11-02.csv
printf 'x\n' > inbox/memo.txt
: > inbox/empty.csv

最後の : > inbox/empty.csv は、何もしないコマンド : とリダイレクトを組み合わせて、0バイトのファイルを作っています。ls inbox で確かめると、中身は次のとおりでした。

  • 1
  • 2
  • 3
  • 4
empty.csv
memo.txt
report_2026-11-01.csv
report_2026-11-02.csv

empty.csv は 0 バイト、2つの report_*.csv は中身があり、memo.txt は CSV ではないので数えません。

organize.py は次のとおりです。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
"""organize.py - 安全なダミー例。

※ファイルの移動・削除・書き換えは一切しない。
  inbox 内の *.csv を数え、0 バイトのものを WARNING でスキップし、
  件数を INFO で記録するだけの「検査」スクリプト。
使い方: python3 organize.py [対象フォルダ]   (省略時はスクリプトと同じ場所の inbox)
"""
import logging
import sys
from pathlib import Path

BASE = Path(__file__).resolve().parent
logging.basicConfig(
    filename=BASE / "app.log",          # 絶対パスなので cron 等でも場所が変わらない
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    encoding="utf-8",
)
logger = logging.getLogger(__name__)


def main(target: Path) -> int:
    logger.info("開始 target=%s", target.name)
    # glob は存在しないフォルダでも例外を出さず空を返すので、明示的に確認する
    if not target.is_dir():
        logger.error("フォルダがありません: %s", target.name)
        return 1
    count = 0
    for csv in sorted(target.glob("*.csv")):
        if csv.stat().st_size == 0:
            logger.warning("skip %s (0バイト)", csv.name)
            continue
        count += 1                        # 移動はしない。数えるだけ
    logger.info("処理 %d件", count)
    logger.info("終了")
    return 0


if __name__ == "__main__":
    target = Path(sys.argv[1]) if len(sys.argv) > 1 else BASE / "inbox"
    try:
        code = main(target)
    except Exception:
        # 想定外の例外は except 内で logger.exception(トレースバック付き)を残して終了コード 1
        logger.exception("想定外のエラー")
        code = 1
    sys.exit(code)   # SystemExit は Exception を継承しないので上の except には捕まらない

ログに書く名前は target.name や csv.name のようにファイル名だけにしています。フルパスを書くと、ユーザー名を含む場所が出ることがあるためです。変数のデータは、メッセージに直接埋め込むより、公式ドキュメントのとおり %s や %d の引数として渡します。

~/logdemo で python3 organize.py を実行すると、終了コードは 0 で、app.log は次のようになりました。

  • 1
  • 2
  • 3
  • 4
2026-11-08 19:04:46 INFO 開始 target=inbox
2026-11-08 19:04:46 WARNING skip empty.csv (0バイト)
2026-11-08 19:04:46 INFO 処理 2件
2026-11-08 19:04:46 INFO 終了

存在しないフォルダを渡すと、終了コードは 1 になります。

  • 1
python3 organize.py ./no_such_dir
  • 1
  • 2
2026-11-08 19:04:46 INFO 開始 target=no_such_dir
2026-11-08 19:04:46 ERROR フォルダがありません: no_such_dir

ここは logger.exception ではなく logger.error を使っています。Path.is_dir() で先に確認しているので、例外は起きません。例外が起きていない場所で logger.exception を呼ぶ必要はありません。なお Path.glob は、存在しないフォルダを対象にしても例外を出さず空を返すことがあるため、件数 0 件で「成功」と誤認しないよう、is_dir() の確認を先に置いています。

実行後も、inbox のファイルは動いていませんでした。

  • 1
ls inbox
  • 1
  • 2
  • 3
  • 4
empty.csv
memo.txt
report_2026-11-01.csv
report_2026-11-02.csv
実際のターミナル:CSV 2件を処理して終了コード 0、存在しないフォルダでは ERROR と終了コード 1、inbox のファイルは動いていない。撮影ではダミーのログを空にして個別に実行しています(通常は追記されます)

あとからログを確認する

ファイルに残したら、朝いちばんに末尾を見る、エラーだけを探す、といった見方ができます。その前に、自動実行に近い条件でも同じ app.log に書かれるかを確かめておきます。

自動実行そのもの(cron)は、この記事では実行していません。代わりに、カレントディレクトリを / に移し、env -i で環境変数を空にした状態でスクリプトを動かして、ログの場所がずれないことを確認しました。

  • 1
cd / && env -i /usr/bin/python3 ~/logdemo/organize.py

このときも終了コードは 0 で、記録先はスクリプトと同じフォルダの app.log のままでした。存在しないパスを渡す次の実行では、終了コード 1 になりました。

  • 1
env -i /usr/bin/python3 ~/logdemo/organize.py /nonexistent

書き先をスクリプト基準の絶対パスにしているので、実行した場所や環境変数が変わっても、朝見るファイルの場所が迷いやすくなりません。

ここまで順番に試したら、app.log を見てみましょう。

  • 1
  • 2
tail -n 5 app.log
grep ERROR app.log

tail -n 5 app.log では、最後の5行が次のように出ました(検証で実行したときの例です)。

  • 1
  • 2
  • 3
  • 4
  • 5
2026-11-08 19:04:46 WARNING skip empty.csv (0バイト)
2026-11-08 19:04:46 INFO 処理 2件
2026-11-08 19:04:46 INFO 終了
2026-11-08 19:04:46 INFO 開始 target=nonexistent
2026-11-08 19:04:46 ERROR フォルダがありません: nonexistent

grep ERROR app.log では、ERROR を含む行だけが出ます。この記事の順番どおりに試した場合は、例外の練習(step3_exception.py)の2行も含めて、次の4行が並びます。下は、上の tail とは別の実行で、最初から順番どおりに試したときの例です。そのため、時刻が上の例と違います(自分で試すと、実行した時刻になります)。

  • 1
  • 2
  • 3
  • 4
2026-11-08 19:15:59 ERROR __main__ 変換に失敗
2026-11-08 19:15:59 ERROR __main__ 変換に失敗(errorのみ)
2026-11-08 19:15:59 ERROR フォルダがありません: no_such_dir
2026-11-08 19:15:59 ERROR フォルダがありません: nonexistent

cat や less、head、tail の使い分けは Linuxでファイルの中身を見る|cat・less・head・tailの使い分け【初心者向け】 で、grep での検索は Linuxのgrep入門|ログや設定ファイルから1行を探し出す【初心者向け】 で整理しています。

スクリプトを実行し、app.logに記録し(ファイル出力の設定が必要)、あとからログを読んで原因を調べる流れ。パスワードやAPIキーはログに書かない

ここで、役割の違いだけ押さえておきましょう。

  • Python の logging:日時・レベル・トレースバックを、意図した形で app.log に残す
  • シェルのリダイレクト(>> ログファイル 2>&1):画面に出たものを丸ごと残す

filename だけを指定したこの記事の設定では、ログは画面に出ません。したがって、画面出力をファイルへ流すリダイレクトだけでは、logging が書いた行は拾えません。あとから見たい内容は、Python 側でファイルへ書くのが確実です。

毎朝決まった時刻に動かす手順自体は、毎朝の作業をPCを触らず動かしたい|cron入門(Pythonを定時実行) が次の一歩になります。パスを絶対パスで書くことと、画面出力の残し方が整理されています。この記事の app.log と組み合わせると、「いつ・何件・なぜ失敗したか」を追いやすくなります。

よくあるつまずきと、ログが大きくなったとき

2回目の basicConfig が効かない

basicConfig は、すでにルートロガーへ出力先(ハンドラ)が付いていると、何もしません。次のような確認では、2回目の呼び出し(level=INFO と別の書式)は無視され、最初の設定(WARNING 以上、1st ... の書式)のままです。

  • 1
  • 2
  • 3
1st WARNING warning after 2nd call (no force)
3rd(force) INFO info after force=True
handlers: 1

force=True を付けると設定は入れ替わり、INFO も出るようになりました。ただし公式ドキュメントのとおり、force=True はルートロガーに付いている既存のハンドラを外して閉じます。ライブラリや他のモジュールが付けた出力先も消えることがあるため、単体スクリプトの入口で1回だけ使う、と考えてください。普段は、スクリプトの先頭で basicConfig を1回書く形で十分です。

相対パスだと、実行した場所にファイルができる

filename="app.log" のように相対パスで書くと、基準は「実行したときのカレントディレクトリ」です。確認では、/tmp に移ってから相対パス版のスクリプトを実行したところ、/tmp/app.log ができました。

  • 1
2026-11-08 19:04:46 INFO relative path version

同じ場所から、絶対パス版の step2.py を実行しても、/tmp/app.log は作られず、スクリプトと同じフォルダの app.log に書き込まれました。自動実行では、作業ディレクトリが自分の想定と違うことがよくあるので、書き先はスクリプト基準の絶対パスにしておくと安心です。

実際のターミナル:/tmp から絶対パス版の step2.py を実行しても /tmp/app.log は作られず、スクリプトと同じフォルダの app.log に書き込まれた。本文のログとは別の撮影で、時刻とサイズはそれまでの追記を含んだ結果です

💬 場所が分からないときの相談例(フルパスは伏字に)

  • 1
  • 2
  • 3
  • 4
logging.basicConfigのfilenameに相対パス "app.log" を指定しています。
別のディレクトリからスクリプトを実行したら、想定と違う場所にファイルができました。
知りたいこと: 実行したカレントディレクトリと、スクリプトファイルの場所、どちらを基準に書けば場所がずれませんか。
パスワードやAPIキー、ホームディレクトリのフルパスは書いていません。
日本語が化けるときは encoding を指定する

encoding="utf-8" は、ファイルを開くときの文字の扱いです。この記事の確認(Python 3.13.5)では、日本語の警告メッセージもそのまま読めました。公式ドキュメントでは、Python 3.9 で basicConfig に encoding 引数が追加されています。

ログが大きくなってきたら回転(ローテーション)

毎回追記していると、app.log は少しずつ大きくなります。標準ライブラリの RotatingFileHandler を使うと、サイズの上限を超える前にファイルを切り替え、古いファイルを番号付きで残すことができます。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
# 回転:RotatingFileHandler(maxBytes=1000, backupCount=2)
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path

d = Path(__file__).resolve().parent / "rotate"
d.mkdir(exist_ok=True)
h = RotatingFileHandler(d / "app.log", maxBytes=1000, backupCount=2, encoding="utf-8")
h.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logger = logging.getLogger("rotate")
logger.setLevel(logging.INFO)
logger.addHandler(h)
for i in range(200):
    logger.info("dummy line %03d", i)

maxBytes=1000、backupCount=2 で 200 行書いたところ、残ったファイルは次の3つだけでした。

  • 1
  • 2
  • 3
app.log      (88バイト)
app.log.1    (968バイト)
app.log.2    (968バイト)

書き込み先は常に app.log で、あふれた分が app.log.1、app.log.2 に移ります。backupCount=2 なので、それより古いファイルは残らない、という動きです。まずは通常の basicConfig で記録を始め、ファイルが大きくなってから検討すれば十分です。

古いバックアップにも、過去の記録が残ります。共有する前には、新しい app.log だけでなく、番号付きのファイルも中身を確認してください。

セキュリティの注意

ログはあとから何度も読まれます。人に見せたり、質問のために貼ったりしがちなので、次の点だけは押さえておきましょう。

パスワード、APIキー、トークン、メールアドレスなどの個人情報は、ログに書かないでください。コードへ直接書かない扱い方は、APIキーをコードに直書きしない|.envと環境変数で安全に管理する(Python・GAS) で整理しています。

例外の詳細(トレースバック)には、スクリプトのフルパスが出ます。検証でも、ファイルの場所がそのまま残っていました。パスにはユーザー名が含まれることがあります。また、例外の種類によっては、メッセージに処理していた値が含まれることがあります。上の例でも 'abc' が残っていました。CSVの中身やメールアドレスを変換していて失敗すると、その値がログに残ることがあります。ログを共有する前に、伏字にできるところは伏せましょう。AIへ相談するときも、秘密情報やフルパス、ログの全文は貼らないでください。

ログファイルの権限は環境によって違います。確認した環境では、できた app.log の権限は -rw-r--r-- で、同じマシンの他のユーザーからも読める設定でした。共有のパソコンでは、置き場所と権限に気をつけてください。

外から受け取った文字列(ファイル名など)に改行が入っていると、ログをあとから見たときに、別の行が紛れているように見えることがあります。確認では、%s で改行を含む名前を出したときに行が分かれ、%r で出すと 'dummy.csv\nERROR fake line' のように1行へ収まりました。外から来た値を記録するときは、%r の方が行の境目が分かりやすいです。

💬 エラー相談のときのお願い

トレースバックやログをAIへ見せるときは、フルパス、ユーザー名、メールアドレス、キーやトークンを伏字にしてください。「どこで何の例外が出たか」と、秘密情報を除いた設定の要点だけで十分伝わることが多いです。

まとめ

自動実行したスクリプトの結果をあとから追うなら、日時・レベル・メッセージがファイルに残っていると助かります。

  • print でもリダイレクトすればファイルへ残せますが、日時やレベルは付きません
  • logging も、設定しないと日時やファイル保存は付きません
  • basicConfig に filename・level・format・datefmt・encoding="utf-8" を渡すと、app.log に追記できます
  • filename を指定しただけだと、画面には出ません
  • logger.exception は except の中で使い、記録したあと失敗を隠さない
  • 練習用の organize.py はファイルを動かさず、件数とスキップと不足フォルダを記録する
  • 書き先はスクリプト基準の絶対パスにすると、実行した場所が変わってもずれにくい

まずは自分のスクリプトの先頭に basicConfig を足して、開始・件数・終了・失敗を app.log で見てみてください。毎朝自動で走らせたくなったら、次は cron の記事へ進むのがおすすめです。

関連記事

より詳しく知りたいときは、公式ドキュメントの Logging HOWTO、logging リファレンス、logging.handlers、ロギングクックブック も参考になります。