python

PythonのWARNINGレベル以上のログをstderrに出力する

2021-10-18

コンソール画面に出力しているログのうち、INFOレベル以下はstdoutに、WARNINGレベル以上をstderrに出力するサンプルを紹介します。

一般的なコンソールはstderrの出力を強調表示してくれるので見落としが減ります。この設定はしないよりはしておいた方がいいです。

ロギングの細かい解説は長期戦になるのでしないことにして、ここで紹介したサンプルの一部をコピペして流用するのに最低限の最低限、必要かなと感じる部分だけサラッと解説するスタンスでいきます。

よくわからない部分は雰囲気でなんとかしてもらうスタンスで。(笑)

Pythonプログラム内で設定

Pythonプログラム内で全ての設定をするサンプルです。(次の章では同じことを設定ファイルでする方法を紹介します)

実際の設定はinit_logging()関数内でしています。

出力先をstdoutとstderrに分けないといけないので、INFOレベルまでのログだけをstdoutに出力するハンドラと、WARNINGレベル以上のログだけをstderrに出力するハンドラの2つを作っています。

from logging import getLogger, StreamHandler, \
    Filter, Formatter, DEBUG, INFO, WARNING
from sys import stdout, stderr


root = getLogger()


class LevelFilter(Filter):
    """ max_levelで指定されたログレベルまでしか出力しなくするフィルタを自作 """

    def __init__(self, max_level=INFO):
        self.max_level = max_level

    def filter(self, record):
        """ 
        このメソッドはログ出力メソッドがコールされた時に呼び出されます。

        役割は、0か1を戻り値として返すことです。
        戻り値が0の場合はログが出力されず、それ以外の場合はログ出力されます。
        """
        return record.levelno <= self.max_level


def init_logging():
    global root

    formatter = Formatter(
        '{asctime} {name:<8s} {levelname:<8s} {message}',
        style='{')
    out_handler = StreamHandler(stdout)
    out_handler.setFormatter(formatter)
    # WARNING以上がstdoutに出力されないように自作したフィルタをセット
    out_handler.addFilter(LevelFilter(INFO))

    err_handler = StreamHandler(stderr)
    err_handler.setLevel(WARNING)
    err_handler.setFormatter(formatter)

    root.addHandler(out_handler)
    root.addHandler(err_handler)
    root.setLevel(DEBUG)


def main():
    global root

    root.debug('これはデバッグログ')
    root.info('これは情報ログ')
    root.warning('これは警告ログ')
    root.error('これはエラーログ')
    root.critical('これはめっちゃヤバい状況のログ')


if __name__ == '__main__':
    init_logging()
    main()

実行結果:

設定ファイルで設定

ログ出力などの設定は、プログラムに直接書かずに設定ファイルに記述するのが一般的です。

まずはconfig.ymlを作成し、設定をYAML形式で記述します。

version: 1

formatters:
  default:
    format: '{asctime} {name:<8s} {levelname:<8s} {message}'
    style: '{'

filters:
  at-most-info:
    '()': filter.LevelFilter
    max_level: INFO

handlers:
  stdout:
    class: logging.StreamHandler
    stream: ext://sys.stdout
    formatter: default
    filters:
      - at-most-info

  stderr:
    class: logging.StreamHandler
    stream: ext://sys.stderr
    formatter: default
    level: WARNING

root:
  level: DEBUG
  handlers:
    - stdout
    - stderr

config.ymlからfilter.LevelFilterとして参照しやすいように、フィルタークラスはメインプログラムとは分けてfilter.pyに定義しました。

from logging import Filter, INFO, getLevelName


class LevelFilter(Filter):
    def __init__(self, max_level=INFO):
        if isinstance(max_level, str):
            max_level = getLevelName(max_level)
        self.max_level = max_level

    def filter(self, record):
        return record.levelno <= self.max_level

設定ファイルを読み込んでDict形式にするために、ruamel.yamlパッケージをインストールします。

$ pip install ruamel.yaml

そして設定ファイルから読み込んでロギング設定を反映するPythonプログラムconsole_output.pyを書きます。

from logging import getLogger
from ruamel.yaml import YAML
from pathlib import Path
from logging.config import dictConfig


def init_logging():
    config_path = Path('config.yml')
    config_content = YAML().load(config_path)
    dictConfig(config_content)


def main():
    root = getLogger()
    root.debug('これはデバッグログ')
    root.info('これは情報ログ')
    root.warning('これは警告ログ')
    root.error('これはエラーログ')
    root.critical('これはめっちゃヤバい状況のログ')


if __name__ == '__main__':
    init_logging()
    main()

設定をするためのinit_logging()関数の中身がかなりスッキリしました。

それぞれのファイルは同じフォルダに入っている状態です。

project_dir
  |- config.yml
  |- filter.py
  |- console_output.py

実行結果:

-python

© 2022 ヂまるBlog