もっと早く導入しておけばよかった
ある程度の規模のソフトウェアを作った後、「運用のことを考えてログ出力の機能を強化しておくか」と考えて、logging
について調べ始めました。
ひととおり調べ終わったところで、「もっと早く導入しておけば、デバッグが楽だったのでは...!?」 ということに気づいて愕然としました。
この記事では、logging
を使ったログ出力について私が調べたことをまとめ、自分なりに工夫したポイントを紹介したいと思います。
今回は下記の記事を参考にさせていただきました。
- Python 公式 - Logging HOWTO
- Good logging practice in Python
- Pythonでprintを卒業してログ出力をいい感じにする
- loggingの簡単な使い方
- Pythonログ出力の基本を解説!【YAMLでのログ出力設定まで紹介します】
print
を卒業すべきタイミングとは
どの言語においても、print
で変数の値を出力させることはデバッグの基本です。もちろん Python も例外ではありません。プログラムを小さく試すだけなら print
で十分でしょう。
しかし、以下に当てはまる場合は、print
を卒業して logging
を使うことを検討した方がよいかもしれません。
- 運用時にバグが発生しても現場からの十分な説明が期待できない
- 重要度などの複雑な条件に応じてログを出し分けたい
- 手間をかけることなく端末とファイルに同じ内容を出力したい
- 実行時間が長いプログラムを効率よくデバッグしたい
logging
を使えば、情報がリッチなログを簡単に出力できるようになるので、運用の監視とバグの特定が効率化されます。ソフトウェアの規模が大きくなるほど、その恩恵も大きくなるでしょう。
どのようなログを出力できるのかというと、例えば、次のようなログを出力できます。
2025-07-11 10:38:03,641 test_logger.py (logger:23) - info : これはテストです。
このログは、たった1行のコードで出力されます。
logger.info("これはテストです。")
もちろん、ログのフォーマットは設定で変更自在ですし、ファイルにだけでなく端末にも同じように出力させることもできます。
YAML を使った設定と基本的な使い方
logging
の設定には、可読性の高い YAML を使うのがオススメです。例えば、10 MB でローテーションするログを ./logs/info.log
に出力するする設定は、下記のとおりです。
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 ファイルを下記のように読み込みます。
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
をよしなに使うだけ。
logger.info("loggerを初期化しました。")
logger.debug("デバッグモードです。")
logger.warning("警告です。")
logger.error("エラーが発生しました。")
logger.critical("回復不可能なエラーです。")
もちろん、f 文字列を使った変数の出力も可能です。
test_variable = 1
logger.info(f"{test_variable}")
1
なお、出力フォーマットには、下記のアイテムを指定できます。
log_config.yaml
の format:
の箇所を変更してみてください。
フォーマット | 役割 |
---|---|
%(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
を併用した設定例です。
これにより、ログのファイル出力とコンソール出力を同時に実現できます。
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 ファイルを作ります。
# --- エラーメッセージ ---
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
からメッセージを取り出せるようにします。
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
をインスタンス化し、下記のように使います。
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}
に値を埋め込むための引数です。
この引数は、いくつでも設定することができて、例えば、
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 株の売りが約定しました。