Lambdaはログを CloudWatch Logs に自動保存しますが、CloudWatch Logs にはJSON形式のログを自動でパースして整形表示したり検索したりする機能があります。是非とも、ログをJSON形式にしたいところです。
しかし、「python lambda logging json」でググって見つかる記事は、いずれも内容に不備があるようでしたので、自分がベストだと思う方法を紹介するのがこの記事です。
お断り
Pythonのログ出力は標準ライブラリのlogging
がスタンダードなので、この記事ではloggingを前提にしています。
printを使うべきではない理由・logging の正しい使い方については「ログ出力のための print と import logging はやめてほしい 」という記事が分かりやすいです(文体は辛辣ですけど)
これが(きっと)ベストプラクティスだ!
import logging
import json
# 冒頭に、独自のフォーマッタの定義と、ルートロガーの設定を書く
class JsonFormatter:
def format(self, record):
return json.dumps(vars(record))
# ルートロガーの設定
logging.basicConfig() # 標準エラーに出力するハンドラーをセット
logging.getLogger().handlers[0].setFormatter(JsonFormatter()) # ハンドラーの出力フォーマットを自作のものに変更
# 以降は普通にloggerを取得して処理を関数を書く
logger = logging.getLogger(__name__)
logger.setLevel(os.environ.get('LOG_LEVEL', 'INFO')) # 余談: 環境変数でログレベルを変更可能にしとくと便利
def lambda_handler(event: dict, context):
# ログに付加情報をもたせたいときは、`extra=` に dict を渡す。
# もちろん、 dict は2要素以上でもOK
logger.info("sample", extra={"foo": 12, "bar": "Hello World!"})
CloudWatch Logsに保存されるログ:
logger.info
の出力がJSONになっています。なお、START RequestId:...
などのログはAWS Lambda自体が出しているものなので変更できません。
START RequestId: 3ba9c9dd-0758-482e-8aa4-f5496fa49f04 Version: $LATEST
{
"name": "lambda_function",
"msg": "sample",
"args": [],
"levelname": "INFO",
"levelno": 20,
"pathname": "/var/task/lambda_function.py",
"filename": "lambda_function.py",
"module": "lambda_function",
"exc_info": null,
"exc_text": null,
"stack_info": null,
"lineno": 23,
"funcName": "lambda_handler",
"created": 1577152740.1250498,
"msecs": 125.04982948303223,
"relativeCreated": 64.58139419555664,
"thread": 140315591210816,
"threadName": "MainThread",
"processName": "MainProcess",
"process": 7,
"foo": 12,
"bar": "Hello World!",
"aws_request_id": "3ba9c9dd-0758-482e-8aa4-f5496fa49f04"
}
END RequestId: 3ba9c9dd-0758-482e-8aa4-f5496fa49f04
REPORT RequestId: 3ba9c9dd-0758-482e-8aa4-f5496fa49f04 Duration: 1.76 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 55 MB Init Duration: 113.06 ms
一応の解説
(詳しくは公式ドキュメントを参照)
logging.Logger
は階層構造になっているので、全体のフォーマットを変えたいならルートロガー(logging.getLogger()
)を変更すればOK。logging.basicConfig()
でルートロガーにハンドラーがセットされるので、あとは .setFormatter()
で独自のフォーマッタをセットします。
.format(record)
にはlogging.LogRecordが渡されます。このオブジェクトは諸々の情報を属性として持っているので、vars(record)
で取得して、JSONに変換しています。
logger.info
のextra=
で渡した値は、LogRecord
の属性としてセットされます。ここで、msg
, funcName
などのキーを使うと、本来の値を上書きしてしまいます。
どうしても上書きを避けたい場合は、.makeRecordをオーバーライドした独自のロガーを定義することもできます。
ただ、私はコードが複雑になるので気が進みません。
ね、簡単でしょ?
ライブラリを使うべき?
ところで、GithubにはログをJSONにフォーマットするライブラリがいくつも転がっています。
しかし、私はそういったライブラリを使うのはオススメしません。ライブラリを導入すると依存関係が生じ、バージョンアップの手間やセキュリティ上のリスクが生じるからです(2016年の left-pad を思い出せ!)。
上述のようにJsonFormatter
はたった3行で書けるので、コードをコピペするのが結局ベストではないかと思います。
どっとはらい。