PythonのWebアプリケーションフレームワーク(WAF)であるaiohttp
でカスタムロガーを使ってアクセスログを出力する。
TL;DR
aiohttp
にはアクセスログを取得する標準の仕組みがある- 出力する内容を変更するときは
AbstractAccessLogger
を実装した独自のAccessLogger
を用意する Application.run_app
メソッドの引数として独自のAccessLogger
を渡してwebサーバを起動するlogger
の設定は忘れずに
from aiohttp import web
from aiohttp.abc import AbstractAccessLogger
from aiohttp.web_app import Application
class CustomAccessLogger(AbstractAccessLogger):
def log(self, request,response,time: float) -> None:
self.logger.info(f'access_log')
app: Application = web.Application()
web.run_app(app, access_log_class=CustomAccessLogger)
環境
- Python 3.8.0
- aiohttp 3.6.2
独自のアクセスログを出力したい
Pythonのwebサーバでアクセスログを出力し、ログの中へリクエストヘッダーに含まれるX-Request-ID
を出力しようと考えた。
利用しているWAFのaiohttp
のドキュメントをみると、アクセスログとカスタマイズの仕組みが標準でサポートされていたため試してみた。
AbstractAccessLoggerを実装した独自のAccessLoggerを用いる。
aiohttp
のアクセスログの仕組みは次のページに記載されている。
- Logging — aiohttp 3.6.2 documentation
基本的にはシグネチャを満たしたlog
メソッドを実装すれば良い。
from aiohttp.abc import BaseRequest, AbstractAccessLogger
from aiohttp.web_response import StreamResponse
class CustomAccessLogger(AbstractAccessLogger):
def log(self,
request: BaseRequest,
response: StreamResponse,
time: float) -> None:
self.logger.info(f'access_log: {request.remote} '
f'"{request.method} {request.path} '
f'done in {time}s: {response.status}')
この自作クラスの型名をwebサーバ起動時に渡せばこれを利用したアクセスログが出力されるようになる。
app: Application = web.Application()
web.run_app(app, access_log_class=CustomAccessLogger)
このクラスはサーバ起動時に自動的に初期化されて利用される。
self.logger
から呼び出されるlogger
オブジェクトは次のように決まる、
run_app
メソッドの引数にaccess_log
として渡したlogging.logger
オブジェクト- 何も渡されなかった場合は
logging.getLogger('aiohttp.access')
で取得できるlogger
オブジェクト
上記の例では、access_log
は未指定なのでlogging.getLogger('aiohttp.access')
で取得できるオブジェクトになる。
実際にログ出力される形式、ログレベルは通常のlogging
パッケージのエコシステムに則る。
そのため、適切なログレベルやhandler
を設定しないと出力されない。
33000
ポートで起動するwebサーバに独自アクセスログを設定するコードが以下になる。
import logging
from aiohttp import web
from aiohttp.abc import BaseRequest, AbstractAccessLogger
from aiohttp.web_app import Application
from aiohttp.web_request import Request
from aiohttp.web_response import StreamResponse
class CustomAccessLogger(AbstractAccessLogger):
def log(self,
request: BaseRequest,
response: StreamResponse,
time: float) -> None:
self.logger.info(f'access_log: {request.remote} '
f'"{request.method} {request.path} '
f'done in {time}s: {response.status}')
async def handler(request: Request) -> StreamResponse:
return web.Response(text='return\n')
access_logger = logging.getLogger('aiohttp.access')
ch = logging.StreamHandler()
access_logger.setLevel(logging.INFO)
access_logger.addHandler(ch)
app: Application = web.Application()
app.router.add_get('/', handler)
web.run_app(app, port=33000, access_log_class=CustomAccessLogger)
'aiohttp.access'
として登録されているlogging.logger
にhandler
としては標準のStreamHandler
を付与している。
また、上記の例ではinfo
メソッドを呼んでいるので、logger
自体のログレベルをINFO
以上にセットしておかないとhandler
が呼ばれない。
Access logs are enabled by default. If the debug flag is set, and the default logger ‘aiohttp.access’ is used, access logs will be output to stderr if no handlers are attached. Furthermore, if the default logger has no log level set, the log level will be set to logging.DEBUG.
ドキュメントには上記のようにデフォルトならDEBUG
レベルで出力されると書いてあったが、明示的にログレベルなどを設定しないと出力されなかった。
上記コードを起動し、別ターミナルからcurl
コマンドでアクセスしたときのログ出力は以下のようになった。
$ python -u main.py
======== Running on http://0.0.0.0:33000 ========
(Press CTRL+C to quit)
access_log: 127.0.0.1 "GET / done in 0.0007638730000003591s: 200
あとは、handler
を調整して、JSON
形式の出力にするなり、ファイルに出力すればよい。
おわりに
Pythonのlogging
パッケージのlogger
、handler
まわりのエコシステムの挙動を理解せず、ログが出力できなくてだいぶハマった。
結局公式ドキュメントを読むことで解決したので、やはり言語仕様はひととおり読まないといけないなと痛感した。
参考
- Logging HOWTO — Python 3.8.2 ドキュメント
- Logging — aiohttp 3.6.2 documentation