Python の logging
モジュールを使用するときに参照した、ドキュメントの場所をまとめました(Python マニュアルなど)。
これらを見て、multiprocessing
や concurrent.futures
で並列処理しているときの「ロギングのコード例」を書いてきました。
以下、自分が見てきたドキュメントの場所を、コメント付きで紹介します。
関連記事
concurrent.futures
でログを記録するコード例。
multiprocessing
でログを記録するコード例。
普通のシングルプロセスでログを記録するコード例。
- ロガーでロギングするコード例【logging】
- ルートロガーでログを記録するコード例【logging】
- ログファイルを『ファイルサイズでローテーション』するコード例
- ログファイルを『時間でローテーション』するコード例
- logging.NullHandler() の使い方
- logging フォーマットの出力例(10種類以上)
- 例外を自作して使うコード例(ユーザー定義例外)
並列処理のコード例。
並列処理
マルチプロセッシング
multiprocessing.Pool()
でロギングするときは、initializer 関数の中でロギング設定をするのが便利でした。
1つ1つの子プロセスの中で、それぞれ1回だけ、ロガーにハンドラを追加していくことができたので、効率的でした。
initializer 関数に渡す引数のリストは、自動的にアンパックして渡してくれました。
initializer
がNone
ではない場合、各ワーカープロセスは開始時にinitializer(*initargs)
を呼び出します。
(Python) multiprocessing.pool.Pool.map(func, iterable[, chunksize])
コンカレントドットフューチャース
Python 3.7 以降であれば、concurrent.futures
の ProcessPoolExecutor()
でロギングするときも、initializer 関数の中でロギング設定をするのが便利でした。
1つ1つの子プロセスの中で、それぞれ1回だけ、ロガーにハンドラを追加していくことができたので、効率的でした。
Python 3.7 で initializer
と 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
これらの説明を見るに、print()
関数で「標準出力」に表示するときは、内容がごちゃ混ぜになってしまう (liable to get all mixed up) ことを防ぐために、Lock (multiprocessing.Lock
) を使用することができる、という感じでした。
おそらくですが、StreamHandler()
の「標準エラー出力」についても、同じようにロックを使用したほうがいいのかもしれません。
Python マニュアルの説明を見る限り、
「ごちゃまぜで済むなら別にいいや」
とも思いますけれども。とりあえず、ロックの手間をかけるか否かは、自身の目的に照らして判断しています。ロックは実行速度にも影響するでしょうし(未検証)、ロックしなくても十分であったなら、省略したままでも良いと思います。
(Python) sys.stdout
と sys.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 には存在しないからです。
ですが、「複数のプロセス」が「それぞれ別のログファイル」に書き込むことについては、特に記載は無いようでした。
別々のファイルに書き込む分には、なにも競合しないのですから、制限は無いと思います。
プロセスごとに「別のファイル」を読み書きしたり、「別のデータベース」に接続したりするのは、ごく普通の処理でした。
ログファイルも同様でした。
multiprocessing
を使用しているときでも FileHandler
は使用できました。
concurrent.futures
を使用しているときでも FileHandler
は使用できました。
要は、書き込みだけ 1 箇所で行えば良い、ということでした。
その具体例として、 Logging クックブックには「SocketHandler
を使用した方法」と「QueueHandler
を使用した方法(Python 3.2 以降)」が載っていました。
ソケットハンドラを使用した方法。
複数のプロセスから単一ファイルへログ記録しなければならないなら、最も良い方法は、すべてのプロセスが
SocketHandler
に対してログ記録を行い、独立したプロセスとしてソケットサーバを動かすことです。ソケットサーバはソケットから読み取ってファイルにログを書き出します。
キューハンドラを使用した方法。
別の方法として、
Queue
とQueueHandler
を使って、マルチプロセスアプリケーションの1つのプロセスに全ての logging イベントを送る事ができます。
Python 3.2 以降であれば、QueueHandler
を使用した方法が簡単でした。
各プロセスのロガーにそれぞれ取り付けた QueueHandler
が、ログを Queue
に書き込んでいって、QueueListener
のスレッドが、キューからログを取得して書き込んでいく、という感じでした。
QueueHandler
と QueueListener
を使えば、複数のプロセスからのログを、1つのログファイルに記録していくことができました。
自身の目的に合った方法で良いと思います。
もし、ログをネットワーク越しに送信したいときは、ソケットハンドラが便利とのことでした。
ログイベントをネットワーク越しに送信し、受信端でそれを処理したいとしましょう。
SocketHandler
インスタンスを送信端の root logger にアタッチすれば、簡単に実現できます:
ロギング
キューリスナー
QueueListener
の説明です。
(Python) 複数のプロセスからの単一ファイルへのログ記録(Logging クックブック)
(Python) ブロックする handler を扱う(Logging クックブック)
(Python) より手の込んだ multiprocessing の例(Logging クックブック)
(Python) class logging.handlers.QueueListener(queue, *handlers, respect_handler_level=False)
キューハンドラ
QueueHandler の説明です。
(Python) class logging.handlers.QueueHandler(queue)
キュー
QueueHandler
と QueueListener
に使用する Queue
の説明です。
(Python) Using concurrent.futures.ProcessPoolExecutor
(Logging クックブック)
この説明によると、concurrent.futures.ProcessPoolExecutor
のワーカープロセスを使う場合は、Proxy オブジェクト multiprocessing.Manager()
の .Queue(-1)
を使う必要がある、とのことでした。
理由は「pickle 化できること」が必要だから、のようでした。
ProcessPoolExecutor
はmultiprocessing
モジュールを利用します。このため Global Interpreter Lock を回避することができますが、pickle 化できるオブジェクトしか実行したり返したりすることができません。
そこで、multiprocessing.Manager
の SyncManager
から作る「Proxy オブジェクト」というタイプの Queue
が役に立つようでした。
プロキシオブジェクトの重要な機能は pickle 化ができることで、これによりプロセス間での受け渡しができます。 そのため、参照対象が 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) class multiprocessing.managers.SyncManager
(Python) multiprocessing.managers.SyncManager.Queue([maxsize])
(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 でした。
もちろん、ローカル変数に入れて使ってもいいと思いますし、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__
で取得できます。
引用符の前に「アール 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 文字列リテラル
以上です。