Logging クックブック¶
- 著者:
Vinay Sajip <vinay_sajip at red-dove dot com>
このページは、過去に有用であるとされていた、logging に関連するいくつものレシピを含んでいます。チュートリアルやリファレンス情報へのリンクについてはその他のリソース を参照してください。
複数のモジュールで logging を使う¶
logging.getLogger('someLogger') の複数回の呼び出しは同じ logger への参照を返します。これは同じ Python インタプリタプロセス上で動いている限り、一つのモジュールの中からに限らず、モジュールをまたいでも当てはまります。同じオブジェクトへの参照という点でも正しいです。さらに、一つのモジュールの中で親 logger を定義して設定し、別のモジュールで子 logger を定義する (ただし設定はしない) ことが可能で、すべての子 logger への呼び出しは親にまで渡されます。まずはメインのモジュールです:
importloggingimportauxiliary_module# create logger with 'spam_application'logger=logging.getLogger('spam_application')logger.setLevel(logging.DEBUG)# create file handler which logs even debug messagesfh=logging.FileHandler('spam.log')fh.setLevel(logging.DEBUG)# create console handler with a higher log levelch=logging.StreamHandler()ch.setLevel(logging.ERROR)# create formatter and add it to the handlersformatter=logging.Formatter('%(asctime)s -%(name)s -%(levelname)s -%(message)s')fh.setFormatter(formatter)ch.setFormatter(formatter)# add the handlers to the loggerlogger.addHandler(fh)logger.addHandler(ch)logger.info('creating an instance of auxiliary_module.Auxiliary')a=auxiliary_module.Auxiliary()logger.info('created an instance of auxiliary_module.Auxiliary')logger.info('calling auxiliary_module.Auxiliary.do_something')a.do_something()logger.info('finished auxiliary_module.Auxiliary.do_something')logger.info('calling auxiliary_module.some_function()')auxiliary_module.some_function()logger.info('done with auxiliary_module.some_function()')
そして補助モジュール (auxiliary module) がこちらです:
importlogging# create loggermodule_logger=logging.getLogger('spam_application.auxiliary')classAuxiliary:def__init__(self):self.logger=logging.getLogger('spam_application.auxiliary.Auxiliary')self.logger.info('creating an instance of Auxiliary')defdo_something(self):self.logger.info('doing something')a=1+1self.logger.info('done doing something')defsome_function():module_logger.info('received a call to "some_function"')
出力はこのようになります:
2005-03-23 23:47:11,663 - spam_application - INFO - creating an instance of auxiliary_module.Auxiliary2005-03-23 23:47:11,665 - spam_application.auxiliary.Auxiliary - INFO - creating an instance of Auxiliary2005-03-23 23:47:11,665 - spam_application - INFO - created an instance of auxiliary_module.Auxiliary2005-03-23 23:47:11,668 - spam_application - INFO - calling auxiliary_module.Auxiliary.do_something2005-03-23 23:47:11,668 - spam_application.auxiliary.Auxiliary - INFO - doing something2005-03-23 23:47:11,669 - spam_application.auxiliary.Auxiliary - INFO - done doing something2005-03-23 23:47:11,670 - spam_application - INFO - finished auxiliary_module.Auxiliary.do_something2005-03-23 23:47:11,671 - spam_application - INFO - calling auxiliary_module.some_function()2005-03-23 23:47:11,672 - spam_application.auxiliary - INFO - received a call to 'some_function'2005-03-23 23:47:11,673 - spam_application - INFO - done with auxiliary_module.some_function()
複数のスレッドからのロギング¶
複数スレッドからのロギングでは特別に何かをする必要はありません。次の例はmain (初期) スレッドとそれ以外のスレッドからのロギングの例です:
importloggingimportthreadingimporttimedefworker(arg):whilenotarg['stop']:logging.debug('Hi from myfunc')time.sleep(0.5)defmain():logging.basicConfig(level=logging.DEBUG,format='%(relativeCreated)6d%(threadName)s%(message)s')info={'stop':False}thread=threading.Thread(target=worker,args=(info,))thread.start()whileTrue:try:logging.debug('Hello from main')time.sleep(0.75)exceptKeyboardInterrupt:info['stop']=Truebreakthread.join()if__name__=='__main__':main()
実行すると、出力は以下のようになるはずです:
0 Thread-1 Hi from myfunc 3 MainThread Hello from main 505 Thread-1 Hi from myfunc 755 MainThread Hello from main1007 Thread-1 Hi from myfunc1507 MainThread Hello from main1508 Thread-1 Hi from myfunc2010 Thread-1 Hi from myfunc2258 MainThread Hello from main2512 Thread-1 Hi from myfunc3009 MainThread Hello from main3013 Thread-1 Hi from myfunc3515 Thread-1 Hi from myfunc3761 MainThread Hello from main4017 Thread-1 Hi from myfunc4513 MainThread Hello from main4518 Thread-1 Hi from myfunc
予想した通りかもしれませんが、ログ出力が散らばっているのが分かります。もちろん、この手法はより多くのスレッドでも上手くいきます。
複数の handler と formatter¶
logger は通常の Python オブジェクトです。addHandler() メソッドは追加されるハンドラの個数について最小値も最大値も定めていません。時にアプリケーションがすべての深刻度のすべてのメッセージをテキストファイルに記録しつつ、同時にエラーやそれ以上のものをコンソールに出力することが役に立ちます。これを実現する方法は、単に適切なハンドラを設定するだけです。アプリケーションコードの中のログ記録の呼び出しは変更されずに残ります。少し前に取り上げた単純なモジュール式の例を少し変えるとこうなります:
importlogginglogger=logging.getLogger('simple_example')logger.setLevel(logging.DEBUG)# create file handler which logs even debug messagesfh=logging.FileHandler('spam.log')fh.setLevel(logging.DEBUG)# create console handler with a higher log levelch=logging.StreamHandler()ch.setLevel(logging.ERROR)# create formatter and add it to the handlersformatter=logging.Formatter('%(asctime)s -%(name)s -%(levelname)s -%(message)s')ch.setFormatter(formatter)fh.setFormatter(formatter)# add the handlers to loggerlogger.addHandler(ch)logger.addHandler(fh)# 'application' codelogger.debug('debug message')logger.info('info message')logger.warning('warn message')logger.error('error message')logger.critical('critical message')
'application' 部分のコードは複数の handler について何も気にしていないことに注目してください。変更した箇所は新しいfh という名の handler を追加して設定したところがすべてです。
新しい handler を、異なる深刻度に対する filter と共に生成できることは、アプリケーションを書いてテストを行うときとても助けになります。デバッグ用にたくさんのprint 文を使う代わりにlogger.debug を使いましょう。あとで消したりコメントアウトしたりしなければならない print 文と違って、logger.debug 命令はソースコードの中にそのまま残しておいて再び必要になるまで休眠させておけます。その時必要になるのはただ logger および/または handler の深刻度の設定をいじることだけです。
複数の出力先にログを出力する¶
コンソールとファイルに、別々のメッセージ書式で、別々の状況に応じたログ出力を行わせたいとしましょう。例えば DEBUG よりも高いレベルのメッセージはファイルに記録し、INFO 以上のレベルのメッセージはコンソールに出力したいという場合です。また、ファイルにはタイムスタンプを記録し、コンソールには出力しないとします。以下のようにすれば、こうした挙動を実現できます:
importlogging# set up logging to file - see previous section for more detailslogging.basicConfig(level=logging.DEBUG,format='%(asctime)s%(name)-12s%(levelname)-8s%(message)s',datefmt='%m-%d %H:%M',filename='/tmp/myapp.log',filemode='w')# define a Handler which writes INFO messages or higher to the sys.stderrconsole=logging.StreamHandler()console.setLevel(logging.INFO)# set a format which is simpler for console useformatter=logging.Formatter('%(name)-12s:%(levelname)-8s%(message)s')# tell the handler to use this formatconsole.setFormatter(formatter)# add the handler to the root loggerlogging.getLogger('').addHandler(console)# Now, we can log to the root logger, or any other logger. First the root...logging.info('Jackdaws love my big sphinx of quartz.')# Now, define a couple of other loggers which might represent areas in your# application:logger1=logging.getLogger('myapp.area1')logger2=logging.getLogger('myapp.area2')logger1.debug('Quick zephyrs blow, vexing daft Jim.')logger1.info('How quickly daft jumping zebras vex.')logger2.warning('Jail zesty vixen who grabbed pay from quack.')logger2.error('The five boxing wizards jump quickly.')
これを実行すると、コンソールには以下のように出力されます:
root : INFO Jackdaws love my big sphinx of quartz.myapp.area1 : INFO How quickly daft jumping zebras vex.myapp.area2 : WARNING Jail zesty vixen who grabbed pay from quack.myapp.area2 : ERROR The five boxing wizards jump quickly.
そして、ファイルには以下のように出力されるはずです:
10-22 22:19 root INFO Jackdaws love my big sphinx of quartz.10-22 22:19 myapp.area1 DEBUG Quick zephyrs blow, vexing daft Jim.10-22 22:19 myapp.area1 INFO How quickly daft jumping zebras vex.10-22 22:19 myapp.area2 WARNING Jail zesty vixen who grabbed pay from quack.10-22 22:19 myapp.area2 ERROR The five boxing wizards jump quickly.
これを見て分かる通り、DEBUG メッセージはファイルだけに出力され、その他のメッセージは両方に出力されます。
この例ではコンソールとファイルのハンドラだけを使っていますが、実際には任意の数のハンドラや組み合わせを使えます。
ここでファイル名として/tmp/myapp.log を選んだということは、一時ファイルの標準的な場所として POSIX システムを想定していることに注意してください。 Windows の場合、ディレクトリが存在し、そのディレクトリに対してファイルを更新するための適切な権限を有することを保証するために、ログファイル向けのディレクトリ名として異なる選択を取る必要があるでしょう。
ログレベルのカスタム処理¶
しきい値以上のログレベル全てがハンドラによって処理される標準的な処理に対して、ときにはわずかに異なる振る舞いを必要とすることもあるでしょう。そのような場合はフィルタを使う必要があります。以下のような処理が必要なシナリオについて取り上げてみましょう:
深刻度
INFOとWARNINGのメッセージをsys.stdoutに送る深刻度
ERROR以上のメッセージをsys.stderrに送る深刻度
DEBUG以上のメッセージをファイルapp.logに送る
logging モジュールを下記の JSON によって構成したとしましょう:
{"version":1,"disable_existing_loggers":false,"formatters":{"simple":{"format":"%(levelname)-8s - %(message)s"}},"handlers":{"stdout":{"class":"logging.StreamHandler","level":"INFO","formatter":"simple","stream":"ext://sys.stdout"},"stderr":{"class":"logging.StreamHandler","level":"ERROR","formatter":"simple","stream":"ext://sys.stderr"},"file":{"class":"logging.FileHandler","formatter":"simple","filename":"app.log","mode":"w"}},"root":{"level":"DEBUG","handlers":["stderr","stdout","file"]}}
This configuration doesalmost what we want, except thatsys.stdout would show messagesof severityERROR and only events of this severity and higher will be trackedas well asINFO andWARNING messages. To prevent this, we can set up a filter whichexcludes those messages and add it to the relevant handler. This can be configured byadding afilters section parallel toformatters andhandlers:
{"filters":{"warnings_and_below":{"()":"__main__.filter_maker","level":"WARNING"}}}
そしてstdout のハンドラに関するセクションにフィルターを追加します:
{"stdout":{"class":"logging.StreamHandler","level":"INFO","formatter":"simple","stream":"ext://sys.stdout","filters":["warnings_and_below"]}}
フィルターは単なる関数なので、filter_maker (ファクトリ関数) は以下のように定義することができます:
deffilter_maker(level):level=getattr(logging,level)deffilter(record):returnrecord.levelno<=levelreturnfilter
この関数は引数として与えられた文字列をログレベルの数値に変換し、ログレコード内のログレベルが指定されたレベル以下の場合にTrue を返す関数を返します。この例ではfilter_maker をコマンドラインから実行するテストスクリプトmain.py で関数が定義されているため、フィルター設定上のモジュール名は__main__ になります。そのためフィルター設定の中では__main__.filter_maker となります。異なるモジュールで関数を定義した場合、この部分を変更する必要があります。
フィルターを追加したmain.py の全体像は以下のようになり、これで実行できる状態になりました:
importjsonimportloggingimportlogging.configCONFIG='''{ "version": 1, "disable_existing_loggers": false, "formatters": { "simple": { "format": "%(levelname)-8s -%(message)s" } }, "filters": { "warnings_and_below": { "()" : "__main__.filter_maker", "level": "WARNING" } }, "handlers": { "stdout": { "class": "logging.StreamHandler", "level": "INFO", "formatter": "simple", "stream": "ext://sys.stdout", "filters": ["warnings_and_below"] }, "stderr": { "class": "logging.StreamHandler", "level": "ERROR", "formatter": "simple", "stream": "ext://sys.stderr" }, "file": { "class": "logging.FileHandler", "formatter": "simple", "filename": "app.log", "mode": "w" } }, "root": { "level": "DEBUG", "handlers": [ "stderr", "stdout", "file" ] }}'''deffilter_maker(level):level=getattr(logging,level)deffilter(record):returnrecord.levelno<=levelreturnfilterlogging.config.dictConfig(json.loads(CONFIG))logging.debug('A DEBUG message')logging.info('An INFO message')logging.warning('A WARNING message')logging.error('An ERROR message')logging.critical('A CRITICAL message')
これを以下のように実行します:
pythonmain.py2>stderr.log>stdout.logすると期待通りの結果を得ることができます:
$more*.log::::::::::::::app.log::::::::::::::DEBUG-ADEBUGmessageINFO-AnINFOmessageWARNING-AWARNINGmessageERROR-AnERRORmessageCRITICAL-ACRITICALmessage::::::::::::::stderr.log::::::::::::::ERROR-AnERRORmessageCRITICAL-ACRITICALmessage::::::::::::::stdout.log::::::::::::::INFO-AnINFOmessageWARNING-AWARNINGmessage
設定サーバの例¶
ログ記録設定サーバを使うモジュールの例です:
importloggingimportlogging.configimporttimeimportos# read initial config filelogging.config.fileConfig('logging.conf')# create and start listener on port 9999t=logging.config.listen(9999)t.start()logger=logging.getLogger('simpleExample')try:# loop through logging calls to see the difference# new configurations make, until Ctrl+C is pressedwhileTrue:logger.debug('debug message')logger.info('info message')logger.warning('warn message')logger.error('error message')logger.critical('critical message')time.sleep(5)exceptKeyboardInterrupt:# cleanuplogging.config.stopListening()t.join()
そしてファイル名を受け取ってそのファイルをサーバに送るスクリプトですが、それに先だってバイナリエンコード長を新しいログ記録の設定として先に送っておきます:
#!/usr/bin/env pythonimportsocket,sys,structwithopen(sys.argv[1],'rb')asf:data_to_send=f.read()HOST='localhost'PORT=9999s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)print('connecting...')s.connect((HOST,PORT))print('sending config...')s.send(struct.pack('>L',len(data_to_send)))s.send(data_to_send)s.close()print('complete')
ブロックする handler を扱う¶
ときどき、logging を行っているスレッドをブロックせずに、handler が動くようにしないといけないときがあります。これは Web アプリケーションではよくあることですし、もちろん他のシナリオでも起きる話です。
動作が鈍くなるときの元凶はたいてい、開発者のコントロール外にあるいくつもの理由で (例えば、残念なパフォーマンスのメールやネットワークのインフラ) 、SMTPHandler: が電子メールを送るのに時間がかかることです。しかし、ほとんどのネットワークをまたぐ handler はブロックする可能性があります:SocketHandler による処理でさえ、裏で DNS への問い合わせというとても遅い処理を行うことがあります (そしてこの問い合わせ処理は、 Python の層より下のあなたの手の届かない、ソケットライブラリの深いところにある可能性もあります)。
解決策の1つは、2パートに分離したアプローチを用いることです。最初のパートは、パフォーマンスが重要なスレッドからアクセスされる、QueueHandler だけをアタッチした logger です。この logger は単に、十分大きい、あるいは無制限の容量を持ったキューに書き込むだけです。キューへの書き込みは通常すぐに完了しますが、念の為にqueue.Full 例外をキャッチする必要があるかもしれません。もしパフォーマンスクリティカルなスレッドを持つライブラリの開発者であるなら、このことを (QueueHandler だけをアタッチした logger についての言及を添えて) ドキュメントに書いておきましょう。
2つ目のパートはQueueHandler の対向として作られたQueueListener です。QueueListener はとてもシンプルで、キューと handler を渡され、内部でQueueHandler (もしくは他のLogRecord の出力元) から送られた LogRecord をキューから受け取るスレッドを起動します。LogRecord をキューから取り出して、 handler に渡して処理させます。
分離したQueueListener クラスを持つメリットは、複数のQueueHandler に対して1つのインスタンスで logging できることです。既存の handler のスレッド利用版を使って handler ごとにスレッドを持つよりはずっとリソースにやさしくなります。
この2つのクラスを利用する例です (import は省略):
que=queue.Queue(-1)# no limit on sizequeue_handler=QueueHandler(que)handler=logging.StreamHandler()listener=QueueListener(que,handler)root=logging.getLogger()root.addHandler(queue_handler)formatter=logging.Formatter('%(threadName)s:%(message)s')handler.setFormatter(formatter)listener.start()# The log output will display the thread which generated# the event (the main thread) rather than the internal# thread which monitors the internal queue. This is what# you want to happen.root.warning('Look out!')listener.stop()
実行すると次のように出力します:
MainThread: Look out!
注釈
上述の議論は特に同期コードにについてのものではなく、むしろ遅いロギングハンドラについてでしたが、非同期コードやネットワーク、あるいはファイルハンドラからのロギングでさえも (イベントループをブロックしてしまう) 問題につながる可能性があることには注意すべきでしょう。これはいくつかのロギングがasyncio 内部で行われることが理由です。何らかの非同期コードがアプリケーションの中で使われている場合、ロギングには上記のアプローチを使っていかなるブロッキングコードもQueueListener スレッド内だけで実行されるようにしておくことが最も良いでしょう。
バージョン 3.5 で変更:Python 3.5 以前は、QueueListener クラスは常にキューから受け取ったメッセージを、初期化元となっているそれぞれのハンドラーに受け渡していました。 ( というのも、レベルフィルターリングは別サイド、つまり、キューが満たされている場所で処理されるということが想定されているからです) Python 3.5以降では、キーワードとなる引数respect_handler_level=True をリスナーのコントラクターに受け渡すことで、この挙動を変更することができるようになっています。これが行われると、各メッセージのレベルをハンドラーのレベルと比較して、そうすることが適切な場合のみ、メッセージをハンドラーに渡します。
バージョン 3.14 で変更:TheQueueListener can be started (and stopped) via thewith statement. For example:
withQueueListener(que,handler)aslistener:# The queue listener automatically starts# when the 'with' block is entered.pass# The queue listener automatically stops once# the 'with' block is exited.
ネットワーク越しの logging イベントの送受信¶
ログイベントをネットワーク越しに送信し、受信端でそれを処理したいとしましょう。SocketHandler インスタンスを送信端の root logger にアタッチすれば、簡単に実現できます:
importlogging,logging.handlersrootLogger=logging.getLogger('')rootLogger.setLevel(logging.DEBUG)socketHandler=logging.handlers.SocketHandler('localhost',logging.handlers.DEFAULT_TCP_LOGGING_PORT)# don't bother with a formatter, since a socket handler sends the event as# an unformatted picklerootLogger.addHandler(socketHandler)# Now, we can log to the root logger, or any other logger. First the root...logging.info('Jackdaws love my big sphinx of quartz.')# Now, define a couple of other loggers which might represent areas in your# application:logger1=logging.getLogger('myapp.area1')logger2=logging.getLogger('myapp.area2')logger1.debug('Quick zephyrs blow, vexing daft Jim.')logger1.info('How quickly daft jumping zebras vex.')logger2.warning('Jail zesty vixen who grabbed pay from quack.')logger2.error('The five boxing wizards jump quickly.')
受信端ではsocketserver モジュールを使って受信プログラムを作成しておきます。簡単な実用プログラムを以下に示します:
importpickleimportloggingimportlogging.handlersimportsocketserverimportstructclassLogRecordStreamHandler(socketserver.StreamRequestHandler):"""Handler for a streaming logging request. This basically logs the record using whatever logging policy is configured locally. """defhandle(self):""" Handle multiple requests - each expected to be a 4-byte length, followed by the LogRecord in pickle format. Logs the record according to whatever policy is configured locally. """whileTrue:chunk=self.connection.recv(4)iflen(chunk)<4:breakslen=struct.unpack('>L',chunk)[0]chunk=self.connection.recv(slen)whilelen(chunk)<slen:chunk=chunk+self.connection.recv(slen-len(chunk))obj=self.unPickle(chunk)record=logging.makeLogRecord(obj)self.handleLogRecord(record)defunPickle(self,data):returnpickle.loads(data)defhandleLogRecord(self,record):# if a name is specified, we use the named logger rather than the one# implied by the record.ifself.server.lognameisnotNone:name=self.server.lognameelse:name=record.namelogger=logging.getLogger(name)# N.B. EVERY record gets logged. This is because Logger.handle# is normally called AFTER logger-level filtering. If you want# to do filtering, do it at the client end to save wasting# cycles and network bandwidth!logger.handle(record)classLogRecordSocketReceiver(socketserver.ThreadingTCPServer):""" Simple TCP socket-based logging receiver suitable for testing. """allow_reuse_address=Truedef__init__(self,host='localhost',port=logging.handlers.DEFAULT_TCP_LOGGING_PORT,handler=LogRecordStreamHandler):socketserver.ThreadingTCPServer.__init__(self,(host,port),handler)self.abort=0self.timeout=1self.logname=Nonedefserve_until_stopped(self):importselectabort=0whilenotabort:rd,wr,ex=select.select([self.socket.fileno()],[],[],self.timeout)ifrd:self.handle_request()abort=self.abortdefmain():logging.basicConfig(format='%(relativeCreated)5d%(name)-15s%(levelname)-8s%(message)s')tcpserver=LogRecordSocketReceiver()print('About to start TCP server...')tcpserver.serve_until_stopped()if__name__=='__main__':main()
先にサーバを起動しておき、次にクライアントを起動します。クライアント側では、コンソールには何も出力されません; サーバ側では以下のようなメッセージを目にするはずです:
About to start TCP server... 59 root INFO Jackdaws love my big sphinx of quartz. 59 myapp.area1 DEBUG Quick zephyrs blow, vexing daft Jim. 69 myapp.area1 INFO How quickly daft jumping zebras vex. 69 myapp.area2 WARNING Jail zesty vixen who grabbed pay from quack. 69 myapp.area2 ERROR The five boxing wizards jump quickly.
注意: pickle にはいくつかのシナリオでセキュリティ上の問題があります。これらが影響する場合は、makePickle() メソッドをオーバーライドし代替シリアライズ手法を実装して、それを使うように上記のスクリプトを修正することもできます。
作成中のロギングソケットのリスナーを実行する¶
作成中のロギングリスナーを実行するためには、Supervisor のようなプロセス管理ツールを使う必要があるかもしれません。こちらの Gist は Supervisor を使って上記の機能を実行するための必要最低限のファイルを提供しています。以下のファイルが必要になります:
ファイル | 目的 |
|---|---|
| 試験用の環境を準備する Bash スクリプト |
| リスナーとマルチプロセスの web アプリケーションのための設定を含む Supervisor の設定ファイル |
| Supervisor が上記の設定で実行されていることを保証するための Bash スクリプト |
| ログイベントを受信してファイルに記録するソケットリスナープログラム |
| リスナーに接続されたソケットを通じてロギングを実行する簡単な web アプリケーション |
| web アプリケーションのための JSON 設定ファイル |
| web アプリケーションを起動するための Python スクリプト |
この web アプリケーションは、リクエストを処理する複数のワーカープロセスを起動する web アプリケーションサーバーであるGunicorn を使っています。ここに例として挙げた設定は、複数のワーカーがいかにして互いに衝突することなく同じログファイルに書き込みを行うことができるかを示しています --- ワーカーは全てソケットリスナーを通じてログを書き込むのです。
これらのファイルを試すためには、 POSIX 環境において以下を行なってください:
Download ZIP ボタンを押してGist を ZIP アーカイブとしてダウンロードしてください。
アーカイブファイルをスクラッチディレクトリに展開してください。
準備のために、スクラッチディレクトリにおいて
bashprepare.shを実行してください。これにより Supervisor 関連のファイルおよびログファイルのためのrunサブディレクトリ、およびbottle,gunicornそしてsupervisorがインストールされる仮想環境を含むvenvサブディレクトリが生成されます。bashensure_app.shを実行して Supervisor が上記の設定で実行されていることを確認してください。venv/bin/pythonclient.pyを実行して web アプリケーションを起動してください。これによりログにレコードが書き込まれるはずです。runサブディレクトリにあるログファイルを調べてください。最新のログは、パターンapp.log*に一致する名前のファイルにあるはずです。ログは異なるワーカープロセスによって非決定論的な形で並行に処理されるため、特定の決まった順番にはなりません。リスナーと web アプリケーションは
venv/bin/supervisorctl-csupervisor.confshutdownを実行することでシャットダウンできます。
ありそうもないことですが、テスト環境で設定したポートが別の設定と衝突してしまった場合、設定ファイルを修正する必要があるかもしれません。
The default configuration uses a TCP socket on port 9020. You can use a UnixDomain socket instead of a TCP socket by doing the following:
In
listener.json, add asocketkey with the path to the domainsocket you want to use. If this key is present, the listener listens on thecorresponding domain socket and not on a TCP socket (theportkey isignored).In
webapp.json, change the socket handler configuration dictionaryso that thehostvalue is the path to the domain socket, and set theportvalue tonull.
コンテキスト情報をログ記録出力に付加する¶
時にはログ記録出力にログ関数の呼び出し時に渡されたパラメータに加えてコンテキスト情報を含めたいこともあるでしょう。たとえば、ネットワークアプリケーションで、クライアント固有の情報 (例: リモートクライアントの名前、 IP アドレス) もログ記録に残しておきたいと思ったとしましょう。extra パラメータをこの目的に使うこともできますが、いつでもこの方法で情報を渡すのが便利なやり方とも限りません。また接続ごとにLogger インスタンスを生成する誘惑に駆られるかもしれませんが、生成したLogger インスタンスはガーベジコレクションで回収されないので、これは良いアイデアとは言えません。この例は現実的な問題ではないかもしれませんが、Logger インスタンスの個数がアプリケーションの中でログ記録が行われるレベルの粒度に依存する場合、Logger インスタンスの個数が事実上無制限にならないと、管理が難しくなります。
LoggerAdapter を使ったコンテキスト情報の伝達¶
logging イベントの情報と一緒に出力されるコンテキスト情報を渡す簡単な方法は、LoggerAdapter を使うことです。このクラスはLogger のように見えるように設計されていて、debug(),info(),warning(),error(),exception(),critical(),log() の各メソッドを呼び出せるようになっています。これらのメソッドは対応するLogger のメソッドと同じ引数を取るので、二つの型を取り替えて使うことができます。
LoggerAdapter のインスタンスを生成する際には、Logger インスタンスとコンテキスト情報を収めた辞書風 (dict-like) のオブジェクトを渡します。LoggerAdapter のログ記録メソッドを呼び出すと、呼び出しをコンストラクタに渡された配下のLogger インスタンスに委譲し、その際コンテキスト情報をその委譲された呼び出しに埋め込みます。LoggerAdapter のコードから少し抜き出してみます:
defdebug(self,msg,/,*args,**kwargs):""" Delegate a debug call to the underlying logger, after adding contextual information from this adapter instance. """msg,kwargs=self.process(msg,kwargs)self.logger.debug(msg,*args,**kwargs)
LoggerAdapter のprocess() メソッドがコンテキスト情報をログ出力に加える場所です。そこではログ記録呼び出しのメッセージとキーワード引数が渡され、加工された (可能性のある) それらの情報を配下のロガーへの呼び出しに渡し直します。このメソッドのデフォルト実装ではメッセージは元のままですが、キーワード引数にはコンストラクタに渡された辞書風オブジェクトを値として "extra" キーが挿入されます。もちろん、呼び出し時に "extra" キーワードを使った場合には何事もなかったかのように上書きされます。
"extra" を用いる利点は辞書風オブジェクトの中の値がLogRecord インスタンスの __dict__ にマージされることで、辞書風オブジェクトのキーを知っているFormatter を用意して文字列をカスタマイズするようにできることです。それ以外のメソッドが必要なとき、たとえばコンテキスト情報をメッセージの前や後ろにつなげたい場合には、LoggerAdapter からprocess() を望むようにオーバライドしたサブクラスを作ることが必要なだけです。次に挙げるのはこのクラスを使った例で、コンストラクタで使われる「辞書風」オブジェクトにどの振る舞いが必要なのかも示しています:
classCustomAdapter(logging.LoggerAdapter):""" This example adapter expects the passed in dict-like object to have a 'connid' key, whose value in brackets is prepended to the log message. """defprocess(self,msg,kwargs):return'[%s]%s'%(self.extra['connid'],msg),kwargs
これを次のように使うことができます:
logger=logging.getLogger(__name__)adapter=CustomAdapter(logger,{'connid':some_conn_id})
これで、この adapter 経由でログした全てのイベントに対して、some_conn_id の値がログメッセージの前に追加されます。
コンテキスト情報を渡すために dict 以外のオブジェクトを使う¶
LoggerAdapter に渡すのは本物の dict でなくても構いません。__getitem__ と__iter__ を実装していて logging が辞書のように扱えるクラスのインスタンスを利用することができます。これは (dict の値が固定されるのに対して) 値を動的に生成できるので便利です。
Filter を使ったコンテキスト情報の伝達¶
ユーザ定義のFilter を使ってログ出力にコンテキスト情報を加えることもできます。Filter インスタンスは、渡されたLogRecords を修正することができます。これにより、適切なフォーマット文字列や必要ならFormatter を使って、出力となる属性を新しく追加することも出来ます。
例えば、web アプリケーションで、処理されるリクエスト (または、少なくともその重要な部分) は、スレッドローカル (threading.local) な変数に保存して、Filter からアクセスすることで、LogRecord にリクエストの情報を追加できます。例えば、リモート IP アドレスやリモートユーザのユーザ名にアクセスしたいなら、上述のLoggerAdapter の例のように属性名 'ip' や 'user' を使うといったようにです。その場合、同じフォーマット文字列を使って以下に示すように似たような出力を得られます。これはスクリプトの例です:
importloggingfromrandomimportchoiceclassContextFilter(logging.Filter):""" This is a filter which injects contextual information into the log. Rather than use actual contextual information, we just use random data in this demo. """USERS=['jim','fred','sheila']IPS=['123.231.231.123','127.0.0.1','192.168.0.1']deffilter(self,record):record.ip=choice(ContextFilter.IPS)record.user=choice(ContextFilter.USERS)returnTrueif__name__=='__main__':levels=(logging.DEBUG,logging.INFO,logging.WARNING,logging.ERROR,logging.CRITICAL)logging.basicConfig(level=logging.DEBUG,format='%(asctime)-15s%(name)-5s%(levelname)-8s IP:%(ip)-15s User:%(user)-8s%(message)s')a1=logging.getLogger('a.b.c')a2=logging.getLogger('d.e.f')f=ContextFilter()a1.addFilter(f)a2.addFilter(f)a1.debug('A debug message')a1.info('An info message with%s','some parameters')forxinrange(10):lvl=choice(levels)lvlname=logging.getLevelName(lvl)a2.log(lvl,'A message at%s level with%d%s',lvlname,2,'parameters')
実行すると、以下のようになります:
2010-09-06 22:38:15,292 a.b.c DEBUG IP: 123.231.231.123 User: fred A debug message2010-09-06 22:38:15,300 a.b.c INFO IP: 192.168.0.1 User: sheila An info message with some parameters2010-09-06 22:38:15,300 d.e.f CRITICAL IP: 127.0.0.1 User: sheila A message at CRITICAL level with 2 parameters2010-09-06 22:38:15,300 d.e.f ERROR IP: 127.0.0.1 User: jim A message at ERROR level with 2 parameters2010-09-06 22:38:15,300 d.e.f DEBUG IP: 127.0.0.1 User: sheila A message at DEBUG level with 2 parameters2010-09-06 22:38:15,300 d.e.f ERROR IP: 123.231.231.123 User: fred A message at ERROR level with 2 parameters2010-09-06 22:38:15,300 d.e.f CRITICAL IP: 192.168.0.1 User: jim A message at CRITICAL level with 2 parameters2010-09-06 22:38:15,300 d.e.f CRITICAL IP: 127.0.0.1 User: sheila A message at CRITICAL level with 2 parameters2010-09-06 22:38:15,300 d.e.f DEBUG IP: 192.168.0.1 User: jim A message at DEBUG level with 2 parameters2010-09-06 22:38:15,301 d.e.f ERROR IP: 127.0.0.1 User: sheila A message at ERROR level with 2 parameters2010-09-06 22:38:15,301 d.e.f DEBUG IP: 123.231.231.123 User: fred A message at DEBUG level with 2 parameters2010-09-06 22:38:15,301 d.e.f INFO IP: 123.231.231.123 User: fred A message at INFO level with 2 parameters
contextvars の利用¶
Python 3.7 以降、contextvars モジュールはthreading とasyncio の両方の処理上のニーズを満たすコンテキストローカルな変数を提供しています。このタイプの変数はスレッドローカルな変数に適しています。以下の例は、マルチスレッド環境において、 いかにして web アプリケーションによって処理されるリクエスト属性のような処理の文脈上の情報とともにログを投入できるかを示します。
説明のため、同じ Python プロセス上で共通のライブラリを使っている独立した複数の web アプリケーションがあるとします。これらのアプリケーションが、クライアントの IP アドレスや HTTP リクエストメソッド、およびクライアントのユーザー名のようなコンテキスト依存の情報を追加情報として含んだ自身のログを、共通のライブラリ (と他のリクエスト処理のためのコード) から各アプリケーションのログファイルへ適切に振り向けるためにはどのようにしたら良いでしょうか?
ここで、ライブラリは以下のコードで模することができるとします:
# webapplib.pyimportloggingimporttimelogger=logging.getLogger(__name__)defuseful():# Just a representative event logged from the librarylogger.debug('Hello from webapplib!')# Just sleep for a bit so other threads get to runtime.sleep(0.01)
複数の web アプリケーションは2つの簡単なクラス,Request とWebApp, によって模倣することができます。これらは実際のスレッド化された web アプリケーションがどのように動作するかを模擬的にあらわしています - すなわち各リクエストはスレッドで処理される状況です:
# main.pyimportargparsefromcontextvarsimportContextVarimportloggingimportosfromrandomimportchoiceimportthreadingimportwebappliblogger=logging.getLogger(__name__)root=logging.getLogger()root.setLevel(logging.DEBUG)classRequest:""" A simple dummy request class which just holds dummy HTTP request method, client IP address and client username """def__init__(self,method,ip,user):self.method=methodself.ip=ipself.user=user# A dummy set of requests which will be used in the simulation - we'll just pick# from this list randomly. Note that all GET requests are from 192.168.2.XXX# addresses, whereas POST requests are from 192.16.3.XXX addresses. Three users# are represented in the sample requests.REQUESTS=[Request('GET','192.168.2.20','jim'),Request('POST','192.168.3.20','fred'),Request('GET','192.168.2.21','sheila'),Request('POST','192.168.3.21','jim'),Request('GET','192.168.2.22','fred'),Request('POST','192.168.3.22','sheila'),]# Note that the format string includes references to request context information# such as HTTP method, client IP and usernameformatter=logging.Formatter('%(threadName)-11s%(appName)s%(name)-9s%(user)-6s%(ip)s%(method)-4s%(message)s')# Create our context variables. These will be filled at the start of request# processing, and used in the logging that happens during that processingctx_request=ContextVar('request')ctx_appname=ContextVar('appname')classInjectingFilter(logging.Filter):""" A filter which injects context-specific information into logs and ensures that only information for a specific webapp is included in its log """def__init__(self,app):self.app=appdeffilter(self,record):request=ctx_request.get()record.method=request.methodrecord.ip=request.iprecord.user=request.userrecord.appName=appName=ctx_appname.get()returnappName==self.app.nameclassWebApp:""" A dummy web application class which has its own handler and filter for a webapp-specific log. """def__init__(self,name):self.name=namehandler=logging.FileHandler(name+'.log','w')f=InjectingFilter(self)handler.setFormatter(formatter)handler.addFilter(f)root.addHandler(handler)self.num_requests=0defprocess_request(self,request):""" This is the dummy method for processing a request. It's called on a different thread for every request. We store the context information into the context vars before doing anything else. """ctx_request.set(request)ctx_appname.set(self.name)self.num_requests+=1logger.debug('Request processing started')webapplib.useful()logger.debug('Request processing finished')defmain():fn=os.path.splitext(os.path.basename(__file__))[0]adhf=argparse.ArgumentDefaultsHelpFormatterap=argparse.ArgumentParser(formatter_class=adhf,prog=fn,description='Simulate a couple of web ''applications handling some ''requests, showing how request ''context can be used to ''populate logs')aa=ap.add_argumentaa('--count','-c',type=int,default=100,help='How many requests to simulate')options=ap.parse_args()# Create the dummy webapps and put them in a list which we can use to select# from randomlyapp1=WebApp('app1')app2=WebApp('app2')apps=[app1,app2]threads=[]# Add a common handler which will capture all eventshandler=logging.FileHandler('app.log','w')handler.setFormatter(formatter)root.addHandler(handler)# Generate calls to process requestsforiinrange(options.count):try:# Pick an app at random and a request for it to processapp=choice(apps)request=choice(REQUESTS)# Process the request in its own threadt=threading.Thread(target=app.process_request,args=(request,))threads.append(t)t.start()exceptKeyboardInterrupt:break# Wait for the threads to terminatefortinthreads:t.join()forappinapps:print('%s processed%s requests'%(app.name,app.num_requests))if__name__=='__main__':main()
上記のコードを実行すると、およそ半分のリクエストがapp1.log に、残り半分がapp2.log にそれぞれ記録され、同時に全てのリクエストがapp.log に記録されることがわかるでしょう。それぞれの web アプリケーション固有のログはそれぞれのアプリケーションからのログエントリだけを含み、リクエスト情報が矛盾なくログに表示されている (すなわち、各ダミーリクエストの情報は常にログの行とともに現れる) はずです。このことは以下のシェルコマンドの出力により例示されています:
~/logging-contextual-webapp$pythonmain.pyapp1processed51requestsapp2processed49requests~/logging-contextual-webapp$wc-l*.log153app1.log147app2.log300app.log600total~/logging-contextual-webapp$head-3app1.logThread-3(process_request)app1__main__jim192.168.3.21POSTRequestprocessingstartedThread-3(process_request)app1webapplibjim192.168.3.21POSTHellofromwebapplib!Thread-5(process_request)app1__main__jim192.168.3.21POSTRequestprocessingstarted~/logging-contextual-webapp$head-3app2.logThread-1(process_request)app2__main__sheila192.168.2.21GETRequestprocessingstartedThread-1(process_request)app2webapplibsheila192.168.2.21GETHellofromwebapplib!Thread-2(process_request)app2__main__jim192.168.2.20GETRequestprocessingstarted~/logging-contextual-webapp$headapp.logThread-1(process_request)app2__main__sheila192.168.2.21GETRequestprocessingstartedThread-1(process_request)app2webapplibsheila192.168.2.21GETHellofromwebapplib!Thread-2(process_request)app2__main__jim192.168.2.20GETRequestprocessingstartedThread-3(process_request)app1__main__jim192.168.3.21POSTRequestprocessingstartedThread-2(process_request)app2webapplibjim192.168.2.20GETHellofromwebapplib!Thread-3(process_request)app1webapplibjim192.168.3.21POSTHellofromwebapplib!Thread-4(process_request)app2__main__fred192.168.2.22GETRequestprocessingstartedThread-5(process_request)app1__main__jim192.168.3.21POSTRequestprocessingstartedThread-4(process_request)app2webapplibfred192.168.2.22GETHellofromwebapplib!Thread-6(process_request)app1__main__jim192.168.3.21POSTRequestprocessingstarted~/logging-contextual-webapp$grepapp1app1.log|wc-l153~/logging-contextual-webapp$grepapp2app2.log|wc-l147~/logging-contextual-webapp$grepapp1app.log|wc-l153~/logging-contextual-webapp$grepapp2app.log|wc-l147
ハンドラ内でのコンテキスト情報の付与¶
Handler はそれぞれ一連のフィルタを独自に持っています。コンテキスト情報を、他のハンドラに漏らすことなくLogRecord に付加したい場合、以下に示すスクリプトのように、直接レコードを編集する代わりに新しいLogRecord を返すフィルタを使うことができます:
importcopyimportloggingdeffilter(record:logging.LogRecord):record=copy.copy(record)record.user='jim'returnrecordif__name__=='__main__':logger=logging.getLogger()logger.setLevel(logging.INFO)handler=logging.StreamHandler()formatter=logging.Formatter('%(message)s from%(user)-8s')handler.setFormatter(formatter)handler.addFilter(filter)logger.addHandler(handler)logger.info('A log message')
複数のプロセスからの単一ファイルへのログ記録¶
ログ記録はスレッドセーフであり、単一プロセスの複数のスレッドからの単一ファイルへのログ記録はサポートされています が、複数プロセス からの単一ファイルへのログ記録はサポートされません 。なぜなら、複数のプロセスをまたいで単一のファイルへのアクセスを直列化する標準の方法が Python には存在しないからです。複数のプロセスから単一ファイルへログ記録しなければならないなら、最も良い方法は、すべてのプロセスがSocketHandler に対してログ記録を行い、独立したプロセスとしてソケットサーバを動かすことです。ソケットサーバはソケットから読み取ってファイルにログを書き出します。 (この機能を実行するために、既存のプロセスの 1 つのスレッドを割り当てることもできます)この節 では、このアプローチをさらに詳細に文書化しています。動作するソケット受信プログラムが含まれているので、アプリケーションに組み込むための出発点として使用できるでしょう。
You could also write your own handler which uses theLockclass from themultiprocessing module to serialize access to thefile from your processes. The stdlibFileHandler and subclasses donot make use ofmultiprocessing.
別の方法として、Queue とQueueHandler を使って、マルチプロセスアプリケーションの1つのプロセスに全ての logging イベントを送る事ができます。次の例はこれを行う方法を示します。この例では独立した listener プロセスが他のプロセスから送られた event を受け取り、それを独自の logging 設定にしたがって保存します。この例は実装の方法の1つを示しているだけ (例えば、 listener プロセスを分離する代わりに listener スレッドを使うこともできます) ですが、これは listener とアプリケーション内の他のプロセスで完全に異なる設定を使う例になっているので、各自の要求に応じたコードを書く叩き台になるでしょう:
# You'll need these imports in your own codeimportloggingimportlogging.handlersimportmultiprocessing# Next two import lines for this demo onlyfromrandomimportchoice,randomimporttime## Because you'll want to define the logging configurations for listener and workers, the# listener and worker process functions take a configurer parameter which is a callable# for configuring logging for that process. These functions are also passed the queue,# which they use for communication.## In practice, you can configure the listener however you want, but note that in this# simple example, the listener does not apply level or filter logic to received records.# In practice, you would probably want to do this logic in the worker processes, to avoid# sending events which would be filtered out between processes.## The size of the rotated files is made small so you can see the results easily.deflistener_configurer():root=logging.getLogger()h=logging.handlers.RotatingFileHandler('mptest.log','a',300,10)f=logging.Formatter('%(asctime)s%(processName)-10s%(name)s%(levelname)-8s%(message)s')h.setFormatter(f)root.addHandler(h)# This is the listener process top-level loop: wait for logging events# (LogRecords)on the queue and handle them, quit when you get a None for a# LogRecord.deflistener_process(queue,configurer):configurer()whileTrue:try:record=queue.get()ifrecordisNone:# We send this as a sentinel to tell the listener to quit.breaklogger=logging.getLogger(record.name)logger.handle(record)# No level or filter logic applied - just do it!exceptException:importsys,tracebackprint('Whoops! Problem:',file=sys.stderr)traceback.print_exc(file=sys.stderr)# Arrays used for random selections in this demoLEVELS=[logging.DEBUG,logging.INFO,logging.WARNING,logging.ERROR,logging.CRITICAL]LOGGERS=['a.b.c','d.e.f']MESSAGES=['Random message #1','Random message #2','Random message #3',]# The worker configuration is done at the start of the worker process run.# Note that on Windows you can't rely on fork semantics, so each process# will run the logging configuration code when it starts.defworker_configurer(queue):h=logging.handlers.QueueHandler(queue)# Just the one handler neededroot=logging.getLogger()root.addHandler(h)# send all messages, for demo; no other level or filter logic applied.root.setLevel(logging.DEBUG)# This is the worker process top-level loop, which just logs ten events with# random intervening delays before terminating.# The print messages are just so you know it's doing something!defworker_process(queue,configurer):configurer(queue)name=multiprocessing.current_process().nameprint('Worker started:%s'%name)foriinrange(10):time.sleep(random())logger=logging.getLogger(choice(LOGGERS))level=choice(LEVELS)message=choice(MESSAGES)logger.log(level,message)print('Worker finished:%s'%name)# Here's where the demo gets orchestrated. Create the queue, create and start# the listener, create ten workers and start them, wait for them to finish,# then send a None to the queue to tell the listener to finish.defmain():queue=multiprocessing.Queue(-1)listener=multiprocessing.Process(target=listener_process,args=(queue,listener_configurer))listener.start()workers=[]foriinrange(10):worker=multiprocessing.Process(target=worker_process,args=(queue,worker_configurer))workers.append(worker)worker.start()forwinworkers:w.join()queue.put_nowait(None)listener.join()if__name__=='__main__':main()
上のスクリプトの亜種で、logging をメインプロセスの別スレッドで行う例:
importloggingimportlogging.configimportlogging.handlersfrommultiprocessingimportProcess,Queueimportrandomimportthreadingimporttimedeflogger_thread(q):whileTrue:record=q.get()ifrecordisNone:breaklogger=logging.getLogger(record.name)logger.handle(record)defworker_process(q):qh=logging.handlers.QueueHandler(q)root=logging.getLogger()root.setLevel(logging.DEBUG)root.addHandler(qh)levels=[logging.DEBUG,logging.INFO,logging.WARNING,logging.ERROR,logging.CRITICAL]loggers=['foo','foo.bar','foo.bar.baz','spam','spam.ham','spam.ham.eggs']foriinrange(100):lvl=random.choice(levels)logger=logging.getLogger(random.choice(loggers))logger.log(lvl,'Message no.%d',i)if__name__=='__main__':q=Queue()d={'version':1,'formatters':{'detailed':{'class':'logging.Formatter','format':'%(asctime)s%(name)-15s%(levelname)-8s%(processName)-10s%(message)s'}},'handlers':{'console':{'class':'logging.StreamHandler','level':'INFO',},'file':{'class':'logging.FileHandler','filename':'mplog.log','mode':'w','formatter':'detailed',},'foofile':{'class':'logging.FileHandler','filename':'mplog-foo.log','mode':'w','formatter':'detailed',},'errors':{'class':'logging.FileHandler','filename':'mplog-errors.log','mode':'w','level':'ERROR','formatter':'detailed',},},'loggers':{'foo':{'handlers':['foofile']}},'root':{'level':'DEBUG','handlers':['console','file','errors']},}workers=[]foriinrange(5):wp=Process(target=worker_process,name='worker%d'%(i+1),args=(q,))workers.append(wp)wp.start()logging.config.dictConfig(d)lp=threading.Thread(target=logger_thread,args=(q,))lp.start()# At this point, the main process could do some useful work of its own# Once it's done that, it can wait for the workers to terminate...forwpinworkers:wp.join()# And now tell the logging thread to finish up, tooq.put(None)lp.join()
こちらは特定の logger に設定を適用する例になっています。foo logger はfoo サブシステムの中の全てのイベントをmplog-foo.log に保存する特別な handler を持っています。これはメインプロセス側の log 処理で使われ、(logging イベントは worker プロセス側で生成されますが) 各メッセージを適切な出力先に出力します。
concurrent.futures.ProcessPoolExecutorの利用¶
もし、ワーカープロセスを起動するためにconcurrent.futures.ProcessPoolExecutor を使いたいのであれば、少し違う方法でキューを作る必要があります。次のような方法の代わりに
queue=multiprocessing.Queue(-1)
次のコードを利用する必要があります
queue=multiprocessing.Manager().Queue(-1)# also works with the examples above
そして、次のようなワーカー作成コードがあったとすると:
workers=[]foriinrange(10):worker=multiprocessing.Process(target=worker_process,args=(queue,worker_configurer))workers.append(worker)worker.start()forwinworkers:w.join()
次のように変更します(最初にconcurrent.futures をインポートするのを忘れないようにしましょう):
withconcurrent.futures.ProcessPoolExecutor(max_workers=10)asexecutor:foriinrange(10):executor.submit(worker_process,queue,worker_configurer)
Gunicorn と uWSGI を用いた Web アプリケーションのデプロイ¶
Gunicorn やuWSGI (またはそれに類するもの) を使って Web アプリケーションをデプロイする場合、クライアントのリクエストを処理するために複数のワーカープロセスが作られます。そのような環境では、 web アプリケーションに直接ファイルベースのハンドラを作ることは避けてください。 代わりに、SocketHandler を使って別のプロセスとして動作するリスナーに web アプリケーションからログを送信するようにしてください。これは Supervisor のようなプロセス管理ツールを使うことで設定できます - 詳しくは作成中のロギングソケットのリスナーを実行する を参照してください。
ファイルをローテートする¶
ログファイルがある大きさに達したら、新しいファイルを開いてそこにログを取りたいことがあります。そのファイルをある数だけ残し、その数のファイルが生成されたらファイルを循環し、ファイルの数も大きさも制限したいこともあるでしょう。この利用パターンのために、 logging パッケージはRotatingFileHandler を提供しています:
importglobimportloggingimportlogging.handlersLOG_FILENAME='logging_rotatingfile_example.out'# Set up a specific logger with our desired output levelmy_logger=logging.getLogger('MyLogger')my_logger.setLevel(logging.DEBUG)# Add the log message handler to the loggerhandler=logging.handlers.RotatingFileHandler(LOG_FILENAME,maxBytes=20,backupCount=5)my_logger.addHandler(handler)# Log some messagesforiinrange(20):my_logger.debug('i =%d'%i)# See what files are createdlogfiles=glob.glob('%s*'%LOG_FILENAME)forfilenameinlogfiles:print(filename)
この結果として、アプリケーションのログ履歴の一部である、6 つに別れたファイルが得られます:
logging_rotatingfile_example.outlogging_rotatingfile_example.out.1logging_rotatingfile_example.out.2logging_rotatingfile_example.out.3logging_rotatingfile_example.out.4logging_rotatingfile_example.out.5
最新のファイルはいつでもlogging_rotatingfile_example.out で、サイズの上限に達するたびに拡張子.1 を付けた名前に改名されます。既にあるバックアップファイルはその拡張子がインクリメントされ (.1 が.2 になるなど)、.6 ファイルは消去されます。
明らかに、ここでは極端な例示のためにファイルの大きさをかなり小さな値に設定しています。実際に使うときはmaxBytes を適切な値に設定してください。
別の format スタイルを利用する¶
logging が Python 標準ライブラリに追加された時、メッセージを動的な内容でフォーマットする唯一の方法は % を使ったフォーマットでした。その後、 Python には新しい文字列フォーマット機構としてstring.Template (Python 2.4 で追加) とstr.format() (Python 2.6 で追加) が加わりました。
logging は (3.2 から) この2つの追加されたフォーマット方法をサポートしています。Formatter クラスがstyle という名前のオプションのキーワード引数を受け取ります。このデフォルト値は'%' ですが、他に'{' と'$' が指定可能で、それぞれのフォーマット方法に対応しています。後方互換性はデフォルト値によって維持されていますが、明示的に style 引数を指定することで、str.format() かstring.Template を使った format を指定する事ができます。次の例はこの機能を使ってみています:
>>>importlogging>>>root=logging.getLogger()>>>root.setLevel(logging.DEBUG)>>>handler=logging.StreamHandler()>>>bf=logging.Formatter('{asctime}{name}{levelname:8s}{message}',...style='{')>>>handler.setFormatter(bf)>>>root.addHandler(handler)>>>logger=logging.getLogger('foo.bar')>>>logger.debug('This is a DEBUG message')2010-10-28 15:11:55,341 foo.bar DEBUG This is a DEBUG message>>>logger.critical('This is a CRITICAL message')2010-10-28 15:12:11,526 foo.bar CRITICAL This is a CRITICAL message>>>df=logging.Formatter('$asctime $name ${levelname} $message',...style='$')>>>handler.setFormatter(df)>>>logger.debug('This is a DEBUG message')2010-10-28 15:13:06,924 foo.bar DEBUG This is a DEBUG message>>>logger.critical('This is a CRITICAL message')2010-10-28 15:13:11,494 foo.bar CRITICAL This is a CRITICAL message>>>
最終的に出力されるログメッセージの format と、各メッセージを生成する部分は完全に独立していることに注意してください。次の例でわかるように、メッセージを生成する部分では % を使い続けています:
>>>logger.error('This is an%s%s%s','other,','ERROR,','message')2010-10-28 15:19:29,833 foo.bar ERROR This is another, ERROR, message>>>
logging の呼び出し (logger.debug() やlogger.info() など) は、ログメッセージのために位置引数しか受け取らず、キーワード引数はそれを処理するときのオプションを指定するためだけに使われます。 (例えば、exc_info キーワード引数を使ってログを取るトレースバック情報を指定したり、extra キーワード引数を使ってログに付与する追加のコンテキスト情報を指定します。) logging パッケージは内部で % を使って format 文字列と引数をマージしているので、str.format() やstring.Template を使って logging を呼び出す事はできません。既存の logging 呼び出しは %-format を使っているので、後方互換性のためにこの部分を変更することはできません。
しかし、{} や $ を使ってログメッセージをフォーマットする方法はあります。ログメッセージには任意のオブジェクトを format 文字列として渡すことができ、logging パッケージはそのオブジェクトに対してstr() を使って実際の format 文字列を生成します。次の2つのクラスを見てください:
classBraceMessage:def__init__(self,fmt,/,*args,**kwargs):self.fmt=fmtself.args=argsself.kwargs=kwargsdef__str__(self):returnself.fmt.format(*self.args,**self.kwargs)classDollarMessage:def__init__(self,fmt,/,**kwargs):self.fmt=fmtself.kwargs=kwargsdef__str__(self):fromstringimportTemplatereturnTemplate(self.fmt).substitute(**self.kwargs)
どちらのクラスも format 文字列の代わりに利用して、 {} や $ を使って実際のログの "%(message)s", "{message}", "$message" で指定された "message" 部分を生成することができます。これは何かログを取りたいときに常に使うには使いにくいクラス名ですが、 __ (アンダースコア2つ ---gettext.gettext() やその仲間によくエイリアスとして使われるアンダースコア1つと混同しないように) などの使いやすいエイリアスを使うことができます。
上のクラスは Python には含まれませんが、自分のコードにコピペして使うのは簡単です。これらは次の例のようにして利用できます。(上のクラスがwherever というモジュールで定義されていると仮定します):
>>>fromwhereverimportBraceMessageas__>>>print(__('Message with{0}{name}',2,name='placeholders'))Message with 2 placeholders>>>classPoint:pass...>>>p=Point()>>>p.x=0.5>>>p.y=0.5>>>print(__('Message with coordinates: ({point.x:.2f},{point.y:.2f})',...point=p))Message with coordinates: (0.50, 0.50)>>>fromwhereverimportDollarMessageas__>>>print(__('Message with $num $what',num=2,what='placeholders'))Message with 2 placeholders>>>
上の例ではフォーマットの動作を示すためにprint() を使っていますが、もちろん実際にこの方法でログを出力するにはlogger.debug() などを使います。
注意点として、この方法には大きなパフォーマンスのペナルティはありません。実際のフォーマット操作は logging の呼び出し時ではなくて、メッセージが実際に handler によって出力されるときに起こります。(出力されないならフォーマットもされません) そのため、この方法で注意しないといけないのは、追加の括弧が書式文字列だけではなく引数も囲わないといけないことだけです。これは __ がXXXMessage クラスのコンストラクタ呼び出しのシンタックスシュガーでしか無いからです。
次の例のように、LoggerAdapter を利用して上と似たような効果を実現する方法もあります:
importloggingclassMessage:def__init__(self,fmt,args):self.fmt=fmtself.args=argsdef__str__(self):returnself.fmt.format(*self.args)classStyleAdapter(logging.LoggerAdapter):deflog(self,level,msg,/,*args,stacklevel=1,**kwargs):ifself.isEnabledFor(level):msg,kwargs=self.process(msg,kwargs)self.logger.log(level,Message(msg,args),**kwargs,stacklevel=stacklevel+1)logger=StyleAdapter(logging.getLogger(__name__))defmain():logger.debug('Hello,{}','world!')if__name__=='__main__':logging.basicConfig(level=logging.DEBUG)main()
上のスクリプトは Python 3.8 以降ではHello,world! というメッセージをログするはずです。
LogRecord のカスタマイズ¶
全ての logging イベントはLogRecord のインスタンスとして表現されます。イベントのログが取られて logger のレベルでフィルタされなかった場合、イベントの情報を含むLogRecord が生成され、その logger (と、 propagate が無効になるまでの上位 logger) の handler に渡されます。 Python 3.2 までは、この生成が行われているのは2箇所だけでした:
Logger.makeRecord(): 通常のイベント logging プロセスで呼ばれます。これはLogRecordを直接読んでインスタンスを生成します。makeLogRecord(): LogRecord に追加される属性を含む辞書を渡して呼び出されます。これはネットワーク越しに (pickle 形式でSocketHandler経由で、あるいは JSON 形式でHTTPHandler経由で) 辞書を受け取った場合などに利用されます。
そのためにLogRecord で何か特別なことをしたい場合は、次のどちらかをしなければなりません。
Logger.makeRecord()をオーバーライドした独自のLoggerのサブクラスを作り、利用したい logger のどれかがインスタンス化される前に、それをsetLoggerClass()を使って登録する。
最初の方法は、複数の異なるライブラリが別々のことをしようとした場合にうまく行きません。各ライブラリが独自のLogger のサブクラスを登録しようとして、最後に登録されたライブラリが生き残ります。
2つ目の方法はほとんどのケースでうまくいきますが、たとえばLogRecord を特殊化したサブクラスを使うことなどはできません。ライブラリの開発者は利用している logger に適切な filter を設定できますが、新しい logger を作るたびに忘れずに設定しないといけなくなります。 (新しいパッケージやモジュールを追加し、モジュールレベルで次の式を実行することで、新しい logger が作られます)
logger=logging.getLogger(__name__)
これでは考えることが余計に1つ増えてしまうでしょう。開発者は、最も高いレベルのロガーに取り付けられたNullHandler に、フィルタを追加することもできますが、アプリケーション開発者が、より低いレベルに対するライブラリのロガーにハンドラを取り付けた場合、フィルタは呼び出されません --- 従って、そのハンドラからの出力はライブラリ開発者の意図を反映したものにはなりません。
Python 3.2 以降では、LogRecord の生成は、指定できるファクトリ経由になります。ファクトリは callable で、setLogRecordFactory() で登録でき、getLogRecordFactory() で現在のファクトリを取得できます。ファクトリはLogRecord コンストラクタと同じシグネチャで呼び出され、LogRecord がデフォルトのファクトリとして設定されています。
このアプローチはカスタムのファクトリが LogRecord の生成のすべての面を制御できるようにしています。たとえば、サブクラスを返したり、次のようなパターンを使って単に属性を追加することができます:
old_factory=logging.getLogRecordFactory()defrecord_factory(*args,**kwargs):record=old_factory(*args,**kwargs)record.custom_attribute=0xdecafbadreturnrecordlogging.setLogRecordFactory(record_factory)
このアプローチでは複数の異なるライブラリがファクトリをチェインさせて、お互いに同じ属性を上書きしたり、標準で提供されている属性を意図せず上書きしない限りはうまくいきます。しかし、チェインのつながり全てが、全ての logging の操作に対しての実行時のオーバーヘッドになることを念頭に置き、このテクニックはFilter を利用するだけでは望む結果が得られない場合にのみ使うべきです。
Subclassing QueueHandler and QueueListener- a ZeroMQ example¶
SubclassQueueHandler¶
QueueHandler のサブクラスを作ってメッセージを他のキュー、例えば ZeroMQ の 'publish' ソケットに送信することができます。下の例では、ソケットを別に作ってそれを handler に ('queue' として) 渡します:
importzmq# using pyzmq, the Python binding for ZeroMQimportjson# for serializing records portablyctx=zmq.Context()sock=zmq.Socket(ctx,zmq.PUB)# or zmq.PUSH, or other suitable valuesock.bind('tcp://*:5556')# or whereverclassZeroMQSocketHandler(QueueHandler):defenqueue(self,record):self.queue.send_json(record.__dict__)handler=ZeroMQSocketHandler(sock)
もちろん同じことを別の設計でもできます。socket を作るのに必要な情報を handler に渡す例です:
classZeroMQSocketHandler(QueueHandler):def__init__(self,uri,socktype=zmq.PUB,ctx=None):self.ctx=ctxorzmq.Context()socket=zmq.Socket(self.ctx,socktype)socket.bind(uri)super().__init__(socket)defenqueue(self,record):self.queue.send_json(record.__dict__)defclose(self):self.queue.close()
SubclassQueueListener¶
QueueListener のサブクラスを作って、メッセージを他のキュー、例えば ZeroMQ の 'subscribe' ソケットから取得する事もできます。サンプルです:
classZeroMQSocketListener(QueueListener):def__init__(self,uri,/,*handlers,**kwargs):self.ctx=kwargs.get('ctx')orzmq.Context()socket=zmq.Socket(self.ctx,zmq.SUB)socket.setsockopt_string(zmq.SUBSCRIBE,'')# subscribe to everythingsocket.connect(uri)super().__init__(socket,*handlers,**kwargs)defdequeue(self):msg=self.queue.recv_json()returnlogging.makeLogRecord(msg)
Subclassing QueueHandler and QueueListener- apynng example¶
In a similar way to the above section, we can implement a listener and handlerusingpynng, which is a Python binding toNNG, billed as a spiritual successor to ZeroMQ.The following snippets illustrate -- you can test them in an environment which haspynng installed. Just for variety, we present the listener first.
SubclassQueueListener¶
# listener.pyimportjsonimportloggingimportlogging.handlersimportpynngDEFAULT_ADDR="tcp://localhost:13232"interrupted=FalseclassNNGSocketListener(logging.handlers.QueueListener):def__init__(self,uri,/,*handlers,**kwargs):# Have a timeout for interruptability, and open a# subscriber socketsocket=pynng.Sub0(listen=uri,recv_timeout=500)# The b'' subscription matches all topicstopics=kwargs.pop('topics',None)orb''socket.subscribe(topics)# We treat the socket as a queuesuper().__init__(socket,*handlers,**kwargs)defdequeue(self,block):data=None# Keep looping while not interrupted and no data received over the# socketwhilenotinterrupted:try:data=self.queue.recv(block=block)breakexceptpynng.Timeout:passexceptpynng.Closed:# sometimes happens when you hit Ctrl-CbreakifdataisNone:returnNone# Get the logging event sent from a publisherevent=json.loads(data.decode('utf-8'))returnlogging.makeLogRecord(event)defenqueue_sentinel(self):# Not used in this implementation, as the socket isn't really a# queuepasslogging.getLogger('pynng').propagate=Falselistener=NNGSocketListener(DEFAULT_ADDR,logging.StreamHandler(),topics=b'')listener.start()print('Press Ctrl-C to stop.')try:whileTrue:passexceptKeyboardInterrupt:interrupted=Truefinally:listener.stop()
SubclassQueueHandler¶
# sender.pyimportjsonimportloggingimportlogging.handlersimporttimeimportrandomimportpynngDEFAULT_ADDR="tcp://localhost:13232"classNNGSocketHandler(logging.handlers.QueueHandler):def__init__(self,uri):socket=pynng.Pub0(dial=uri,send_timeout=500)super().__init__(socket)defenqueue(self,record):# Send the record as UTF-8 encoded JSONd=dict(record.__dict__)data=json.dumps(d)self.queue.send(data.encode('utf-8'))defclose(self):self.queue.close()logging.getLogger('pynng').propagate=Falsehandler=NNGSocketHandler(DEFAULT_ADDR)# Make sure the process ID is in the outputlogging.basicConfig(level=logging.DEBUG,handlers=[logging.StreamHandler(),handler],format='%(levelname)-8s%(name)10s%(process)6s%(message)s')levels=(logging.DEBUG,logging.INFO,logging.WARNING,logging.ERROR,logging.CRITICAL)logger_names=('myapp','myapp.lib1','myapp.lib2')msgno=1whileTrue:# Just randomly select some loggers and levels and log awaylevel=random.choice(levels)logger=logging.getLogger(random.choice(logger_names))logger.log(level,'Message no.%5d'%msgno)msgno+=1delay=random.random()*2+0.5time.sleep(delay)
You can run the above two snippets in separate command shells. If we run thelistener in one shell and run the sender in two separate shells, we should seesomething like the following. In the first sender shell:
$pythonsender.pyDEBUG myapp 613 Message no. 1WARNING myapp.lib2 613 Message no. 2CRITICAL myapp.lib2 613 Message no. 3WARNING myapp.lib2 613 Message no. 4CRITICAL myapp.lib1 613 Message no. 5DEBUG myapp 613 Message no. 6CRITICAL myapp.lib1 613 Message no. 7INFO myapp.lib1 613 Message no. 8(and so on)
In the second sender shell:
$pythonsender.pyINFO myapp.lib2 657 Message no. 1CRITICAL myapp.lib2 657 Message no. 2CRITICAL myapp 657 Message no. 3CRITICAL myapp.lib1 657 Message no. 4INFO myapp.lib1 657 Message no. 5WARNING myapp.lib2 657 Message no. 6CRITICAL myapp 657 Message no. 7DEBUG myapp.lib1 657 Message no. 8(and so on)
In the listener shell:
$pythonlistener.pyPress Ctrl-C to stop.DEBUG myapp 613 Message no. 1WARNING myapp.lib2 613 Message no. 2INFO myapp.lib2 657 Message no. 1CRITICAL myapp.lib2 613 Message no. 3CRITICAL myapp.lib2 657 Message no. 2CRITICAL myapp 657 Message no. 3WARNING myapp.lib2 613 Message no. 4CRITICAL myapp.lib1 613 Message no. 5CRITICAL myapp.lib1 657 Message no. 4INFO myapp.lib1 657 Message no. 5DEBUG myapp 613 Message no. 6WARNING myapp.lib2 657 Message no. 6CRITICAL myapp 657 Message no. 7CRITICAL myapp.lib1 613 Message no. 7INFO myapp.lib1 613 Message no. 8DEBUG myapp.lib1 657 Message no. 8(and so on)
As you can see, the logging from the two sender processes is interleaved in thelistener's output.
辞書ベースで構成する例¶
次の例は辞書を使った logging の構成です。この例はDjango プロジェクトのドキュメント から持ってきました。この辞書をdictConfig() に渡して設定を有効にします:
LOGGING={'version':1,'disable_existing_loggers':False,'formatters':{'verbose':{'format':'{levelname}{asctime}{module}{process:d}{thread:d}{message}','style':'{',},'simple':{'format':'{levelname}{message}','style':'{',},},'filters':{'special':{'()':'project.logging.SpecialFilter','foo':'bar',},},'handlers':{'console':{'level':'INFO','class':'logging.StreamHandler','formatter':'simple',},'mail_admins':{'level':'ERROR','class':'django.utils.log.AdminEmailHandler','filters':['special']}},'loggers':{'django':{'handlers':['console'],'propagate':True,},'django.request':{'handlers':['mail_admins'],'level':'ERROR','propagate':False,},'myproject.custom':{'handlers':['console','mail_admins'],'level':'INFO','filters':['special']}}}
この構成方法についてのより詳しい情報は、 Django のドキュメントの該当のセクション で見ることができます。
rotator と namer を使ってログローテートをカスタマイズする¶
namer と rotator を定義する方法の例は以下の実行可能なスクリプトに示されています。ここではログファイルを gzip により圧縮する例を示しています:
importgzipimportloggingimportlogging.handlersimportosimportshutildefnamer(name):returnname+".gz"defrotator(source,dest):withopen(source,'rb')asf_in:withgzip.open(dest,'wb')asf_out:shutil.copyfileobj(f_in,f_out)os.remove(source)rh=logging.handlers.RotatingFileHandler('rotated.log',maxBytes=128,backupCount=5)rh.rotator=rotatorrh.namer=namerroot=logging.getLogger()root.setLevel(logging.INFO)root.addHandler(rh)f=logging.Formatter('%(asctime)s%(message)s')rh.setFormatter(f)foriinrange(1000):root.info(f'Message no.{i+1}')
このスクリプトを実行すると、6つの新しいファイルが生成され、そのうち5つは圧縮されています:
$lsrotated.log*rotated.log rotated.log.2.gz rotated.log.4.gzrotated.log.1.gz rotated.log.3.gz rotated.log.5.gz$zcatrotated.log.1.gz2023-01-20 02:28:17,767 Message no. 9962023-01-20 02:28:17,767 Message no. 9972023-01-20 02:28:17,767 Message no. 998
より手の込んだ multiprocessing の例¶
次の実際に動作する例は、logging を multiprocessing と設定ファイルを使って利用する方法を示しています。設定内容はかなりシンプルですが、より複雑な構成を実際の multiprocessing を利用するシナリオで実装する方法を示しています。
この例では、メインプロセスは listener プロセスといくつかのワーカープロセスを起動します。メイン、listener、ワーカープロセスはそれぞれ分離した設定を持っています (ワーカープロセス群は同じ設定を共有します)。この例から、メインプロセスの logging, ワーカーが QueueHandler でログを送っているところ、listener が利用する QueueListener の実装、複雑な設定、キューから受け取ったイベントを設定された handler に分配する部分を見ることができます。この設定は説明用のものですが、この例を自分のシナリオに適応させることができるでしょう。
これがそのスクリプトです。docstring とコメントで動作を説明しています:
importloggingimportlogging.configimportlogging.handlersfrommultiprocessingimportProcess,Queue,Event,current_processimportosimportrandomimporttimeclassMyHandler:""" A simple handler for logging events. It runs in the listener process and dispatches events to loggers based on the name in the received record, which then get dispatched, by the logging system, to the handlers configured for those loggers. """defhandle(self,record):ifrecord.name=="root":logger=logging.getLogger()else:logger=logging.getLogger(record.name)iflogger.isEnabledFor(record.levelno):# The process name is transformed just to show that it's the listener# doing the logging to files and consolerecord.processName='%s (for%s)'%(current_process().name,record.processName)logger.handle(record)deflistener_process(q,stop_event,config):""" This could be done in the main process, but is just done in a separate process for illustrative purposes. This initialises logging according to the specified configuration, starts the listener and waits for the main process to signal completion via the event. The listener is then stopped, and the process exits. """logging.config.dictConfig(config)listener=logging.handlers.QueueListener(q,MyHandler())listener.start()ifos.name=='posix':# On POSIX, the setup logger will have been configured in the# parent process, but should have been disabled following the# dictConfig call.# On Windows, since fork isn't used, the setup logger won't# exist in the child, so it would be created and the message# would appear - hence the "if posix" clause.logger=logging.getLogger('setup')logger.critical('Should not appear, because of disabled logger ...')stop_event.wait()listener.stop()defworker_process(config):""" A number of these are spawned for the purpose of illustration. In practice, they could be a heterogeneous bunch of processes rather than ones which are identical to each other. This initialises logging according to the specified configuration, and logs a hundred messages with random levels to randomly selected loggers. A small sleep is added to allow other processes a chance to run. This is not strictly needed, but it mixes the output from the different processes a bit more than if it's left out. """logging.config.dictConfig(config)levels=[logging.DEBUG,logging.INFO,logging.WARNING,logging.ERROR,logging.CRITICAL]loggers=['foo','foo.bar','foo.bar.baz','spam','spam.ham','spam.ham.eggs']ifos.name=='posix':# On POSIX, the setup logger will have been configured in the# parent process, but should have been disabled following the# dictConfig call.# On Windows, since fork isn't used, the setup logger won't# exist in the child, so it would be created and the message# would appear - hence the "if posix" clause.logger=logging.getLogger('setup')logger.critical('Should not appear, because of disabled logger ...')foriinrange(100):lvl=random.choice(levels)logger=logging.getLogger(random.choice(loggers))logger.log(lvl,'Message no.%d',i)time.sleep(0.01)defmain():q=Queue()# The main process gets a simple configuration which prints to the console.config_initial={'version':1,'handlers':{'console':{'class':'logging.StreamHandler','level':'INFO'}},'root':{'handlers':['console'],'level':'DEBUG'}}# The worker process configuration is just a QueueHandler attached to the# root logger, which allows all messages to be sent to the queue.# We disable existing loggers to disable the "setup" logger used in the# parent process. This is needed on POSIX because the logger will# be there in the child following a fork().config_worker={'version':1,'disable_existing_loggers':True,'handlers':{'queue':{'class':'logging.handlers.QueueHandler','queue':q}},'root':{'handlers':['queue'],'level':'DEBUG'}}# The listener process configuration shows that the full flexibility of# logging configuration is available to dispatch events to handlers however# you want.# We disable existing loggers to disable the "setup" logger used in the# parent process. This is needed on POSIX because the logger will# be there in the child following a fork().config_listener={'version':1,'disable_existing_loggers':True,'formatters':{'detailed':{'class':'logging.Formatter','format':'%(asctime)s%(name)-15s%(levelname)-8s%(processName)-10s%(message)s'},'simple':{'class':'logging.Formatter','format':'%(name)-15s%(levelname)-8s%(processName)-10s%(message)s'}},'handlers':{'console':{'class':'logging.StreamHandler','formatter':'simple','level':'INFO'},'file':{'class':'logging.FileHandler','filename':'mplog.log','mode':'w','formatter':'detailed'},'foofile':{'class':'logging.FileHandler','filename':'mplog-foo.log','mode':'w','formatter':'detailed'},'errors':{'class':'logging.FileHandler','filename':'mplog-errors.log','mode':'w','formatter':'detailed','level':'ERROR'}},'loggers':{'foo':{'handlers':['foofile']}},'root':{'handlers':['console','file','errors'],'level':'DEBUG'}}# Log some initial events, just to show that logging in the parent works# normally.logging.config.dictConfig(config_initial)logger=logging.getLogger('setup')logger.info('About to create workers ...')workers=[]foriinrange(5):wp=Process(target=worker_process,name='worker%d'%(i+1),args=(config_worker,))workers.append(wp)wp.start()logger.info('Started worker:%s',wp.name)logger.info('About to create listener ...')stop_event=Event()lp=Process(target=listener_process,name='listener',args=(q,stop_event,config_listener))lp.start()logger.info('Started listener')# We now hang around for the workers to finish their work.forwpinworkers:wp.join()# Workers all done, listening can now stop.# Logging in the parent still works normally.logger.info('Telling listener to stop ...')stop_event.set()lp.join()logger.info('All done.')if__name__=='__main__':main()
SysLogHandler に送るメッセージに BOM を挿入する¶
RFC 5424 は syslog デーモンに Unicode メッセージを送る時、次の構造を要求しています: オプショナルなピュア ASCII部分、続けて UTF-8 の Byte Order Mark (BOM)、続けて UTF-8 でエンコードされた Unicode. (仕様の該当セクション を参照)
Python 3.1 でSysLogHandler に、 message に BOM を挿入するコードが追加されました。しかし、そのときの実装が悪くて、 message の先頭に BOM をつけてしまうのでピュアASCII 部分をその前に書くことができませんでした。
この動作は壊れているので、Python 3.2.4 以降では BOM を挿入するコードが、削除されました。書き直されるのではなく削除されたので、RFC 5424 準拠の、BOM と、オプションのピュア ASCII部分をBOMの前に、任意の Unicode を BOM の後ろに持つ UTF-8 でエンコードされた message を生成したい場合、次の手順に従う必要があります:
SysLogHandlerのインスタンスに、次のような format 文字列を持ったFormatterインスタンスをアタッチする:'ASCII section\ufeffUnicode section'
Unicode のコードポイント U+FEFF は、UTF-8 でエンコードすると BOM --
b'\xef\xbb\xbf'-- になります。ASCII セクションを好きなプレースホルダに変更する。ただしその部分の置換結果が常に ASCII になるように注意する(UTF-8 でエンコードされてもその部分が変化しないようにする)。
Unicode セクションを任意のプレースホルダに置き換える。この部分を置換したデータに ASCII 外の文字が含まれていても、それは単に UTF-8 でエンコードされるだけです。
フォーマットされた message はSysLogHandler によって UTF-8 でエンコードされます。上のルールに従えば、RFC 5424 準拠のメッセージを生成できます。上のルールに従わない場合、logging は何もエラーを起こしませんが、message はRFC 5424 に準拠しない形で送られるので、syslog デーモン側で何かエラーが起こる可能性があります。
構造化ログを実装する¶
多くのログメッセージは人間が読むために書かれるため、機械的に処理しにくくなっていますが、場合によっては (複雑な正規表現を使ってログメッセージをパースしなくても) プログラムがパースできる 構造化されたフォーマットでメッセージを出力したい場合があります。logging パッケージを使うと、これを簡単に実現できます。実現する方法は幾つもありますが、次の例は JSON を使ってイベントを、機械でパースできる形にシリアライズする単純な方法です:
importjsonimportloggingclassStructuredMessage:def__init__(self,message,/,**kwargs):self.message=messageself.kwargs=kwargsdef__str__(self):return'%s >>>%s'%(self.message,json.dumps(self.kwargs))_=StructuredMessage# optional, to improve readabilitylogging.basicConfig(level=logging.INFO,format='%(message)s')logging.info(_('message 1',foo='bar',bar='baz',num=123,fnum=123.456))
上のスクリプトを実行すると次のように出力されます:
message 1 >>> {"fnum": 123.456, "num": 123, "bar": "baz", "foo": "bar"}要素の順序は Python のバージョンによって異なることに注意してください。
より特殊な処理が必要な場合、次の例のように、カスタムの JSON エンコーダを作ることができます:
importjsonimportloggingclassEncoder(json.JSONEncoder):defdefault(self,o):ifisinstance(o,set):returntuple(o)elifisinstance(o,str):returno.encode('unicode_escape').decode('ascii')returnsuper().default(o)classStructuredMessage:def__init__(self,message,/,**kwargs):self.message=messageself.kwargs=kwargsdef__str__(self):s=Encoder().encode(self.kwargs)return'%s >>>%s'%(self.message,s)_=StructuredMessage# optional, to improve readabilitydefmain():logging.basicConfig(level=logging.INFO,format='%(message)s')logging.info(_('message 1',set_value={1,2,3},snowman='\u2603'))if__name__=='__main__':main()
上のスクリプトを実行すると次のように出力されます:
message 1 >>> {"snowman": "\u2603", "set_value": [1, 2, 3]}要素の順序は Python のバージョンによって異なることに注意してください。
handler をdictConfig() を使ってカスタマイズする¶
logging handler に特定のカスタマイズを何度もしたい場合で、dictConfig() を使っているなら、サブクラスを作らなくてもカスタマイズが可能です。例えば、ログファイルの owner を設定したいとします。これは POSIX 環境ではshutil.chown() を使って簡単に実現できますが、標準ライブラリの file handler はこの機能を組み込みでサポートしていません。 handler の生成を通常の関数を使ってカスタマイズすることができます:
defowned_file_handler(filename,mode='a',encoding=None,owner=None):ifowner:ifnotos.path.exists(filename):open(filename,'a').close()shutil.chown(filename,*owner)returnlogging.FileHandler(filename,mode,encoding)
そして、dictConfig() に渡される構成設定の中で、この関数を使って logging handler を生成するように指定することができます:
LOGGING={'version':1,'disable_existing_loggers':False,'formatters':{'default':{'format':'%(asctime)s%(levelname)s%(name)s%(message)s'},},'handlers':{'file':{# The values below are popped from this dictionary and# used to create the handler, set the handler's level and# its formatter.'()':owned_file_handler,'level':'DEBUG','formatter':'default',# The values below are passed to the handler creator callable# as keyword arguments.'owner':['pulse','pulse'],'filename':'chowntest.log','mode':'w','encoding':'utf-8',},},'root':{'handlers':['file'],'level':'DEBUG',},}
この例は説明用のものですが、owner の user と group をpulse に設定しています。これを動くスクリプトにchowntest.py に組み込んでみます:
importlogging,logging.config,os,shutildefowned_file_handler(filename,mode='a',encoding=None,owner=None):ifowner:ifnotos.path.exists(filename):open(filename,'a').close()shutil.chown(filename,*owner)returnlogging.FileHandler(filename,mode,encoding)LOGGING={'version':1,'disable_existing_loggers':False,'formatters':{'default':{'format':'%(asctime)s%(levelname)s%(name)s%(message)s'},},'handlers':{'file':{# The values below are popped from this dictionary and# used to create the handler, set the handler's level and# its formatter.'()':owned_file_handler,'level':'DEBUG','formatter':'default',# The values below are passed to the handler creator callable# as keyword arguments.'owner':['pulse','pulse'],'filename':'chowntest.log','mode':'w','encoding':'utf-8',},},'root':{'handlers':['file'],'level':'DEBUG',},}logging.config.dictConfig(LOGGING)logger=logging.getLogger('mylogger')logger.debug('A debug message')
これを実行するには、root 権限で実行する必要があるかもしれません:
$sudopython3.3chowntest.py$catchowntest.log2013-11-05 09:34:51,128 DEBUG mylogger A debug message$ls-lchowntest.log-rw-r--r-- 1 pulse pulse 55 2013-11-05 09:34 chowntest.log
shutil.chown() が追加されたのが Python 3.3 からなので、この例は Python 3.3 を使っています。このアプローチ自体はdictConfig() をサポートした全ての Python バージョン - Python 2.7, 3.2 以降 - で利用できます。 3.3 以前のバージョンでは、オーナーを変更するのにos.chown() を利用する必要があるでしょう。
実際には、handler を生成する関数はプロジェクト内のどこかにあるユーティリティモジュールに置くことになるでしょう。設定の中で直接関数を参照する代わりに:
'()':owned_file_handler,
次のように書くこともできます:
'()':'ext://project.util.owned_file_handler',
project.util は関数がある実際の場所に置き換えてください。上のスクリプトでは'ext://__main__.owned_file_handler' で動くはずです。dictConfig() はext:// から実際の callable を見つけます。
この例は他のファイルに対する変更を実装する例にもなっています。例えばos.chmod() を使って、同じ方法で POSIX パーミッションを設定できるでしょう。
もちろん、このアプローチはFileHandler 以外の handler 、ローテートする file handler のいずれかやその他の handler にも適用できます。
固有の書式化スタイルをアプリケーション全体で使う¶
Python 3.2 では、Formatter クラスがstyle という名前のオプションのキーワード引数を受け取ります。このデフォルト値は後方互換性を維持するために% となっていますが、{ か$ を指定することで、str.format() かstring.Template でサポートされているのと同じ書式化のアプローチを採れます。これは最終的に出力されるログメッセージの書式化に影響を与えますが、個々のログメッセージが構築される方法とは完全に直交していることに注意してください。
ロギングの呼び出し (debug(),info() など) はログメッセージのためには位置引数しか取らず、ロギングの呼び出しを処理する方法を決定するためだけにキーワード引数を指定できます (たとえばexc_info キーワード引数はトレースバックの情報をログに含めるかどうかを指定し、extra キーワード引数はログに付加する追加のコンテキスト情報を指定します)。ロギングパッケージは内部で %-format を使って書式文字列に可変長引数を埋め込んでいるため、str.format() やstring.Template の構文を使って直接ロギングの呼び出しを行うことはできません。既存のコードにおける全てのロギングの呼び出しは %-format 文字列を使っているため、後方互換性を維持する限りこの部分が変更されることはないでしょう。
特定のロガーに関連付ける書式スタイルへの提案がなされてきましたが、そのアプローチは同時に後方互換性の問題にぶち当たります。あらゆる既存のコードはロガーの名前を使っているでしょうし、 % 形式書式化を使っているでしょう。
あらゆるサードパーティのライブラリ、あらゆるあなたのコードの間で相互運用可能なようにロギングを行うには、書式化についての決定は、個々のログ呼び出しのレベルで行う必要があります。これは受け容れ可能な代替書式化スタイルに様々な手段の可能性を広げます。
LogRecord ファクトリを使う¶
Python 3.2 において、上述したFormatter の変更とともに、setLogRecordFactory() 関数を使ってLogRecord のサブクラスをユーザに指定することを可能にするロギングパッケージの機能拡張がありました。これにより、getMessage() をオーバライドして た だ し い ことをする、あなた自身の手によるLogRecord のサブクラスをセットすることが出来ます。このメソッドの実装は基底クラスではmsg%args 書式化をし、あなたの代替の書式化の置換が出来る場所ですが、他のコードとの相互運用性を保障するために、全ての書式化スタイルをサポートするよう注意深く行うべきであり、また、%-書式化をデフォルトで認めるべきです。基底クラスの実装がそうしているように、str(self.msg) 呼び出しもしてください。
さらに詳しい情報は、リファレンスのsetLogRecordFactory(),LogRecord を参照してください。
カスタムなメッセージオブジェクトを使う¶
あなた独自のログメッセージを構築するのに {}- および $- 書式化を使えるようにするための、もうひとつの、おそらくもっと簡単な方法があります。ロギングの際には、あなたはメッセージ書式文字列として、任意のオブジェクトを使えることを(任意のオブジェクトをメッセージに使用する より)思い出してみましょう、そしてロギングパッケージはそのオブジェクトに対して実際の書式文字列を得るためにstr() を呼び出すことも。以下 2 つのクラスを検討してみましょう:
classBraceMessage:def__init__(self,fmt,/,*args,**kwargs):self.fmt=fmtself.args=argsself.kwargs=kwargsdef__str__(self):returnself.fmt.format(*self.args,**self.kwargs)classDollarMessage:def__init__(self,fmt,/,**kwargs):self.fmt=fmtself.kwargs=kwargsdef__str__(self):fromstringimportTemplatereturnTemplate(self.fmt).substitute(**self.kwargs)
どちらのクラスも format 文字列の代わりに利用して、 {} や $ を使って実際のログの “%(message)s”, “{message}”, “$message” で指定された "message" 部分を生成することができます。これは何かログを取りたいときに常に使うには使いにくいクラス名ですが、使いやすいようにエイリアスを作れば良いでしょう、M であるとか_ のような(あるいは地域化のために既に_ を使っているのであれば__ が良いかもしれません)。
このアプローチによる例をお見せします。最初はstr.format() を使ってフォーマットする例です:
>>>__=BraceMessage>>>print(__('Message with{0}{1}',2,'placeholders'))Message with 2 placeholders>>>classPoint:pass...>>>p=Point()>>>p.x=0.5>>>p.y=0.5>>>print(__('Message with coordinates: ({point.x:.2f},{point.y:.2f})',point=p))Message with coordinates: (0.50, 0.50)
2つめはstring.Template でフォーマットする例です:
>>>__=DollarMessage>>>print(__('Message with $num $what',num=2,what='placeholders'))Message with 2 placeholders>>>
注意点として、この方法には大きなパフォーマンスのペナルティはありません。実際のフォーマット操作は logging の呼び出し時ではなくて、メッセージが実際に handler によって出力されるときに起こります。(出力されないならフォーマットもされません) そのため、この方法で注意しないといけないのは、追加の括弧が書式文字列だけではなく引数も囲わないといけないことだけです。これは上記のとおり __ がXXXMessage クラスのコンストラクタ呼び出しのシンタックスシュガーでしか無いからです。
filter をdictConfig() を使ってカスタマイズする¶
dictConfig() によってフィルタを設定出来ます が、どうやってそれを行うのかが初見では明快とは言えないでしょう(そのためのこのレシピです)。Filter のみが唯一標準ライブラリに含まれているだけですし、それは何の要求にも応えてはくれません(ただの基底クラスですから)ので、典型的にはfilter() メソッドをオーバライドしたFilter のサブクラスをあなた自身で定義する必要があります。これをするには、設定辞書内のフィルタ指定部分に、() キーでそのフィルタを作るのに使われる callable を指定してください(クラスを指定するのが最もわかりやすいですが、Filter インスタンスを返却する callable を提供することでも出来ます)。以下に完全な例を示します:
importloggingimportlogging.configimportsysclassMyFilter(logging.Filter):def__init__(self,param=None):self.param=paramdeffilter(self,record):ifself.paramisNone:allow=Trueelse:allow=self.paramnotinrecord.msgifallow:record.msg='changed: '+record.msgreturnallowLOGGING={'version':1,'filters':{'myfilter':{'()':MyFilter,'param':'noshow',}},'handlers':{'console':{'class':'logging.StreamHandler','filters':['myfilter']}},'root':{'level':'DEBUG','handlers':['console']},}if__name__=='__main__':logging.config.dictConfig(LOGGING)logging.debug('hello')logging.debug('hello - noshow')
どのようにして設定データとして、そのインスタンスを構築する callable をキーワードパラメータの形で渡すのか、をこの例は教えてくれます。上記スクリプトは実行すると、このような出力をします:
changed: hello
設定した通りに動いていますね。
ほかにもいくつか特筆すべき点があります:
設定内で直接その callable を参照出来ない場合(例えばそれが異なるモジュール内にあり、設定辞書のある場所からそれを直接インポート出来ない、など)には、外部オブジェクトへのアクセス に記述されている
ext://...形式を使えます。例えば、上記例のようにMyFilterと指定する代わりに、'ext://__main__.MyFilter'と記述することが出来ます。フィルタについてとともに、このテクニックは、カスタムハンドラ、カスタムフォーマッタに対しても同様に使えます。ロギングが設定において、どのようにユーザ定義のオブジェクトをサポートするのかについてのさらなる詳細については、ユーザ定義オブジェクト と、本クックブックの上の方のレシピhandler を dictConfig() を使ってカスタマイズする を参照してください。
例外の書式化をカスタマイズする¶
例外の書式化をカスタマイズしたいことがあるでしょう - わかりやすさのために、例外情報がある場合でもログイベントごとに一行に収まることを死守したいと望むとしましょう。フォーマッタのクラスをカスタマイズして、このように出来ます:
importloggingclassOneLineExceptionFormatter(logging.Formatter):defformatException(self,exc_info):""" Format an exception so that it prints on a single line. """result=super().formatException(exc_info)returnrepr(result)# or format into one line however you want todefformat(self,record):s=super().format(record)ifrecord.exc_text:s=s.replace('\n','')+'|'returnsdefconfigure_logging():fh=logging.FileHandler('output.txt','w')f=OneLineExceptionFormatter('%(asctime)s|%(levelname)s|%(message)s|','%d/%m/%Y %H:%M:%S')fh.setFormatter(f)root=logging.getLogger()root.setLevel(logging.DEBUG)root.addHandler(fh)defmain():configure_logging()logging.info('Sample message')try:x=1/0exceptZeroDivisionErrorase:logging.exception('ZeroDivisionError:%s',e)if__name__=='__main__':main()
実行してみましょう。このように正確に2行の出力を生成します:
28/01/2015 07:21:23|INFO|Sample message|28/01/2015 07:21:23|ERROR|ZeroDivisionError: division by zero|'Traceback (most recent call last):\n File "logtest7.py", line 30, in main\n x = 1 / 0\nZeroDivisionError: division by zero'|
これは扱いとしては単純過ぎますが、例外情報をどのようにしてあなた好みの書式化出来るかを示しています。さらに特殊なニーズが必要な場合にはtraceback モジュールが有用です。
ロギングメッセージを喋る¶
ロギングメッセージを目で見る形式ではなく音で聴く形式として出力したい、という状況があるかもしれません。これはあなたのシステムで text- to-speech (TTS) 機能が利用可能であれば、容易です。それが Python バインディングを持っていなくとも、です。ほとんどの TTS システムはあなたが実行出来るコマンドラインプログラムを持っていて、このことで、subprocess を使うことでハンドラが呼び出せます。ここでは、TTS コマンドラインプログラムはユーザとの対話を期待せず、完了には時間がかかり、そしてログメッセージの頻度はユーザをメッセージで圧倒してしまうほどには高くはなく、そして並列で喋るよりはメッセージ一つにつき一回喋ることが受け容れられる、としておきます。ここでお見せする実装例では、次が処理される前に一つのメッセージを喋り終わるまで待ち、結果としてほかのハンドラを待たせることになります。espeak TTS パッケージが手許にあるとして、このアプローチによる短い例はこのようなものです:
importloggingimportsubprocessimportsysclassTTSHandler(logging.Handler):defemit(self,record):msg=self.format(record)# Speak slowly in a female English voicecmd=['espeak','-s150','-ven+f3',msg]p=subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)# wait for the program to finishp.communicate()defconfigure_logging():h=TTSHandler()root=logging.getLogger()root.addHandler(h)# the default formatter just returns the messageroot.setLevel(logging.DEBUG)defmain():logging.info('Hello')logging.debug('Goodbye')if__name__=='__main__':configure_logging()sys.exit(main())
実行すれば、女性の声で "Hello" に続き "Goodbye" と喋るはずです。
このアプローチは、もちろんほかの TTS システムにも採用出来ますし、メッセージをコマンドライン経由で外部プログラムに渡せるようなものであれば、ほかのシステムであっても全く同じです。
ロギングメッセージをバッファリングし、条件に従って出力する¶
メッセージを一次領域に記録し、ある種の特定の状況になった場合にだけ出力したい、ということがあるかもしれません。たとえばある関数内でのデバッグのためのログ出力をしたくても、エラーなしで終了する限りにおいては収集されたデバッグ情報による混雑は喰らいたくはなく、エラーがあった場合にだけエラー出力とともにデバッグ情報を見たいのだ、のようなことがあるでしょう。
このような振る舞いをするロギングをしたい関数に対して、デコレータを用いてこれを行う例をお見せします。それにはlogging.handlers.MemoryHandler を使います。これにより何か条件を満たすまでロギングイベントを溜め込むことが出来、条件を満たせば溜め込まれたイベントがflushed として他のハンドラ (target のハンドラ)に渡されます。デフォルトでは、MemoryHandler はそのバッファが一杯になるか、指定された閾値のレベル以上のイベントが起こるとフラッシュされます。何か特別なフラッシュの振る舞いをしたければ、このレシピはさらに特殊化したMemoryHandler とともに利用出来ます。
スクリプト例では、foo という、単に全てのログレベルについて、sys.stderr にもどのレベルを出力したのかについて書き出しながら実際のログ出力も行う、という単純な関数を使っています。foo に真を与えると ERROR と CRITICAL の出力をし、そうでなければ DEBUG, INFO, WARNING だけを出力します。
スクリプトが行うことは単に、foo を必要とされている特定の条件でのロギングを行うようにするデコレータで修飾することだけです。このデコレータはパラメータとしてロガーを取り、修飾された関数が呼ばれている間だけメモリハンドラをアタッチします。追加のパラメータとして、ターゲットのハンドラ、フラッシュが発生すべきレベル、バッファの容量(バッファされたレコード数)も受け取れます。これらのデフォルトは順にsys.stderr へ書き出すStreamHandler,logging.ERROR,100 です。
スクリプトはこれです:
importloggingfromlogging.handlersimportMemoryHandlerimportsyslogger=logging.getLogger(__name__)logger.addHandler(logging.NullHandler())deflog_if_errors(logger,target_handler=None,flush_level=None,capacity=None):iftarget_handlerisNone:target_handler=logging.StreamHandler()ifflush_levelisNone:flush_level=logging.ERRORifcapacityisNone:capacity=100handler=MemoryHandler(capacity,flushLevel=flush_level,target=target_handler)defdecorator(fn):defwrapper(*args,**kwargs):logger.addHandler(handler)try:returnfn(*args,**kwargs)exceptException:logger.exception('call failed')raisefinally:super(MemoryHandler,handler).flush()logger.removeHandler(handler)returnwrapperreturndecoratordefwrite_line(s):sys.stderr.write('%s\n'%s)deffoo(fail=False):write_line('about to log at DEBUG ...')logger.debug('Actually logged at DEBUG')write_line('about to log at INFO ...')logger.info('Actually logged at INFO')write_line('about to log at WARNING ...')logger.warning('Actually logged at WARNING')iffail:write_line('about to log at ERROR ...')logger.error('Actually logged at ERROR')write_line('about to log at CRITICAL ...')logger.critical('Actually logged at CRITICAL')returnfaildecorated_foo=log_if_errors(logger)(foo)if__name__=='__main__':logger.setLevel(logging.DEBUG)write_line('Calling undecorated foo with False')assertnotfoo(False)write_line('Calling undecorated foo with True')assertfoo(True)write_line('Calling decorated foo with False')assertnotdecorated_foo(False)write_line('Calling decorated foo with True')assertdecorated_foo(True)
実行すればこのような出力になるはずです:
Calling undecorated foo with Falseabout to log at DEBUG ...about to log at INFO ...about to log at WARNING ...Calling undecorated foo with Trueabout to log at DEBUG ...about to log at INFO ...about to log at WARNING ...about to log at ERROR ...about to log at CRITICAL ...Calling decorated foo with Falseabout to log at DEBUG ...about to log at INFO ...about to log at WARNING ...Calling decorated foo with Trueabout to log at DEBUG ...about to log at INFO ...about to log at WARNING ...about to log at ERROR ...Actually logged at DEBUGActually logged at INFOActually logged at WARNINGActually logged at ERRORabout to log at CRITICAL ...Actually logged at CRITICAL
見ての通り、実際のログ出力は重要度 ERROR かそれより大きい場合にのみ行っていますが、この場合はそれよりも重要度の低い ERROR よりも前に発生したイベントも出力されます。
当然のことですが、デコレーションはいつものやり方でどうぞ:
@log_if_errors(logger)deffoo(fail=False):...
バッファリングしながらロギングメッセージを email で送信する¶
ログメッセージをメールで、特に1つのメールにつき複数のログメッセージを、送信する方法を例示するため、BufferingHandler を継承します。以下の例は、必要に応じて改変することもできますが、 SMTP 経由でログを送信するのに必要な情報をコマンドライン引数で指定してスクリプトを実行できるように簡単なテストハーネスも提供しています (必須の引数およびオプション引数の詳細を見るためには、ダウンロードしたスクリプトを-h 引数をつけて実行してください)。
importloggingimportlogging.handlersimportsmtplibclassBufferingSMTPHandler(logging.handlers.BufferingHandler):def__init__(self,mailhost,port,username,password,fromaddr,toaddrs,subject,capacity):logging.handlers.BufferingHandler.__init__(self,capacity)self.mailhost=mailhostself.mailport=portself.username=usernameself.password=passwordself.fromaddr=fromaddrifisinstance(toaddrs,str):toaddrs=[toaddrs]self.toaddrs=toaddrsself.subject=subjectself.setFormatter(logging.Formatter("%(asctime)s%(levelname)-5s%(message)s"))defflush(self):iflen(self.buffer)>0:try:smtp=smtplib.SMTP(self.mailhost,self.mailport)smtp.starttls()smtp.login(self.username,self.password)msg="From:%s\r\nTo:%s\r\nSubject:%s\r\n\r\n"%(self.fromaddr,','.join(self.toaddrs),self.subject)forrecordinself.buffer:s=self.format(record)msg=msg+s+"\r\n"smtp.sendmail(self.fromaddr,self.toaddrs,msg)smtp.quit()exceptException:iflogging.raiseExceptions:raiseself.buffer=[]if__name__=='__main__':importargparseap=argparse.ArgumentParser()aa=ap.add_argumentaa('host',metavar='HOST',help='SMTP server')aa('--port','-p',type=int,default=587,help='SMTP port')aa('user',metavar='USER',help='SMTP username')aa('password',metavar='PASSWORD',help='SMTP password')aa('to',metavar='TO',help='Addressee for emails')aa('sender',metavar='SENDER',help='Sender email address')aa('--subject','-s',default='Test Logging email from Python logging module (buffering)',help='Subject of email')options=ap.parse_args()logger=logging.getLogger()logger.setLevel(logging.DEBUG)h=BufferingSMTPHandler(options.host,options.port,options.user,options.password,options.sender,options.to,options.subject,10)logger.addHandler(h)foriinrange(102):logger.info("Info index =%d",i)h.flush()h.close()
SMTP サーバーを正しく設定した上でスクリプトを実行すると、指定したアドレス宛てに11通のメールを受け取るでしょう。最初の10通のメールはそれぞれ10個のログメッセージを含み、11通目のメールは2つのログメッセージを含むはずです。これらのログメッセージはスクリプト内で指定された102個のログメッセージから構成されています。
設定によって時刻を UTC(GMT) で書式化する¶
時刻を UTC でフォーマットしたい場合もあるでしょう。以下に示すように、そのようなフォーマット処理はUTCFormatter のようなクラスを使って行うことができます:
importloggingimporttimeclassUTCFormatter(logging.Formatter):converter=time.gmtime
そしてコード中でUTCFormatter をFormatter の代わりに使えます。これを設定を通して行いたい場合、dictConfig() API を以下の完全な例で示すようなアプローチで使うことが出来ます:
importloggingimportlogging.configimporttimeclassUTCFormatter(logging.Formatter):converter=time.gmtimeLOGGING={'version':1,'disable_existing_loggers':False,'formatters':{'utc':{'()':UTCFormatter,'format':'%(asctime)s%(message)s',},'local':{'format':'%(asctime)s%(message)s',}},'handlers':{'console1':{'class':'logging.StreamHandler','formatter':'utc',},'console2':{'class':'logging.StreamHandler','formatter':'local',},},'root':{'handlers':['console1','console2'],}}if__name__=='__main__':logging.config.dictConfig(LOGGING)logging.warning('The local time is%s',time.asctime())
実行すれば、このような出力になるはずです:
2015-10-17 12:53:29,501 The local time is Sat Oct 17 13:53:29 20152015-10-17 13:53:29,501 The local time is Sat Oct 17 13:53:29 2015
時刻をローカル時刻と UTC の両方に書式化するのに、それぞれのハンドラにそれぞれフォーマッタを与えています。
ロギングの選択にコンテキストマネージャを使う¶
一時的にロギングの設定を変えて、作業をした後に設定を戻せると便利なときがあります。こういうときの、ロギングコンテキストの保存と復元をする方法ではコンテキストマネージャを使うのが一番です。以下にあるのがそのためのコンテキストマネージャの簡単な例で、これを使うと、任意にロギングレベルを変更し、コンテキストマネージャのスコープ内で他に影響を及ぼさずロギングハンドラを追加できるようになります:
importloggingimportsysclassLoggingContext:def__init__(self,logger,level=None,handler=None,close=True):self.logger=loggerself.level=levelself.handler=handlerself.close=closedef__enter__(self):ifself.levelisnotNone:self.old_level=self.logger.levelself.logger.setLevel(self.level)ifself.handler:self.logger.addHandler(self.handler)def__exit__(self,et,ev,tb):ifself.levelisnotNone:self.logger.setLevel(self.old_level)ifself.handler:self.logger.removeHandler(self.handler)ifself.handlerandself.close:self.handler.close()# implicit return of None => don't swallow exceptions
レベル値を指定した場合、コンテキストマネージャがカバーする with ブロックのスコープ内でロガーのレベルがその値に設定されます。ハンドラーを指定した場合、ブロックに入るときにロガーに追加され、ブロックから抜けるときに取り除かれます。ブロックを抜けるときに、自分で追加したハンドラをクローズするようコンテキストマネージャに指示することもできます - そのハンドラがそれ以降必要無いのであればクローズしてしまって構いません。
どのように動作するのかを示すためには、次のコード群を上のコードに付け加えるとよいです:
if__name__=='__main__':logger=logging.getLogger('foo')logger.addHandler(logging.StreamHandler())logger.setLevel(logging.INFO)logger.info('1. This should appear just once on stderr.')logger.debug('2. This should not appear.')withLoggingContext(logger,level=logging.DEBUG):logger.debug('3. This should appear once on stderr.')logger.debug('4. This should not appear.')h=logging.StreamHandler(sys.stdout)withLoggingContext(logger,level=logging.DEBUG,handler=h,close=True):logger.debug('5. This should appear twice - once on stderr and once on stdout.')logger.info('6. This should appear just once on stderr.')logger.debug('7. This should not appear.')
最初はロガーのレベルをINFO に設定しているので、メッセージ #1 は現れ、メッセージ #2 は現れません。次に、その後のwith ブロック内で一時的にレベルをDEBUG に変更したため、メッセージ #3 が現れます。そのブロックを抜けた後、ロガーのレベルはINFO に復元され、メッセージ #4 は現れません。次のwith ブロック内では、再度レベルをDEBUG に設定し、sys.stdout に書き出すハンドラを追加します。そのおかげでメッセージ #5 が 2 回 (1回はstderr を通して、もう1回はstdout を通して) コンソールに出力されます。with 文が完了すると、前の状態になるので (メッセージ #1 のように) メッセージ #6 が現れ、(まさにメッセージ #2 のように) メッセージ #7 は現れません。
出来上がったスクリプトを実行すると、結果は次のようになります:
$pythonlogctx.py1. This should appear just once on stderr.3. This should appear once on stderr.5. This should appear twice - once on stderr and once on stdout.5. This should appear twice - once on stderr and once on stdout.6. This should appear just once on stderr.
stderr を/dev/null へパイプした状態でもう一度実行すると、次のようになり、これはstdout の方に書かれたメッセージだけが現れています:
$pythonlogctx.py2>/dev/null5. This should appear twice - once on stderr and once on stdout.
stdout を/dev/null へパイプした状態でさらにもう一度実行すると、こうなります:
$pythonlogctx.py>/dev/null1. This should appear just once on stderr.3. This should appear once on stderr.5. This should appear twice - once on stderr and once on stdout.6. This should appear just once on stderr.
この場合では、stdout の方に出力されたメッセージ #5 は予想通り現れません。
もちろんここで説明した手法は、例えば一時的にロギングフィルターを取り付けたりするのに一般化できます。上のコードは Python 2 だけでなく Python 3 でも動くことに注意してください。
CLIアプリケーションスターターテンプレート¶
ここのサンプルでは次のことを説明します:
コマンドライン引数に応じてログレベルを使用する
複数のファイルに分割されたサブコマンドにディスパッチする。すべて一貫して同じレベルでログ出力を行う
シンプルで最小限の設定で行えるようにする
サービスを停止したり、開始したり、再起動する役割を持ったコマンドラインアプリケーションがあるとします。説明のために、アプリケーションのメインスクリプトがapp.py 、個々のコマンドがstart.py 、stop.py 、restart.py に実装されているものとします。デフォルトはlogging.INFO ですが、コマンドライン引数を使ってアプリケーションのログの冗長性を制御したいとします。app.py は次のコードのようになるでしょう:
importargparseimportimportlibimportloggingimportosimportsysdefmain(args=None):scriptname=os.path.basename(__file__)parser=argparse.ArgumentParser(scriptname)levels=('DEBUG','INFO','WARNING','ERROR','CRITICAL')parser.add_argument('--log-level',default='INFO',choices=levels)subparsers=parser.add_subparsers(dest='command',help='Available commands:')start_cmd=subparsers.add_parser('start',help='Start a service')start_cmd.add_argument('name',metavar='NAME',help='Name of service to start')stop_cmd=subparsers.add_parser('stop',help='Stop one or more services')stop_cmd.add_argument('names',metavar='NAME',nargs='+',help='Name of service to stop')restart_cmd=subparsers.add_parser('restart',help='Restart one or more services')restart_cmd.add_argument('names',metavar='NAME',nargs='+',help='Name of service to restart')options=parser.parse_args()# the code to dispatch commands could all be in this file. For the purposes# of illustration only, we implement each command in a separate module.try:mod=importlib.import_module(options.command)cmd=getattr(mod,'command')except(ImportError,AttributeError):print('Unable to find the code for command\'%s\''%options.command)return1# Could get fancy here and load configuration from file or dictionarylogging.basicConfig(level=options.log_level,format='%(levelname)s%(name)s%(message)s')cmd(options)if__name__=='__main__':sys.exit(main())
start 、stop 、restart コマンドは個別のモジュールとして実装されます。次は起動コマンドのソースです:
# start.pyimportlogginglogger=logging.getLogger(__name__)defcommand(options):logger.debug('About to start%s',options.name)# actually do the command processing here ...logger.info('Started the\'%s\' service.',options.name)
停止コマンドのソースは次の通りです:
# stop.pyimportlogginglogger=logging.getLogger(__name__)defcommand(options):n=len(options.names)ifn==1:plural=''services='\'%s\''%options.names[0]else:plural='s'services=', '.join('\'%s\''%namefornameinoptions.names)i=services.rfind(', ')services=services[:i]+' and '+services[i+2:]logger.debug('About to stop%s',services)# actually do the command processing here ...logger.info('Stopped the%s service%s.',services,plural)
同様に、再起動のコマンドは次の通りです:
# restart.pyimportlogginglogger=logging.getLogger(__name__)defcommand(options):n=len(options.names)ifn==1:plural=''services='\'%s\''%options.names[0]else:plural='s'services=', '.join('\'%s\''%namefornameinoptions.names)i=services.rfind(', ')services=services[:i]+' and '+services[i+2:]logger.debug('About to restart%s',services)# actually do the command processing here ...logger.info('Restarted the%s service%s.',services,plural)
このアプリケーションをデフォルトのログレベルで実行すると、次のような出力が得られます:
$pythonapp.pystartfooINFO start Started the 'foo' service.$pythonapp.pystopfoobarINFO stop Stopped the 'foo' and 'bar' services.$pythonapp.pyrestartfoobarbazINFO restart Restarted the 'foo', 'bar' and 'baz' services.
最初のワードはログレベルで、次のワードはイベントのログ出力が行われたモジュールまたはパッケージ名です。
ログレベルを変更し、ログに出力する情報を変更できるようにしましょう。もし、より詳細な情報が必要だとしましょう:
$pythonapp.py--log-levelDEBUGstartfooDEBUG start About to start fooINFO start Started the 'foo' service.$pythonapp.py--log-levelDEBUGstopfoobarDEBUG stop About to stop 'foo' and 'bar'INFO stop Stopped the 'foo' and 'bar' services.$pythonapp.py--log-levelDEBUGrestartfoobarbazDEBUG restart About to restart 'foo', 'bar' and 'baz'INFO restart Restarted the 'foo', 'bar' and 'baz' services.
あるいは情報を減らしたい場合もあるでしょう:
$pythonapp.py--log-levelWARNINGstartfoo$pythonapp.py--log-levelWARNINGstopfoobar$pythonapp.py--log-levelWARNINGrestartfoobarbaz
この場合、コマンドはコンソールに何も出力しなくなります。
Qt GUIのログ出力¶
A question that comes up from time to time is about how to log to a GUIapplication. TheQt framework is a popularcross-platform UI framework with Python bindings usingPySide2orPyQt5 libraries.
次のサンプルはQt GUIでログ出力を行うサンプルです。ここではシンプルなQtHandler クラスを作成しています。これは呼び出し可能オブジェクトを受け取ります。これはGUI更新を行うメインスレッドの中で利用されるスロットです。ワーカースレッドも作成し、UI自身からボタンを使ってログを出力したり、バックグラウンドのタスクを行うワーカースレッドからログ出力を行います(ここではランダムな期間にランダムなレベルでメッセージを出しています)。
ワーカースレッドはthreading モジュールではなく、QtのQThread クラスを使っています。これは他のQt コンポーネントとうまく統合できるように、QThread を使う必要があるからです。
The code should work with recent releases of any ofPySide6,PyQt6,PySide2 orPyQt5. You should be able to adapt the approach to earlierversions of Qt. Please refer to the comments in the code snippet for moredetailed information.
importdatetimeimportloggingimportrandomimportsysimporttime# Deal with minor differences between different Qt packagestry:fromPySide6importQtCore,QtGui,QtWidgetsSignal=QtCore.SignalSlot=QtCore.SlotexceptImportError:try:fromPyQt6importQtCore,QtGui,QtWidgetsSignal=QtCore.pyqtSignalSlot=QtCore.pyqtSlotexceptImportError:try:fromPySide2importQtCore,QtGui,QtWidgetsSignal=QtCore.SignalSlot=QtCore.SlotexceptImportError:fromPyQt5importQtCore,QtGui,QtWidgetsSignal=QtCore.pyqtSignalSlot=QtCore.pyqtSlotlogger=logging.getLogger(__name__)## Signals need to be contained in a QObject or subclass in order to be correctly# initialized.#classSignaller(QtCore.QObject):signal=Signal(str,logging.LogRecord)## Output to a Qt GUI is only supposed to happen on the main thread. So, this# handler is designed to take a slot function which is set up to run in the main# thread. In this example, the function takes a string argument which is a# formatted log message, and the log record which generated it. The formatted# string is just a convenience - you could format a string for output any way# you like in the slot function itself.## You specify the slot function to do whatever GUI updates you want. The handler# doesn't know or care about specific UI elements.#classQtHandler(logging.Handler):def__init__(self,slotfunc,*args,**kwargs):super().__init__(*args,**kwargs)self.signaller=Signaller()self.signaller.signal.connect(slotfunc)defemit(self,record):s=self.format(record)self.signaller.signal.emit(s,record)## This example uses QThreads, which means that the threads at the Python level# are named something like "Dummy-1". The function below gets the Qt name of the# current thread.#defctname():returnQtCore.QThread.currentThread().objectName()## Used to generate random levels for logging.#LEVELS=(logging.DEBUG,logging.INFO,logging.WARNING,logging.ERROR,logging.CRITICAL)## This worker class represents work that is done in a thread separate to the# main thread. The way the thread is kicked off to do work is via a button press# that connects to a slot in the worker.## Because the default threadName value in the LogRecord isn't much use, we add# a qThreadName which contains the QThread name as computed above, and pass that# value in an "extra" dictionary which is used to update the LogRecord with the# QThread name.## This example worker just outputs messages sequentially, interspersed with# random delays of the order of a few seconds.#classWorker(QtCore.QObject):@Slot()defstart(self):extra={'qThreadName':ctname()}logger.debug('Started work',extra=extra)i=1# Let the thread run until interrupted. This allows reasonably clean# thread termination.whilenotQtCore.QThread.currentThread().isInterruptionRequested():delay=0.5+random.random()*2time.sleep(delay)try:ifrandom.random()<0.1:raiseValueError('Exception raised:%d'%i)else:level=random.choice(LEVELS)logger.log(level,'Message after delay of%3.1f:%d',delay,i,extra=extra)exceptValueErrorase:logger.exception('Failed:%s',e,extra=extra)i+=1## Implement a simple UI for this cookbook example. This contains:## * A read-only text edit window which holds formatted log messages# * A button to start work and log stuff in a separate thread# * A button to log something from the main thread# * A button to clear the log window#classWindow(QtWidgets.QWidget):COLORS={logging.DEBUG:'black',logging.INFO:'blue',logging.WARNING:'orange',logging.ERROR:'red',logging.CRITICAL:'purple',}def__init__(self,app):super().__init__()self.app=appself.textedit=te=QtWidgets.QPlainTextEdit(self)# Set whatever the default monospace font is for the platformf=QtGui.QFont('nosuchfont')ifhasattr(f,'Monospace'):f.setStyleHint(f.Monospace)else:f.setStyleHint(f.StyleHint.Monospace)# for Qt6te.setFont(f)te.setReadOnly(True)PB=QtWidgets.QPushButtonself.work_button=PB('Start background work',self)self.log_button=PB('Log a message at a random level',self)self.clear_button=PB('Clear log window',self)self.handler=h=QtHandler(self.update_status)# Remember to use qThreadName rather than threadName in the format string.fs='%(asctime)s%(qThreadName)-12s%(levelname)-8s%(message)s'formatter=logging.Formatter(fs)h.setFormatter(formatter)logger.addHandler(h)# Set up to terminate the QThread when we exitapp.aboutToQuit.connect(self.force_quit)# Lay out all the widgetslayout=QtWidgets.QVBoxLayout(self)layout.addWidget(te)layout.addWidget(self.work_button)layout.addWidget(self.log_button)layout.addWidget(self.clear_button)self.setFixedSize(900,400)# Connect the non-worker slots and signalsself.log_button.clicked.connect(self.manual_update)self.clear_button.clicked.connect(self.clear_display)# Start a new worker thread and connect the slots for the workerself.start_thread()self.work_button.clicked.connect(self.worker.start)# Once started, the button should be disabledself.work_button.clicked.connect(lambda:self.work_button.setEnabled(False))defstart_thread(self):self.worker=Worker()self.worker_thread=QtCore.QThread()self.worker.setObjectName('Worker')self.worker_thread.setObjectName('WorkerThread')# for qThreadNameself.worker.moveToThread(self.worker_thread)# This will start an event loop in the worker threadself.worker_thread.start()defkill_thread(self):# Just tell the worker to stop, then tell it to quit and wait for that# to happenself.worker_thread.requestInterruption()ifself.worker_thread.isRunning():self.worker_thread.quit()self.worker_thread.wait()else:print('worker has already exited.')defforce_quit(self):# For use when the window is closedifself.worker_thread.isRunning():self.kill_thread()# The functions below update the UI and run in the main thread because# that's where the slots are set up@Slot(str,logging.LogRecord)defupdate_status(self,status,record):color=self.COLORS.get(record.levelno,'black')s='<pre><font color="%s">%s</font></pre>'%(color,status)self.textedit.appendHtml(s)@Slot()defmanual_update(self):# This function uses the formatted message passed in, but also uses# information from the record to format the message in an appropriate# color according to its severity (level).level=random.choice(LEVELS)extra={'qThreadName':ctname()}logger.log(level,'Manually logged!',extra=extra)@Slot()defclear_display(self):self.textedit.clear()defmain():QtCore.QThread.currentThread().setObjectName('MainThread')logging.getLogger().setLevel(logging.DEBUG)app=QtWidgets.QApplication(sys.argv)example=Window(app)example.show()ifhasattr(app,'exec'):rc=app.exec()else:rc=app.exec_()sys.exit(rc)if__name__=='__main__':main()
RFC5424 をサポートする syslog へのロギング¶
RFC 5424 は 2009 年から始まっていますが、ほとんどの syslog サーバーはデフォルトで、 2001 年から使われている古いRFC 3164 を使って構成されています。2003年にlogging モジュールが Python に追加されたとき、モジュールは古い (そして当時唯一存在した) プロトコルをサポートしていました。 RFC5424 は、それが登場して以来、 syslog サーバーにおいて広く使われることがなかったために、SysLogHandler の機能は更新されてきませんでした。
RFC 5424 は構造化データのサポートなど、いくつかの有用な機能を持っています。その機能をサポートする syslog サーバーへのロギングを可能にする必要がある場合、以下のような派生ハンドラクラスを使うことで実現することができます:
importdatetimeimportlogging.handlersimportreimportsocketimporttimeclassSysLogHandler5424(logging.handlers.SysLogHandler):tz_offset=re.compile(r'([+-]\d{2})(\d{2})$')escaped=re.compile(r'([\]"\\])')def__init__(self,*args,**kwargs):self.msgid=kwargs.pop('msgid',None)self.appname=kwargs.pop('appname',None)super().__init__(*args,**kwargs)defformat(self,record):version=1asctime=datetime.datetime.fromtimestamp(record.created).isoformat()m=self.tz_offset.match(time.strftime('%z'))has_offset=Falseifmandtime.timezone:hrs,mins=m.groups()ifint(hrs)orint(mins):has_offset=Trueifnothas_offset:asctime+='Z'else:asctime+=f'{hrs}:{mins}'try:hostname=socket.gethostname()exceptException:hostname='-'appname=self.appnameor'-'procid=record.processmsgid='-'msg=super().format(record)sdata='-'ifhasattr(record,'structured_data'):sd=record.structured_data# This should be a dict where the keys are SD-ID and the value is a# dict mapping PARAM-NAME to PARAM-VALUE (refer to the RFC for what these# mean)# There's no error checking here - it's purely for illustration, and you# can adapt this code for use in production environmentsparts=[]defreplacer(m):g=m.groups()return'\\'+g[0]forsdid,dvinsd.items():part=f'[{sdid}'fork,vindv.items():s=str(v)s=self.escaped.sub(replacer,s)part+=f'{k}="{s}"'part+=']'parts.append(part)sdata=''.join(parts)returnf'{version}{asctime}{hostname}{appname}{procid}{msgid}{sdata}{msg}'
上記のコードを完全に理解するには RFC 5424 を熟知する必要があります。また、上記の例とはやや異なる要求を持つこともあるでしょう (たとえば構造化データをログに渡す方法について)。にもかかわらず、上記のコードは特有の要求に対する順応性があります。上記のハンドラにより、構造化データは以下のように渡すことができるでしょう:
sd={'foo@12345':{'bar':'baz','baz':'bozz','fizz':r'buzz'},'foo@54321':{'rab':'baz','zab':'bozz','zzif':r'buzz'}}extra={'structured_data':sd}i=1logger.debug('Message%d',i,extra=extra)
ロガーを出力ストリームのように取り扱う方法¶
書き込み先として file-like オブジェクトを期待するサードパーティの API に接続する必要がある一方で、その API の出力を直接ロガーに送りたいということがときどきあります。これは file-like な API でロガーをラップするクラスを使うことで実現できます。以下はそのようなクラスを例解する短いスクリプトです:
importloggingclassLoggerWriter:def__init__(self,logger,level):self.logger=loggerself.level=leveldefwrite(self,message):ifmessage!='\n':# avoid printing bare newlines, if you likeself.logger.log(self.level,message)defflush(self):# doesn't actually do anything, but might be expected of a file-like# object - so optional depending on your situationpassdefclose(self):# doesn't actually do anything, but might be expected of a file-like# object - so optional depending on your situation. You might want# to set a flag so that later calls to write raise an exceptionpassdefmain():logging.basicConfig(level=logging.DEBUG)logger=logging.getLogger('demo')info_fp=LoggerWriter(logger,logging.INFO)debug_fp=LoggerWriter(logger,logging.DEBUG)print('An INFO message',file=info_fp)print('A DEBUG message',file=debug_fp)if__name__=="__main__":main()
このスクリプトを実行すると、次のように出力されます。
INFO:demo:An INFO messageDEBUG:demo:A DEBUG message
また、sys.stdout やsys.stderr をリダイレクトするにはLoggerWriter を使って以下のようにします:
importsyssys.stdout=LoggerWriter(logger,logging.INFO)sys.stderr=LoggerWriter(logger,logging.WARNING)
上記の操作は、必要に応じてロギングを設定した後に 行うべきです。上記の例では、basicConfig() の呼び出しが (LoggerWriter インスタンスで上書きされる前のsys.stderr を使って) 設定を行います。そして、以下のような結果を得るでしょう:
>>>print('Foo')INFO:demo:Foo>>>print('Bar',file=sys.stderr)WARNING:demo:Bar>>>
言うまでもなく上記の例はbasicConfig() で使われている書式にもとづく出力を示していますが、ロギングの設定で異なるフォーマッタを使うことができます。
上記の例では、バッファリングや奪取した書き込み呼び出しのシーケンスの扱いについてはなすがままになっています。たとえば、上記のLoggerWriter の定義で、次のようなコードの断片があったとします。
sys.stderr=LoggerWriter(logger,logging.WARNING)1/0
このスクリプトを実行すると以下のような結果が得られます。
WARNING:demo:Traceback (most recent call last):WARNING:demo: File "/home/runner/cookbook-loggerwriter/test.py", line 53, in <module>WARNING:demo:WARNING:demo:main()WARNING:demo: File "/home/runner/cookbook-loggerwriter/test.py", line 49, in mainWARNING:demo:WARNING:demo:1 / 0WARNING:demo:ZeroDivisionErrorWARNING:demo::WARNING:demo:division by zero
As you can see, this output isn't ideal. That's because the underlying codewhich writes tosys.stderr makes multiple writes, each of which results in aseparate logged line (for example, the last three lines above). To get aroundthis problem, you need to buffer things and only output log lines when newlinesare seen. Let's use a slightly better implementation ofLoggerWriter:
classBufferingLoggerWriter(LoggerWriter):def__init__(self,logger,level):super().__init__(logger,level)self.buffer=''defwrite(self,message):if'\n'notinmessage:self.buffer+=messageelse:parts=message.split('\n')ifself.buffer:s=self.buffer+parts.pop(0)self.logger.log(self.level,s)self.buffer=parts.pop()forpartinparts:self.logger.log(self.level,part)
この実装は改行があらわれるまでログをバッファリングし、行全体をログに出力するだけです。このアプローチにより、より適切な出力が得られます:
WARNING:demo:Traceback (most recent call last):WARNING:demo: File "/home/runner/cookbook-loggerwriter/main.py", line 55, in <module>WARNING:demo: main()WARNING:demo: File "/home/runner/cookbook-loggerwriter/main.py", line 52, in mainWARNING:demo: 1/0WARNING:demo:ZeroDivisionError: division by zero
How to uniformly handle newlines in logging output¶
Usually, messages that are logged (say to console or file) consist of a singleline of text. However, sometimes there is a need to handle messages withmultiple lines - whether because a logging format string contains newlines, orlogged data contains newlines. If you want to handle such messages uniformly, sothat each line in the logged message appears uniformly formatted as if it waslogged separately, you can do this using a handler mixin, as in the followingsnippet:
# Assume this is in a module mymixins.pyimportcopyclassMultilineMixin:defemit(self,record):s=record.getMessage()if'\n'notins:super().emit(record)else:lines=s.splitlines()rec=copy.copy(record)rec.args=Noneforlineinlines:rec.msg=linesuper().emit(rec)
You can use the mixin as in the following script:
importloggingfrommymixinsimportMultilineMixinlogger=logging.getLogger(__name__)classStreamHandler(MultilineMixin,logging.StreamHandler):passif__name__=='__main__':logging.basicConfig(level=logging.DEBUG,format='%(asctime)s%(levelname)-9s%(message)s',handlers=[StreamHandler()])logger.debug('Single line')logger.debug('Multiple lines:\nfool me once ...')logger.debug('Another single line')logger.debug('Multiple lines:\n%s','fool me ...\ncan\'t get fooled again')
The script, when run, prints something like:
2025-07-02 13:54:47,234 DEBUG Single line2025-07-02 13:54:47,234 DEBUG Multiple lines:2025-07-02 13:54:47,234 DEBUG fool me once ...2025-07-02 13:54:47,234 DEBUG Another single line2025-07-02 13:54:47,234 DEBUG Multiple lines:2025-07-02 13:54:47,234 DEBUG fool me ...2025-07-02 13:54:47,234 DEBUG can't get fooled again
If, on the other hand, you are concerned aboutlog injection, you can use aformatter which escapes newlines, as per the following example:
importlogginglogger=logging.getLogger(__name__)classEscapingFormatter(logging.Formatter):defformat(self,record):s=super().format(record)returns.replace('\n',r'\n')if__name__=='__main__':h=logging.StreamHandler()h.setFormatter(EscapingFormatter('%(asctime)s%(levelname)-9s%(message)s'))logging.basicConfig(level=logging.DEBUG,handlers=[h])logger.debug('Single line')logger.debug('Multiple lines:\nfool me once ...')logger.debug('Another single line')logger.debug('Multiple lines:\n%s','fool me ...\ncan\'t get fooled again')
You can, of course, use whatever escaping scheme makes the most sense for you.The script, when run, should produce output like this:
2025-07-09 06:47:33,783 DEBUG Single line2025-07-09 06:47:33,783 DEBUG Multiple lines:\nfool me once ...2025-07-09 06:47:33,783 DEBUG Another single line2025-07-09 06:47:33,783 DEBUG Multiple lines:\nfool me ...\ncan't get fooled again
Escaping behaviour can't be the stdlib default , as it would break backwardscompatibility.
避けるべきパターン¶
これまでのセクションではログ出力を行うときに必要なこと、考慮すべきことなどを説明してきました。このセクションでは、役に立たない 利用パターンについて触れます。これは多くの場合避けるべきことです。これらの説明はどこから読んでも構いません。
同じログファイルを何度も開く¶
Windowsでは同じファイルを何度も開くことができず「このファイルは他のプロセスから利用されています」というエラーが表示されます。しかし、POSIXプラットフォームではエラーがおきることなく、同じファイルを何度も開けます。これは次のように間違って使われる可能性があります。
同じファイルを指すファイルハンドラを1度以上追加する(例えばコピー&ペーストして書き換え忘れによるエラー)。
異なる名前を持つ、一見異なる2つのファイルを開くが、片方が他方へのシンボリックリンクとなっている。
プロセスをフォークするが、その後親プロセスと子プロセスが同じファイルへの参照を維持する。これは例えば、
multiprocessingモジュールなどを使うと発生する可能性があります。
ファイルを複数回開くことは、一見 動作しているように見えますが、さまざまな問題を引き起こす可能性があります:
複数のスレッドが同一ファイルに書き出そうとすると、ログ出力が文字化けする可能性があります。ログモジュールは同じハンドラーのインスタンスに対して並列で利用しても正しく動くようになっていますが、同じファイルを参照する2つの異なるハンドラーのインスタンスに対し、2つのスレッドから同時に書き込みをした場合にはそのような保護は働きません。
(たとえばファイルのローテーションの間に) ファイルを削除しようとすると、そのファイルを指す別の参照が残っているために、何のエラーも発しないまま失敗します。これは混乱や不要なデバッグの時間のもととなる可能性があります - ログが思いもしない場所に記録されたり、完全に失われたりします。もしくは移動したと思われていたファイルが残っていたり、ファイルサイズにもとづくローテーションが行われているにもかかわらずファイルサイズが予想外に増加したりすることもあります。
この問題を回避するには複数のプロセスからの単一ファイルへのログ記録 で紹介したテクニックを使用してください。
ロガーをクラスの属性にするか、パラメータで渡す¶
ロガーはシングルトンであるため、一般的には意味がなく、これを行う必要が出てくることは滅多にありません。コードからは名前を使ってlogging.getLogger(name) 経由でロガーインスタンスにアクセスできるため、インスタンスを持って回って、インスタンス属性として保持することは意味がありません。JavaやC#といった他の言語ではよく静的クラス属性にしてます。しかし、Pythonにおいてはこのパターンはクラスではなくモジュールがソフトウェア分解の単位となっているため、無意味です。
ライブラリ内でロガーにNullHandler 以外のハンドラーを追加する¶
ハンドラーやフォーマッター、フィルターを追加してログ出力をカスタマイズするのはライブラリ開発者ではなく、アプリケーション開発者の責務です。もしあなたがライブラリのメンテナンスをしているのであれば、NullHandler インスタンス以外のロガーを追加してはいけない、ということを意味します。
大量のロガーを作成する¶
ロガーはシングルトンであり、スクリプトの実行中に解放されることがないため、大量のロガーを作成すると、メモリが解放されることなく消費されます。ファイルの処理単位やネットワーク接続単位でロガーを作るのではなく、既存のメカニズム を使ってコンテキスト依存の情報をログに渡し、ロガーはアプリケーション内の説明の単位(通常はモジュールだが、場合によってはそれよりも小さい可能性もある)で作るように制限してください。
その他のリソース¶
参考
loggingモジュールlogging モジュールの API リファレンス。
logging.configモジュールlogging モジュールの環境設定 API です。
logging.handlersモジュールlogging モジュールに含まれる、便利なハンドラです。