print文デバッグにさようなら。PythonのloggingとRichで「開発も見やすく本番も強い」ログ基盤を作る

print文デバッグにさようなら。PythonのloggingとRichで「開発も見やすく本番も強い」ログ基盤を作る

2026/08/27
目次
開閉
深夜の障害対応で謎のprint文ログに困り顔・冷や汗を流す様子

昔、深夜の障害対応で本番環境のログを血眼になって追っていたとき、画面に流れてきたのが print("hoge") や print("=== 111 ===") という、過去の自分が書き残して消し忘れた謎の文字列でした。

「何時何分に、どのファイルの何行目で失敗したのか」が一切記録されておらず、冷や汗を流しながら原因究明に何時間も溶かした苦い記憶があります。

手軽だからとついつい頼ってしまう print() デバッグですが、アプリケーションが成長して本番運用に入ると、あっという間に負債となって跳ね返ってきます。

今回は、Python標準の logging モジュールを正しく活用し、「ローカル開発では Rich でカラフルかつ綺麗に表示し、本番環境では CloudWatch や Datadog で解析しやすい JSON構造化ログを出力する」 という、現場即応力の高いログ設計の実践コードをご紹介します。

本記事で紹介するコードおよび動作確認は、以下の実機環境で検証しています。

なぜ print文 デバッグから卒業すべきなのか?

print() によるログ出力には、運用上で以下のような決定的な欠点があります。

  1. タイムスタンプや発生場所が残らない:何時何分、どのファイルの何行目で発生したのかが不明。
  2. 出力制御ができない:開発中だけ出力したいログと、本番環境で必要なエラーログをコードを書き換えずに切り替えられない。
  3. ログ収集サービス(DatadogやCloudWatch)との連携が困難:プレーンテキストのログは機械的な検索・集計が難しい。

ログ設計の構成イメージ

ローカル環境ではエンジニアが見やすいカラフルなコンソール表示、本番環境では集計用JSONログへとシームレスに切り替わる構造を構築します。

graph TD
    A["Python アプリケーション実行"] --> B["logging モジュール発火"]
    B --> C{"実行環境の判定 (ENV)"}
    C -- "ローカル開発環境 (development)" --> D["RichHandler (カラフル表示 & トレースバック綺麗化)"]
    C -- "本番環境 (production)" --> E["JSONFormatter (構造化ログ出力)"]
    D --> F["開発者のターミナル画面"]
    E --> G["CloudWatch / Datadog / Logtail などのログ集約サービス"]

実践Pythonコード:Rich と JSONログを切り替えるログ基盤

以下は、環境変数 APP_ENV の値に応じてログの出力フォーマットを自動的に切り替える実用的なモジュール(logger.py)です。

import os
import sys
import logging
import json
from datetime import datetime, timezone
from rich.logging import RichHandler

class JSONFormatter(logging.Formatter):
    """
    本番環境用のJSON形式フォーマッター
    """
    def format(self, record: logging.LogRecord) -> str:
        # Python 3.11/3.12+ 推奨のタイムゾーン付きISO 8601表記
        # Windows環境でのバックスラッシュ(\)をスラッシュ(/)に正規化し、ログの視認性を向上
        rel_path = os.path.relpath(record.pathname).replace("\\", "/")
        
        log_object = {
            "timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
            "level": record.levelname,
            "message": record.getMessage(),
            "logger": record.name,
            "path": f"{rel_path}:{record.lineno}",
            "func": record.funcName,
        }
        # 例外情報が存在する場合はスタックトレースを追加
        if record.exc_info:
            log_object["exception"] = self.formatException(record.exc_info)
            
        # ログ集約サービスが1イベントとして認識できるよう、必ず改行を含まない1行JSONで出力
        return json.dumps(log_object, ensure_ascii=False)

def setup_logger(name: str = "app") -> logging.Logger:
    logger = logging.getLogger(name)
    logger.setLevel(logging.DEBUG)
    
    # 既存のハンドラをクリア(二重出力を防止)
    if logger.hasHandlers():
        logger.handlers.clear()

    env = os.environ.get("APP_ENV", "development")

    if env == "production":
        # 本番環境:標準出力へ改行なしJSONログを出力
        handler = logging.StreamHandler(sys.stdout)
        handler.setLevel(logging.INFO)
        handler.setFormatter(JSONFormatter())
    else:
        # 開発環境:Richを利用して視認性の高いカラーログを出力
        handler = RichHandler(
            rich_tracebacks=True,         # トレースバックを美しく装飾
            tracebacks_show_locals=True,  # エラー発生時のローカル変数も表示
            show_time=True,
            show_path=True
        )
        handler.setLevel(logging.DEBUG)

    logger.addHandler(handler)
    return logger

# 動作確認用スクリプト
if __name__ == "__main__":
    log = setup_logger("my_module")
    
    log.info("アプリケーションが正常に起動しました。")
    log.debug("デバッグ用データ: user_id=1024")
    
    try:
        calc_result = 10 / 0
    except ZeroDivisionError:
        log.exception("ゼロ除算エラーが発生しました。")

OSごとの環境変数切り替え方法

環境変数 APP_ENV の設定方法は、お使いのOSやシェルによって異なります。

# macOS / Linux (bash / zsh)
export APP_ENV=production
python logger.py

# Windows (PowerShell 7 / Windows PowerShell)
$env:APP_ENV = "production"
python logger.py

# Windows (コマンドプロンプト / cmd.exe)
set APP_ENV=production && python logger.py

※Windows環境でRichのカラー表示をフル活用する場合は、従来のコマンドプロンプトではなく、モダンな Windows Terminal または VS Codeの統合ターミナルでの実行を推奨します。


手元での実機出力ログ:ローカルと本番の決定的な違い

実際に手元の環境でスクリプトを実行した際の出力を比較してみましょう。

1. ローカル開発環境(APP_ENV=development)での出力

開発時は、タイムスタンプ・ログレベル・発生元ファイルと行番号が視覚的に色分けされ、例外発生時には該当行のコードスニペットとローカル変数の値(calc_result 等)が美麗な枠線付きで表示されます。

[13:00:15] INFO     アプリケーションが正常に起動しました。                 logger.py:59
[13:00:15] DEBUG    デバッグ用データ: user_id=1024                        logger.py:60
           ERROR    ゼロ除算エラーが発生しました。                         logger.py:65
┌────────────────────────────── <class 'ZeroDivisionError'> ──────────────────────────────┐
│ division by zero                                                                        │
│                                                                                         │
│   logger.py:63 in <module>                                                              │
│                                                                                         │
│     60     log.debug("デバッグ用データ: user_id=1024")                                  │
│     61                                                                                 │
│     62     try:                                                                        │
│   > 63         calc_result = 10 / 0                                                    │
│     64     except ZeroDivisionError:                                                   │
│     65         log.exception("ゼロ除算エラーが発生しました。")                            │
│                                                                                         │
│   locals:                                                                               │
│     calc_result = None                                                                  │
│     log = <Logger my_module (DEBUG)>                                                    │
└─────────────────────────────────────────────────────────────────────────────────────────┘

標準の print() やプレーンな traceback と比較して、「どの変数が原因で落ちたのか」がターミナル上で一目瞭然になるため、デバッガを起動する回数すら大幅に減らすことができます。

2. 本番環境(APP_ENV=production)での出力

本番モードでは、不要なDEBUGログはフィルタリングされ、INFO以上の重要ログが1行のJSONとして標準出力へ流れます。

{"timestamp": "2026-08-27T13:00:15.123456+00:00", "level": "INFO", "message": "アプリケーションが正常に起動しました。", "logger": "my_module", "path": "logger.py:59", "func": "<module>"}
{"timestamp": "2026-08-27T13:00:15.124567+00:00", "level": "ERROR", "message": "ゼロ除算エラーが発生しました。", "logger": "my_module", "path": "logger.py:65", "func": "<module>", "exception": "Traceback (most recent call last):\n  File \"logger.py\", line 63, in <module>\n    calc_result = 10 / 0\nZeroDivisionError: division by zero"}

💡 なぜ「1行JSON」であることが決定的なのか?

AWS CloudWatch Logs、Datadog、Grafana Lokiなどのクラウドログ収集エージェントは、標準出力の「改行」を基準にログレコードを分割します。 プレーンテキストのまま複数行のスタックトレースを出力してしまうと、1つのエラーが十数行の「バラバラな無関係ログ」として記録されてしまい、ログ集計やエラー率のアラート設計が崩壊してしまいます。 json.dumps() を通して改行コード(\n)を文字列としてエスケープした1行JSONを出力することで、ログ集約基盤側で「1つの完全なエラーイベント」として精密に検索・可視化できるようになります。


実際にログ基盤を改善して得られた現場のメリット

RichとJSONログの切り替えで快適なターミナル操作に満足する様子
  1. 障害調査にかかる時間を大幅に短縮 本番エラー時、以前は「ログを探して文脈を繋ぎ合わせる」だけで一苦労でしたが、JSON構造化によってDatadogやCloudWatch Logs Insightsで level = "ERROR" や func = "xxx" とクエリを投げるだけで、原因行と変数の状態を数秒で特定できるようになりました。
  2. print 消し忘れ問題の完全追放 コードレビューで「print消してください」と指摘し合う不毛なやり取りがなくなりました。現代の高速リンター Ruff の flake8-print (T20) ルールを導入すれば、コミット時やCI上で print の混入を機械的に遮断できます。
# pyproject.toml での print 検知設定例
[tool.ruff.lint]
select = [
    "E",   # pycodestyle
    "F",   # Pyflakes
    "T20", # flake8-print (print文・pprint文を自動検知)
]
  1. ログレベルによる環境別コスト最適化 開発時は DEBUG で詳細なコンテキストを確認しつつ、本番環境では INFO や WARNING に絞ることで、ログ収集サービスのデータ転送量とストレージコストを最小限に抑えられます。

まとめ:正しいログ出力は未来の自分への投資

障害が発生した深夜、真っ暗な部屋でエンジニアの支えになってくれる唯一の証拠が「ログ」です。

開発の初期段階で logging と rich による基盤をしっかり整えておくことは、後々発生する障害調査のストレスと対応コストを大幅に削減してくれます。

まずは、手元のPythonプロジェクトに pip install rich でライブラリを追加し、ターミナルのログ表示を整えるところから始めてみてください。開発中の視認性が上がり、作業効率も格段に向上するはずです。

Pythonのコード品質やデバッグ環境を整えるとともに、エディタでの実装作業スピードを格段に引き上げるテクニックについては以下の記事もおすすめです。

Lowom 編集長
この記事を書いた人:Lowom 編集長 現役Webエンジニア

都内IT企業に勤める業界20年のWebエンジニア。業務効率化・自動化スクリプトや快適な開発環境の構築、厳選したツール・ガジェットの活用法など、手元で実際に検証したリアルな一次情報をお届けします。

運営者プロフィール詳細を見る