BUNSEN

個人的な学習ログ

logging.config.dictConfig()を使用してFilterを実装

はじめに

ログフィルターの実装はfileConfig()では不可能でdictConfig()では可能。

サンプルなどを探してはみたが自身の欲しかったものは見つからなかった。

だからまとめる。

ログフィルター

loggingでは出力するログメッセージはlogging.LogRecordオブジェクトに格納されLogger間でやり取りされている。

Loggerが受け取ったLogRecordを出力するか否かを判断するために使用するものがログフィルター

HandlerやFormatterにはある程度テンプレートのようなクラスが存在したがFilterには存在せず、条件の記述がユーザー独自に柔軟にカスタマイズできるようになっている。 それゆえにサンプルコードもあまり落ちていない気がする。

要するに以下みたいなことができる

  • メッセージ内にpasswordが入っているからこのログはドロップする
  • INFOだけ出力し、それ以外(DEBUG,ERROR...)はドロップする
  • 特定の時間に出力されたメッセージのみ出力する(こんな使い道は存在するのか不明だが可能)

やりたいこと

  1. loggingの設定記述を外部ファイルにまとめる
  2. main.pyの宣言部分をなるべく簡素にする
  3. メッセージ内に特定のメッセージが含まれる場合はメッセージにモザイクをかける
  4. フィルタリングする特定のメッセージは外部ファイルで複数定義できる
  5. 複数ファイル間で同一のlogging設定を有効にする

構成

├main.py
├mylogging.py # Logger設定実行ファイル
├account.py # クラス定義ファイル
└logging.json # Logger設定ファイル

実行結果

> python .\main.py
[DEBUG]account -> account has been imported
[DEBUG]__main__ -> default id:example
[DEBUG]__main__ -> Filterd: password
[DEBUG]account.GoogleAccount -> id:example@gmail.com
[DEBUG]account.GoogleAccount -> Filterd: password
[DEBUG]account.YahooAccount -> id:example@yahoo.com
[DEBUG]account.YahooAccount -> Filterd: password
[DEBUG]__main__ -> accounts initialize finish
[DEBUG]__main__ -> Filterd: secret

main.py

import mylogging
from logging import getLogger

mylogging.setLoggerConfig("./logging.json")
logger = getLogger(__name__)

import account

def main():
    username = "example"
    password = "P@ssw0rd"

    logger.debug("default id:" + username)
    logger.debug("default password:" + password)

    google = account.GoogleAccount(username, password)
    yahoo = account.YahooAccount(username, password)

    logger.debug("accounts initialize finish")
    logger.debug("secret word!")
    

if __name__ == "__main__":
    main()

mylogging.py

def setLoggerConfig(file_path):
    from json import load
    from logging import config, Filter
    
    class MyFilter(Filter):
        def __init__(self, words=None):
            if not isinstance(words, list):
                words = None
            self.words = words

        def filter(self, record):
            if self.words is not None:
                for word in self.words:
                    if word in record.msg:
                        record.msg = "Filterd: " + word
                        break
            return True

    with open(file_path, "r", encoding="utf-8") as f:
        log_conf_dic = load(f)
    
    log_conf_dic["filters"]["filterExample"]["()"] = MyFilter

    config.dictConfig(log_conf_dic)

account.py

from logging import getLogger

logger = getLogger(__name__)
logger.debug("account has been imported")

class Credential(object):
    domain = ""
    def __init__(self, username, password):
        self.logger = logger.getChild(self.__class__.__name__)

        self.username = username + "@" + self.domain
        self.password = password

        self.logger.debug("id:" + self.username)
        self.logger.debug("password:" + self.password)

class GoogleAccount(Credential):
    domain = "gmail.com"
    pass

class YahooAccount(Credential):
    domain = "yahoo.com"
    pass

logging.json

{
  "version": 1,
  "disable_existing_loggers": false,
  "root": {
    "level": "DEBUG",
    "handlers": [
      "consoleHandler"
    ]
  },
  "handlers": {
    "consoleHandler": {
      "class": "logging.StreamHandler",
      "level": "DEBUG",
      "formatter": "simpleFormatter",
      "filters": [
        "filterExample"
      ],
      "stream": "ext://sys.stdout"
    }
  },
  "formatters": {
    "simpleFormatter": {
      "format": "[%(levelname)s]%(name)s -> %(message)s"
    }
  },
  "filters": {
    "filterExample": {
      "()": "",
      "words": [
        "password",
        "secret"
      ]
    }
  }
}

解説

上のほうに書いたやりたいことリストをもとに解説

1. loggingの設定記述を外部ファイルにまとめる

  • JSONYAMLなど好きな形式で外部ファイルに保存する(上記の例ではlogging.json)
  • プログラム内では設定ファイルを辞書化してそれをlogging.config.dictConfig()に渡す。

2. main.pyの宣言部分をなるべく簡素にする

  • 別ファイルで定義(mylogging.py)

3. メッセージ内に特定のメッセージが含まれる場合はメッセージにモザイクをかける

  • logging.json

    • dict["handlers"]["filters"]要素の追加
      • 指定のHandlerに適用するFilterを配列で指定
    • dict["filters"][<FILTER_NAME>]要素を追加
      • "()": ユーザ定義FilterClassを格納。別ファイルでは格納不可なので入れ物だけ定義
  • mylogging.setLoggerConfig()

    • ユーザ定義FlterClassの作成
      • logging.Filterを継承(推奨※filter()さえちゃんとしてれば動くらしい)
      • filter()関数を定義(必須)
        • 引数はlogging.LogRecordインスタンスを一つとる
        • 返り値はbooleanとし、Trueであれば出力、Falseであればドロップ
        • 条件を自由に記述
    • 設定格納辞書にユーザ定義FilterClassを登録
      • dict["filters"][<FILTER_NAME>]["()"]の値に作成したユーザ定義FilterClassを代入

4. フィルタリングする特定のメッセージは外部ファイルで複数定義できる

  • logging.json
    • dict["filters"][<FILTER_NAME>]["words"]要素を追加
      • "()"に格納されるユーザ定義FlterClassの__init__に渡す引数。例ではwordsとしたが__init__の引数と一致すれば組み合わせは自由
  • mylogging.setLoggerConfig()
    • ユーザ定義FlterClassの編集
      • __init__を実装する
        • 引数はキーワード引数とし、dict["filters"][<FILTER_NAME>]配下の"()"以外のすべてが渡される

5. 複数ファイル間で同一のlogging設定を有効にする

  • main.py
    • 他モジュールのimport位置をlogging.config.dictConfig()以降にする
  • account.py
    • あまりやりたいことと関係ないけど子Loggerを作成したりしてる