Python アプリケーションの監視に Sentry というソフトウェア監視ツールを使用している。
もし Sentry の雰囲気や簡単な使い方を知りたければ、以下の記事も参照して欲しい。
問題
Python SDK の capture_message や capture_exception メソッドを実行しても Sentry に通知が飛ばなかった(未処理の例外が発生した場合も同様だが、今回使いたいのは明示的なキャプチャの方だった)。
作業環境は以下のような感じだ。
python==3.9
Django==4.2.1
djangorestframework==3.14.0
sentry-sdk==1.43.0
AWS ECS 上で uWSGI を使って起動している。wsgi.ini の一部を以下に示す。
enable-threads=true
threads=4
上記のようにマルチスレッドにしている。
試行錯誤
まず、Sentry の WSGI のページに、「uWSGI でスレッドサポートするなら --enable-threads と --py-call-uwsgi-fork-hooks を ON にしてね」とある。--py-call-uwsgi-fork-hooks の方を使用していなかったため試したが、改善しなかった。
py-call-uwsgi-fork-hooks=true
sentry_sdk.init の DSN の設定や環境変数、SDK のバージョン、ECS のアウトバウンドルールなどを疑ってみたが問題は無さそうだった。
configure_scope や push_scope でスコープの変更をしてみたが、これもうまくいかない。
その後、軽い気持ちでキャプチャメソッドの直前に sentry_sdk.init を試したら通知が飛んだ。
複数回の init は避けたい
通知を飛ばす手段が得られたのは良かった。
しかし、以下の Github Issue に「設計的にアプリケーションライフサイクルの中で SDK を初期化できるのは1度だけ。複数回やると、未定義の振る舞いにつながる」と書かれているのを見つけた。今回の件に直接は関係あるかわからなかったが、sentry_sdk.init の使用は避けたかった。
sentry-sdk 読み
それで初めて、capture_message の実装を追ってみた。
@hubmethod
def capture_message(
message, # type: str
level=None, # type: Optional[LogLevelStr]
scope=None, # type: Optional[Any]
**scope_kwargs # type: Any
):
# type: (...) -> Optional[str]
return Hub.current.capture_message(message, level, scope=scope, **scope_kwargs)
Hub というのは SDK のコンカレンシー管理をラップしていて、各スレッド毎に持っているらしい。Hub.current は Hub の現在のインスタンスを返す。
で、そいつが呼んでる capture_message がこれ。
def capture_message(self, message, level=None, scope=None, **scope_kwargs):
"""
:returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`).
"""
client, top_scope = self._stack[-1]
if client is None:
return None
last_event_id = top_scope.capture_message(
message, level=level, client=client, scope=scope, **scope_kwargs
)
if last_event_id is not None:
self._last_event_id = last_event_id
return last_event_id
※ docstring は一部割愛させていただいた。
SDK がイベントを送信することにしたら、event_id が返ってくる、と。
そこで以下のコードで確認したところ、event_id の値は None だった。
from sentry_sdk import capture_message
event_id = capture_message("capture message!")
hub.py から、client が空なのでは?と考えた。
たどり着いた解決方法
sentry_sdk には Client というクラスがある。Client は、イベントをキャプチャして Sentry へ転送する責任を持つ。
Client を初期化し、これを引数として新しい Hub オブジェクトを作る。その Hub から capture_message する。
import os
from sentry_sdk import Hub, Client
client = Client(
dsn=os.environ.get('SENTRY_DSN')
)
hub = Hub(client)
hub.capture_message('capture message!')
すると、Sentry に通知が飛んでくれた。
感想
とりあえず通知は動くようになった。
しかし対応方法に根拠が無くてもやもやしている。
「Hub はスレッド毎にある」とか「Python SDK はスレッドローカルを使用している」という情報から、「マルチスレッドが問題なのかな」とフワッと理解している。
Client のドキュメントが見当たらなかったが、どこかにあるのだろうか。
Scope については、基本的に SDK が自動で管理するため、開発者はトップレベルの API を使用することが推奨されている。Client も同じでは?と思わなくはない。
コードではなく設定で解決できるのでは?と未だに疑っている。
今回使用した Sentry SDK のバージョンは 1 系だった。2 系にバージョンアップしたうえで、マルチスレッドの設定を適切に行えば直ったりしないかは気になる。