概要
今回はPython標準パッケージであるloggingモジュールの基本的な操作と、ロギングの操作を辞書型で設定できる「dictConfig」について学んだので共有したいと思います。
というのも、わたくしが使っているWebフレームワークのDjangoではロギングの設定を辞書型で定義しているので、ジャンガーにとっては必須のスキルかと思われます。
実行環境 |
---|
Windows Subsystem for Linux |
Python 3.6.9 |
ロギングで何をするのか?
ロギングのモジュールに初めて触れ時は、一体全体このモジュールは何をするためにあってどう使うのか?という疑問を持ったまま距離を置いていました。
Pythonの公式ドキュメントを見ても今一理解できなかったりしました。
距離を縮めるきっかけとなったのは、WebフレームワークのDjangoを使っている時で、ユーザーのアクセスやエラーなどが発生した際のログをファイル等に保存したり、特定の処理がしっかり通るか監視したりと、ロギング機能に少しずつ触れているときでした。
ログレベルという1~5段階のログを表示することができ、ソースコードの任意の場所に挿入することによってログを取ることができる。print()
文で良いのでは?と思っていたが、WebフレームワークのDjango然り、loggingモジュールを学んで行くにつれてprint()
文では良くないという事が分かりました。
簡単なデバッグとしてならprint()
文で処理を確認するには良い方法ですが、大規模且つ複雑な処理になればなるほどログを取る重要性が増してくることだと分かりました。
しかも、print()
文で確認したところでそのコードを残しておくわけにはいきません。
loggingモジュールを使うことによって、ログレベル(デバッグ、インフォ、ワーニング、エラー、クリティカル)による出力ができるので、デバッグならデバック、インフォならインフォと1つの値だけを切り替えてメッセージを確認することができます。
個人的にはコードチェッカーのリント、そしてユニットテストとロギングは同じ立ち位置にあって、ソフトウェアの信用性を保つために必要なツールである。と言うことが分かりました。
loggingモジュールの操作方法は様々ありますが、単純なログ出力は関数を使う事です。
import logging
logging.debug('debug message')
logging.info('info message')
logging.warn('warning message')
logging.error('error message')
logging.critical('critical message')
WARNING:root:warn message
ERROR:root:error message
CRITICAL:root:critical message
上記の結果では、「WAENING」から「CRITICAL」までのログレベルのメッセージが表示されました。
出力結果のフォーマットは以下です。
ログレベル:ロガー名:メッセージ
WARNING:root:warn message
log関数によるログメッセージの出力も同じような結果が得られます。
logging.log(ログレベルの番号, メッセージ)
のように定義します。
import logging
logging.log(logging.DEBUG, 'debug')
print(logging.DEBUG)
logging.log(logging.INFO, 'info')
print(logging.INFO)
logging.log(logging.WARNING, 'warning')
print(logging.WARNING)
logging.log(logging.ERROR, 'error')
print(logging.ERROR)
logging.log(logging.CRITICAL, 'critical')
print(logging.CRITICAL)
10
20
WARNING:root:warning
30
ERROR:root:error
40
CRITICAL:root:critical
50
上記の番号はロギングレベルに対応された番号です。
ログレベルの表は以下のリンク先に掲載されています。
ロギングレベルは全部で5種類(厳密には6種類)あり、デフォルトでは「WARNING」から上のレベルのログが出力されています。
デフォルト値を変更するにはbasicConfig関数を使用します。
ログが出力される際は、ルートロガーである「basicConfig」関数が毎回呼ばれています。
なので「basicConfig」の設定値を変更してあげると好きなログレベルを出力させることができます。
デフォルトのログレベルは「WARNING」となっているので、「DEBUG」のログレベルから出力されるようにセットしてみます。
import logging
logging.basicConfig(level=logging.DEBUG) # 10でもOK
logging.debug('debug message')
logging.info('info message')
logging.warn('warning message')
logging.error('error message')
logging.critical('critical message')
DEBUG:root:debug message
INFO:root:info message
WARNING:root:warning message
ERROR:root:error message
CRITICAL:root:critical message
他にも、ログの出力フォーマットの設定・ログファイルとしての書き出しなんかも設定することができます。
ログの出力フォーマットを変更するには、basicConfig(format)
引数に、ある決められた文字列を与えます。
決められた文字列とは、%(message)s
のようなフォーマットです。
文字列として与えられるフォーマットの一覧は以下のドキュメントに掲載されています。
basicConfigのデフォルトフォーマットは「ログレベル:ロガー名:メッセージ」となっていたので、先頭に日付を設けたいと思います。
import logging
logging.basicConfig(
format='%(asctime)s:%(levelname)s:%(name)s:%(message)s', # フォーマットの設定
level=logging.DEBUG, # ログレベル
)
logging.debug('debug message')
logging.info('info message')
logging.warn('warning message')
logging.error('error message')
logging.critical('critical message')
2024-07-22 11:16:42,953:DEBUG:root:debug message
2024-07-22 11:16:42,953:INFO:root:info message
2024-07-22 11:16:42,953:WARNING:root:warning message
2024-07-22 11:16:42,953:ERROR:root:error message
2024-07-22 11:16:42,953:CRITICAL:root:critical message
日付のフォーマットに関してはミリ秒単位まで出力されるようになっているので、日付自体のフォーマットを変更したい場合はbasicConfig(datefmt)
の引数にdatetimeモジュールで使われている書式を設定します。
ミリ秒を除外し、「日」と「時刻」の間に曜日を設定してみます。
import logging
logging.basicConfig(
format='%(asctime)s:%(levelname)s:%(name)s:%(message)s',
datefmt='%Y-%m-%d %a %H:%M:%S', # 日付自体のフォーマット設定
level=logging.DEBUG,
)
2024-07-22 Mon 11:31:40:DEBUG:root:debug message
2024-07-22 Mon 11:31:40:INFO:root:info message
2024-07-22 Mon 11:31:40:WARNING:root:warning message
2024-07-22 Mon 11:31:40:ERROR:root:error message
2024-07-22 Mon 11:31:40:CRITICAL:root:critical message
フォーマットのスタイルは%(message)s
のように定義していましたが、他のスタイルに変更したい場合はbasicConfig(style)
の引数に{
か$
のどちらかの記号文字列を指定します。
{
を与えた場合は以下のようなフォーマットで定義することができます。
logging.basicConfig(
format='{asctime}:{levelname}:{name}:{message}',
style='{', # フォーマットのスタイル
)
$
を与えた場合は以下のようなフォーマットで定義することができます。
logging.basicConfig(
format='$asctime:$levelname:$name:$message',
style='$',
)
basicConfig関数のデフォルトでは、print()
文のようにコンソールへ出力されるようになっています。
それはデフォルトで使用されているStreamHandlerというロギングハンドラーの1つです。
コンソールではなくファイルへログを書き込みたい場合は、basicConfig(filename)
の引数にファイル名を定義します。
filename
の引数に値が設定されると、デフォルトのハンドラーであるStreamHandlerからFileHandlerへと切り替わります。
debug.log
というファイルにログを書き込んでいきます。
import logging
logging.basicConfig(
filename='debug.log', # ログの書き込み先ファイル名
level=logging.DEBUG,
)
logging.debug('debug message')
logging.info('info message')
logging.warn('warning message')
logging.error('error message')
logging.critical('critical message')
実行してもコンソールには何も表示されません。
代わりに、カレントディレクトリにあるdebug.log
の中身を確認してみます。
DEBUG:root:debug message
INFO:root:info message
WARNING:root:warning message
ERROR:root:error message
CRITICAL:root:critical message
もう一度ログを実行するとログが追加されていくと思います。
DEBUG:root:debug message
INFO:root:info message
WARNING:root:warning message
ERROR:root:error message
CRITICAL:root:critical message
DEBUG:root:debug message
INFO:root:info message
WARNING:root:warning message
ERROR:root:error message
CRITICAL:root:critical message
追加するのではなく、毎回新規に書き出したい場合は、basicConfig(filemode)
の引数にw
モードを設定します。
import logging
logging.basicConfig(
filename='debug.log',
filemode='w', # デフォルトは'a'
level=logging.DEBUG,
)
logging.debug('debug message')
logging.info('info message')
logging.warn('warning message')
logging.error('error message')
logging.critical('critical message')
debug.log
ファイルの中身を確認してみます。
DEBUG:root:debug message
INFO:root:info message
WARNING:root:warning message
ERROR:root:error message
CRITICAL:root:critical message
basicConfig関数はルートロガーなので、全体のログに対して影響を及ぼします。
個別やファイルごとにログを取りたいとなった場合は、getLogger関数を使用すると良いでしょう。
getLogger関数の戻り値はLoggerオブジェクトとなるので、様々な用途に変形させることができます。
まずは単純なログを出力するために各種ログレベルのメソッドを使います。
import logging
# logger = logging.getLogger(name=__name__) # モジュール名が付けられる(公式推奨)
logger = logging.getLogger(name='logger')
logger.debug('debug message')
logger.info('info message')
logger.warn('warning message')
logger.error('error message')
logger.critical('critical message')
warning message
error message
critical message
上記の結果では、「WAENING」から「CRITICAL」までのログレベルのメッセージが表示されました。
出力結果のフォーマットは以下です。
メッセージ
warning message
logメソッドによるログメッセージの出力も同じような結果が得られます。
logger.log(ログレベルの番号, メッセージ)
のように定義します。
import logging
logger = logging.getLogger('logger')
logger.log(logging.DEBUG, 'debug')
print(logging.DEBUG)
logger.log(logging.INFO, 'info')
print(logging.INFO)
logger.log(logging.WARNING, 'warning')
print(logging.WARNING)
logger.log(logging.ERROR, 'error')
print(logging.ERROR)
logger.log(logging.CRITICAL, 'critical')
print(logging.CRITICAL)
10
20
warning
30
error
40
critical
50
上記の番号はロギングレベルに対応された番号です。
ログレベルの表は以下のリンク先に掲載されています。
ロギングレベルは全部で5種類(厳密には6種類)あり、デフォルトでは「WARNING」から上のレベルのログが出力されています。
ログレベルの変更やフォーマットの設定、ファイルへの書き出しも行えます。
コンソールによるログ出力はStreamHandlerクラスを使用して設定します。
デフォルトでもStreamHandlerクラスによるコンソールへの出力設定ですが、ログレベルやフォーマットを変更したい場合に再設定します。
まずは、ロガーオブジェクト自体のログレベルを変更します。
logger.setLeve(level)
メソッドにログレベルを設定します。
import logging
logger = logging.getLogger('logger')
logger.setLevel(level=logging.DEBUG) # オブジェクト自体にログレベルを設定
次にStreamhandlerクラスのオブジェクトを作成し、ハンドラーオブジェクト自体にもログレベルを設定します。
import logging
logger = logging.getLogger('logger')
logger.setLevel(level=logging.DEBUG)
sh = logging.StreamHandler() # ハンドラーオブジェクトの作成
sh.setLevel(logging.DEBUG) # ログレベルの設定
最後にlogger.addHandler(hdlr)
メソッドの引数にインスタン変数を与えます。
import logging
logger = logging.getLogger('logger')
logger.setLevel(level=logging.DEBUG)
sh = logging.StreamHandler()
sh.setLevel(logging.DEBUG)
logger.addHandler(hdlr=sh) # ハンドラーオブジェクトを追加
logger.debug('debug message')
logger.info('info message')
logger.warn('warning message')
logger.error('error message')
logger.critical('critical message')
debug message
info message
warning message
error message
critical message
ログレベルは、ロガーオブジェクトとハンドラーオブジェクト共に「DEBUG」に設定しているので、「DEBUG」から上のレベルが出力されています。
StreamHandlerオブジェクトに設定したログレベルは、あくまでロガーオブジェクトのログレベルの上で成り立っているので、ロガーオブジェクトのログレベルを「WARNING」とすると追加されたハンドラーオブジェクトのログレベルは「WARNING」から出力されるようになります。
import logging
logger = logging.getLogger('logger')
logger.setLevel(level=logging.WARNING) # ログレベルをWARNINGに設定
sh = logging.StreamHandler()
sh.setLevel(logging.DEBUG) # コンソールに出力するログレベルはDEBUGから上
logger.addHandler(hdlr=sh)
logger.debug('debug message')
logger.info('info message')
logger.warn('warning message')
logger.error('error message')
logger.critical('critical message')
warning message
error message
critical message
ファイルにログを書き出したい場合は、FileHandlerクラスを使用します。
まずは、ロガーオブジェクト自体のログレベルを変更します。
logger.setLeve(level)
メソッドにログレベルを設定します。
import logging
logger = logging.getLogger('logger')
logger.setLevel(level=logging.DEBUG) # オブジェクト自体にログレベルを設定
そしてFileHandler(filename)
の引数にファイル名を設定してから、setLevel(level)
メソッドの引数にログレベルを設定します。
import logging
logger = logging.getLogger('logger')
logger.setLevel(level=logging.DEBUG)
fh = logging.FileHandler(filename='error.log') # ファイル名の設定
fh.setLevel(level=logging.ERROR) # ログレベルの設定
最後にlogger.addHandler(hdlr)
メソッドの引数にインスタン変数を与えます。
import logging
logger = logging.getLogger('logger')
logger.setLevel(level=logging.DEBUG)
fh = logging.FileHandler(filename='error.log')
fh.setLevel(logging.ERROR)
logger.addHandler(hdlr=fh) # インスタンス変数を追加
logger.debug('debug message')
logger.info('info message')
logger.warn('warning message')
logger.error('error message')
logger.critical('critical message')
コンソールには出力されず、カレントディレクトリの「error.log」ファイルにログが書き出されます。
# error.log
error message
critical message
ファイルハンドラーのログレベルを「ERROR」としているので、「ERROR」から上のログレベルの出力結果が書き出されています。
FileHandlerのファイルモードはデフォルトでa
となっているので、再度実行すると後方にログが追加されます。
# error.log
error message
critical message
error message
critical message
追加ではなく、毎回新規作成する場合はFileHandler(mode)
の引数にw
を設定します。
fh = logging.FileHandler(
filename='error.log',
mode='w' # ファイルモードの設定
)
もちろん、ロガーオブジェクトのログレベルを引き上げてしまうとそれ以上のログレベルが書き出されます。
import logging
logger = logging.getLogger('logger')
logger.setLevel(level=logging.CRITICAL) # CRITICALに変更
fh = logging.FileHandler(
filename='error.log',
mode='w'
)
fh.setLevel(logging.ERROR)
logger.addHandler(hdlr=fh)
logger.debug('debug message')
logger.info('info message')
logger.warn('warning message')
logger.error('error message')
logger.critical('critical message')
# error.log
critical message
ログのフォーマットを変更したい場合は、Formatterクラスを使用します。
文字列として与えられるフォーマットの一覧は以下のドキュメントに掲載されています。
Formatter(fmt)
の引数に%(message)s
のようなスタイルでフォーマットを定義したオブジェクトを作成します。
import logging
logger = logging.getLogger(name='logger')
formatter = logging.Formatter(fmt='%(asctime)s:%(levelname)s:%(name)s:%(message)s') # フォーマッターオブジェクトの作成
ハンドラーにフォーマッターオブジェクトを与える必要があるので、StreamHandlerクラスを使用してコンソールに出力していきます。
StreamHandlerオブジェクトを作成したら、setFormatter(fmt)
メソッドの引数に作成したフォーマッターオブジェクトを与えます。
import logging
logger = logging.getLogger(name='logger')
logger.setLevel(level=logging.DEBUG)
formatter = logging.Formatter(fmt='%(asctime)s:%(levelname)s:%(name)s:%(message)s') # フォーマッターオブジェクト
sh = logging.StreamHandler() # ハンドラーオブジェクトの作成
sh.setFormatter(formatter) # フォーマットの設定
最後にロガーオブジェクトのaddHandler(hdlr)
メソッドの引数にハンドラーオブジェクトを与えます。
import logging
logger = logging.getLogger(name='logger') # ロガーオブジェクト
logger.setLevel(level=logging.DEBUG)
formatter = logging.Formatter(fmt='%(asctime)s:%(levelname)s:%(name)s:%(message)s') # フォーマッターオブジェクト
sh = logging.StreamHandler() # ハンドラーオブジェクト
sh.setFormatter(formatter)
logger.addHandler(hdlr=sh) # ハンドラーオブジェクトを与える
logger.debug('debug message')
logger.info('info message')
logger.warn('warning message')
logger.error('error message')
logger.critical('critical message')
2024-07-24 08:57:48,755:DEBUG:logger:debug message
2024-07-24 08:57:48,755:INFO:logger:info message
2024-07-24 08:57:48,755:WARNING:logger:warning message
2024-07-24 08:57:48,755:ERROR:logger:error message
2024-07-24 08:57:48,755:CRITICAL:logger:critical message
日付のフォーマットに関してはミリ秒単位まで出力されるようになっているので、日付自体のフォーマットを変更したい場合はFormatter(datefmt)
の引数にdatetimeモジュールで使われている書式を設定します。
ミリ秒を除外し、「日」と「時刻」の間に曜日を設定してみます。
formatter = logging.Formatter(
fmt='%(asctime)s:%(levelname)s:%(name)s:%(message)s',
datefmt='%Y-%m-%d %a %H:%M', # 日付のフォーマット
)
2024-07-24 Wed 09:19:DEBUG:logger:debug message
2024-07-24 Wed 09:19:INFO:logger:info message
2024-07-24 Wed 09:19:WARNING:logger:warning message
2024-07-24 Wed 09:19:ERROR:logger:error message
2024-07-24 Wed 09:19:CRITICAL:logger:critical message
フォーマットのスタイルは%(message)s
のように定義していましたが、他のスタイルに変更したい場合はFormatter(style)
の引数に{
か$
のどちらかの記号文字列を指定します。
$
か{
を与えた場合は以下のようなフォーマットで定義することができます。
formatter = logging.Formatter(
fmt='$asctime:$levelname:$name:$message',
style='$'
# fmt='{asctime}:{levelname}:{name}:{message}',
# style='{'
)
複数のハンドラー(StreamHandlerやFileHandler)を追加することもでき、用途に合わせた設定をすることもできます。
例えば、コンソールにはログレベル「INFO」以上を出力して、ファイルには「ERROR」ログから書き出すように設定しフォーマットもそれぞれ違った定義にします。
import logging
# ロガーの作成
# ログレベル「DEBUG」に設定
logger = logging.getLogger('logger') # ロガーオブジェクト
logger.setLevel(logging.DEBUG)
# コンソール・ファイル用フォーマットの設定
formatter_console = logging.Formatter(
fmt='%(asctime)s:%(levelname)s:%(name)s:%(message)s',
datefmt='%Y-%m-%d %a %H:%M',
)
formatter_file = logging.Formatter(
fmt='%(asctime)s:%(levelname)s:%(name)s:%(message)s',
)
# ハンドラーの作成
# コンソール用ハンドラー
sh = logging.StreamHandler()
sh.setLevel(logging.INFO)
sh.setFormatter(formatter_console)
# ファイル用ハンドラー
fh = logging.FileHandler(
filename='error.log',
mode='w'
)
fh.setLevel(logging.ERROR)
fh.setFormatter(formatter_file)
# 作成したハンドラーをロガーに追加
logger.addHandler(hdlr=sh)
logger.addHandler(hdlr=fh)
logger.debug('debug message')
logger.info('info message')
logger.warn('warning message')
logger.error('error message')
logger.critical('critical message')
2024-07-24 Wed 10:10:INFO:logger:info message
2024-07-24 Wed 10:10:WARNING:logger:warning message
2024-07-24 Wed 10:10:ERROR:logger:error message
2024-07-24 Wed 10:10:CRITICAL:logger:critical message
error.log
ファイルには「ERROR」ログ以上の出力が書き出されています。
# error.log
2024-07-24 10:10:24,094:ERROR:logger:error message
2024-07-24 10:10:24,095:CRITICAL:logger:critical message
コンソール出力用のmodule_console.py
とファイルに書き出し用のmodule_file.py
を作成し、それらのモジュールを呼び出してロギングを実行してみます。
こちらのモジュールは主に、基本情報をコンソールに出力するログを設定しています。
# module_console.py
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(filename)s:%(funcName)s:%(name)s:%(message)s')
sh = logging.StreamHandler()
sh.setLevel(logging.INFO)
sh.setFormatter(formatter)
logger.addHandler(sh)
def func_console():
logger.debug('debug message')
logger.info('info message')
logger.warn('warn message')
logger.error('error message')
logger.critical('critical message')
return None
こちらのモジュールは主にエラー以上のログを出力するように設定し、メッセージにエラー内容を出力して書き出すように設定しています。
# module_file.py
import logging
import traceback
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s%(message)s')
fh = logging.FileHandler('error.log', 'w')
fh.setLevel(logging.ERROR)
fh.setFormatter(formatter)
logger.addHandler(fh)
def func_file():
try:
1/0
except:
logger.error(traceback.format_exc())
return None
module_console.py
とmodule_file.py
を呼び出して実行してみます。
import module_console
import module_file
print('console -------------')
module_console.func_console()
print('file ----------------')
module_file.func_file()
console -------------
module_console.py:func_console:module_console:info message
module_console.py:func_console:module_console:warn message
module_console.py:func_console:module_console:error message
module_console.py:func_console:module_console:critical message
file ----------------
error.log
の中身を確認するとエラー内容が書き込まれています。
# error.log
2024-07-24 11:01:12,770Traceback (most recent call last):
File "/mnt/c/Users/Documentsr/module_file.py", line 16, in func_file
1/0
ZeroDivisionError: division by zero
getLogger(__name__)
でロガーオブジェクトを作成することができますが、同じ環境にbasicConfig()
関数が作成されると他のロガーにも出力の影響が出てしまいます。
例えばコンソールにログを出力するロガーオブジェクトとbasicConfig関数を作成してみます。
import logging
logging.basicConfig(level=logging.DEBUG) # ルートロガーの設定値
logger = logging.getLogger('logger') # ロガーの作成
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s:%(levelname)s:%(name)s:%(message)s')
sh = logging.StreamHandler()
sh.setLevel(logging.INFO)
sh.setFormatter(formatter)
logger.addHandler(sh)
print('logger -------------')
logger.debug('debug message')
logger.info('info message')
logger.warn('warning message')
logger.error('error message')
logger.critical('critical message')
print('root ---------------')
logging.debug('debug message')
logging.info('info message')
logging.warn('warning message')
logging.error('error message')
logging.critical('critical message')
logger -------------
DEBUG:logger:debug message
2024-07-24 11:57:09,807:INFO:logger:info message
INFO:logger:info message
2024-07-24 11:57:09,807:WARNING:logger:warning message
WARNING:logger:warning message
2024-07-24 11:57:09,807:ERROR:logger:error message
ERROR:logger:error message
2024-07-24 11:57:09,807:CRITICAL:logger:critical message
CRITICAL:logger:critical message
root ---------------
DEBUG:root:debug message
INFO:root:info message
WARNING:root:warning message
ERROR:root:error message
CRITICAL:root:critical message
ロガーオブジェクトのログが複数回出力されています。
basicConfig関数は「ルート」に当たるので、その「子」であるロガーにも設定値が伝播してしまいます。
ルートロガーの設定値が伝播しないようにするには、logger.propagate
属性に「False」値を与えます。
デフォルトでは「True」となっているので、basicConfig関数と共存した際にログメッセージが複数回呼ばれてしまうので、必要であれば設定します。
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('logger')
logger.propagate = False # ルートロガーの影響を受けない場合
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s:%(levelname)s:%(name)s:%(message)s')
sh = logging.StreamHandler()
sh.setLevel(logging.INFO)
sh.setFormatter(formatter)
logger.addHandler(sh)
print('logger -------------')
logger.debug('debug message')
logger.info('info message')
logger.warn('warning message')
logger.error('error message')
logger.critical('critical message')
print('root ---------------')
logging.debug('debug message')
logging.info('info message')
logging.warn('warning message')
logging.error('error message')
logging.critical('critical message')
logger -------------
2024-07-24 12:07:53,417:INFO:logger:info message
2024-07-24 12:07:53,417:WARNING:logger:warning message
2024-07-24 12:07:53,417:ERROR:logger:error message
2024-07-24 12:07:53,417:CRITICAL:logger:critical message
root ---------------
DEBUG:root:debug message
INFO:root:info message
WARNING:root:warning message
ERROR:root:error message
CRITICAL:root:critical message
ロガーにはフィルターを追加することができます。
フィルターを追加することによって、ログの情報が含まれているLogRecordインスタンスを操作することができ、ログ出力の挙動を変えることができます。
ではFilterクラスを継承してカスタムフィルターを作成します。
import logging
class LogFilter(logging.Filter):
def filter(self, record):
# recordにLogRecordインスタンスが与えられる
if record.levelname.lower() == 'warning':
# warningだった場合ログ出力しない
return False
elif record.levelname.lower() == 'critical':
# criticalだった場合ログ出力しない
return False
return True
ロガーに上記のLogFilterクラスを与えると、ログ情報が含まれた「LogRecordインスタンス」がfilter
メソッドの引数record
に与えられてログ情報を引き出すことができます。
ログレベルの数だけ繰り返しfilter
メソッドが呼ばれ、「True」の場合は出力し「False」の場合は出力されない動きとなります。
ロガーにカスタムフィルターのLogFilterクラスを与えてログを出力してみます。
import logging
class LogFilter(logging.Filter):
def filter(self, record):
if record.levelname.lower() == 'warning':
return False
elif record.levelname.lower() == 'critical':
return False
return True
logger = logging.getLogger('logger')
logger.setLevel(logging.DEBUG)
sh = logging.StreamHandler()
sh.setLevel(logging.DEBUG)
logger.addHandler(sh)
lf = LogFilter()
logger.addFilter(lf)
print('logger ------------')
logger.debug('debug')
logger.info('info')
logger.warn('warning')
logger.error('error')
logger.critical('critical')
logger ------------
debug
info
error
「WARNING」と「CRITICAL」以外のログが出力されました。
ロギングの設定値は、basicConfig関数やロガーオブジェクトからハンドラーオブジェクトを追加していく方法がありますが、他にもファイルに設定値を定義する方法や辞書に定義する方法もあります。
ここでは辞書に定義する方法で設定するのでdictConfigを使用していきます。
logging.config
モジュールをインポートし、dictConfig(config)
の引数に規定の辞書を設定します。
import logging
import logging.config
# ロギングの設定構成
LOGGING = {
'version': 1, # 後方互換性を保つための必須キー
}
logging.config.dictConfig(LOGGING) # 設定を読み込む
print('root -------------------')
logging.debug('debug')
logging.info('info')
logging.warn('warning')
logging.error('error')
logging.critical('critical')
root -------------------
WARNING:root:warning
ERROR:root:error
CRITICAL:root:critical
辞書のLOGGING
には何も設定していないので、ルートロガーはデフォルト値のログが出力されています。
辞書に設定している{version: 1}
は、設計上必要なバージョンであるため定義していない場合はエラーが返されます。
ValueError: dictionary doesn't specify a version
既定のキーを辞書に設定してログ出力を変更していきます。
ルートベースのロガーを設定するには、{root: ...}
キーを定義します。
ログレベルを「DEBUG」から出力できるように変更します。
LOGGING = {
'version': 1,
'root': {
'level': 'DEBUG',
},
}
logging.config.dictConfig(LOGGING)
print('root -------------------')
logging.debug('debug')
logging.info('info')
logging.warn('warning')
logging.error('error')
logging.critical('critical')
root -------------------
DEBUG:root:debug
INFO:root:info
WARNING:root:warning
ERROR:root:error
CRITICAL:root:critical
次に{handlers: ...}
キーを定義して、ルートロガーに追加するハンドラーを作成します。
ハンドラーにはStreamHandlerクラスを設定し、「INFO」から出力されるように定義します。
LOGGING = {
'version': 1,
'handlers': {
'console': { # ハンドラーの作成
'class': 'logging.StreamHandler',
'level': 'INFO',
},
},
"root": {
"level": "DEBUG",
"handlers": ["console"] # ハンドラーを追加
},
}
root -------------------
info
warning
error
critical
root
キーのlevel
では「DEBUG」から出力できるように設定していますが、handlers
キーに追加したconsole
のハンドラーには「INFO」から出力するように設定しているので、ログレベル「info」から出力されています。
そしてフォーマットは何も設定していないので、メッセージのみが出力されました。
次に{formatters: ...}
キーを定義して、{handlers: {console: ...}}
キーに渡すフォーマットを作成します。
LOGGING = {
'version': 1,
'formatters': {
'root_format': { # フォーマットの作成
'format': '%(levelname)s:%(name)s:%(message)s',
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'root_format', # フォーマットの追加
},
},
"root": {
"level": "DEBUG",
"handlers": ["console"]
},
}
root -------------------
INFO:root:info
WARNING:root:warning
ERROR:root:error
CRITICAL:root:critical
ルートロガーの設定値を変更する節で設定した辞書を使って、ロガーの設定値を変更していきます。
{loggers: ...}
キーを設定し、その中にlogger
の設定値を作成します。
まずルートベースの影響を受けないようにlogger : {propagate: False}
を定義し、ログレベルをlogger: {level: DEBUG}
にして「DEBUG」から出力できるように変更し、ハンドラーには既存のhandlers: {console: ...
を使用してlogger: {handlers: ['console']}
とします。
LOGGING = {
'version': 1,
'formatters': {
'root_format': {
'format': '%(levelname)s:%(name)s:%(message)s',
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'root_format',
},
},
"loggers":{
"logger": { # ロガーの作成
"propagate": False,
"level": "DEBUG",
"handlers": ["console"],
},
},
"root": {
"level": "DEBUG",
"handlers": ["console"],
},
}
logging.config.dictConfig(LOGGING)
logger = logging.getLogger('logger') # ロガーの作成
print('logger ----------------')
logger.debug('debug')
logger.info('info')
logger.warning('warning')
logger.error('error')
logger.critical('critical')
print('root -------------------')
logging.debug('debug')
logging.info('info')
logging.warn('warning')
logging.error('error')
logging.critical('critical')
logger ----------------
INFO:logger:info
WARNING:logger:warning
ERROR:logger:error
CRITICAL:logger:critical
root -------------------
INFO:root:info
WARNING:root:warning
ERROR:root:error
CRITICAL:root:critical
新たに「logger」専用のフォーマットを持ったハンドラーを作成して設定してみます。
LOGGING = {
'version': 1,
'formatters': {
'root_format': {
'format': '%(levelname)s:%(name)s:%(message)s',
},
'logger_format': { # logger_consoleのフォーマットを作成
'format': '%(asctime)s %(levelname)s:%(name)s:%(message)s',
'datefmt': '%Y-%m-%d %H:%M',
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'root_format',
},
'logger_console': { # loggerのハンドラーを作成
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'logger_format',
},
},
"loggers":{
"logger": {
"propagate": False,
"level": "DEBUG",
"handlers": ["logger_console"], # ハンドラーの追加
},
},
"root": {
"level": "DEBUG",
"handlers": ["console"],
},
}
logger ----------------
2024-07-27 11:55 INFO:logger:info
2024-07-27 11:55 WARNING:logger:warning
2024-07-27 11:55 ERROR:logger:error
2024-07-27 11:55 CRITICAL:logger:critical
root -------------------
INFO:root:info
WARNING:root:warning
ERROR:root:error
CRITICAL:root:critical
ログをファイルに書き出す用のハンドラーを作成して、「logger」のハンドラーに追加してみます。
ファイルに書き出すログは「ERROR」が発生した際に書き出されるように設定します。
LOGGING = {
'version': 1,
'formatters': {
'root_format': {
'format': '%(levelname)s:%(name)s:%(message)s',
},
'logger_format': {
'format': '%(asctime)s %(levelname)s:%(name)s:%(message)s',
'datefmt': '%Y-%m-%d %H:%M',
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'root_format',
},
'logger_console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'logger_format',
},
"file": { # ファイルハンドラーの作成
"level": "ERROR",
"class": "logging.FileHandler",
"filename": "error.log",
"mode": "w",
"formatter": 'logger_format', # logger_consoleと同じフォーマットを使用
},
},
"loggers":{
"logger": {
"propagate": False,
"level": "DEBUG",
"handlers": ["logger_console", "file"], # ファイルハンドラーの追加
},
},
"root": {
"level": "DEBUG",
"handlers": ["console"],
},
}
logger ----------------
2024-07-27 13:03 INFO:logger:info
2024-07-27 13:03 WARNING:logger:warning
2024-07-27 13:03 ERROR:logger:error
2024-07-27 13:03 CRITICAL:logger:critical
root -------------------
INFO:root:info
WARNING:root:warning
ERROR:root:error
CRITICAL:root:critical
file: {filename: error.log}
に設定したerror.log
には「ERROR」ログ以上が書き出されています。
# error.log
2024-07-27 13:03 ERROR:logger:error
2024-07-27 13:03 CRITICAL:logger:critical
以下の辞書設定でカスタムフィルターを使ったフィルタリングをしていきます。
import logging
import logging.config
LOGGING = {
'version': 1,
'formatters': {
'formatter': {
'format': '%(asctime)s:%(levelname)s:%(name)s:%(message)s',
'datefmt': '%Y-%m-%d %H:%M',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'formatter',
},
},
"loggers":{
"logger": {
"propagate": False,
"level": "DEBUG",
"handlers": ["console"],
},
},
"root": {
"level": "DEBUG",
"handlers": ["console"],
},
}
logging.config.dictConfig(config=LOGGING)
logger = logging.getLogger('logger')
print('logger ----------------')
logger.debug('debug')
logger.info('info')
logger.warning('warning')
logger.error('error')
logger.critical('critical')
print('root -------------------')
logging.debug('debug')
logging.info('info')
logging.warn('warning')
logging.error('error')
logging.critical('critical')
logger ----------------
2024-07-28 10:42:INFO:logger:info
2024-07-28 10:42:WARNING:logger:warning
2024-07-28 10:42:ERROR:logger:error
2024-07-28 10:42:CRITICAL:logger:critical
root -------------------
2024-07-28 10:42:INFO:root:info
2024-07-28 10:42:WARNING:root:warning
2024-07-28 10:42:ERROR:root:error
2024-07-28 10:42:CRITICAL:root:critical
loggers: {logger: ..}
キーとroot: ...
キーは共にhandlers: {console: ..}
キーのハンドラーを使用しています。
双方同じログ出力をしていますが、「logger」のロガーにだけ、「INFO」と「ERROR」のログレベルのみ出力されるようなカスタムフィルターを作成して設定していきます。
logging.Filter
クラスを継承したfilter(self, record)
メソッドを持つLogFilter
クラスを、辞書の前に定義します。
import logging
import logging.config
class LogFilter(logging.Filter):
def filter(self, record):
# record引数にはLogRecordインスタンスが与えられる
if record.name is not 'root':
# rootロガー以外の処理
if record.levelname.lower() == 'warning':
# warningの出力は除外する
return False
elif record.levelname.lower() == 'critical':
# criticalの出力は除外する
return False
return True
LOGGING = {
....
}
辞書には以下のように定義します。
LOGGING = {
'version': 1,
'formatters': {
'formatter': {
'format': '%(asctime)s:%(levelname)s:%(name)s:%(message)s',
'datefmt': '%Y-%m-%d %H:%M',
},
},
'filters': {
'filter': { # フィルターの作成
'()': LogFilter # カスタムフィルターの定義
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'formatter',
'filters': ['filter'], # フィルターの設定
},
},
"loggers":{
"logger": {
"propagate": False,
"level": "DEBUG",
"handlers": ["console"],
},
},
"root": {
"level": "DEBUG",
"handlers": ["console"],
},
}
logger ----------------
2024-07-28 11:02:INFO:logger:info
2024-07-28 11:02:ERROR:logger:error
root -------------------
2024-07-28 11:02:INFO:root:info
2024-07-28 11:02:WARNING:root:warning
2024-07-28 11:02:ERROR:root:error
2024-07-28 11:02:CRITICAL:root:critical
柔軟なカスタムフィルターを作成したい場合は、Djangoのログモジュールでも定義されているようなCallbackFilter
クラスを作成します。
import logging
class CallbackFIlter(logging.Filter):
# 関数呼び出し専用のカスタムフィルタークラス
def __init__(self, callback):
# callbackの引数は、辞書のcallbackに定義される関数
self.callback = callback
def filter(self, record):
# recordの引数は、LogRecordインスタンスが与えられる
return self.callback(record)
LOGGING = {
...
'filters': {
'filter': {
'()': CallbackFilter,
'callback': function, # CallbackFilterクラスの引数に関数を定義
},
},
...
}
CallbackFilterクラスは関数を使用するためのパイプ役として存在するフィルタークラスで、フィルターを関数で使用することができます。
関数は1つの引数を持ったfunction(record)
のような関数で、引数にはLogRecordインスタンスが与えられます。
詰まり、辞書設定におけるcallback: function
の設定はCallbackFilter(callback=function)
という処理に繋がります。
CallbackFilterクラスを使ってルートロガーとロガーで異なるフィルターを設定してみます。
import logging
class CallbackFIlter(logging.Filter):
# 関数呼び出し専用のカスタムフィルタークラス
def __init__(self, callback):
# callbackの引数は、辞書のcallbackに定義される関数
self.callback = callback
def filter(self, record):
# recordの引数は、LogRecordインスタンスが与えられる
return self.callback(record)
def loggerFilter(record):
record.msg += ':loggerFilter'
return True
def rootFilter(record):
record.msg += ':rootFilter'
return True
LOGGING = {
...
'filters': {
'logger_filter': {
'()': CallbackFilter,
'callback': loggerFilter, # CallbackFilterクラスの引数に関数を定義
},
'root_filter': {
'()': CallbackFilter,
'callback': rootFilter, # CallbackFilterクラスの引数に関数を定義
},
},
...
"loggers":{
"logger": {
"propagate": False,
"level": "DEBUG",
"handlers": ["console"],
"filters": ["logger_filter"] # カスタムフィルター
},
},
"root": {
"level": "DEBUG",
"handlers": ["console"],
"filters": ["root_filter"] # カスタムフィルター
},
}
logger ----------------
2024-07-28 11:52:INFO:logger:info:loggerFilter
2024-07-28 11:52:WARNING:logger:warning:loggerFilter
2024-07-28 11:52:ERROR:logger:error:loggerFilter
2024-07-28 11:52:CRITICAL:logger:critical:loggerFilter
root -------------------
2024-07-28 11:52:INFO:root:info:rootFilter
2024-07-28 11:52:WARNING:root:warning:rootFilter
2024-07-28 11:52:ERROR:root:error:rootFilter
2024-07-28 11:52:CRITICAL:root:critical:rootFilter
既存のロガーを無効・有効にするには、disable_existing_loggers
キーに「True」か「False」を設定します。
既存のロガーとは、dictConfig(LOGGING)
が実行される前に作成されたロガーです。
ロギング辞書設定のデフォルトではdisable_existing_loggers
キーは「True」となっており、既存のロガーは無効化(出力されない)状態になっています。
import logging
LOGGING = {
'version': 1
}
exist_logger = logging.getLogger('exist_logger') # 既存のロガー
logging.config.dictConfig(config=LOGGING) # ここでロギング設定が実行される
print('exist_logger ---------------')
exist_logger.debug('debug')
exist_logger.info('info')
exist_logger.warn('warning')
exist_logger.error('error')
exist_logger.critical('critical')
exist_logger ---------------
disable_existing_loggers
キーに「False」を与えると既存のロガーは有効となって出力されるようになります。
LOGGING = {
'version': 1,
'disable_existing_loggers': False
}
exist_logger ---------------
warning
error
critical
気になったのが、ロギングの使いどころです。
いったいどのように使われているのか気になるところです。
ということで、PythonのWebフレームワークであるDjangoのロギング設定を弄り、サーバーを起動するとコンソールに流れてくるアクセスログがどのように定義されているのか見てみたいと思います。
Djangoプロジェクトの設定ファイルsettngs.py
にデフォルトのロギング設定をコピペします。
# settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'formatters': {
'django.server': {
'()': 'django.utils.log.ServerFormatter',
'format': '[{server_time}] {message}',
'style': '{',
}
},
'handlers': {
'console': {
'level': 'INFO',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
},
'django.server': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'django.server',
},
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'django': {
'handlers': ['console', 'mail_admins'],
'level': 'INFO',
},
'django.server': {
'handlers': ['django.server'],
'level': 'INFO',
'propagate': False,
},
}
}
formatters: {django.server: {format: ...}}
キーのフォーマットを修正します。
ログが発生したファイルのパスを出力するように設定します。
# settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
...
'formatters': {
'django.server': {
'()': 'django.utils.log.ServerFormatter',
# 'format': '[{server_time}] {message}',
'format': '{pathname}', # ログが実行されたファイルの相対パス
'style': '{',
}
},
...
}
サーバーを起動するとファイルの相対パスが出力されます。
basehttp.py
ファイルの中身を確認してみると以下のように定義されています。
# django/core/servers/basehttp.py
"""
HTTP server that implements the Python WSGI protocol (PEP 333, rev 1.21).
Based on wsgiref.simple_server which is part of the standard library since 2.5.
...
"""
import logging
...
...
class WSGIRequestHandler(simple_server.WSGIRequestHandler):
protocol_version = 'HTTP/1.1'
def address_string(self):
# Short-circuit parent method to not call socket.getfqdn
return self.client_address[0]
def log_message(self, format, *args):
extra = {
'request': self.request,
'server_time': self.log_date_time_string(),
}
if args[1][0] == '4':
# 0x16 = Handshake, 0x03 = SSL 3.0 or TLS 1.x
if args[0].startswith('\x16\x03'):
extra['status_code'] = 500
logger.error(
"You're accessing the development server over HTTPS, but "
"it only supports HTTP.\n", extra=extra,
)
return
if args[1].isdigit() and len(args[1]) == 3:
status_code = int(args[1])
extra['status_code'] = status_code
if status_code >= 500:
level = logger.error
elif status_code >= 400:
level = logger.warning
else:
level = logger.info
else:
level = logger.info
level(format, *args, extra=extra)
def get_environ(self):
# Strip all headers with underscores in the name before constructing
# the WSGI environ. This prevents header-spoofing based on ambiguity
# between underscores and dashes both normalized to underscores in WSGI
# env vars. Nginx and Apache 2.4+ both do this as well.
for k in self.headers:
if '_' in k:
del self.headers[k]
return super().get_environ()
def handle(self):
self.close_connection = True
self.handle_one_request()
while not self.close_connection:
self.handle_one_request()
try:
self.connection.shutdown(socket.SHUT_WR)
except (socket.error, AttributeError):
pass
def handle_one_request(self):
"""Copy of WSGIRequestHandler.handle() but with different ServerHandler"""
self.raw_requestline = self.rfile.readline(65537)
if len(self.raw_requestline) > 65536:
self.requestline = ''
self.request_version = ''
self.command = ''
self.send_error(414)
return
if not self.parse_request(): # An error code has been sent, just exit
return
handler = ServerHandler(
self.rfile, self.wfile, self.get_stderr(), self.get_environ()
)
handler.request_handler = self # backpointer for logging & connection closing
handler.run(self.server.get_app())
...
上記のソースコードを見た現段階での理解ですが、ログをそれぞれのファイルに定義しているのではなく、フィルターのように1回1回ステータスコードをWSGIRequestHandlerクラスで判定し出力しているのだなと思いました。
最後までご覧いただきありがとうございました。