Quantcast
Channel: メドピア開発者ブログ
Viewing all articles
Browse latest Browse all 214

Railsの「ActiveSupport::ErrorReporter」って知ってる?

$
0
0

こんにちは。サーバーサイドエンジニアの三村(@t_mimura39)です。

またまた「ClinPeerアプリ開発の裏側連載記事」です。 tech.medpeer.co.jp

今回はClinPeerで活用しているRailsの ActiveSupport::ErrorReporterについてご紹介します。

目次

ActiveSupport::ErrorReporterとは

Railsに標準添付されているエラー管理の仕組みです。

↓これが

begin
  do_something
rescueSomethingIsBroken => error
  MyErrorReportingService.notify(error)
end

↓こうなります。

Rails.error.handle(SomethingIsBroken) do
  do_something
end

詳細はRails公式ドキュメントをご参照ください。

guides.rubyonrails.org

以上になります。ありがとうございました。

と、終わるわけにもいかないのでもう少し深い話を書きます。

なぜ 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.reportRails.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時点

これを執筆している今現在も ActiveSupport::ErrorReporterの活用が進んでいます。
以下は2025年4月時点でのmainに取り込まれているPRです。

Rails以外の利用例

などのRails以外のGemでも ActiveSupport::ErrorReporterが利用が進んでいます。

また、ClinPeerでは自前でRollbarのSubscriberを定義していますが、 RollbarSentryが公式で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


Viewing all articles
Browse latest Browse all 214

Trending Articles