Pythonでのロギング

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:ファイルを指定します。

  • filemodefilenameが指定されている場合、ファイルはこのモードで開かれます。 デフォルトはaで、これは追加を意味します。

  • format:これはログメッセージの形式です。

levelパラメータを使用すると、記録するログメッセージのレベルを設定できます。 これは、クラスで使用可能な定数の1つを渡すことで実行できます。これにより、そのレベル以上のすべてのロギング呼び出しがログに記録されます。 例を示しましょう。

import logging

logging.basicConfig(level=logging.DEBUG)
logging.debug('This will get logged')
DEBUG:root:This will get logged

DEBUGレベル以上のすべてのイベントがログに記録されるようになりました。

同様に、コンソールではなくファイルにログを記録する場合は、filenamefilemodeを使用でき、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_infoTrueに設定されていない場合、上記のプログラムの出力は例外について何も教えてくれません。実際のシナリオでは、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は、StreamHandlerFileHandlerSMTPHandlerHTTPHandlerなどのサブクラスのベースです。 これらのサブクラスは、ロギング出力をsys.stdoutやディスクファイルなどの対応する宛先に送信します。

  • Formatter:ここで、出力に含める必要のある属性をリストする文字列形式を指定して、出力の形式を指定します。

これらのうち、モジュールレベルの関数logging.getLogger(name)を使用してインスタンス化されるLoggerクラスのオブジェクトを主に扱います。 同じnamegetLogger()を複数回呼び出すと、同じ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はレベルWARNINGStreamHandlerであり、LogRecordから情報を取得して、指定された形式で出力を生成し、コンソールに出力します。 f_handlerはレベルERRORFileHandlerであり、レベルがWARNINGであるため、このLogRecordは無視されます。

logger.error()が呼び出されると、c_handlerは以前とまったく同じように動作し、f_handlerERRORのレベルで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

落ち着いてログを読む

ロギングモジュールは非常に柔軟であると見なされます。 その設計は非常に実用的であり、箱から出してユースケースに適合する必要があります。 基本的なロギングを小さなプロジェクトに追加することも、大きなプロジェクトで作業している場合は、独自のカスタムログレベル、ハンドラクラスなどを作成することもできます。

アプリケーションでログを使用していない場合は、今が開始する良い機会です。 正しく行われると、ロギングは開発プロセスから多くの摩擦を確実に取り除き、アプリケーションを次のレベルに引き上げる機会を見つけるのに役立ちます。