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 系にバージョンアップしたうえで、マルチスレッドの設定を適切に行えば直ったりしないかは気になる。