【Python】自作loggerの作り方

こんにちは、にわこまです!

今回はloggerを自作します。pythonにはloggingというログ関連の機能をまとめたライブラリがあります。しかし、一般的にはloggingライブラリをそのまま使用するのは非推奨です。ゆえに、自分専用のloggerを作成していきます!

 

本サイトで紹介するソースコードを以下に示します。

 

 

スポンサードサーチ


実現したいこと

1.自分専用のloggerを作成すること

2.フォーマットを変更すること

3.exceptionが発生した行/関数名/ファイル名を出力すること

 

1は先述した通り、loggingライブラリをそのまま使用するのは非推奨であるため自分専用のloggerを作成したいです。

 

2はデフォルトのフォーマットが読みづらいという理由からフォーマットを変更したいです。

 

3はログ出力した行とexceptionが発生した行に差があります。そのため動作確認やログ解析のときに使いにくいです。ゆえに、exceptionが発生した行/関数名/ファイル名を出力したいです。

 

 

他に「こんなこと」や「あんなこと」できないのか?と疑問がある方はメールにて教えてください!

 

 

logger version 1

フォーマットの変更を実現した自分専用loggerのソースコードを以下に示します。

from logging import getLoggerClass
from logging import INFO
from logging import Formatter
from logging import StreamHandler


class Logger(getLoggerClass()):
    def __init__(self):
        super().__init__(__name__)
        self.setLevel(INFO)
        self.propagate = False
        for hdlr in self.handlers:
            self.removeHandler(hdlr)
        self.add_handler_info()
    def add_handler_info(self):
        format = "[%(levelname)s] [%(filename)s] [%(funcName)s] [%(lineno)d] %(message)s"
        formatter = Formatter(format)
        handler = StreamHandler()
        handler.setLevel(INFO)
        handler.setFormatter(formatter)
        self.addHandler(handler)

 

 

ソースコードの解説

1行目から4行目の「import文」は、自分専用のloggerクラスを作成するのに必要な、関数や定数、クラスをインポートしています。

 

7行目から21行目の「Loggerクラス」は自分専用のloggerクラスです。

 

8行目から14行目の「__init__関数」は初期処理を実行しています。

 

10行目の「setlevel関数」は出力するログレベルを設定しています。今回は「INFO」を設定しているため、INFO以上のログを出力します。具体的には「INFO」「WARNING」「ERROR」「CRITICAL」のログを出力します。

 

11行目の「propagate」はログの伝播を許可しないように設定しています。

 

12行目と13行目はデフォルトで設定されているフォーマットを削除しています。この処理を実施しないと、同じメッセージのログが複数回出力されてしまいます。

handlersはログ出力時に使用するフォーマットが格納されているリストです。

 

14行目は後述する出力フォーマットを追加しています。

 

15行目から21行目の「add_handler_info関数」はINFO時の出力フォーマットを追加しています。

 

levelname」はログレベルの名前のことです。例えば、INFOやWARNING、ERROR、CRITICALのことです。

filename」はログ出力が実行されたファイル名のことです。

funcName」はログ出力が実行された関数名のことです。

lineno」はログ出力が実行された行番号のことです。

message」はログ出力時に代入する文字列のことです。

 

以下に出力例を示します。

[INFO] [logger_test01.py] [parent] [6] Start.
=====
levelnameは「INFO」です。
filenameは「logger_test01.py」です。
funcNameは「parent」です。
linenoは「6」です。
messageは「Start.」です。

 

17行目はフォーマットオブジェクトを作成しています。

18行目はハンドラーオブジェクトを作成しています。

19行目はハンドラーにログレベルを設定しています。今回はINFOを設定しているため、INFO以上のログ出力時に先述したフォーマットでログが出力されます。

20行目はハンドラーにフォーマットを設定しています。

 

21行目はLoggerクラスが使用するハンドラーリストにハンドラーを追加しています。つまり、フォーマットを追加しています。

 

 

使用例

先述のLoggerクラスを使用したソースコードを以下に示します。

from logger_v1 import Logger

logger = Logger()

def parent():
    logger.info("Start.")
    child()
    logger.info("End.")

def child():
    logger.info("Start.")
    try:
        d = {}
        print(d["title"])
    except Exception as e:
        msg = "{0}: {1}".format(e.__class__.__name__, e)
        logger.error(msg)
    logger.info("End.")

if __name__ == "__main__":
    parent()

 

上記ファイルを実行して出力したログを以下に示します。

[INFO] [logger_v1_test.py] [parent] [6] Start.
[INFO] [logger_v1_test.py] [child] [11] Start.
[ERROR] [logger_v1_test.py] [child] [17] KeyError: 'title'
[INFO] [logger_v1_test.py] [child] [18] End.
[INFO] [logger_v1_test.py] [parent] [8] End.

 

 

問題点

上記の使用例から分かる通りエラーログの行番号が、exceptionが発生した行番号ではなくエラーログの出力を実行した行番号になっています。

 

次に解説するloggerでは上記の問題を解決します。

 

 

スポンサードサーチ


logger version 2

フォーマットを追加した自分専用loggerのソースコードを以下に示します。

from logging import getLoggerClass
from logging import INFO, ERROR
from logging import Formatter
from logging import StreamHandler
import sys


class Logger(getLoggerClass()):
    def __init__(self):
        super().__init__(__name__)
        self.setLevel(INFO)
        self.propagate = False
        for hdlr in self.handlers:
            self.removeHandler(hdlr)
        self.add_handler_info()
        self.add_handler_exc()
    def add_handler_info(self):
        format = "[%(levelname)s] [%(filename)s] [%(funcName)s] [%(lineno)d] %(message)s"
        formatter = Formatter(format)
        handler = StreamHandler()
        handler.setLevel(INFO)
        handler.setFormatter(formatter)
        self.addHandler(handler)
    def add_handler_exc(self):
        format = "[%(levelname)s] [%(filename)s] [%(funcName)s] [%(exc_lineno)d] %(message)s"
        formatter = Formatter(format, defaults={"exc_lineno": 0})
        handler = StreamHandler()
        handler.setLevel(ERROR)
        handler.setFormatter(formatter)
        self.addHandler(handler)
    def error(self, msg, *args, **kwargs):
        stacklevel = kwargs.get("stacklevel", 1)
        kwargs["stacklevel"] = stacklevel + 1
        if(kwargs.get("exc_info", None)):
            exc_type, exc_value, exc_tb = sys.exc_info()
            exc_lineno = exc_tb.tb_lineno
            kwargs["extra"] = {
                "exc_lineno": exc_lineno
            }
            super().error(msg, *args, **kwargs)
            return
        super().error(msg, *args, **kwargs)

 

 

ソースコードの解説

1行目から5行目の「import文」は、自分専用のloggerクラスを作成するのに必要な、関数や定数、クラスをインポートしています。

 

以降はversion 1から追加したコードのみを解説します。

 

16行目は後述する出力フォーマットを追加しています。

 

24行目から30行目の「add_handler_exc」はexception発生時の出力フォーマットを追加しています。

「levelname」はログレベルの名前のことです。例えば、INFOやWARNING、ERROR、CRITICALのことです。

「filename」はログ出力が実行されたファイル名のことです。

「funcName」はログ出力が実行された関数名のことです。

exc_lineno」はログ出力時に代入するexceptionが発生した行番号のことです。

「message」はログ出力時に代入する文字列のことです。

 

以下に出力例を示します。

[ERROR] [logger_v2_test.py] [child] [17] KeyError: 'title'
=====
levelnameは「ERROR」です。
filenameは「logger_test02.py」です。
funcNameは「child」です。
exc_linenoは「17」です。
messageは「KeyError: 'title'」です。

 

26行目はフォーマットオブジェクトを作成しています。

27行目はハンドラーオブジェクトを作成しています。

28行目はハンドラーにログレベルを設定しています。今回はERRORを設定しているため、ERROR以上のログ出力時に先述したフォーマットでログが出力されます。

29行目はハンドラーにフォーマットを設定しています。

 

30行目はLoggerクラスが使用するハンドラーリストにハンドラーを追加しています。つまり、フォーマットを追加しています。

 

31行目から42行目の「error」はerror関数をカスタマイズしています。

 

32行目と33行目はstacklevelを取得し、stacklevelの値を1増やしています。

stacklevelは関数名やファイル名、行番号の情報を参照する際に用いられる値です。デフォルトは1であり、関数名やファイル名、行番号はerror関数が実行された箇所の情報を参照します。
stacklevelの値を1増やすことでerror関数が呼ばれた箇所の情報を参照するようにすることができます。

 

34行目のif文はkwargsにexc_infoが設定されているか確認しています。exc_infoが設定されている場合は、35行目から41行目を実行します。

 

35行目の「sys.exc_info()」はexceptionが発生した箇所の情報を取得しています。

 

36行目の「exc_lineno」はexceptionが発生した箇所の行番号が代入されています。

 

37行目から39行目はkwargsのextraにexc_linenoを設定しています。

 

40行目は親クラスのerror関数を実行しています。

 

41行目はerror関数を終了しています。

 

42行目は親クラスのerror関数を実行しています。

 

 

使用例

先述のLoggerクラスを使用したソースコードを以下に示します。

from logger_v2 import Logger

logger = Logger()

def parent():
    logger.info("Start.")
    child()
    logger.info("End.")

def child():
    logger.info("Start.")
    try:
        d = {}
        print(d["title"])
    except Exception as e:
        msg = "{0}: {1}".format(e.__class__.__name__, e)
        logger.error(msg)
    logger.info("End.")

if __name__ == "__main__":
    parent()

 

上記ファイルを実行して出力したログを以下に示します。

[INFO] [logger_v2_test.py] [parent] [6] Start.
[INFO] [logger_v2_test.py] [child] [11] Start.
[ERROR] [logger_v2_test.py] [child] [17] KeyError: 'title'
[ERROR] [logger_v2_test.py] [child] [0] KeyError: 'title'
[INFO] [logger_v2_test.py] [child] [18] End.
[INFO] [logger_v2_test.py] [parent] [8] End.

 

 

問題点

error関数を1回しか実行していないのに2行出力されています。

これはINFO用のフォーマットでも出力されているため発生しています。

 

次に解説するloggerでは上記の問題を解決します。

 

 

logger version 3

フォーマットを追加した自分専用loggerのソースコードを以下に示します。

from logging import LogRecord, getLoggerClass
from logging import INFO, ERROR
from logging import Formatter
from logging import StreamHandler
from logging import Filter
import sys


class InfoFilter(Filter):
    def filter(self, record: LogRecord) -> bool:
        return not(record.exc_info)

class ExcFilter(Filter):
    def filter(self, record: LogRecord) -> bool:
        tof = record.exc_info
        record.exc_info = None
        return tof

class Logger(getLoggerClass()):
    def __init__(self):
        super().__init__(__name__)
        self.setLevel(INFO)
        self.propagate = False
        for hdlr in self.handlers:
            self.removeHandler(hdlr)
        self.add_handler_info()
        self.add_handler_exc()
    def add_handler_info(self):
        format = "[%(levelname)s] [%(filename)s] [%(funcName)s] [%(lineno)d] %(message)s"
        formatter = Formatter(format)
        handler = StreamHandler()
        handler.setLevel(INFO)
        handler.setFormatter(formatter)
        handler.addFilter(InfoFilter())
        self.addHandler(handler)
    def add_handler_exc(self):
        format = "[%(levelname)s] [%(filename)s] [%(funcName)s] [%(exc_lineno)d] %(message)s"
        formatter = Formatter(format, defaults={"exc_lineno": 0})
        handler = StreamHandler()
        handler.setLevel(ERROR)
        handler.setFormatter(formatter)
        handler.addFilter(ExcFilter())
        self.addHandler(handler)
    def error(self, msg, *args, **kwargs):
        # 省略

 

 

ソースコードの解説

1行目から6行目の「import文」は、自分専用のloggerクラスを作成するのに必要な、関数や定数、クラスをインポートしています。

 

以降はversion 2から追加したコードのみを解説します。

 

9行目から11行目の「InfoFilter」はINFO用のフォーマットを使用する際に実行されるフィルタークラスです。

10行目の「filter関数」がtrueを返した場合のみINFO用のフォーマットでログが出力されます。レコードにexc_infoが設定されていない場合はtrueを返したいため、「not(record.exc_info)」という条件にしています。

 

13行目から17行目の「ExcFilter」はexceptionが発生した場合のフォーマットを使用する際に実行されるフィルタ―クラスです。

14行目の「filter関数」がtrueを返した場合のみexceptionが発生した場合のフォーマットでログが出力されます。レコードのexc_infoが設定されている場合はtrueを返したいため「record.exc_info」という条件にしています。

 

16行目はレコードに含まれているexc_infoをNoneにしています。

 

上記フィルタークラスはそれぞれ34行目の42行目でハンドラーオブジェクトに設定しています。

 

 

使用例

先述のLoggerクラスを使用したソースコードを以下に示します。

from logger_v3 import Logger

logger = Logger()

def parent():
    logger.info("Start.")
    child()
    logger.info("End.")

def child():
    logger.info("Start.")
    try:
        d = {}
        print(d["title"])
    except Exception as e:
        msg = "{0}: {1}".format(e.__class__.__name__, e)
        logger.error(msg)
    logger.info("End.")

if __name__ == "__main__":
    parent()

 

上記ファイルを実行して出力したログを以下に示します。

[INFO] [logger_v2_test.py] [parent] [6] Start.
[INFO] [logger_v2_test.py] [child] [11] Start.
[ERROR] [logger_v2_test.py] [child] [17] KeyError: 'title'
[INFO] [logger_v2_test.py] [child] [18] End.
[INFO] [logger_v2_test.py] [parent] [8] End.

 

 

スポンサードサーチ


まとめ

今回はloggerを自作しました。自分専用のloggerを作成することで、フォーマットを変更することができたり、ログレベルによってフォーマットを使い分けられたり、出力内容を編集することができました。

 

以下に参考にさせていただいたサイトを紹介します。

 

本サイトで紹介したソースコードを以下に示します。

 

最後までお読みいただきありがとうございます。


スポンサードサーチ