Python のロギングを使いやすく。

過去に何度も書いたが、改めて書き直すことにした。
設定ファイルと、logger.py という logging を書いたスクリプトで構成する。

JSON で記述する設定ファイル(logsetting.json というファイル名)で、 Logger 生成で引数 name に対する以下属性を設定する。
Logger 生成で引数 name を省略する場合は、'default' として以下設定を記述する。

format ログメッセージフォーマット
dateformat ログメッセージ内の時刻フォーマット
datetime.strftime の規則と同じ書式で指定する。
rotation ローテーション方法を指定
daily or size
maxHistory 履歴として保持する最大数
maxbytes ログファイルの最大サイズをバイトで指定
(サイズローテーションでは必須)
level ログ出力レベル
CRITICAL, ERROR, WARN, INFO, DEBUG
stdout 標準出力するか否か デフォルトは false
path ログファイルパス

logsetting.json のサンプル

{
  "default": {
      "format": "%(asctime)s.%(msecs)03d %(levelname)7s[%(processName)s:%(threadName)s] %(message)s",
      "dateformat": "%Y-%m-%d %H:%M:%S",
      "rotation": "daily",
      "maxHistory": 7,
      "level": "DEBUG",
      "path": "/var/log/project.log"
  },
  "alpha": {
      "format": "%(asctime)s.%(msecs)03d %(levelname)7s %(message)s",
      "dateformat": "%Y-%m-%d %H:%M:%S",
      "rotation": "daily",
      "maxHistory": 7,
      "level": "DEBUG",
      "stdout": true,
      "path": "/var/log/project.log"
  },
  "beta": {
      "format": "%(asctime)s.%(msecs)03d %(levelname)7s %(message)s",
      "dateformat": "%Y-%m-%d %H:%M:%S",
      "rotation": "size",
      "maxbytes": 1048576,
      "maxHistory": 4,
      "level": "INFO",
      "stdout": false,
      "path": "/var/log/sample.log"
  }
}

logsetting.json と同じディレクトリに、以下、logger.py を用意する。

# -*- coding: UTF-8 -*-
#  logger.py : logsetting.json によるロガー
#
from logging import Formatter, handlers, StreamHandler, getLogger, CRITICAL, ERROR, WARN, INFO, DEBUG
import os
from pathlib import Path
import codecs
import json

class Logger:
    def __init__(self, name='default'):
        leveldict = {"DEBUG": DEBUG, "INFO": INFO, "WARN": WARN, "ERROR": ERROR, "CRITICAL": CRITICAL}
        with codecs.open("%s/logsetting.json" % Path(os.path.abspath(__file__)).parent.resolve(), 'r', 'utf-8') as f:
            self.logger = getLogger(name)
            if len(self.logger.handlers) > 0:
                return
            setting = json.load(f, strict=False)
            if name=='default':
                if not 'default' in setting: raise RuntimeError('logsetting.json ERROR default Not Found')
            dict = setting[name] if name in setting else setting['root']
            if not 'format' in dict: raise RuntimeError('logsetting.json ERROR format Not Found at %s' % name)
            if not 'dateformat' in dict: raise RuntimeError('logsetting.json ERROR dateformat Not Found at %s' % name)
            if not 'maxHistory' in dict: raise RuntimeError('logsetting.json ERROR maxHistory Not Found at %s' % name)
            self.logger.setLevel(leveldict[dict['level']] if 'level' in dict else INFO)
            formatter = Formatter(fmt=dict['format'], datefmt=dict['dateformat'])
            if dict['rotation']=='daily':
                # 時刻ローテーション
                handler = handlers.TimedRotatingFileHandler(filename=dict['path'], encoding='UTF-8', when='D',
                                                            backupCount=dict['maxHistory'] if 'maxHistory' in dict else 7)
            elif dict['rotation']=='size':
                # サイズローテーション
                if not 'maxbytes' in dict: raise RuntimeError('logsetting.json ERROR maxbytes Not Found at %s' % name)
                handler = handlers.RotatingFileHandler(filename=dict['path'], encoding='UTF-8',
                                                      maxBytes=dict['maxbytes'] if 'maxbytes' in dict else 1048576,
                                                      backupCount=dict['maxHistory'] if 'maxHistory' in dict else 3)
            else:
                raise RuntimeError('logsetting.json ERROR rotation Not Found')
            # ログファイル設定
            handler.setLevel(leveldict[dict['level']] if 'level' in dict else INFO)
            handler.setFormatter(formatter)
            self.logger.addHandler(handler)
            if dict['stdout'] if 'stdout' in dict else False:
                # 標準出力用 設定
                sthandler = StreamHandler()
                sthandler.setLevel(dict['level'] if 'level' in dict else INFO)
                sthandler.setFormatter(formatter)
                self.logger.addHandler(sthandler)

    def debug(self, msg):
        self.logger.debug(msg)
    def info(self, msg):
        self.logger.info(msg)
    def warn(self, msg):
        self.logger.warning(msg)
    def error(self, msg, stack_info=False):
        self.logger.error(msg, stack_info=stack_info)
    def critical(self, msg):
        self.logger.critical(msg, stack_info=True)

この logger.py が示す中で注目すべきは、
ロガーを生成して呼出し元で生成いたロガーに伝播させない方法に、propagate 属性を False にする方法があるが、
logger = logging.getLogger(name)
logger.propagate = False

これが、効力を持たないことがある。
self.logger に getLogger した後、既に addHandler でハンドラ追加されている数を
len関数で調べて、コンストラクタを抜けるようにしている。

使用例、
from インポート文は、状況に合わせるとして、

from logger import Logger

logger = Logger()
logger.error("メッセージ  ERROR")
logger.warn("メッセージ  WARN")
logger.info("メッセージ  INFO")
logger.debug("メッセージ  DEBUG")
from logger import Logger

logger = Logger(’alpha')
logger.error("メッセージ  ERROR")
logger.warn("メッセージ  WARN")
logger.info("メッセージ  INFO")
logger.debug("メッセージ  DEBUG")