サイトアイコン アマチュア無線局JS2IIU

【Streamlit】高度なロギングテクニックと実践ガイド

こんにちは、JS2IIUです。
今回は、PythonのWebアプリケーションフレームワークであるStreamlitにおけるロギングの活用方法について解説します。
ロギングは、エラーの検知やアプリの動作確認、パフォーマンスチューニングなど、多くの場面で不可欠な技術です。しかし単純にログを取るだけでなく、ログの一貫性やパフォーマンスへの配慮、さらにはCI環境での運用まで考慮すると、かなり奥が深い領域でもあります。
この記事では、Streamlitアプリでの基本のロギング方法から、カスタムロガーの作成、非同期ロギング、ログの構造化、分析・可視化、複数モジュールでのログ統合、さらには自動テストや継続的インテグレーション(CI)における実践的なロギング運用まで、実用的なコード例とともにステップバイステップでご紹介します。

1. はじめに:ロギングの重要性と役割

ロギングは、アプリケーションの状態やエラー情報を記録し、問題発生時の原因解析や動作確認に役立ちます。特にWebアプリケーションでは、ユーザーの操作ログや処理結果を記録し続けることが、安定的なサービス運用に不可欠です。

Streamlitは手軽にインタラクティブアプリが作れますが、その分、内部の状態確認やエラー解析が難しくなりがちです。きちんとログを取ることで、開発・運用の双方で問題をスピーディに特定できるようになります。

本記事では以下の内容を順に解説します。

2. ロギングの基本

Pythonの標準ライブラリloggingを使い、Streamlitアプリ内にログを出力する基本的方法をご紹介します。

ステップ1:importとlogger作成

Python
import streamlit as st
import logging

# loggerの作成とレベル設定
logger = logging.getLogger('streamlit_app')
logger.setLevel(logging.DEBUG)

# handlerの設定(コンソール出力)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)

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

# loggerにhandlerを追加
if not logger.hasHandlers():
    logger.addHandler(console_handler)

コードの説明

  1. import streamlit as st
    • Streamlitライブラリを st という名前でインポートします。Streamlitアプリを作成する際には必須です。
  2. import logging
    • Pythonの標準ライブラリである logging をインポートします。プログラムの動作を記録するための機能を提供します。
  3. logger = logging.getLogger('streamlit_app')
    • streamlit_app という名前のロガーを作成(または取得)します。アプリケーション内の異なるモジュールで同じ名前を指定すれば、同じロガーを共有できます。
  4. logger.setLevel(logging.DEBUG)
    • このロガーが処理するログの最低レベルを DEBUG に設定します。DEBUG は最も詳細なレベルで、これ以降 INFO, WARNING, ERROR, CRITICAL の順に重要度が高くなります。これにより、DEBUG レベル以上のすべてのログが処理対象となります。
  5. console_handler = logging.StreamHandler()
    • ログの出力先としてコンソール(ターミナル)を指定するための「ハンドラ」を作成します。
  6. console_handler.setLevel(logging.DEBUG)
    • このコンソールハンドラが出力するログの最低レベルを DEBUG に設定します。
  7. formatter = logging.Formatter(...)
    • ログの出力フォーマット(書式)を定義します。
      • %(asctime)s: ログが出力された日時
      • %(name)s: ロガーの名前(この場合は streamlit_app
      • %(levelname)s: ログのレベル(DEBUG, INFOなど)
      • %(message)s: 実際のログメッセージ
  8. console_handler.setFormatter(formatter)
    • 先ほど作成したコンソールハンドラに、定義したフォーマットを適用します。
  9. if not logger.hasHandlers(): logger.addHandler(console_handler)
    • このロガーにまだハンドラが設定されていない場合(if not logger.hasHandlers())に限り、作成したコンソールハンドラを追加します。これにより、同じハンドラが重複して追加されるのを防ぎます。

ステップ2:Streamlitアプリ内でのログ出力

Python
def main():
    logger.info('アプリが起動しました')
    st.title("Streamlit Logging Example")

    user_input = st.text_input("文字を入力してください")
    if user_input:
        logger.debug(f'ユーザー入力: {user_input}')
        st.write(f'あなたの入力: {user_input}')

if __name__ == "__main__":
    main()

ポイント

3. カスタムロガーの作成と設定

より柔軟にログを管理したい場合は、自分でカスタムロガーを作成し、ファイル出力や複数ハンドラーの設定を行います。

ステップ1:ロガー設定関数を作成

Python
import os

def setup_custom_logger(name, log_file='app.log', level=logging.DEBUG):
    logger = logging.getLogger(name)
    logger.setLevel(level)

    # 既にhandlerがある場合は重複防止
    if logger.hasHandlers():
        logger.handlers.clear()

    # コンソール用ハンドラー
    ch = logging.StreamHandler()
    ch.setLevel(level)
    ch_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    ch.setFormatter(ch_formatter)

    # ファイル用ハンドラー
    fh = logging.FileHandler(log_file, encoding='utf-8')
    fh.setLevel(level)
    fh_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    fh.setFormatter(fh_formatter)

    logger.addHandler(ch)
    logger.addHandler(fh)

    return logger

コードの解説

  1. 関数定義
Python
def setup_custom_logger(name, log_file='app.log', level=logging.DEBUG):
  1. ロガーの取得とレベル設定
Python
    logger = logging.getLogger(name)
    logger.setLevel(level)
  1. ハンドラの重複防止
Python
    # 既にhandlerがある場合は重複防止
    if logger.hasHandlers():
        logger.handlers.clear()
  1. コンソール用ハンドラの設定
Python
    # コンソール用ハンドラー
    ch = logging.StreamHandler()
    ch.setLevel(level)
    ch_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    ch.setFormatter(ch_formatter)
  1. ファイル用ハンドラの設定
Python
    # ファイル用ハンドラー
    fh = logging.FileHandler(log_file, encoding='utf-8')
    fh.setLevel(level)
    fh_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    fh.setFormatter(fh_formatter)
  1. ロガーにハンドラを追加して返す
Python
    logger.addHandler(ch)
    logger.addHandler(fh)

    return logger

ステップ2:ロガーを使う

Python
logger = setup_custom_logger('streamlit_custom_logger', 'streamlit_app.log')

def main():
    logger.info('アプリ開始')
    st.title("カスタムロガーの例")

    val = st.number_input("数値を入力")
    if val:
        logger.debug(f'入力された数値: {val}')
    st.write(f"あなたの入力値は {val} です。")

if __name__ == "__main__":
    main()

ポイント

4. 非同期ロギングの導入とその利点

ロギング処理はI/O操作が含まれるため、特に高頻度のログでは処理速度に影響が出ることがあります。非同期ロギングは、メイン処理をブロックせずにログ処理を別スレッドに任せる方法で、パフォーマンス向上が期待できます。

ステップ1:QueueHandler, QueueListenerを使って非同期化

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

def setup_async_logger(name, log_file='async_app.log', level=logging.DEBUG):
    log_queue = queue.Queue(-1)  # 無制限サイズのキュー
    logger = logging.getLogger(name)
    logger.setLevel(level)

    # 基本のファイルハンドラー(Listenerが使用)
    file_handler = logging.FileHandler(log_file, encoding='utf-8')
    formatter = logg

ing.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    file_handler.setFormatter(formatter)

    # QueueHandlerをloggerにセット(非同期でキューに積むのみ)
    queue_handler = QueueHandler(log_queue)
    logger.handlers.clear()
    logger.addHandler(queue_handler)

    # QueueListenerを作成し専用スレッドでハンドリング
    listener = QueueListener(log_queue, file_handler)
    listener.start()

    return logger, listener

コードの解説

  1. 必要なモジュールのインポート
Python
import queue
from logging.handlers import QueueHandler, QueueListener
import threading
  1. 関数定義とキューの作成
Python
def setup_async_logger(name, log_file='async_app.log', level=logging.DEBUG):
    log_queue = queue.Queue(-1)  # 無制限サイズのキュー
  1. ロガー本体とファイルハンドラの設定
Python
    logger = logging.getLogger(name)
    logger.setLevel(level)

    # 基本のファイルハンドラー(Listenerが使用)
    file_handler = logging.FileHandler(log_file, encoding='utf-8')
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    file_handler.setFormatter(formatter)
  1. QueueHandlerをロガーに接続
Python
    # QueueHandlerをloggerにセット(非同期でキューに積むのみ)
    queue_handler = QueueHandler(log_queue)
    logger.handlers.clear()
    logger.addHandler(queue_handler)
  1. QueueListenerの作成と起動
Python
    # QueueListenerを作成し専用スレッドでハンドリング
    listener = QueueListener(log_queue, file_handler)
    listener.start()
  1. ロガーとリスナーを返す
Python
    return logger, listener

ステップ2:Streamlitアプリ内への組み込み

Python
logger, listener = setup_async_logger('async_streamlit')

def main():
    logger.info('非同期ロギング開始')
    st.title("非同期ロギングのサンプル")

    text = st.text_input("入力してください")
    if text:
        logger.debug(f'入力内容: {text}')
        st.write(f'はじめまして、{text}さん。')

if __name__ == "__main__":
    try:
        main()
    finally:
        listener.stop()  # アプリ終了時にListener停止

非同期ロギングの利点

5. ログメッセージの構造化と一貫性の確保

ログの解析を効率化するためには、ログメッセージを構造化(例:JSON形式)し、一貫したフォーマットで出力することが重要です。

ステップ1:JSONフォーマッターの導入

Bash
pip install python-json-logger

ステップ2:JSONFormatterを使う例

Python
from pythonjsonlogger import jsonlogger

def setup_json_logger(name, log_file='json_app.log', level=logging.DEBUG):
    logger = logging.getLogger(name)
    logger.setLevel(level)

    handler = logging.FileHandler(log_file, encoding='utf-8')
    formatter = jsonlogger.JsonFormatter('%(asctime)s %(name)s %(levelname)s %(message)s %(filename)s %(lineno)d')
    handler.setFormatter(formatter)

    logger.handlers.clear()
    logger.addHandler(handler)
    return logger

json_logger = setup_json_logger('json_logger')

json_logger.info('ユーザーログ', extra={'user_id': 1234, 'action': 'login'})

ポイント

6. パフォーマンスへの影響評価

ログは便利ですが、多量のログ出力はアプリケーションの性能に影響を与えることがあります。ここでは、簡単な遅延計測でログの影響を評価する方法を紹介します。

測定例(基本同期ロギング)

Python
import time

start = time.perf_counter()

for i in range(1000):
    logger.debug(f'ログメッセージ{i}')

end = time.perf_counter()
st.write(f'同期ロギングにかかった時間: {end - start:.4f}秒')

非同期ロギングでの比較

上記のコードを非同期ロギング環境で試し、処理時間の違いを比較してください。
非同期ロギングは大抵の場合、処理時間を大幅に短縮できます。

7. ログデータの分析と可視化

収集したログは放置せず、適切に分析して改善に活かしましょう。ここでは代表的な分析手法とツールを紹介します。

8. 複数モジュール間でのログ統合

大規模アプリでは複数のモジュール・パッケージからログを出力します。これらのログを1つにまとめ、一貫した形で管理することが重要です。

ポイント

例:ルートロガー設定

Python
# ルートロガー設定(main.pyなど)
root_logger = setup_custom_logger('my_project')

# 各モジュール
import logging

logger = logging.getLogger('my_project.moduleA')

def process():
    logger.info("モジュールAの処理が開始されました")

この方法で、すべてのログはmy_project.logに統合され、管理や分析が容易になります。

9. 効率的なトラブルシューティングとログ活用法

トラブル時にはログから原因特定を迅速に行いたいものです。具体的な活用法をいくつかご紹介します。

事例:セッションID付与によるトレース

Python
import uuid

def log_with_session_id(logger, message, session_id=None, level=logging.INFO):
    if not session_id:
        session_id = str(uuid.uuid4())
    logger.log(level, f"[session_id={session_id}] {message}")
    return session_id

session_id = log_with_session_id(logger, "処理開始")
# 処理中...
log_with_session_id(logger, "処理中のイベント発生", session_id, logging.DEBUG)

セッションIDが付与されたログは後から絞り込みやすく、問題の流れを追跡しやすくなります。

10. 自動テスト環境でのロギング設定

テスト実行時はログ出力が冗長になりがちですが、適切に設定すればテスト結果の診断に役立ちます。

ポイント

pytestでの例

Python
import logging

def setup_test_logger():
    logger = logging.getLogger('streamlit_test')
    logger.setLevel(logging.WARNING)
    if not logger.hasHandlers():
        handler = logging.StreamHandler()
        formatter = logging.Formatter('%(levelname)s - %(message)s')
        handler.setFormatter(formatter)
        logger.addHandler(handler)
    return logger

def test_sample():
    logger = setup_test_logger()
    logger.info("このログは表示されません")
    logger.error("テスト失敗時のエラーログ")
    assert 1 == 1

11. 継続的インテグレーション(CI)でのロギングのベストプラクティス

CI環境では、ビルドやテストのログが問題の早期発見に役立ちます。以下のポイントに注意しましょう。

CircleCIやGitHub Actionsの例

streamlit_app.logをCIのアーティファクトに設定し、テスト失敗時にはログファイルの中身を確認可能にする運用がよく行われます。

12. まとめ

本記事では、Streamlitアプリでのロギングについて、基礎から応用、さらにCI環境での運用まで幅広く解説しました。

ロギングは単なる情報出力だけでなく、アプリの品質を大きく向上させる重要な技術です。ぜひ段階的に導入・活用して、Streamlit開発に役立ててください。

参考リンク

最後まで読んでいただきありがとうございます。
ご質問がある方は、コメント欄をご活用ください。

モバイルバージョンを終了