Python で書いたサーバのエラーログを CloudWatch Logs に吐き出していたところ、Traceback の改行文字で分割されていてエラー内容が非常に追いづらいという体験をした。
どうやら CloudWatch Logs は JSON でログを吐けばよしなにパースしてくれるらしく、本番環境のログは JSON で吐き出したほうが良さそうということで、Python のログを Traceback つきで JSON 出力するようにしてみた。
初期設定
from os import getenv
import logging
import json
class JSONFormatter(logging.Formatter):
"""ログを JSON で出力するフォーマッタ"""
def format(self, record: logging.LogRecord) -> str:
try:
data = vars(record)
exc_info = data.pop("exc_info")
if exc_info:
data["traceback"] = self.formatException(exc_info).splitlines()
return json.dumps(data)
except:
return super().format(record)
log_handler = logging.StreamHandler()
if getenv("LOG_FORMAT", "default").lower() == "json":
log_handler.setFormatter(JSONFormatter())
logging.basicConfig(handlers=[log_handler], level=getenv("LOG_LEVEL", logging.WARNING))
- 環境変数で設定を切り替えれるようにしているが、そのへんは好みで
- Traceback を行ごとに区切っている (splitlines) が、これは CloudWatch Logs で見やすくするためで、区切らなくてもいいとは思う
-
json.dumps
が何らかの理由で失敗したときなどにログをロストしないようにsuper().format(record)
している
使い方
from logging import getLogger
logger = getLogger(__name__)
try:
1/0 # 例外を発生させる
except:
logger.exception("Unexpected error occurred")
- ルートロガーの設定を変更しているので、
getLogger
で新しくロガーを作成した場合でも特に何も設定する必要はない - 例外をキャッチした際に
logger.exception
を使っておけば LogRecord にexc_info
が含まれるので、特に何もしなくても Traceback がログ出力されるので便利-
logger.exception("...")
はlogger.error("...", exc_info=True)
と同じ (たぶん) なので、ログレベルは ERROR になる
-
- extra に渡した追加データもそのままログに出力される
- 例:
logger.exception("...", extra={"user_id": "..."})
- 例:
出力されるエラー
※ 本来は改行されていない1行の JSON 文字列が出力されるが、ここでは見やすくするために整形している
{
"name": "__main__",
"msg": "Unexpected error occurred",
"args": [],
"levelname": "ERROR",
"levelno": 40,
"pathname": "main.py",
"filename": "main.py",
"module": "main",
"exc_text": null,
"stack_info": null,
"lineno": 8,
"funcName": "<module>",
"created": 1643609361.1079879,
"msecs": 107.98788070678711,
"relativeCreated": 3.8950443267822266,
"thread": 4410674688,
"threadName": "MainThread",
"processName": "MainProcess",
"process": 17896,
"traceback": [
"Traceback (most recent call last):",
" File \"main.py\", line 6, in <module>",
" 1/0",
"ZeroDivisionError: division by zero"
]
}