こんにちは。サーバーサイドエンジニアの三村(@t_mimura39)です。
またまた「ClinPeerアプリ開発の裏側連載記事」です。 tech.medpeer.co.jp
今回はClinPeerで活用しているRailsの ActiveSupport::ErrorReporter
についてご紹介します。
目次
- ActiveSupport::ErrorReporter とは
- なぜ ActiveSupport::ErrorReporter を使うのか
- 実行コンテキストの注入
- なぜ ActiveSupport::ErrorReporter を使うのか(本当のメリット)
- おわり
ActiveSupport::ErrorReporter
とは
Railsに標準添付されているエラー管理の仕組みです。
↓これが
begin do_something rescueSomethingIsBroken => error MyErrorReportingService.notify(error) end
↓こうなります。
Rails.error.handle(SomethingIsBroken) do do_something end
詳細はRails公式ドキュメントをご参照ください。
以上になります。ありがとうございました。
と、終わるわけにもいかないのでもう少し深い話を書きます。
なぜ ActiveSupport::ErrorReporter
を使うのか
上述の通り典型的な例外ハンドリング処理に対して統一的なI/Fを提供してくれるのですが、それ以外にもいくつかの利点があります。
まず一つ目はPubSub的な仕組みになっているため「例外発生時に実行したい処理」の増減に対して柔軟に対応可能という点です。
ClinPeerでは例外発生時に以下2種類の処理を実行しています。
- Rollbarへの例外通知
- エラー管理サービスとして利用しているRollbarに例外情報を通知します。他SaaSを利用しているプロジェクトは良いように読み替えてください。
- ログ出力
- 上記のようなエラー管理サービスが不調な場合でも例外状況を把握できるようにログも出力しています。
例えば以下のように初期設定をしてみます。
# config/initializers/rails_error_subscriber.rbclassRailsLoggerErrorSubscriberdefreport(error, handled:, severity:, context:, source: nil) error_message = error.message message = "#{error.class}: #{error_message}\n#{(error.backtrace || caller)&.join("\n")}" severity = :warnif severity == :warningRails.logger.public_send(severity, message) endendclassRollbarErrorSubscriberdefreport(error, handled:, severity:, context:, source: nil) extra = context.is_a?(Hash) ? context.deep_dup : {} Rollbar.log(severity, error, extra) endendRails.application.config.after_initialize doRails.error.subscribe(RailsLoggerErrorSubscriber.new) Rails.error.subscribe(RollbarErrorSubscriber.new) end
Rails.error.report
や Rails.error.handle
で例外を処理する際に、登録したサブスクライバー全てに例外情報を通知することができます。
# これをbegin do_something rescueSomethingIsBroken => error Rails.logger.error("#{error.class}: #{error_message}\n#{(error.backtrace || caller)&.join("\n")}") Rollbar.log(:error, error) end# こう書き換えられてbegin do_something rescueSomethingIsBroken => error Rails.error.report(error) end# こう書くこともできるRails.error.handle(SomethingIsBroken) do do_something end
この仕組みは「Rollbarから別のサービスに乗り換えるケース」でもとても役立ちます。そうしたケースでもRailsアプリケーション内の例外ハンドリング処理に手を加えずにinitializerの中身を変更するだけで対応が可能になります。
実行コンテキストの注入
どのリクエスト・ジョブで発生した例外なのかを表す「実行コンテキスト」情報がエラー通知には付与されて欲しいものです。それを便利に取り扱うための仕組みが ActiveSupport::ErrorReporter
には用意されています。
各サブスクライバーに定義するreportメソッドの引数には context
というものがあります。
defreport(error, handled:, severity:, context:, source: nil)
本連載記事を購読してくださっている方はもうお気づきかもしれません。
はい、この context
の実体は ActiveSupport::ExecutionContext
です。
https://github.com/rails/rails/blob/v8.0.2/activesupport/lib/active_support/error_reporter.rb#L224
ActiveSupport::ExecutionContext
の詳細については以下の記事をご参照ください。
ClinPeer Railsプロジェクトのオブザーバビリティ強化施策#実行コンテキスト
かなり掻い摘んで説明すると、 context
にはリクエストやジョブが実行されているActionControllerやActiveJobのインスタンスが格納されています。
ClinPeerではこのようなSubscriberを定義することで、Rollbarへの全てのエラー通知に自動的に実行コンテキスト情報が付与されるようにしています。
classRollbarErrorSubscriberdefreport(error, handled:, severity:, context:, source: nil) # rubocop:disable Lint/UnusedMethodArgument extra = context.is_a?(Hash) ? context.deep_dup : {} controller = extra[:controller] extract_context!(extra) extra[:custom_data_method_context] = source scope = { request: controller&.rollbar_request_data, person: controller&.rollbar_person_data } Rollbar.scoped(scope) { Rollbar.log(severity, error, extra) } endprivatedefextract_context!(context) # 現在実行されているコントローラまたはジョブの情報が設定されている# https://github.com/rails/rails/blob/v8.0.2/actionpack/lib/action_controller/metal/instrumentation.rb#L60# https://github.com/rails/rails/blob/v8.0.2/activejob/lib/active_job/execution.rb#L66 controler_or_job = context.delete(:controller) || context.delete(:job) returnunless controler_or_job.present? && controler_or_job.respond_to?(:_execution_context) context.reverse_merge!(controler_or_job._execution_context) endend
なぜ ActiveSupport::ErrorReporter
を使うのか(本当のメリット)
「PubSubな仕組みの便利さ」「実行コンテキスト注入の仕組み」について説明しましたが、実はこの程度であれば十分に自前で実装することが可能です。
その2点よりも遥かに大きな強みとして私が考えるのは「Railsが提供しているI/F」という点です。
この Rails.error.report
というI/FがRails公式で提供されているため、Rails内やフレームワーク的なGemでの例外処理にデフォルトで組み込まれやすくなります。
実際にRails内でも何箇所か ActiveSupport::ErrorReporter
が利用されています。
Rails内での利用例
v8.0.2時点
- ActionDispatch内での例外発生時
- ActiveRecord DBレベルの警告発生時(db_warnings_action=reportオプション指定時のみ)
- ExecutionWrapper内での例外発生時(SolidQueueなどで利用されているユーザー定義プログラムの実行環境)
- Cache操作での例外発生時
- Rails内での非推奨警告発生時(reportオプション指定時のみ)
これを執筆している今現在も ActiveSupport::ErrorReporter
の活用が進んでいます。
以下は2025年4月時点でのmainに取り込まれているPRです。
Rails以外の利用例
などのRails以外のGemでも ActiveSupport::ErrorReporter
が利用が進んでいます。
また、ClinPeerでは自前でRollbarのSubscriberを定義していますが、 Rollbarや Sentryが公式でSubscriberを定義していたりします。 実行コンテキスト周りにこだわる必要がなければそれらのGemを導入するだけでほどほどに例外通知される状態になります。
各フレームワーク層で ActiveSupport::ErrorReporter
を活用した例外通知を実装してくれることで、アプリケーション内での例外補足を一定サボれるだけでなく、これまで考慮外にあった例外なんかを漏れなく補足することもできるようになり嬉しいですね。
おわり
偉そうに ActiveSupport::ErrorReporter
について語りましたが、実はClinPeerの開発を始めるまで存在も知りませんでした(Railsの更新はマメにウォッチしているつもりなのですが)。まだまだRailsには伸び代があり、痒い所に手が届く感じが気持ちが良いですね。
既存のRailsシステムの例外ハンドリング処理に手を加えるのは大変ですが、こういった所でも小まめにRails Wayに乗っておくと将来の技術的負債の解消に繋がるので導入を検討してはいかがでしょうか。
是非読者になってください!
メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!
■募集ポジションはこちら medpeer.co.jp
■エンジニア紹介ページはこちら engineer.medpeer.co.jp
■メドピア公式YouTube www.youtube.com
■メドピア公式note
style.medpeer.co.jp