【Python】logging でログを記録する方法のドキュメントの場所(コメント付き)

Python

Python の logging モジュールを使用するときに参照した、ドキュメントの場所をまとめました(Python マニュアルなど)。

これらを見て、multiprocessingconcurrent.futures で並列処理しているときの「ロギングのコード例」を書いてきました。

以下、自分が見てきたドキュメントの場所を、コメント付きで紹介します。

関連記事

concurrent.futures でログを記録するコード例。

multiprocessing でログを記録するコード例。

普通のシングルプロセスでログを記録するコード例。

並列処理のコード例。

並列処理

マルチプロセッシング

multiprocessing.Pool() でロギングするときは、initializerイニシャライザー 関数の中でロギング設定をするのが便利でした。

1つ1つのプロセスの中で、それぞれ1回だけ、ロガーにハンドラを追加していくことができたので、効率的でした。

(Python) class multiprocessing.pool.Pool([processes[, initializer[, initargs[, maxtasksperchild[, context]]]]])

initializer 関数に渡す引数のリストは、自動的にアンパックして渡してくれました。

initializerNone ではない場合、各ワーカープロセスは開始時に initializer(*initargs) を呼び出します。

(Python) class multiprocessing.pool.Pool([processes[, initializer[, initargs[, maxtasksperchild[, context]]]]])

(Python) multiprocessing.pool.Pool.map(func, iterable[, chunksize])

コンカレントドットフューチャース

Python 3.7 以降であれば、concurrent.futuresProcessPoolExecutor() でロギングするときも、initializerイニシャライザー 関数の中でロギング設定をするのが便利でした。

1つ1つのプロセスの中で、それぞれ1回だけ、ロガーにハンドラを追加していくことができたので、効率的でした。

Python 3.7 で initializerinitargs の引数が追加された、とのことでした。

(Python) class concurrent.futures.ProcessPoolExecutor(max_workers=None, mp_context=None, initializer=None, initargs=())

(Python) concurrent.futures.ProcessPoolExecutor.map(func, *iterables, timeout=None, chunksize=1)

複数のプロセスからの標準出力や標準エラー出力への書き込み

print() 関数による標準出力 (stdout) への書き込みと、 StreamHandler() による標準エラー出力 (stderr) への書き込みについてです。

並列処理の子プロセスで print() 関数や StreamHandler() を使用するときは、ロックの仕組みを追加してから使用するのが良い(かもしない)、という話です。

何も考えずに使っても、さしたる問題はなかったのですが、Python マニュアルに少し説明がありました。

以下の例では、ロックを使用して、一度に1つのプロセスしか標準出力に書き込まないようにしています:
(中略)
ロックを使用しないで標準出力に書き込んだ場合は、各プロセスからの出力がごちゃまぜになってしまいます。

(Python) multiprocessing ⇒ はじめに ⇒ プロセス間の同期

(Python) multiprocessing ⇒ Introduction ⇒ Synchronization between processes

(Python) class multiprocessing.Lock

これらの説明を見るに、print() 関数で「標準出力」に表示するときは、内容がごちゃ混ぜになってしまう (liable to get all mixed up) ことを防ぐために、Lockロック (multiprocessing.Lock) を使用することができる、という感じでした。

おそらくですが、StreamHandler() の「標準エラー出力」についても、同じようにロックを使用したほうがいいのかもしれません。

Python マニュアルの説明を見る限り、

「ごちゃまぜで済むなら別にいいや」

とも思いますけれども。とりあえず、ロックの手間をかけるか否かは、自身の目的に照らして判断しています。ロックは実行速度にも影響するでしょうし(未検証)、ロックしなくても十分であったなら、省略したままでも良いと思います。

(Python) sys.stdoutsys.stderr

(Python) print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)

(Python) class logging.StreamHandler(stream=None)(stream を指定しなければ sys.stderr が使われる)

複数のプロセスからの単一ファイルへのログ記録

Python の Logging クックブックによると、「1 つのログファイル」に「複数のプロセスが書き込むことはできない」とのことでした。

ログ記録はスレッドセーフであり、単一プロセスの複数のスレッドからの単一ファイルへのログ記録はサポート されています が、 複数プロセス からの単一ファイルへのログ記録はサポート されません 。なぜなら、複数のプロセスをまたいで単一のファイルへのアクセスを直列化する標準の方法が Python には存在しないからです。

(Python) 複数のプロセスからの単一ファイルへのログ記録(Logging クックブック)

ですが、「複数のプロセス」が「それぞれ別のログファイル」に書き込むことについては、特に記載は無いようでした。

別々のファイルに書き込む分には、なにも競合しないのですから、制限は無いと思います。

プロセスごとに「別のファイル」を読み書きしたり、「別のデータベース」に接続したりするのは、ごく普通の処理でした。

ログファイルも同様でした。

multiprocessing を使用しているときでも FileHandler は使用できました。

concurrent.futures を使用しているときでも FileHandler は使用できました。

要は、書き込みだけ 1 箇所で行えば良い、ということでした。

その具体例として、 Logging クックブックには「SocketHandlerソケットハンドラ を使用した方法」と「QueueHandlerキューハンドラ を使用した方法(Python 3.2 以降)」が載っていました。

ソケットハンドラを使用した方法。

複数のプロセスから単一ファイルへログ記録しなければならないなら、最も良い方法は、すべてのプロセスが SocketHandler に対してログ記録を行い、独立したプロセスとしてソケットサーバを動かすことです。ソケットサーバはソケットから読み取ってファイルにログを書き出します。

(Python) 複数のプロセスからの単一ファイルへのログ記録(Logging クックブック)

キューハンドラを使用した方法。

別の方法として、 QueueQueueHandler を使って、マルチプロセスアプリケーションの1つのプロセスに全ての logging イベントを送る事ができます。

(Python) 複数のプロセスからの単一ファイルへのログ記録(Logging クックブック)

Python 3.2 以降であれば、QueueHandler を使用した方法が簡単でした。

各プロセスのロガーにそれぞれ取り付けた QueueHandler が、ログを Queueキュー に書き込んでいって、QueueListenerキューリスナー のスレッドが、キューからログを取得して書き込んでいく、という感じでした。

QueueHandlerQueueListener を使えば、複数のプロセスからのログを、1つのログファイルに記録していくことができました。

自身の目的に合った方法で良いと思います。

もし、ログをネットワーク越しに送信したいときは、ソケットハンドラが便利とのことでした。

ログイベントをネットワーク越しに送信し、受信端でそれを処理したいとしましょう。 SocketHandler インスタンスを送信端の root logger にアタッチすれば、簡単に実現できます:

(Python) ネットワーク越しの logging イベントの送受信(Logging クックブック)

ロギング

キューリスナー

QueueListenerキューリスナー の説明です。

(Python) 複数のプロセスからの単一ファイルへのログ記録(Logging クックブック)

(Python) ブロックする handler を扱う(Logging クックブック)

(Python) より手の込んだ multiprocessing の例(Logging クックブック)

(Python) QueueListener

(Python) class logging.handlers.QueueListener(queue, *handlers, respect_handler_level=False)

キューハンドラ

QueueHandlerキューハンドラ の説明です。

(Python) class logging.handlers.QueueHandler(queue)

キュー

QueueHandlerQueueListener に使用する Queueキュー の説明です。

(Python) Using concurrent.futures.ProcessPoolExecutor(Logging クックブック)

この説明によると、concurrent.futures.ProcessPoolExecutor のワーカープロセスを使う場合は、Proxyプロキシ オブジェクト multiprocessing.Manager().Queue(-1) を使う必要がある、とのことでした。

理由は「pickle 化できること」が必要だから、のようでした。

ProcessPoolExecutormultiprocessing モジュールを利用します。このため Global Interpreter Lock を回避することができますが、pickle 化できるオブジェクトしか実行したり返したりすることができません。

(Python) ProcessPoolExecutor

そこで、multiprocessing.Managerマルチプロセッシング ドット マネージャーSyncManager から作る「Proxyプロキシ オブジェクト」というタイプの Queue が役に立つようでした。

プロキシオブジェクトの重要な機能は pickle 化ができることで、これによりプロセス間での受け渡しができます。 そのため、参照対象が Proxy オブジェクト を持てます。

(Python) Proxy オブジェクト

実際に試してみたら、concurrent.futures.ProcessPoolExecutor に加えて、multiprocessing.Pool のほうも multiprocessing.Manager().Queue(-1) でロギングできました。

(ところで、.Queue(-1)-1 は別に書かなくてもいいと思うのですが、Python ドキュメントにならって一応書いています。)

なので、自分はどちらでも「Proxy オブジェクト」の Queue を使用しています。

どんな時に違いが出るのかは、今のところわかりません。

キューの取得例です。

import multiprocessing

def main():
    with multiprocessing.Manager() as manager:
        print(f'manager._process: {manager._process}')
        q = manager.Queue(-1)
        print(f'type(q): {type(q)}')
    print(f'manager._process: {manager._process}')
    return

if __name__ == '__main__':
    main()

または(with 文の代わりに try 文を使用して)

import multiprocessing

def main():
    manager = multiprocessing.Manager()
    try:
        print(f'manager._process: {manager._process}')
        q = manager.Queue(-1)
        print(f'type(q): {type(q)}')
    finally:
        manager.shutdown()
        print(f'manager._process: {manager._process}')
    return

if __name__ == '__main__':
    main()

実行結果です。

manager._process: <SpawnProcess name='SyncManager-1' pid=2544 parent=9968 started>
type(q): <class 'multiprocessing.managers.AutoProxy[Queue]'>
manager._process: <SpawnProcess name='SyncManager-1' pid=2544 parent=9968 stopped exitcode=0>

multiprocessing.Manager()if __name__ == '__main__': のブロックの中で呼び出す必要がありました。

そうしないと RuntimeError が発生しました(Windows 10 Pro 64-bit, Python 3.10.4 64-bit の環境で確認)。

import multiprocessing
manager = multiprocessing.Manager()
print('end')
Traceback (most recent call last):
  File "<string>", line 1, in <module>
(中略)
  File "c:\***\app_main.py", line 2, in <module>
    manager = multiprocessing.Manager()
(中略)
  File "C:***\Python310\lib\multiprocessing\spawn.py",
        line 134, in _check_not_importing_main
    raise RuntimeError('''
RuntimeError:
        An attempt has been made to start a new process before the
        current process has finished its bootstrapping phase.

        This probably means that you are not using fork to start your
        child processes and you have forgotten to use the proper idiom
        in the main module:

            if __name__ == '__main__':
                freeze_support()
                ...

        The "freeze_support()" line can be omitted if the program
        is not going to be frozen to produce an executable.

(Python) multiprocessing.freeze_support()

(Python) multiprocessing.Manager()

(Python) multiprocessing.managers.BaseManager.shutdown()

ところで、multiprocessing.Manager() を実行したら、新しく子プロセスとして、マネージャープロセスが起動しました。

通常は with 文や .shutdown() メソッドを使用して、使い終わったら明示的にマネージャープロセスを停止してあげれば良いと思います。

このマネージャープロセスは、親プロセスが終了したら自動的に停止するとのことでした(実験したら確かに終了していました)。

マネージャープロセスは親プロセスが終了するか、ガベージコレクトされると停止します。

(Python) multiprocessing.Manager()

(Python) class multiprocessing.managers.SyncManager

(Python) multiprocessing.managers.SyncManager.Queue([maxsize])

(Python) Proxy オブジェクト

(Python) class multiprocessing.Queue([maxsize])

ロガー

(Python) logging.getLogger(name=None)

(Python) ロガー名の付け方(上級ロギングチュートリアル)
logger = logging.getLogger(__name__)

(Python) logging.Logger.propagate 属性

(Python) logging.Logger.setLevel(level) 「名前付きロガー」取得時のロギングレベルは NOTSET で、「ルートロガー」取得時のロギングレベルは WARNING である旨の説明がありました。

(Python) ロギングレベル (..., logging.DEBUG, logging.INFO, ...)

(Python) logging.Logger.addHandler(hdlr)

(Python) logging.Logger.removeHandler(hdlr)

(Python) logging.Logger.debug(msg, *args, **kwargs)

ロガーはグローバル変数に入れて使うのが便利でした。普通にグローバルのところで代入しても OK でしたし、global 文グローバル ぶんを使用してから代入しても OK でした。

(Python) global

もちろん、ローカル変数に入れて使ってもいいと思いますし、logging.getLogger(name=None) で都度取得して使用してもいいと思います。お好みで。

ハンドラ

(Python) 便利なハンドラ(各種ハンドラの日本語の説明)

(Python) class logging.StreamHandler(stream=None) 引数の stream を指定しない場合は、sys.stderr (標準エラー出力)が使われるとのことでした。

(Python) class logging.FileHandler(filename, mode='a', encoding=None, delay=False, errors=None)

(Python) logging.FileHandler.close()

(Python) logging.Handler.setLevel(level) 「ハンドラ」生成時のロギングレベルは NOTSET(すべてのメッセージが処理される)である旨の説明がありました。

(Python) logging.Handler.setFormatter(fmt)

フォーマッタ

(Python) class logging.Formatter(fmt=None, datefmt=None, style='%', validate=True, *, defaults=None)

(Python) LogRecord 属性 %(asctime)s, %(levelname)s, %(levelno)s, %(message)s, %(name)s, ...

モジュールレベルの関数

(Python) logging.getLevelName(level) ログレベルの数値から、DEBUG や INFO といったレベル名を取得する関数です。

組み込み機能

(Python) __main__(トップレベルのスクリプト環境)

(Python) __name__(インポート関連のモジュール属性)

モジュールの中では、(文字列の) モジュール名をグローバル変数 __name__ で取得できます。

(Python) モジュール(Python チュートリアル)

引用符いんようふの前に「アール r 」を付けた文字列もじれつ

(Python) r''raw stringsロウ ストリングス、raw 文字列)(チュートリアル)

(Python) r''raw stringsロウ ストリングス、raw 文字列)(言語リファレンス)

引用符いんようふの前に「エフ f 」を付けた文字列もじれつ

(Python) f''f-stringエフ ストリング、formatted string literal、フォーマット済み文字列リテラル)(言語リファレンス)

r''f'' を組み合わせた文字列もじれつ rf''

(Python) rf'' フォーマット済みの raw 文字列リテラル

以上です。

スポンサーリンク
シェアする(押すとSNS投稿用の『編集ページ』に移動します)
フォローする(RSSフィードに移動します)
スポンサーリンク
シラベルノート
タイトルとURLをコピーしました