フリーキーズ | 独学プログラミング

Pythonloggingモジュールを解説!ログ出力のベストプラクティス

リンドくん

リンドくん

先生、Pythonでプログラムを作ってるんですけど、エラーが起きた時に原因がわからなくて困ってます...

たなべ

たなべ

それはログ出力が足りていないからだね!
Pythonにはloggingという強力なモジュールがあるんだ。これを使えば、プログラムの動作を詳しく記録できるよ。

プログラミングを学び始めて間もない方は、「なぜプログラムが思った通りに動かないのか」「どこでエラーが発生しているのか」といった問題に直面することが多いのではないでしょうか?

そんな時に役立つのがログ出力です。
ログとは、プログラムの動作状況や発生したエラーなどを記録する仕組みのことで、プロのエンジニアにとって欠かせない技術の一つです。

Pythonにはloggingという標準モジュールが用意されており、これを使うことで効果的にプログラムの状況を記録できます。
最初は「print文で十分では?」と思うかもしれませんが、ログ出力には多くのメリットがあります。

この記事では、Python初心者の方でも理解できるよう、loggingモジュールの基本的な使い方から実践的なテクニックまで、段階的に解説していきます。

プログラミング学習でお悩みの方へ

HackATAは、エンジニアを目指す方のためのプログラミング学習コーチングサービスです。 経験豊富な現役エンジニアがあなたの学習をサポートします。

✓ 質問し放題

✓ β版公開中(2025年内の特別割引)

HackATAの詳細を見る

そもそもログ出力とは何か?print文との違い

リンドくん

リンドくん

でも先生、printでも情報を出力できますよね?ログとは何が違うんですか?

たなべ

たなべ

いい質問だね!確かにprintでも情報を表示できるけど、ログには「レベル」という概念があるんだ。
これによって、状況に応じて必要な情報だけを表示できるようになるよ。

ログ出力の基本概念

ログ出力とは、プログラムの実行中に発生した出来事や状態を記録することです。
単純に情報を画面に表示するprint文とは異なり、ログには以下のような特徴があります。

  • レベル別の分類 → 情報の重要度に応じて分類される
  • 詳細な情報 → 時刻、ファイル名、行番号なども自動で記録
  • 出力先の制御 → 画面だけでなく、ファイルにも保存可能
  • フィルタリング機能 → 必要な情報だけを表示できる

print文の限界とloggingの利点

print文を使った場合とloggingを使った場合を比較してみましょう。

# print文を使った場合
def divide_numbers(a, b):
    print(f"計算開始: {a} ÷ {b}")
    if b == 0:
        print("エラー: ゼロで割ろうとしました")
        return None
    result = a / b
    print(f"計算結果: {result}")
    return result

# loggingを使った場合
import logging

def divide_numbers_with_logging(a, b):
    logging.info(f"計算開始: {a} ÷ {b}")
    if b == 0:
        logging.error("エラー: ゼロで割ろうとしました")
        return None
    result = a / b
    logging.info(f"計算結果: {result}")
    return result

一見同じように見えますが、loggingを使うことで以下のメリットがあります。

  • レベル設定により、本番環境では詳細なログを非表示にできる
  • 時刻やファイル名が自動で記録される
  • ファイルへの保存が簡単にできる
  • 複数のプログラムから統一されたフォーマットでログを出力できる

これらの違いにより、プロのエンジニアはほぼ例外なくloggingモジュールを使用しています。

loggingモジュールの基本的な使い方

最もシンプルな使い方

まずは最も基本的な使い方から始めましょう。

import logging

# 基本的なログ設定
logging.basicConfig(level=logging.INFO)

# ログ出力の例
logging.info("プログラムが開始されました")
logging.warning("これは警告メッセージです")
logging.error("エラーが発生しました")

実行すると、以下のような出力が得られます。

INFO:root:プログラムが開始されました
WARNING:root:これは警告メッセージです
ERROR:root:エラーが発生しました

ログレベルの種類と使い分け

Pythonのloggingには5つの標準的なレベルがあります。

  • DEBUG → 開発時の詳細な情報(変数の値など)
  • INFO → 一般的な情報(処理の開始・終了など)
  • WARNING → 警告(問題はないが注意が必要)
  • ERROR → エラー(処理は継続可能だが問題がある)
  • CRITICAL → 致命的エラー(プログラムの継続が困難)

それぞれの使い分けの例を見てみましょう。

import logging

logging.basicConfig(level=logging.DEBUG)

def user_login(username, password):
    logging.debug(f"ログイン試行: ユーザー名={username}")
    
    if not username:
        logging.error("ユーザー名が入力されていません")
        return False
    
    if len(password) < 8:
        logging.warning("パスワードが短すぎます(推奨: 8文字以上)")
    
    # 実際の認証処理(仮)
    if username == "admin" and password == "password123":
        logging.info(f"ユーザー '{username}' がログインしました")
        return True
    else:
        logging.error("認証に失敗しました")
        return False

# 使用例
user_login("admin", "pass")
user_login("admin", "password123")

このように、状況に応じて適切なレベルを使い分けることで、問題の特定や動作の確認が効率的に行えます。

ログ設定とファイル出力の例

リンドくん

リンドくん

ログをファイルに保存することもできるんですか?

たなべ

たなべ

もちろん!特に本番環境では必須の機能だよ。
ログファイルがあれば、後からでも問題を調査できるからね。

ログファイルへの出力設定

実際の開発では、ログを画面に表示するだけでなく、ファイルに保存することが重要です。

import logging
from datetime import datetime

# ログファイル名に日付を含める
log_filename = f"app_{datetime.now().strftime('%Y%m%d')}.log"

# ログの設定
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler(log_filename, encoding='utf-8'),  # ファイル出力
        logging.StreamHandler()  # 画面出力
    ]
)

def process_user_data(user_id, data):
    logging.info(f"ユーザーデータ処理開始: ID={user_id}")
    
    try:
        # 何らかの処理
        if not data:
            raise ValueError("データが空です")
        
        # 処理成功
        logging.info(f"ユーザーデータ処理完了: ID={user_id}")
        return True
        
    except Exception as e:
        logging.error(f"データ処理エラー: ID={user_id}, エラー内容={str(e)}")
        return False

# 使用例
process_user_data(123, {"name": "田中太郎"})
process_user_data(456, None)  # エラーケース

カスタムフォーマットの設定

ログのフォーマットをカスタマイズすることで、より読みやすく有用な情報を記録できます。

import logging

# カスタムフォーマットの定義
formatter = logging.Formatter(
    '%(asctime)s | %(levelname)-8s | %(filename)s:%(lineno)d | %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

# ログ設定
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

# ファイルハンドラ
file_handler = logging.FileHandler('detailed.log', encoding='utf-8')
file_handler.setFormatter(formatter)
file_handler.setLevel(logging.INFO)

# コンソールハンドラ
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
console_handler.setLevel(logging.WARNING)

# ハンドラをロガーに追加
logger.addHandler(file_handler)
logger.addHandler(console_handler)

def calculate_average(numbers):
    logger.debug(f"平均値計算開始: データ={numbers}")
    
    if not numbers:
        logger.error("空のリストが渡されました")
        return None
    
    if not all(isinstance(n, (int, float)) for n in numbers):
        logger.warning("数値以外のデータが含まれています")
        numbers = [n for n in numbers if isinstance(n, (int, float))]
    
    average = sum(numbers) / len(numbers)
    logger.info(f"平均値計算完了: 結果={average}")
    
    return average

# 使用例
calculate_average([1, 2, 3, 4, 5])
calculate_average([])
calculate_average([1, "text", 3, None, 5])

このような設定により、ファイルには詳細な情報(INFO以上)を記録し、画面には重要な情報(WARNING以上)のみを表示するといった使い分けが可能になります。

エラーハンドリングとログの組み合わせ

try-except文との効果的な組み合わせ

エラーハンドリングとログ出力を組み合わせることで、問題が発生した際の原因特定が格段に楽になります。

import logging
import traceback

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def safe_divide(a, b):
    try:
        logging.debug(f"除算処理開始: {a} ÷ {b}")
        
        # 入力値の検証
        if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
            raise TypeError("引数は数値である必要があります")
        
        if b == 0:
            raise ZeroDivisionError("ゼロで除算しようとしました")
        
        result = a / b
        logging.info(f"除算成功: {a} ÷ {b} = {result}")
        return result
        
    except ZeroDivisionError as e:
        logging.error(f"ゼロ除算エラー: {e}")
        return None
        
    except TypeError as e:
        logging.error(f"型エラー: {e}")
        return None
        
    except Exception as e:
        logging.critical(f"予期しないエラーが発生しました: {e}")
        logging.critical(f"詳細なエラー情報:\n{traceback.format_exc()}")
        return None

# 使用例
safe_divide(10, 2)    # 正常ケース
safe_divide(10, 0)    # ゼロ除算エラー
safe_divide("10", 2)  # 型エラー

デコレータを使った自動ログ出力

関数の実行開始・終了を自動でログ出力するデコレータを作ることで、効率的にログを記録できます。

import logging
import functools
import time

logging.basicConfig(level=logging.INFO)

def log_execution(func):
    """関数の実行をログ出力するデコレータ"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        func_name = func.__name__
        logging.info(f"関数 '{func_name}' 実行開始")
        
        start_time = time.time()
        
        try:
            result = func(*args, **kwargs)
            execution_time = time.time() - start_time
            logging.info(f"関数 '{func_name}' 実行完了 (実行時間: {execution_time:.3f}秒)")
            return result
            
        except Exception as e:
            execution_time = time.time() - start_time
            logging.error(f"関数 '{func_name}' でエラー発生 (実行時間: {execution_time:.3f}秒): {e}")
            raise
    
    return wrapper

@log_execution
def fetch_user_data(user_id):
    """ユーザーデータを取得する関数(例)"""
    time.sleep(1)  # データベースアクセスをシミュレート
    
    if user_id < 0:
        raise ValueError("ユーザーIDは正の値である必要があります")
    
    return {"id": user_id, "name": f"User{user_id}"}

# 使用例
try:
    user = fetch_user_data(123)
    print(f"取得したユーザー: {user}")
    
    user = fetch_user_data(-1)  # エラーケース
except ValueError as e:
    print(f"エラーをキャッチしました: {e}")

このデコレータを使用することで、すべての関数で一貫したログ出力を実現できます。

ログのローテーションと管理

ファイルサイズに基づくローテーション

本番環境では、ログファイルが肥大化するのを防ぐため、ローテーション機能を使用します。

import logging
from logging.handlers import RotatingFileHandler

# ローテーション設定
# maxBytes: 最大ファイルサイズ(バイト)
# backupCount: 保持するバックアップファイル数
rotating_handler = RotatingFileHandler(
    'app.log',
    maxBytes=1024*1024,  # 1MB
    backupCount=5,       # 5個のバックアップを保持
    encoding='utf-8'
)

# フォーマット設定
formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
rotating_handler.setFormatter(formatter)

# ロガー設定
logger = logging.getLogger('MyApp')
logger.setLevel(logging.INFO)
logger.addHandler(rotating_handler)

# 大量のログを出力してローテーションをテスト
for i in range(1000):
    logger.info(f"ログメッセージ {i}: 何らかの重要な処理が実行されました")

時間に基づくローテーション

日付ごとにログファイルを分割したい場合は、TimedRotatingFileHandlerを使用します。

import logging
from logging.handlers import TimedRotatingFileHandler

# 時間ベースのローテーション
timed_handler = TimedRotatingFileHandler(
    'daily.log',
    when='midnight',  # 毎日午前0時にローテーション
    interval=1,       # 1日間隔
    backupCount=7,    # 7日分保持
    encoding='utf-8'
)

formatter = logging.Formatter(
    '%(asctime)s - %(levelname)s - %(message)s'
)
timed_handler.setFormatter(formatter)

logger = logging.getLogger('DailyApp')
logger.setLevel(logging.INFO)
logger.addHandler(timed_handler)

logger.info("日次ローテーションログの開始")

本番環境でのログ運用ベストプラクティス

リンドくん

リンドくん

実際にアプリを作る時は、どんなことに気をつけてログを設定すればいいんですか?

たなべ

たなべ

本番環境ではセキュリティパフォーマンスを特に意識する必要があるんだ。
個人情報を誤ってログに出力しないよう注意が必要だよ。

環境別ログ設定

開発環境と本番環境でログ設定を切り替える方法を紹介します。

import logging
import os
from logging.handlers import RotatingFileHandler

def setup_logging():
    """環境に応じたログ設定"""
    
    # 環境変数から環境を判定
    environment = os.getenv('ENVIRONMENT', 'development')
    
    if environment == 'production':
        # 本番環境: ファイル出力のみ、INFOレベル以上
        level = logging.INFO
        handlers = [
            RotatingFileHandler(
                'app.log',
                maxBytes=10*1024*1024,  # 10MB
                backupCount=10,
                encoding='utf-8'
            )
        ]
        
    elif environment == 'testing':
        # テスト環境: ファイル出力、DEBUGレベル
        level = logging.DEBUG
        handlers = [
            logging.FileHandler('test.log', encoding='utf-8')
        ]
        
    else:
        # 開発環境: コンソール出力、DEBUGレベル
        level = logging.DEBUG
        handlers = [
            logging.StreamHandler()
        ]
    
    # 共通設定
    logging.basicConfig(
        level=level,
        format='%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s',
        handlers=handlers
    )

# アプリケーション開始時に呼び出し
setup_logging()

logger = logging.getLogger(__name__)
logger.info("アプリケーションが開始されました")

セキュリティを考慮したログ出力

個人情報や機密情報をログに出力しないよう注意が必要です。

import logging
import re

class SecurityFilter(logging.Filter):
    """機密情報をマスクするフィルタ"""
    
    def __init__(self):
        super().__init__()
        # マスクするパターンを定義
        self.patterns = [
            (re.compile(r'\b\d{4}-\d{4}-\d{4}-\d{4}\b'), 'XXXX-XXXX-XXXX-XXXX'),  # クレジットカード
            (re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'), 'XXX@XXX.com'),  # メールアドレス
            (re.compile(r'\bpassword["\']?\s*[:=]\s*["\']?([^"\'\s]+)["\']?', re.IGNORECASE), 'password: [MASKED]'),  # パスワード
        ]
    
    def filter(self, record):
        # ログメッセージから機密情報を除去
        for pattern, replacement in self.patterns:
            record.msg = pattern.sub(replacement, str(record.msg))
        return True

# セキュリティフィルタを適用
logger = logging.getLogger('SecureApp')
logger.addFilter(SecurityFilter())

# ハンドラ設定
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s'))
logger.addHandler(handler)
logger.setLevel(logging.INFO)

# テスト(機密情報は自動でマスクされる)
logger.info("ユーザー登録: email=user@example.com, password=secret123")
logger.info("決済情報: card=1234-5678-9012-3456")

パフォーマンスを考慮した設定

ログ出力がアプリケーションのパフォーマンスに与える影響を最小限に抑える方法です。

import logging
import queue
from logging.handlers import QueueHandler, QueueListener
import threading

def setup_async_logging():
    """非同期ログ出力の設定"""
    
    # ログキューの作成
    log_queue = queue.Queue()
    
    # キューハンドラ(メインスレッド用)
    queue_handler = QueueHandler(log_queue)
    
    # 実際の出力ハンドラ(別スレッド用)
    file_handler = logging.FileHandler('async.log', encoding='utf-8')
    file_handler.setFormatter(
        logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    )
    
    # キューリスナー(別スレッドでログを処理)
    listener = QueueListener(log_queue, file_handler)
    listener.start()
    
    # ルートロガーにキューハンドラを設定
    root_logger = logging.getLogger()
    root_logger.addHandler(queue_handler)
    root_logger.setLevel(logging.INFO)
    
    return listener

# 非同期ログの開始
listener = setup_async_logging()

# 使用例
logger = logging.getLogger('PerformanceApp')

# 大量のログを高速で出力してもブロックされない
for i in range(10000):
    logger.info(f"高速処理 {i} が完了しました")

print("すべてのログ出力が完了しました(非同期なので即座に完了)")

# アプリケーション終了時
# listener.stop()

まとめ

リンドくん

リンドくん

ログって奥が深いんですね!これでエラーの原因もすぐに分かりそうです。

たなべ

たなべ

そう!適切なログ出力はデバッグ効率を劇的に向上させるんだ。
最初は基本的な使い方から始めて、徐々に高度な機能も取り入れていこうね。

この記事では、Pythonのloggingモジュールについて、基本概念から実践的な活用方法まで幅広く解説しました。

重要なポイントをまとめると以下の通りです。

  • 適切なログレベルを使い分けることで、状況に応じた情報収集が可能
  • ファイル出力と画面出力を使い分けることで、効率的な問題解決ができる
  • エラーハンドリングとの組み合わせにより、予期しない問題にも対応可能
  • 本番環境では、セキュリティとパフォーマンスを考慮した設定が必要

ログ出力は、単なるデバッグツールではありません。アプリケーションの品質向上運用時の問題解決ユーザー体験の改善など、様々な場面で重要な役割を果たします。

最初は基本的なlogging.info()logging.error()から始めて、徐々にファイル出力やカスタムフォーマットなどの高度な機能を取り入れていきましょう。
継続的に適切なログ出力を心がけることで、あなたのプログラミングスキルは確実に向上するはずです。

この記事をシェア

関連するコンテンツ