python fileconfig - murachi/nop GitHub Wiki

Python: logging.config.fileConfig() に使用する設定ファイルを configparser で出力する場合の注意点

logging.config.fileConfig() メソッドを使うと、いわゆる .ini 形式の設定ファイルとしてログ出力の設定を記述することが出来る。この .ini ファイルを、アプリケーションの初期設定時に設定したディレクトリにログファイルを出力するような内容で自動生成したいので、 configparser を使って設定ファイルを出力するということを行った。この際、いくつか躓きポイントがあったので以下にメモする。

フォーマット指定に % 記号が使えない?

formatter の設定を記述する際、普通に作った ConfigParser オブジェクトに対して以下のように記述した場合、

conf["formatter_file"] = {
    "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    "datefmt": "%Y-%m-%d %H:%M:%S",
}

この代入操作の時点で ValueError 例外が送出されてしまう。

>>> conf = configparser.ConfigParser()
>>> conf["formatter_file"] = {
...   "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
...   "datefmt": "%Y-%m-%d %H:%M:%S",
... }
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.8/configparser.py", line 974, in __setitem__
    self.read_dict({key: value})
  File "/usr/lib/python3.8/configparser.py", line 754, in read_dict
    self.set(section, key, value)
  File "/usr/lib/python3.8/configparser.py", line 1201, in set
    super().set(section, option, value)
  File "/usr/lib/python3.8/configparser.py", line 894, in set
    value = self._interpolation.before_set(self, section, option,
  File "/usr/lib/python3.8/configparser.py", line 402, in before_set
    raise ValueError("invalid interpolation syntax in %r at "
ValueError: invalid interpolation syntax in '%Y-%m-%d %H:%M:%S' at position 0
>>> 

ここで、 fileconfig valueerror invalid interpolation syntax とかでググると、「% が入っているとエラーになるから %% に置換してから代入してあげるといいよ」といった内容のブログ記事がいくつか hit する。そこで、その通りにしてファイルに出力してみると、

>>> conf["formatter_file"] = {
...   "format": "%%(asctime)s [%%(levelname)s] %%(name)s: %%(message)s",
...   "datefmt": "%%Y-%%m-%%d %%H:%%M:%%S",
... }
>>> conf["formatter_file"]["format"]
'%(asctime)s [%(levelname)s] %(name)s: %(message)s'
>>> conf["formatter_file"]["datefmt"]
'%Y-%m-%d %H:%M:%S'
>>> import sys
>>> conf.write(sys.stdout)
[formatter_file]
format = %%(asctime)s [%%(levelname)s] %%(name)s: %%(message)s
datefmt = %%Y-%%m-%%d %%H:%%M:%%S

>>> 

なるほど代入した値の %% はオブジェクトに格納された値としては % にエスケープされているようだが、実際にファイルに出力される値を見ると %% のままになっている。

設定ファイル中のフォーマット指定に %% は使えない

実際この出力結果を logging.config.fileConfig() に食わせた場合、ログには %%% にエスケープされただけの文字列が延々出力され続けることになる。

image

設定ファイル中の %%% に直してあげることによって、これらの出力は本来の期待通りの結果に変わる。つまり、設定ファイルに %% を書き出すようではダメということだ。

ConfigParser 生成時に interpolation = None を指定すべし

それではどうすればよいか。そもそもこうした挙動は ConfigParser が設定ファイルから読み取った値の解釈に用いる Interpolation と呼ばれるクラスオブジェクトの処理内容に基づいている。これは、 ConfigParser を初期値のまま (特にオプションを指定せずに) 生成した場合、デフォルトで BasicInterpolation クラスが使用されることになっている。設定値に % を使いたい場合のエスケープ記法として %% を使えというのはここの説明にもサンプルコードの中でちゃんと触れられている。

一方で、この Interpolation が行うような変数のインサーションを必要としないのであれば、 ConfigParser オブジェクト生成時に Interpolation を指定しないようオプション指定することも可能である。そのためには、以下のように ConfigParser() コンストラクタの引数に interpolation = None を指定してあげれば良い。

>>> conf = configparser.ConfigParser(interpolation = None)
>>> conf["formatter_file"] = {
...   "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
...   "datefmt": "%Y-%m-%d %H:%M:%S",
... }
>>> conf["formatter_file"]["format"]
'%(asctime)s [%(levelname)s] %(name)s: %(message)s'
>>> conf["formatter_file"]["datefmt"]
'%Y-%m-%d %H:%M:%S'
>>> conf.write(sys.stdout)
[formatter_file]
format = %(asctime)s [%(levelname)s] %(name)s: %(message)s
datefmt = %Y-%m-%d %H:%M:%S

>>> 

設定ファイル自体の注意点

logging.config.fileConfig() を使う場合の設定ファイルの書き方で実際に躓いた点をいくつか。

root ロガーは必須

root という名前のロガーが存在しないと、 fileConfig() 関数に食わせた時点で ValueError 例外を送出する。

[loggers]
keys = access, error, debug

[handlers]
keys = acc_cons, acc_file, err_cons, err_file, dbg_cons, dbg_file

[formatters]
keys = console, file

[logger_access]
level = INFO
handlers = acc_cons, acc_file
qualname = access

[logger_error]
level = WARNING
handlers = err_cons, err_file
qualname = error

[logger_debug]
level = DEBUG
handlers = dbg_cons, dbg_file
qualname = debug

[handler_acc_cons]
...(以下略)
>>> import logging.config
>>> logging.config.fileConfig("src/conf.d/sample.ini")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.8/logging/config.py", line 80, in fileConfig
    _install_loggers(cp, handlers, disable_existing_loggers)
  File "/usr/lib/python3.8/logging/config.py", line 190, in _install_loggers
    llist.remove("root")
ValueError: list.remove(x): x not in list
>>> 

ロガーを一つしか使わないのであれば、その名前は root にしておくのが無難だと思うが、用途別に複数使用する場合、むしろ root という名前のロガーなど使いたくないと思うケースのほうが大半だと思う。その場合、出力先のハンドラを指定しないプレースホルダーだけ用意するというのがセオリーのようだ。

[loggers]
keys = root, access, error, debug

...(中略)

[logger_root]
level = NOTSET
handlers =

ロガーの qualname は必須

qualname は python コード中でロガーを取得する際 logging.getLogger() に指定する名前を設定する項目である。設定ファイル中でロガーの名前を設定しているのでこの項目は勝手にオプショナルだと思いこんでいたのだが、省略すると fileConfig() 関数に食わせた時点で KeyError 例外を送出する。

>>> logging.config.fileConfig("src/conf.d/sample.ini")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.8/logging/config.py", line 80, in fileConfig
    _install_loggers(cp, handlers, disable_existing_loggers)
  File "/usr/lib/python3.8/logging/config.py", line 227, in _install_loggers
    qn = section["qualname"]
  File "/usr/lib/python3.8/configparser.py", line 1254, in __getitem__
    raise KeyError(key)
KeyError: 'qualname'
>>> 

同じ名前でいいのでとりあえず設定しておくべし。

⚠️ **GitHub.com Fallback** ⚠️