🐍

logging.config.dictConfigとpydantic-settingsを組み合わせてファイルや環境変数からログ設定を読み込む

2025/01/12に公開

概要

ファイルからログ設定を読み込むにはlogging.config.fileConfigがありますが、次の理由から使う気になれません。

  • 設定ファイルの仕様を覚えるのが面倒

  • 設定の一部を環境変数で上書きしたいけれど、できない

  • logging.config.dictConfigに比べて機能不足という注意書きがある

    注釈 fileConfig() API は dictConfig() API よりも古く、ロギングのある種の側面についてカバーする機能に欠けています。たとえば fileConfig() では数値レベルを超えたメッセージを単に拾うフィルタリングを行う Filter オブジェクトを構成出来ません。 Filter のインスタンスをロギングの設定において持つ必要があるならば、 dictConfig() を使う必要があるでしょう。設定の機能における将来の拡張は dictConfig() に対して行われることに注意してください。ですから、そうするのが便利であるときに新しい API に乗り換えるのは良い考えです。

これらのことからlogging.config.dictConfigpydantic-settingsを組み合わせてファイルからログ設定を読み込みつつ一部を環境変数で上書きできる方法を考えました。

ログ設定を読み込むコード

pydantic-settingsで.envファイルおよび環境変数から設定値を読み込むための設定クラスを定義し、読み込んだ設定値をもとにlogging.config.dictConfigでログを設定しています。

from logging.config import dictConfig
from typing import Any

from pydantic_settings import (
    BaseSettings,
    SettingsConfigDict,
)


class LoggingSettings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        env_nested_delimiter="__",
    )

    logging: dict[str, Any] = {}


dictConfig(LoggingSettings().logging)

pydantic-settingsで.envファイルから設定値を読み込むためのクラスはDotEnvSettingsSource、環境変数から設定値を読み込むためのクラスはEnvSettingsSourceですが、これらは設定値にJSONを書くとdecode_complex_valueメソッドで辞書型に変換してくれます。

この性質を利用することで.envファイルの内容は例えば次のように書けます。

LOGGING='
{
  "version": 1,
  "formatters": {
    "default": {
      "format": "[%(levelname)s] %(name)s - %(message)s"
    }
  },
  "handlers": {
    "console": {
      "class": "logging.StreamHandler",
      "formatter": "default"
    }
  },
  "loggers": {
    "foo": {
      "level": "DEBUG",
      "handlers": ["console"]
    },
    "bar": {
      "level": "INFO",
      "handlers": ["console"]
    }
  }
}
'

また、設定クラスのmodel_configenv_nested_delimiterを指定しているため、次のようにしてネストした設定値を環境変数で表現できます。

LOGGING__LOGGERS__BAR__LEVEL=DEBUG

動作確認

いくつかのロガーを定義してINFOレベルとDEBUGレベルでログを出力するコードを用いて動作確認します。

loggers = [
    getLogger("foo"),
    getLogger("bar"),
    getLogger("baz"),
]

for logger in loggers:
    logger.info("message1")
    logger.debug("message2")

設定値は先ほど示した通りです。
環境変数で上書きしない場合、各ロガーの設定は.envファイルの内容が適用されます。

ロガー 設定
foo DEBUGレベルのログを出力する
bar INFOレベルのログを出力する
baz 設定されていないのでログを出力しない

まずは環境変数で上書きせず実行してみます。

$ python -m app.main
[INFO] foo - message1
[DEBUG] foo - message2
[INFO] bar - message1

次に環境変数で設定値を上書きし、barDEBUGレベルのログを出力できるようにします。

$ LOGGING__LOGGERS__BAR__LEVEL=DEBUG python -m app.main
[INFO] foo - message1
[DEBUG] foo - message2
[INFO] bar - message1
[DEBUG] bar - message2

最後に環境変数でJSON形式の設定値を追加してみます。
bazにハンドラーを設定してDEBUGレベルのログを出力できるようにします。

$ LOGGING__LOGGERS__BAZ='
{
  "level": "DEBUG",
  "handlers": ["console"]
}
' python -m app.main
[INFO] foo - message1
[DEBUG] foo - message2
[INFO] bar - message1
[INFO] baz - message1
[DEBUG] baz - message2

期待通りに動作してくれました。

参考

logging.config.dictConfigへ渡す辞書について説明しているページです。

https://docs.python.org/ja/3.13/library/logging.config.html#dictionary-schema-details

pyproject.tomlなども含むコード例の全体です。

https://github.com/backpaper0/sandbox/tree/3afcc41607b89a3a48922f910fc70a3a654b96b5/python-logging-example

Discussion