コンテンツへスキップ

ログレベル設計とローテーション — なぜ DEBUG/INFO/WARNING/ERROR を使い分けるのか

アプリのログを見ていると、同じ「記録」なのに DEBUG INFO WARNING ERROR という別々のラベルが付いていることに気づく。なぜ単純に「起きたことを全部書く」のではなく、わざわざ段階を分けているのか。今回はこのログレベルという仕組みと、レベルを分けたログをどう扱えば運用が破綻しないか(ローテーション)を整理する。

ログレベルは「フィルタの閾値」である

補足: ロギング(logging)とは、プログラムの実行中に起きた出来事を後から確認できるよう、ファイルや画面に記録として残す仕組みのこと。

Python の標準 logging モジュールには 5 段階のレベルが定義されている。

レベル 数値 意味
DEBUG 10 開発者が動作を細かく追跡するための詳細情報
INFO 20 正常な処理の進行記録
WARNING 30 想定外だが処理は継続できる状態
ERROR 40 処理が失敗した
CRITICAL 50 アプリ全体が継続不能なレベルの致命的事態

重要なのは、この数値が単なる分類ラベルではなく「フィルタの閾値」として機能する点だ。logger.setLevel(logging.INFO) と設定すると、数値が 20 以上のログ(INFO・WARNING・ERROR・CRITICAL)だけが出力され、10 の DEBUG は黙って捨てられる。つまりレベル設計とは「何が起きたら記録するか」だけでなく「今この瞬間、どこまで詳しく見たいか」を後から自由に絞り込める仕組みを作ることに等しい。本番運用中は INFO 以上だけを見ておき、不具合調査のときだけ一時的に DEBUG まで下げて詳細を見る、という使い分けができる。

本アプリでの実際の設定

maintenance_agent.py の冒頭では、ログ出力先を 2 つのハンドラーに分けて登録している。

_rotating_handler = logging.handlers.RotatingFileHandler(
    "maintenance.log",
    maxBytes=10 * 1024 * 1024,  # 10 MB
    backupCount=5,
    encoding='utf-8'
)
_stream_handler = logging.StreamHandler()

logging.basicConfig(level=logging.INFO, handlers=[_rotating_handler, _stream_handler])

level=logging.INFO としているため、DEBUG レベルのログは通常の運用では一切出力されない。コードベース全体を数えると、logger.debug() の呼び出しは 19 箇所ある。これらは「普段は静かにしておき、原因調査で明示的にレベルを下げたときだけ意味を持つ」記録として書かれている。閾値を変えずに DEBUG のログを埋め込んでおけるのが、レベル分けの実務上の利点でもある。将来調査したくなったときにコードを書き足す必要がなく、閾値を下げるだけで既存の記録が見えるようになる。

実際のログレベルの分布から見える設計思想

同じコードベースで logger.info / logger.warning / logger.error の呼び出し回数を数えると、次のような分布になっている。

  • INFO: 135 箇所
  • WARNING: 114 箇所
  • ERROR: 28 箇所
  • CRITICAL: 0 箇所

INFO が最多なのは、複数サイトを順番に処理するメンテナンス作業の性質上、「今どのサイトの何を処理しているか」という進行記録が大量に必要になるためだ。次に多い WARNING は、たとえば core/alert_utils.pysend_alert_email() がメール設定不足を検知したときのように、「処理は止めないが、後で振り返れるようにしておきたい異常」を表す。

if not all([settings.get('smtp_host'), settings.get('smtp_user'), settings.get('to_email')]):
    logger.warning(t(
        "メール送信設定が不足しているためスキップします。",
        "Email settings incomplete. Skipping notification."
    ))
    return False

ここでは logger.error ではなく logger.warning を選んでいる。メール送信ができなくても、他のメンテナンス処理(バックアップ・更新・ロールバック判定)自体は継続可能であり、「アプリが失敗した」わけではないからだ。ERROR は実際に処理が失敗した箇所(更新コマンドの実行失敗・メール送信の例外発生など)にだけ絞って使われており、28 箇所と最も少ない。

そして CRITICAL は 1 箇所も使われていない。これは書き忘れではなく、設計上の帰結と言える。このアプリでは「サイト単位の処理が失敗したらそのサイトだけロールバックして次のサイトへ進む」という設計になっており、アプリ全体を継続不能にするような事態がそもそも起きにくい。さらに、ユーザーへの緊急通知が必要な事態は、ログレベルではなく send_alert_email() という別のチャネル(メール通知)で扱われている。「ログに何と書くか」と「ユーザーに何を知らせるか」は別の判断軸であり、後者を担うのはログレベルではなくアプリのビジネスロジック側という役割分担になっている。

ローテーション — 「記録し続ける」を安全にする仕組み

ログは記録するほど有用になる一方で、そのまま無制限に書き続けるとディスクを圧迫する。この問題に対処するのが RotatingFileHandler のローテーション機能だ。

_rotating_handler = logging.handlers.RotatingFileHandler(
    "maintenance.log",
    maxBytes=10 * 1024 * 1024,  # 10 MB
    backupCount=5,
    encoding='utf-8'
)

maxBytes=10 * 1024 * 1024 は「ファイルサイズが 10MB に達したらローテーションする」という指定で、backupCount=5 は「切り替え後の世代を最大 5 つまで保持する」という指定になる。動作としては、maintenance.log が 10MB に達すると、まず既存の maintenance.logmaintenance.log.1 にリネームされ、新しい空の maintenance.log に書き込みが続く。次に上限に達すると maintenance.log.1maintenance.log.2 に、新しいログは再び maintenance.log.1 になる。これを繰り返し、backupCount を超えた最も古い世代(この場合は maintenance.log.5 を超える分)は削除される。

つまりディスク使用量の上限は「10MB × (1 + backupCount)」、このアプリの場合は最大でも約 60MB に固定される。長期間動かし続けるデスクトップアプリにとって、ログが際限なく肥大化してディスクを圧迫する事故を避けつつ、直近の履歴は一定量残しておいて後から調査できる、という 2 つの要求をこの 1 つの仕組みで両立させている。

「永続ログ」と「その場限りのログ」を分けるもう一つのハンドラー

本アプリにはもう 1 つ、_SiteLogCapture という独自のログハンドラーがある。

class _SiteLogCapture(logging.Handler):
    """サイト単位の実行ログを一時的にキャプチャするハンドラー"""
    def emit(self, record):
        self.lines.append(self.format(record))

これは RotatingFileHandler とは全く別の目的で使われる。1 サイトのメンテナンス処理が始まるたびに一時的にロガーへ追加され、その処理中に出力されたログ行だけをメモリ上のリストに溜め込む。処理が終わったら、この溜め込んだ内容がホワイトラベルレポートやメール本文の「実行ログ」セクションとして使われ、役目を終えたハンドラーは取り外される。

同じ logger オブジェクトから出るログでも、ハンドラーが違えば「そのログをどう扱うか」を完全に別々に設計できる。RotatingFileHandler は「アプリ全体の履歴を、容量を制限しながら長期保存する」ためのもの、_SiteLogCapture は「1 回の処理単位のログだけを、使い捨てで一時的に集める」ためのもの、という役割の違いになっている。ログレベルが「縦方向のフィルタ(どこまで詳しく残すか)」だとすれば、ハンドラーの使い分けは「横方向のフィルタ(どの記録を誰向けにどう使うか)」を実現する仕組みと言える。

まとめ

ログレベルは単なる分類ラベルではなく、後から自由に絞り込める閾値として機能する。DEBUG は普段は隠しておく詳細情報、INFO は正常な進行記録、WARNING は継続可能な異常、ERROR は実際の失敗、CRITICAL はアプリ全体が継続不能な事態、という役割の違いを踏まえてレベルを選ぶと、ログの分布そのものがアプリの設計思想を映す指標になる。そしてローテーションは、記録し続けることの価値とディスク容量の制約を両立させるための実務的な仕組みであり、サイズと世代数の上限を決めておくだけで、運用中に手動でログを消して回る必要がなくなる。