Pythonでのロギング
ロギングは、プログラマのツールボックスで非常に便利なツールです。 プログラムの流れをよりよく理解し、開発中には考えもしなかったシナリオを発見するのに役立ちます。
ログは、アプリケーションが通過するフローを常に見ている余分な目を開発者に提供します。 どのユーザーまたはIPがアプリケーションにアクセスしたかなどの情報を保存できます。 エラーが発生した場合、エラーが発生したコード行に到達する前のプログラムの状態を知らせることにより、スタックトレースよりも多くの洞察を提供できます。
適切な場所から有用なデータを記録することで、エラーを簡単にデバッグできるだけでなく、そのデータを使用してアプリケーションのパフォーマンスを分析し、スケーリングを計画したり、使用パターンを調べてマーケティングを計画したりできます。
Pythonは、標準ライブラリの一部としてロギングシステムを提供するため、アプリケーションにロギングをすばやく追加できます。 この記事では、このモジュールを使用することがアプリケーションにロギングを追加する最良の方法である理由と、すぐに開始する方法を学び、利用可能な高度な機能のいくつかを紹介します。
Free Bonus:5 Thoughts On Python Masteryは、Python開発者向けの無料コースで、Pythonスキルを次のレベルに引き上げるために必要なロードマップと考え方を示しています。
ロギングモジュール
Pythonのログモジュールは、エンタープライズチームだけでなく初心者のニーズにも対応できるように設計された、すぐに使用できる強力なモジュールです。 ほとんどのサードパーティPythonライブラリで使用されるため、ログメッセージをそれらのライブラリのライブラリと統合して、アプリケーションの同種のログを生成できます。
Pythonプログラムへのロギングの追加は、次のように簡単です。
import logging
ロギングモジュールをインポートすると、「ロガー」と呼ばれるものを使用して、表示したいメッセージを記録できます。 デフォルトでは、イベントの重大度を示す5つの標準レベルがあります。 それぞれに、その重大度のレベルでイベントを記録するために使用できる対応するメソッドがあります。 定義されたレベルは、重大度の高い順に次のとおりです。
-
デバッグ
-
INFO
-
警告
-
エラー
-
クリティカル
ロギングモジュールは、多くの設定を行うことなく開始できるデフォルトのロガーを提供します。 次の例に示すように、各レベルに対応するメソッドを呼び出すことができます。
import logging
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')
上記のプログラムの出力は次のようになります。
WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message
出力には、各メッセージの前の重大度レベルと、ロギングモジュールがデフォルトのロガーに付ける名前であるroot
が表示されます。 (ロガーについては、後のセクションで詳しく説明します。)レベル、名前、メッセージをコロン(:
)で区切って表示するこの形式は、タイムスタンプなどを含めるように構成できるデフォルトの出力形式です。行番号、およびその他の詳細。
debug()
およびinfo()
メッセージがログに記録されなかったことに注意してください。 これは、デフォルトで、ロギングモジュールが重大度レベルWARNING
以上のメッセージをログに記録するためです。 必要に応じて、すべてのレベルのイベントをログに記録するようにロギングモジュールを構成することで、これを変更できます。 構成を変更して独自の重大度レベルを定義することもできますが、使用している一部のサードパーティライブラリのログと混同する可能性があるため、通常はお勧めできません。
基本設定
+basicConfig(**+`
+ kwargs + +)+ `メソッドを使用して、ロギングを構成できます。
「ロギングモジュールがPEP8スタイルガイドに違反し、
camelCase
の命名規則を使用していることに気付くでしょう。 これは、JavaのロギングユーティリティであるLog4jから採用されたためです。 これはパッケージの既知の問題ですが、標準ライブラリに追加することが決定された時点で、すでにユーザーに採用されており、PEP8要件を満たすように変更すると、下位互換性の問題が発生します。」 (Source)
basicConfig()
に一般的に使用されるパラメーターのいくつかは、次のとおりです。
-
level
:ルートロガーは指定された重大度レベルに設定されます。 -
filename
:ファイルを指定します。 -
filemode
:filename
が指定されている場合、ファイルはこのモードで開かれます。 デフォルトはa
で、これは追加を意味します。 -
format
:これはログメッセージの形式です。
level
パラメータを使用すると、記録するログメッセージのレベルを設定できます。 これは、クラスで使用可能な定数の1つを渡すことで実行できます。これにより、そのレベル以上のすべてのロギング呼び出しがログに記録されます。 例を示しましょう。
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug('This will get logged')
DEBUG:root:This will get logged
DEBUG
レベル以上のすべてのイベントがログに記録されるようになりました。
同様に、コンソールではなくファイルにログを記録する場合は、filename
とfilemode
を使用でき、format
を使用してメッセージの形式を決定できます。 次の例は、3つすべての使用法を示しています。
import logging
logging.basicConfig(filename='app.log', filemode='w', format='%(name)s - %(levelname)s - %(message)s')
logging.warning('This will get logged to a file')
root - ERROR - This will get logged to a file
メッセージは次のようになりますが、コンソールではなくapp.log
という名前のファイルに書き込まれます。 filemodeはw
に設定されます。これは、basicConfig()
が呼び出されるたびにログファイルが「書き込みモード」で開かれ、プログラムを実行するたびにファイルが書き換えられることを意味します。 filemodeのデフォルト設定はa
で、これは追加です。
hereにあるbasicConfig()
のパラメーターをさらに使用することで、ルートロガーをさらにカスタマイズできます。
ルートロガーを構成するためのbasicConfig()
の呼び出しは、ルートロガーが以前に構成されていない場合にのみ機能することに注意してください。 Basically, this function can only be called once.
debug()
、info()
、warning()
、error()
、およびcritical()
も、以前に呼び出されたことがない場合、引数なしでbasicConfig()
を自動的に呼び出します。 つまり、上記の関数の1つが最初に呼び出された後は、basicConfig()
関数が内部的に呼び出されるため、ルートロガーを構成できなくなります。
basicConfig()
のデフォルト設定では、次の形式でコンソールに書き込むようにロガーを設定します。
ERROR:root:This is an error message
出力のフォーマット
プログラムからメッセージとして文字列として表すことができる任意の変数をログに渡すことができますが、すでにLogRecord
の一部であり、出力形式に簡単に追加できるいくつかの基本的な要素があります。 レベルとメッセージとともにプロセスIDをログに記録する場合は、次のようにします。
import logging
logging.basicConfig(format='%(process)d-%(levelname)s-%(message)s')
logging.warning('This is a Warning')
18472-WARNING-This is a Warning
format
は、任意の配置でLogRecord
属性を持つ文字列を受け取ることができます。 使用可能な属性の全リストはhereにあります。
次に、日付と時刻の情報を追加できる別の例を示します。
import logging
logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO)
logging.info('Admin logged in')
2018-07-11 20:12:06,288 - Admin logged in
%(asctime)s
は、LogRecord
の作成時間を追加します。 フォーマットは、datefmt
属性を使用して変更できます。この属性は、time.strftime()
などのdatetimeモジュールのフォーマット関数と同じフォーマット言語を使用します。
import logging
logging.basicConfig(format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S')
logging.warning('Admin logged out')
12-Jul-18 20:53:19 - Admin logged out
ガイドhereを見つけることができます。
変数データのロギング
ほとんどの場合、アプリケーションの動的な情報をログに含める必要があります。 ロギングメソッドは文字列を引数として使用することを見てきました。別の行で変数データを使用してストリングをフォーマットし、それをログメソッドに渡すのは自然に思えるかもしれません。 しかし、これは実際にはメッセージにフォーマット文字列を使用し、変数データを引数として追加することにより直接行うことができます。 例を示しましょう。
import logging
name = 'John'
logging.error('%s raised an error', name)
ERROR:root:John raised an error
メソッドに渡される引数は、メッセージに変数データとして含まれます。
任意のフォーマットスタイルを使用できますが、Python 3.6で導入されたf-stringsは、フォーマットを短くして読みやすくするのに役立つため、文字列をフォーマットするための優れた方法です。
import logging
name = 'John'
logging.error(f'{name} raised an error')
ERROR:root:John raised an error
スタックトレースのキャプチャ
ロギングモジュールを使用すると、アプリケーション内の完全なスタックトレースをキャプチャすることもできます。 exc_info
パラメーターがTrue
として渡され、ロギング関数が次のように呼び出される場合、Exception informationをキャプチャできます。
import logging
a = 5
b = 0
try:
c = a / b
except Exception as e:
logging.error("Exception occurred", exc_info=True)
ERROR:root:Exception occurred
Traceback (most recent call last):
File "exceptions.py", line 6, in
c = a / b
ZeroDivisionError: division by zero
[Finished in 0.2s]
exc_info
がTrue
に設定されていない場合、上記のプログラムの出力は例外について何も教えてくれません。実際のシナリオでは、ZeroDivisionError
ほど単純ではない可能性があります。 )s。 これだけを示すログを使用して、複雑なコードベースでエラーをデバッグしようとすると想像してください。
ERROR:root:Exception occurred
簡単なヒントを次に示します。例外ハンドラからログを記録する場合は、logging.exception()
メソッドを使用します。このメソッドは、レベルERROR
のメッセージをログに記録し、メッセージに例外情報を追加します。 簡単に言うと、logging.exception()
を呼び出すことはlogging.error(exc_info=True)
を呼び出すことに似ています。 ただし、このメソッドは常に例外情報をダンプするため、例外ハンドラーからのみ呼び出す必要があります。 この例を見てください。
import logging
a = 5
b = 0
try:
c = a / b
except Exception as e:
logging.exception("Exception occurred")
ERROR:root:Exception occurred
Traceback (most recent call last):
File "exceptions.py", line 6, in
c = a / b
ZeroDivisionError: division by zero
[Finished in 0.2s]
logging.exception()
を使用すると、ERROR
のレベルでログが表示されます。 これが不要な場合は、debug()
からcritical()
までの他のロギングメソッドを呼び出して、exc_info
パラメータをTrue
として渡すことができます。
クラスと関数
これまで、root
という名前のデフォルトのロガーを見てきました。これは、関数が次のように直接呼び出されるたびにロギングモジュールによって使用されます:logging.debug()
。 特にアプリケーションに複数のモジュールがある場合は、Logger
クラスのcreating an objectで独自のロガーを定義できます(定義する必要があります)。 モジュールのクラスと関数のいくつかを見てみましょう。
ロギングモジュールで定義される最も一般的に使用されるクラスは次のとおりです。
-
Logger
:これは、関数を呼び出すためにアプリケーションコードでオブジェクトが直接使用されるクラスです。 -
LogRecord
:ロガーは、ロガーの名前、関数、行番号、メッセージなど、ログに記録されているイベントに関連するすべての情報を含むLogRecord
オブジェクトを自動的に作成します。 -
Handler
:ハンドラーは、LogRecord
をコンソールやファイルなどの必要な出力先に送信します。Handler
は、StreamHandler
、FileHandler
、SMTPHandler
、HTTPHandler
などのサブクラスのベースです。 これらのサブクラスは、ロギング出力をsys.stdout
やディスクファイルなどの対応する宛先に送信します。 -
Formatter
:ここで、出力に含める必要のある属性をリストする文字列形式を指定して、出力の形式を指定します。
これらのうち、モジュールレベルの関数logging.getLogger(name)
を使用してインスタンス化されるLogger
クラスのオブジェクトを主に扱います。 同じname
でgetLogger()
を複数回呼び出すと、同じLogger
オブジェクトへの参照が返されます。これにより、ロガーオブジェクトを必要なすべての部分に渡す必要がなくなります。 例を示しましょう。
import logging
logger = logging.getLogger('example_logger')
logger.warning('This is a warning')
This is a warning
これにより、example_logger
という名前のカスタムロガーが作成されますが、ルートロガーとは異なり、カスタムロガーの名前はデフォルトの出力形式の一部ではないため、構成に追加する必要があります。 ロガーの名前を表示する形式に設定すると、次のような出力が得られます。
WARNING:example_logger:This is a warning
この場合も、ルートロガーとは異なり、basicConfig()
を使用してカスタムロガーを構成することはできません。 ハンドラーとフォーマッターを使用して構成する必要があります。
「
__name__
を名前パラメーターとしてgetLogger()
に渡して、モジュールレベルのロガーを使用してロガーオブジェクトを作成することをお勧めします。ロガー自体の名前から、イベントがログに記録されている場所がわかります。 。__name__
は、現在のモジュールの名前に評価されるPythonの特別な組み込み変数です。」 (Source)
ハンドラーを使用する
ハンドラーは、独自のロガーを構成し、生成されたログを複数の場所に送信するときに表示されます。 ハンドラーは、ログメッセージを、標準出力ストリームやファイルなどの構成済みの宛先に送信するか、HTTP経由で送信するか、SMTP経由で電子メールに送信します。
作成するロガーには複数のハンドラーを含めることができます。つまり、ログファイルに保存するように設定し、電子メールで送信することもできます。
ロガーと同様に、ハンドラーで重大度レベルを設定することもできます。 これは、同じロガーに複数のハンドラーを設定したいが、それぞれに異なる重大度レベルが必要な場合に便利です。 たとえば、レベルWARNING
以上のログをコンソールに記録したいが、レベルERROR
以上のログもすべてファイルに保存する必要があります。 これを行うプログラムは次のとおりです。
# logging_example.py
import logging
# Create a custom logger
logger = logging.getLogger(__name__)
# Create handlers
c_handler = logging.StreamHandler()
f_handler = logging.FileHandler('file.log')
c_handler.setLevel(logging.WARNING)
f_handler.setLevel(logging.ERROR)
# Create formatters and add it to handlers
c_format = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
f_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
c_handler.setFormatter(c_format)
f_handler.setFormatter(f_format)
# Add handlers to the logger
logger.addHandler(c_handler)
logger.addHandler(f_handler)
logger.warning('This is a warning')
logger.error('This is an error')
__main__ - WARNING - This is a warning
__main__ - ERROR - This is an error
ここで、logger.warning()
は、イベントのすべての情報を保持するLogRecord
を作成し、それを持っているすべてのハンドラー(c_handler
およびf_handler
)に渡します。
c_handler
はレベルWARNING
のStreamHandler
であり、LogRecord
から情報を取得して、指定された形式で出力を生成し、コンソールに出力します。 f_handler
はレベルERROR
のFileHandler
であり、レベルがWARNING
であるため、このLogRecord
は無視されます。
logger.error()
が呼び出されると、c_handler
は以前とまったく同じように動作し、f_handler
はERROR
のレベルでLogRecord
を取得するため、出力の生成に進みます。 c_handler
と同様ですが、コンソールに出力する代わりに、次の形式で指定されたファイルに書き込みます。
2018-08-03 16:12:21,723 - __main__ - ERROR - This is an error
__name__
変数に対応するロガーの名前は__main__
としてログに記録されます。これは、Pythonが実行を開始するモジュールに割り当てる名前です。 このファイルが他のモジュールによってインポートされた場合、__name__
変数はその名前logging_exampleに対応します。 外観は次のとおりです。
# run.py
import logging_example
logging_example - WARNING - This is a warning
logging_example - ERROR - This is an error
その他の構成方法
モジュールおよびクラス関数を使用するか、構成ファイルまたはdictionaryを作成し、それぞれfileConfig()
またはdictConfig()
を使用してロードすることにより、上記のようにロギングを構成できます。 これらは、実行中のアプリケーションでロギング構成を変更する場合に役立ちます。
ファイル構成の例を次に示します。
[loggers]
keys=root,sampleLogger
[handlers]
keys=consoleHandler
[formatters]
keys=sampleFormatter
[logger_root]
level=DEBUG
handlers=consoleHandler
[logger_sampleLogger]
level=DEBUG
handlers=consoleHandler
qualname=sampleLogger
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=sampleFormatter
args=(sys.stdout,)
[formatter_sampleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
上記のファイルには、2つのロガー、1つのハンドラー、および1つのフォーマッターがあります。 名前が定義された後、アンダースコアで区切られた名前の前にlogger、handler、formatterの単語を追加することで設定されます。
この設定ファイルをロードするには、fileConfig()
を使用する必要があります。
import logging
import logging.config
logging.config.fileConfig(fname='file.conf', disable_existing_loggers=False)
# Get the logger specified in the file
logger = logging.getLogger(__name__)
logger.debug('This is a debug message')
2018-07-13 13:57:45,467 - __main__ - DEBUG - This is a debug message
構成ファイルのパスはパラメーターとしてfileConfig()
メソッドに渡され、disable_existing_loggers
パラメーターは、関数が呼び出されたときに存在するロガーを保持または無効にするために使用されます。 特に記載がない場合、デフォルトでTrue
になります。
辞書によるアプローチのYAML形式の同じ構成を次に示します。
version: 1
formatters:
simple:
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
console:
class: logging.StreamHandler
level: DEBUG
formatter: simple
stream: ext://sys.stdout
loggers:
sampleLogger:
level: DEBUG
handlers: [console]
propagate: no
root:
level: DEBUG
handlers: [console]
yaml
ファイルから構成をロードする方法を示す例を次に示します。
import logging
import logging.config
import yaml
with open('config.yaml', 'r') as f:
config = yaml.safe_load(f.read())
logging.config.dictConfig(config)
logger = logging.getLogger(__name__)
logger.debug('This is a debug message')
2018-07-13 14:05:03,766 - __main__ - DEBUG - This is a debug message
落ち着いてログを読む
ロギングモジュールは非常に柔軟であると見なされます。 その設計は非常に実用的であり、箱から出してユースケースに適合する必要があります。 基本的なロギングを小さなプロジェクトに追加することも、大きなプロジェクトで作業している場合は、独自のカスタムログレベル、ハンドラクラスなどを作成することもできます。
アプリケーションでログを使用していない場合は、今が開始する良い機会です。 正しく行われると、ロギングは開発プロセスから多くの摩擦を確実に取り除き、アプリケーションを次のレベルに引き上げる機会を見つけるのに役立ちます。