web-dev-qa-db-ja.com

Python複数のスレッドからのロギング

少なくとも2つの他のモジュール(log.pyおよびserver.py)で使用されるdevice.pyモジュールがあります。

次のグローバルがあります。

fileLogger = logging.getLogger()
fileLogger.setLevel(logging.DEBUG)
consoleLogger = logging.getLogger()
consoleLogger.setLevel(logging.DEBUG)

file_logging_level_switch = {
    'debug':    fileLogger.debug,
    'info':     fileLogger.info,
    'warning':  fileLogger.warning,
    'error':    fileLogger.error,
    'critical': fileLogger.critical
}

console_logging_level_switch = {
    'debug':    consoleLogger.debug,
    'info':     consoleLogger.info,
    'warning':  consoleLogger.warning,
    'error':    consoleLogger.error,
    'critical': consoleLogger.critical
}

次の2つの機能があります。

def LoggingInit( logPath, logFile, html=True ):
    global fileLogger
    global consoleLogger

    logFormatStr = "[%(asctime)s %(threadName)s, %(levelname)s] %(message)s"
    consoleFormatStr = "[%(threadName)s, %(levelname)s] %(message)s"

    if html:
        logFormatStr = "<p>" + logFormatStr + "</p>"

    # File Handler for log file
    logFormatter = logging.Formatter(logFormatStr)
    fileHandler = logging.FileHandler( 
        "{0}{1}.html".format( logPath, logFile ))
    fileHandler.setFormatter( logFormatter )
    fileLogger.addHandler( fileHandler )

    # Stream Handler for stdout, stderr
    consoleFormatter = logging.Formatter(consoleFormatStr)
    consoleHandler = logging.StreamHandler() 
    consoleHandler.setFormatter( consoleFormatter )
    consoleLogger.addHandler( consoleHandler )

そして:

def WriteLog( string, print_screen=True, remove_newlines=True, 
        level='debug' ):

    if remove_newlines:
        string = string.replace('\r', '').replace('\n', ' ')

    if print_screen:
        console_logging_level_switch[level](string)

    file_logging_level_switch[level](string)

ファイルロガーとコンソールロガーを初期化するserver.pyからLoggingInitを呼び出します。次に、WriteLogをあらゆる場所から呼び出します。そのため、複数のスレッドがfileLoggerconsoleLoggerにアクセスしています。

ログファイルをさらに保護する必要がありますか?ドキュメントには、スレッドロックはハンドラーによって処理されると記載されています。

22
nckturner

良いニュースは、スレッドセーフのために特別なことをする必要はなく、クリーンシャットダウンのために特別なことはほとんど必要ないか、ささいなことです。詳細については後で説明します。

悪いニュースは、その点に到達する前であってもコードに深刻な問題があることです。fileLoggerconsoleLoggerは同じオブジェクトです。 getLogger() のドキュメントから:

指定された名前のロガーを返すか、名前が指定されていない場合は、階層のルートロガーであるロガーを返します。

したがって、ルートロガーを取得してfileLoggerとして保存し、次にルートロガーを取得してconsoleLoggerとして保存します。そのため、LoggingInitfileLoggerを初期化してから、同じオブジェクトを異なる名前で異なる値で再初期化します。

can同じロガーに複数のハンドラーを追加します。それぞれに対して実際に行う初期化はaddHandlerだけなので、コードは意図したとおりに動作しますが、偶然です。そして、ちょっとだけ。 _print_screen=True_を渡すと、両方のログで各メッセージのコピーが2つ取得され、_print_screen=False_を渡してもコンソールでコピーが取得されます。

実際、グローバル変数を使用する理由はまったくありません。 getLogger()の要点は、必要なときにいつでも呼び出してグローバルルートロガーを取得できるため、どこにでも保存する必要がないということです。


さらに小さな問題は、HTMLに挿入したテキストをエスケープしないことです。ある時点で、文字列_"a < b"_をログに記録しようとして、問題が発生します。

それほど深刻ではありませんが、_<p>_内の_<body>_内にない_<html>_タグのシーケンスは、有効なHTMLドキュメントではありません。しかし、多くの視聴者がそれを自動的に処理するか、ログを表示する前に後処理することができます。しかし、本当にこれを正しくしたい場合は、FileHandlerをサブクラス化し、空のファイルが指定されている場合は___init___にヘッダーを追加し、フッターが存在する場合はフッターを削除してからcloseフッターを追加します。


実際の質問に戻る:

追加のロックは必要ありません。ハンドラーがcreateLockacquire、およびreleaseを正しく実装している場合(およびスレッドを備えたプラットフォームで呼び出される場合)、ロギングマシンは必要に応じて自動的にロックを取得します各メッセージがアトミックに記録されるようにします。

私の知る限り、ドキュメンテーションは直接ではなく、StreamHandlerFileHandlerがこれらのメソッドを実装していると言っているわけではなく、( 質問 で言及したテキストは、「ロギングモジュールは、クライアントによる特別な作業を必要とせずにスレッドセーフにすることを目的としている」など)と述べています。また、実装のソース(例: CPython 3.3 )を見ると、両方とも_logging.Handler_から正しく実装されたメソッドを継承していることがわかります。


同様に、ハンドラーがflushおよびcloseを正しく実装している場合、ロギングマシンは通常のシャットダウン中に正しくファイナライズされることを確認します。

ここで、ドキュメントはStreamHandler.flush()FileHandler.flush()、およびFileHandler.close()について説明しています。 StreamHandler.close()がノーオペレーションであることを除いて、ほとんど期待どおりです。つまり、コンソールへの最終的なログメッセージが失われる可能性があることを意味します。ドキュメントから:

close()メソッドはHandlerから継承されるため、出力は行われないため、時々明示的なflush()呼び出しが必要になる場合があります。

これがあなたにとって重要であり、それを修正したい場合は、次のようなことをする必要があります。

_class ClosingStreamHandler(logging.StreamHandler):
    def close(self):
        self.flush()
        super().close()
_

そして、ClosingStreamHandler()の代わりにStreamHandler()を使用します。

FileHandlerにはこのような問題はありません。


ログを2か所に送信する通常の方法は、それぞれ独自のフォーマッターを持つ2つのハンドラーでルートロガーを使用することです。

また、2つのロガーが必要な場合でも、個別の_console_logging_level_switch_および_file_logging_level_switch_マップは必要ありません。 Logger.debug(msg)の呼び出しは、Logger.log(DEBUG, msg)の呼び出しとまったく同じです。カスタムレベル名debugなどを標準名DEBUGなどにマッピングする方法が必要ですが、1回につき1回検索するのではなく、1回だけ検索できますロガー(さらに、あなたの名前がキャストの異なる標準名である場合は、ごまかすことができます)。

これはすべて、「 Multiple handlers and formatters 」セクション、およびロギングクックブックの残りの部分で説明されています。

これを行う標準的な方法の唯一の問題は、メッセージごとにコンソールロギングを簡単にオフにできないことです。それは普通のことではないからです。通常、レベルごとにログを記録し、ファイルログでログレベルを高く設定します。

しかし、さらに制御したい場合は、フィルターを使用できます。たとえば、FileHandlerにすべてを受け入れるフィルターを与え、ConsoleHandlerconsoleで始まるものを必要とするフィルターを与えてから、フィルター_'console' if print_screen else ''_を使用します。これにより、WriteLogがほぼ1行になります。

改行を削除するにはさらに2行が必要ですが、必要に応じて、フィルターで、またはアダプターを介してthatを実行することもできます。 (もう一度、クックブックを参照してください。)そして、WriteLog本当にisワンライナーです。

43
abarnert

Pythonロギングはスレッドセーフです。

したがって、Python(ライブラリ)コードでは問題ありません。

複数のスレッド(WriteLog)から呼び出すルーチンは、共有状態に書き込みません。したがって、コードに問題はありません。

だからあなたは大丈夫です。

5
andrew cooke