こんにちは!Zeals で Python をメインに書いている新卒 1 年目のソフトウェアエンジニアの玉城です。
Python で Cloud Logging(旧: Stackdriver Logging) や Datadog にログを出力するとき、複数行に分割されてしまい、元のログイベントとの関連付けが難しくなり困ったことはないでしょうか。
例えば、以下のようにログを書いたときに
logger.info("test\n I want to eat sushi")
Cloud Logging にはこのように表示されてしまうといった感じです。
これを JSON フォーマットにすることで解決してみましょう。
- What's JSON log formatter
- File structure description
- JSON customization using json_log_formatter
- Set a custom class
- Check logs with pytest
- Create a simple server in Python
- おわりに
What's JSON log formatter
Python でログを JSON フォーマットにするとき、柔軟性が高く設定できるJSON log formatter
を使うのがおすすめです。
JSON log formatter は Python のログ出力を JSON フォーマットで出力できるパッケージです。
pypi.org
他に Python でよく使われる JSON フォーマッタにpython-json-loggerがありますが、こちらを使用するのは下記の理由です。
- JSON フィールドをカスタマイズするクラスが python-json-logger よりも直感的
- python-json-logger よりもドキュメントがわかりやすい
ではJSON log formatter
をインストールしていきます。
pip install JSON-log-formatter
これを使って簡単に Web サーバーを作り、それを Google App Engine にあげてログを確認してみます。
File structure description
今回作成したファイルの構造はこのようになってます。
. ├── app.yaml ├── config │ ├── __init__.py │ ├── customised_log.py │ └── log_config.py ├── main.py ├── requirements.txt └── tests ├── __init__.py └── test_log.py
↓ のリポジトリに置いているので、よろしければご覧ください。
JSON customization using json_log_formatter
まずは、JSON-log-formatter
を使ってログの設定をするcustomised_log.py
を見てみましょう。
from logging import LogRecord import json_log_formatter class CustomisedJSONFormatter(json_log_formatter.JSONFormatter): def json_record(self, message: str, extra: dict, record: LogRecord) -> dict: '''log output to JSON format https://github.com/marselester/json-log-formatter ''' # https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry extra['message'] = message extra['severity'] = record.levelname extra['logger_name'] = record.name extra['module'] = record.module if record.exc_info: extra['exc_info'] = self.formatException(record.exc_info) return extra
extra['hoge'] = ~~
と辞書に追加していくことで、JSON のフィールドを設定することができます。
必ず欲しいものはこちらで宣言しておくといいでしょう。オプションで付けたいような場合は後ほど説明します。
Set a custom class
先ほど作成したcustomised_log.py
のクラスを、ユーザー定義オブジェクトで設定します。
5 行目を見てください。特殊キー'()'
を使い、上記で作成したクラスを import し、フォーマッタを設定します。
ちなみにこちらは Python ファイル内で dict 形式で設定していますが、YAML で書いても動きます。
LOGGER = { 'version': 1, 'formatters': { 'default': { '()': 'config.customised_log.CustomisedJSONFormatter' } }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'level': 'INFO', 'formatter': 'default', 'stream': 'ext://sys.stdout' }, }, 'loggers': { 'zeals': { 'level': 'DEBUG', 'handlers': ['console'] } } }
では、本当に JSON フォーマットになっているのかどうかを試してみます。
Check logs with pytest
pytest を使って確認してみます。ユーザー定義オブジェクトで出力先を標準出力にしたので、標準出力をキャプチャします。
import json from logging import config, getLogger from config.log_config import LOGGER def test_log(capsys): config.dictConfig(LOGGER) logger = getLogger('zeals') logger.warning('occur error') expected = json.dumps({ "message": "occur error", "severity": "WARNING", "logger_name": "zeals", "module": "test_log", }) + "\n" out, err = capsys.readouterr() assert out == expected assert err == '' print(out)
出力を確認してみましょう。
pytest -s -v tests/test_log.py
tests/test_log.py::test_log {“message”: “occur error”, “severity”: “WARNING”, “logger_name”: “zeals”, “module”: “test_log”}
いい感じに JSON フォーマットになっています!
では、Python で簡単なサーバーを作って Google App Engine にアップしてみます。
Create a simple server in Python
下記が簡単なサーバーの例です。14 行目の logger.info()のように extra=<dict>
とすることで、オプションのような形で JSON フォーマットにフィールドを追加することもできます。
また、18 行目の logger.exception のようにexc_info=True
とすることで、トレースバックを JSON フィールドに加えることもできます。
from logging import config, getLogger from flask import Flask from config.log_config import LOGGER app = Flask(__name__) @app.route('/') def hello(): config.dictConfig(LOGGER) logger = getLogger('zeals') logger.info('test\n I want to eat sushi', {"id": 1234, "name": "tamaki"}) try: raise ValueError('something wrong') except ValueError: logger.exception(f'Request failed.', exc_info=True) return 'Hello, World!' if __name__ == '__main__': app.run()
これを Google App Engine に上げて、ログを表示してみます。
ログのテキストに改行を挟んでも、1 つのログメッセージとして記録されています。これは便利。
また、Cloud Logging は JSON 形式でログ出力をすると、jsonPayload に構造化ペイロードとして認識されます。
jsonPayloadの JSON フィールド 内を見てみると、オプションで extra
に追加したid
やname
がきちんと付け加えられています。
このような形で JSON フィールドに加えることで、ログビューワとして検索しやすくなります。
おわりに
ちなみに、弊社では Python や GCP を使った開発をしています。興味を持っていただけましたら、ぜひ一度私とお話しましょう!🤝