3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

まだ print で消耗してるの?〜 logging を使ったログ出力のポイント

Posted at

もっと早く導入しておけばよかった

ある程度の規模のソフトウェアを作った後、「運用のことを考えてログ出力の機能を強化しておくか」と考えて、logging について調べ始めました。

ひととおり調べ終わったところで、「もっと早く導入しておけば、デバッグが楽だったのでは...!?」 ということに気づいて愕然としました。

この記事では、logging を使ったログ出力について私が調べたことをまとめ、自分なりに工夫したポイントを紹介したいと思います。

今回は下記の記事を参考にさせていただきました。

print を卒業すべきタイミングとは

どの言語においても、print で変数の値を出力させることはデバッグの基本です。もちろん Python も例外ではありません。プログラムを小さく試すだけなら print で十分でしょう。

しかし、以下に当てはまる場合は、print を卒業して logging を使うことを検討した方がよいかもしれません。

  • 運用時にバグが発生しても現場からの十分な説明が期待できない
  • 重要度などの複雑な条件に応じてログを出し分けたい
  • 手間をかけることなく端末とファイルに同じ内容を出力したい
  • 実行時間が長いプログラムを効率よくデバッグしたい

logging を使えば、情報がリッチなログを簡単に出力できるようになるので、運用の監視とバグの特定が効率化されます。ソフトウェアの規模が大きくなるほど、その恩恵も大きくなるでしょう。

どのようなログを出力できるのかというと、例えば、次のようなログを出力できます。

2025-07-11 10:38:03,641 test_logger.py (logger:23) - info : これはテストです。

このログは、たった1行のコードで出力されます。

test_logger.py
logger.info("これはテストです。")

もちろん、ログのフォーマットは設定で変更自在ですし、ファイルにだけでなく端末にも同じように出力させることもできます。

YAML を使った設定と基本的な使い方

logging の設定には、可読性の高い YAML を使うのがオススメです。例えば、10 MB でローテーションするログを ./logs/info.log に出力するする設定は、下記のとおりです。

log_conf.yaml
version: 1
disable_existing_loggers: False

formatters:
  file:
    format: "%(asctime)s %(filename)s (%(name)s:%(lineno)s) - %(thread)d %(funcName)s [%(levelname)s]: %(message)s"

handlers:
  file_handler:
    class: logging.handlers.RotatingFileHandler
    level: DEBUG
    formatter: file
    filename: ./logs/info.log
    maxBytes: 10485760
    backupCount: 20
    encoding: utf8

root:
  level: INFO
  handlers: [file_handler]

YAML ファイルを読み込むために、「PyYAML」パッケージが必要になることに注意してください。事前にインストールしておきましょう。

そして、この YAML ファイルを下記のように読み込みます。

test.py
import sys
import yaml
from logging import config, getLogger

logger = getLogger(__name__)

try:
    with open("log_conf.yaml", "rt") as f:
        config.dictConfig(yaml.safe_load(f.read()))
except FileNotFoundError:
    logger.critical("log_conf.yamlが存在しません。")
    sys.exit(1)

あとは、この logger をよしなに使うだけ。

test.py
logger.info("loggerを初期化しました。")
logger.debug("デバッグモードです。")
logger.warning("警告です。")
logger.error("エラーが発生しました。")
logger.critical("回復不可能なエラーです。")

もちろん、f 文字列を使った変数の出力も可能です。

test_variable = 1
logger.info(f"{test_variable}")
実行結果
1

なお、出力フォーマットには、下記のアイテムを指定できます。
log_config.yamlformat: の箇所を変更してみてください。

フォーマット 役割
%(asctime)s 実行時刻
%(filename)s ファイル名
%(funcName)s ログ呼び出しを含む関数名
%(levelname)s ログレベル名
%(lineno)d 行番号
%(message)s ログメッセージ
%(module)s モジュール名
%(name)s ロガー名
%(process)d プロセスID
%(thread)d スレッドID

RotatingFileHandler と RichHandler の併用

複数のハンドラーを併用することができます。下記は、前述の RotatingFileHandler と、コンソール出力用の RichHandler を併用した設定例です。

これにより、ログのファイル出力とコンソール出力を同時に実現できます。

log_conf.yaml
version: 1
disable_existing_loggers: False

formatters:
  console:
    format: "%(filename)s (%(name)s:%(lineno)s) - %(thread)d %(funcName)s [%(levelname)s]: %(message)s"

  file:
    format: "%(asctime)s %(filename)s (%(name)s:%(lineno)s) - %(thread)d %(funcName)s [%(levelname)s]: %(message)s"
  
  file_plain:
    '()': 'misc.StripRichFormatter'
    format: "%(asctime)s %(filename)s (%(name)s:%(lineno)s) - %(thread)d %(funcName)s [%(levelname)s]: %(message)s"

handlers:
  console:
    class: rich.logging.RichHandler
    level: INFO
    markup: True

  file_handler:
    class: logging.handlers.RotatingFileHandler
    level: DEBUG
    formatter: file_plain
    filename: ./logs/stocktrading.log
    maxBytes: 10485760
    backupCount: 20
    encoding: utf8

loggers:
  my_module:
    level: ERROR
    handlers: [console]
    propagate: no

root:
  level: INFO
  handlers: [console, file_handler]

RichHandler を使うには、「rich」パッケージが必要になることに注意してください。事前にインストールしておきましょう。

この RichHandler を使えば、コンソール出力を rich にできるので、例えば、

logger.info("[green]プログラムを開始します。[/]")
logger.error("[red bold]エラーが発生しました。[/]")

などのように、マークアップによりメッセージに色をつけることができます。
ログがとても読みやすくて大変よろしい☺️

参考:美麗なコンソールアプリを手軽に作成! richライブラリを使ってみた

【発展】 メッセージマネージャーを使ってコードからメッセージを切り離す

プログラムの規模が大きくなり、メッセージの種類が多岐にわたるようになると、各メッセージをプログラム内にハードコードするのは見通しが悪くなります。

そこで、ログメッセージをコードから切り離して一元管理することを考えます。例えば、次のようなメッセージ定義用の YAML ファイルを作ります。

log_message.yaml
# --- エラーメッセージ ---
errors:
  # ファイルが見つからない
  file_not_found: "ログ設定ファイルが '{path}' に見つかりませんでした。プログラムを終了します。"

  # 環境変数が見つからない
  env_not_found: "ログ設定ファイルに関する定義が環境変数 {env_file} に設定されていません。プログラムを終了します。"
  api_not_found: "[bold red]APIパスワードが環境変数に正しく設定されていません。[/]"
  ip_address_not_found: "[bold red]IPアドレスが環境変数に正しく設定されていません。[/]"
  port_not_found: "[bold red]ポート番号が環境変数に正しく設定されていません。[/]"

# --- 情報メッセージ ---
info:
  # プログラムの開始・終了
  program_start: "[green]=== プログラムを開始します({today})===[/]"
  program_end: "[green]=== プログラムを終了します ===[/]"

そして、下記のような MessageManager クラスを実装し、log_message.yaml からメッセージを取り出せるようにします。

message_manager.py
import yaml

class MessageManager:
    def __init__(self, file_path="log_messages.yaml"):
        try:
            with open(file_path, "rt", encoding="utf-8") as f:
                self._messages = yaml.safe_load(f)
        except FileNotFoundError:
            self._messages = {}
            print(f"警告: メッセージ定義ファイル '{file_path}' が見つかりません。")

    def get(self, key: str, **kwargs) -> str:
        try:
            template = self._messages
            for k in key.split("."):
                template = template[k]

            if not isinstance(template, str):
                return f"メッセージキー '{key}' は有効な文字列ではありません。"

            return template.format(**kwargs)

        except (KeyError, TypeError):
            return f"メッセージキー '{key}' が見つかりませんでした。"

あとは、MessageManager をインスタンス化し、下記のように使います。

test.py(抜粋)
from message_manager import MessageManager
msg = MessageManager()
...
path_name = "/path/to/directory"
except FileNotFoundError:
    logger.critical(msg.get("errors.file_not_found", path=path_name))
    sys.exit(1)

errors.file_not_found は、log_message.yaml における「error セクションの file_not_found メッセージ」を特定していることを意味します。

path=path_name は、file_not_found メッセージの中にある {path} に値を埋め込むための引数です。

この引数は、いくつでも設定することができて、例えば、

log_message.yaml
sell_executed: "[yellow]{disp_name}[/]([cyan]{symbol})[/]:[cyan]{price}[/] 円で [cyan]{qty}[/] 株の売りが約定しました。"

のように設定した上で、次のように呼び出すことができます。

logger.info(
    msg.get(
    "sell_executed",
    disp_name=disp_name,
    symbol=symbol,
    price=f"{int(price):,}" if float(price).is_integer() else price,
    qty=f"{int(qty):,}" if float(qty).is_integer() else qty,
    )
)
実行結果
2025-07-09 15:30:19,304 stock.py (stock.1578:235) - 6175453184 record_execution : 日興225M/ETF(1578):3,171 円で 1 株の売りが約定しました。
3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?