【Python】printじゃないよ、loggingだよ!効率的なログ管理のすすめ

最近はPythonでいろいろなプログラムを書くことが増えてきました。デバッグprint文をよく使っていたのですが、本番環境でのログ管理となると、もう少しちゃんとした方法を使わないといけないのではと度々思っていました。ただ、変更するのはかなり億劫でもあります😫

2025年となり、そういえばloggingモジュールを使えば、ログレベルとか出力先とかいろいろ制御できるよな~🙄と一念発起し、高機能なログ出力ができるPythonのライブラリを導入してみることにしました。

docs.python.org

正直、最初は設定が多くて「やっぱりprintでいいんじゃない?🤔」と思っていましたが、使ってみると結構便利で、特に本番環境でのデバッグトラブルシューティングが格段にやりやすくなりました!

loggingモジュールってどんなもの?

初心者の自分が使ってみて感じたloggingモジュールの特徴を簡単にまとめてみました:

  • ログレベル(DEBUGINFOWARNINGERRORCRITICAL)で出力を制御できる(本番環境ではERRORだけ出力とか)
  • ファイルとコンソールに同時出力できる(ログファイルは残しつつ、画面でも確認できる😊)
  • 時刻やファイル名、行番号なども自動で記録してくれる(デバッグが捗る!)
  • マルチスレッド環境でも安全(printだと出力が混ざることも😱)
  • プロジェクト全体で統一したログ形式が使える(チーム開発でめっちゃ重要…チーム開発はしたことないけど🙄)

導入と基本的な使い方

以下のように導入を行っていきます。

1. 基本的な使い方

まずは一番シンプルな使い方から。これならprintとそんなに変わらない感じで使えます:

import logging
# 基本設定
logging.basicConfig(level=logging.INFO)
# ログの出力
logging.info('プログラム開始')
logging.warning('警告が発生しました')
logging.error('エラーが発生しました')

個々までの作業としてはprintよりキータイプする量は多い程度ですが、ログレベルでの分類ができる点はいいかな~って程度です。

2. ファイルにログを出力する

続いては、ログをファイルに保存する設定です。日付ごとにログファイルを作る方法は良い機能かなと思います。

import logging
import os
from datetime import datetime
# ログ用のディレクトリを作成
log_directory = 'logs'
if not os.path.exists(log_directory):
os.makedirs(log_directory)
# 日付付きログファイル名生成
log_file = f'{log_directory}/app_{datetime.now().strftime("%Y%m%d")}.log'
# loggingの基本設定
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(name)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
handlers=[
logging.FileHandler(log_file, 'a', 'utf-8'),
logging.StreamHandler() #コンソールにも出力
]
)
logger = logging.getLogger(__name__)
# ログの出力
logging.info('プログラム開始')
logging.warning('警告が発生しました')
logging.error('エラーが発生しました')

これを実行することで、プログラム実行中の標準出力にログ表示できることに加えて、logsディレクトリ(ない場合には作成)に名前に日付のついたログファイル(app_YYYYMMDD.log)が作成され、ログが追記されます。

1つのログ出力でコンソールへの出力とログファイルへ追記ができ、非常に便利ですが、設定は結構大変ですね。もし使うならもっと簡単できる、再利用方法がほしいところです。

(注意)logging.basicConfig()は設定は初回のものだけが有効となり、2回目以降の呼び出しは無視されます。。複数の設定を入れる場合には、この関数内の引数設定を細かくすることになります。

3. 効率的な設定方法(logging.conf編)

先程のコードをプログラム毎に書くというのはさすがにキツイです。そこで、以下のような設定ファイルを使う方法がおすすめですのようです。以下のように記載ができます。※日付付きのファイル名の設定が難しいので省いています。

設定をlogging.confというファイルに記述して呼び出します。(YAML形式でも記述できる 後述)

# logging.conf
[loggers]
keys=root
[handlers]
keys=fileHandler,consoleHandler
[formatters]
keys=simpleFormatter
[logger_root]
level=INFO
handlers=fileHandler,consoleHandler
[handler_fileHandler]
class=FileHandler
formatter=simpleFormatter
args=('logs/app.log', 'a', 'utf-8')
[handler_consoleHandler]
class=StreamHandler
formatter=simpleFormatter
args=(sys.stdout,)
[formatter_simpleFormatter]
format=%(asctime)s [%(levelname)s] %(name)s - %(message)s
datefmt=%Y-%m-%d %H:%M:%S

そして、Pythonファイルではこれだけで OK!

import logging
import logging.config
logging.config.fileConfig('logging.conf')
logger = logging.getLogger(__name__)
logger.info('プログラム開始') # これだけでファイルとコンソールに出力されます

この方法の利点は以下になります。

  • 設定がコードから分離される
  • 環境による設定変更が容易
  • チーム開発で設定の共有がしやすい

確かに一度設定ファイルを作成すればあとの運用はかなり楽になりますね。

4. 効率的な設定方法(YAML編)

今度はYAMLファイルで設定を行ってみます。

YAMLファイルを扱う場合には以下のコマンドを実行してライブラリをインストールしてください。

$ pip install pyyaml

logging設定ファイル(logging_config.yaml

version: 1
formatters:
simple:
format: '%(asctime)s [%(levelname)s] %(name)s - %(message)s'
datefmt: '%Y-%m-%d %H:%M:%S'
handlers:
console:
class: logging.StreamHandler
level: INFO
formatter: simple
stream: ext://sys.stdout
file:
class: logging.FileHandler
level: INFO
formatter: simple
# ここは実行時に動的に設定します
filename: __dynamic_log_path__
encoding: utf-8
mode: a
loggers:
'': # root logger
level: INFO
handlers: [console, file]
propagate: false

YAMLファイルを使用すれば工夫をして日付付きファイルへの対応もできます。以下のようにユーティリティの関数を作成し、importします。

日付付きログファイル生成用ユーティリティ(logging_utils.py)

import os
from datetime import datetime
def get_log_filename():
log_dir = 'logs'
if not os.path.exists(log_dir):
os.makedirs(log_dir)
return f'logs/app_{datetime.now().strftime("%Y%m%d")}.log'

実際に使用するには以下のように記述します。

main.py

import yaml
import logging.config
from logging_utils import get_log_filename
def setup_logging():
with open('logging_config.yaml', 'r') as f:
config = yaml.safe_load(f)
# 動的にログファイル名を設定
config['handlers']['file']['filename'] = get_log_filename()
logging.config.dictConfig(config)
# ロギングの設定
setup_logging()
logger = logging.getLogger(__name__)
# 使用例
logger.info('プログラム開始')
logger.warning('警告メッセージ')
logger.error('エラーメッセージ')

YAMLを使用する利点はconf形式の利点に加えて以下でしょうか。

  • 可読性が高い
  • ネストされた構造が分かりやすい
  • コメントが書きやすい

ただし、pyyamlをインストールすることには注意が必要です。

おわりに

最初は「printで十分じゃない?」と思っていたloggingモジュールですが、使ってみると結構便利で手放せなくなってきました。特にチーム開発や本番環境での運用を考慮するなら、きちんとしたログ管理は必須なんだろうなと考えてしまいます。

loggingの設定は最初は面倒ですが、一度設定ファイルを作ってしまえば、あとは簡単になりますね。print文だらけのコードとはさようなら👋 今年からはちゃんとしたログ管理を目指します!

トラブったときのチェックポイント

私がハマったポイントはこんなところでした。

  • ログが出力されない → ログレベルの設定を確認(DEBUGレベルの出力なのにINFO以上の設定になってたり)
  • 日本語が文字化けする → エンコーディング指定を忘れてない?
  • ログが追記されない → FileHandlerのモードが'w'になってない?正解は'a'
  • 設定が反映されない → basicConfigは最初の1回しか効かないので注意

参考リンク

docs.python.org