【Python】自作loggerの作り方
こんにちは、にわこまです!
今回はloggerを自作します。pythonにはloggingというログ関連の機能をまとめたライブラリがあります。しかし、一般的にはloggingライブラリをそのまま使用するのは非推奨です。ゆえに、自分専用のloggerを作成していきます!
本サイトで紹介するソースコードを以下に示します。
【Python】自作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時の出力フォーマットを追加しています。
16行目は出力フォーマットを定義しています。詳しくはこちらを参照してください。
「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発生時の出力フォーマットを追加しています。
25行目は出力フォーマットを定義しています。詳しくはこちらを参照してください。
「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を作成することで、フォーマットを変更することができたり、ログレベルによってフォーマットを使い分けられたり、出力内容を編集することができました。
以下に参考にさせていただいたサイトを紹介します。
本サイトで紹介したソースコードを以下に示します。
【Python】自作loggerの作り方 - ソースコード
最後までお読みいただきありがとうございます。
スポンサードサーチ