Rails アプリケーションのエラー通知

このガイドは、Ruby on Railsアプリケーションで発生するエラーの管理方法を解説します。

本ガイドの内容:

  • RailsのErrorReporterでエラーをキャプチャして通知する方法
  • エラー通知サービス用のカスタムサブスクライバの作成方法

1 エラー通知

RailsのErrorReporterは、アプリケーションで発生したエラーを収集して、好みのサービスや場所に通知する標準的な方法を提供します(例: Sentryなどの監視サービスに通知する)。

この機能の目的は、以下のような定型的なエラー処理コードを置き換えることです。

begin
  do_something
rescue SomethingIsBroken => error
  MyErrorReportingService.notify(error)
end

上の定形コードを、以下のようなインターフェイスで統一できます。

Rails.error.handle(SomethingIsBroken) do
  do_something
end

Railsはすべての実行(HTTPリクエスト、ジョブrails runnerの起動など)をErrorReporterにラップするので、アプリで発生した未処理のエラーは、そのサブスクライバを介してエラーレポートサービスに自動的に通知されます。

これにより、サードパーティのエラー通知ライブラリは、Rackミドルウェアを挿入したり、未処理の例外をキャプチャするモンキーパッチを適用したりする必要がなくなります。また、Active Supportを使うライブラリがこの機能を利用して、従来ログに出力されなかった警告を、コードに手を加えずに通知できるようになります。

このエラーレポーターの利用は必須ではありません。エラーをキャプチャする他の手法はすべて引き続き利用できます。

1.1 エラーレポーターにサブスクライブする

エラーレポーターを利用するにはサブスクライバ(subscriber)が必要です。サブスクライバは、reportメソッドを持つ任意のオブジェクトのことです。アプリケーションでエラーが発生したり、手動で通知されたりすると、Railsのエラーレポーターはエラーオブジェクトといくつかのオプションを使ってこのメソッドを呼び出します。

SentryやHoneybadgerなどのように、自動的にサブスクライバを登録してくれるエラー通知ライブラリもあります。

また、以下のようにカスタムサブスクライバを作成することも可能です。

# config/initializers/error_subscriber.rb
class ErrorSubscriber
  def report(error, handled:, severity:, context:, source: nil)
    MyErrorReportingService.report_error(error, context: context, handled: handled, level: severity)
  end
end

Subscriberクラスを定義したら、Rails.error.subscribeメソッドを呼び出して登録します。

Rails.error.subscribe(ErrorSubscriber.new)

サブスクライバはいくつでも登録できます。Railsはサブスクライバを登録順に呼び出します。

Rails.error.unsubscribeを呼び出すことで、サブスクライバを登録解除することも可能です。これは、依存関係で追加されたサブスクライバを置換・削除する場合に便利です。subscribeunsubscribeには、サブスクライバを渡すことも、サブスクライバのクラスを渡すことも可能です。

subscriber = ErrorSubscriber.new
Rails.error.unsubscribe(subscriber)
# or
Rails.error.unsubscribe(ErrorSubscriber)

Railsのエラーレポーターは、どの環境でも常に登録されたサブスクライバーを呼び出します。しかし多くのエラー通知サービスは、デフォルトではproduction環境でのみエラーを通知します。必要に応じて、複数の環境で設定を行ってテストする必要があります。

1.2 エラーレポーターを利用する

Railsのエラーレポーターには、エラー通知用の4つのメソッドがあります。

  • Rails.error.handle
  • Rails.error.record
  • Rails.error.report
  • Rails.error.unexpected
1.2.1 エラーを通知して握りつぶす

Rails.error.handle は、ブロック内で発生したエラーを通知してから、そのエラーを握りつぶします。ブロックの外の残りのコードは通常通り続行されます。

result = Rails.error.handle do
  1 + "1" # TypeErrorが発生
end
result # => nil
1 + 1 # ここは実行される

ブロック内でエラーが発生しなかった場合、Rails.error.handleはブロックの結果を返し、エラーが発生した場合はnilを返します。

以下のようにfallbackを指定することで、この振る舞いをオーバーライドできます。

user = Rails.error.handle(fallback: -> { User.anonymous }) do
  User.find(params[:id])
end
1.2.2 エラーを通知して再度raiseする

Rails.error.record はすべての登録済みレポーターにエラーを通知し、その後エラーを再度raiseします。残りのコードは実行されません。

Rails.error.record do
1 + "1" # TypeErrorが発生
end
1 + 1 # ここは実行されない

ブロック内でエラーが発生しなかった場合、Rails.error.recordはそのブロックの結果を返します。

1.2.3 エラーを手動で通知する

Rails.error.reportを呼び出して手動でエラーを通知することも可能です。

begin
  # code
rescue StandardError => e
  Rails.error.report(e)
end

渡したオプションは、すべてエラーサブスクライバに渡されます。

1.2.4 想定外のエラーを報告する

Rails.error.unexpectedを呼び出すことで、想定外のエラーを報告できます。

production環境で呼び出された場合、このメソッドはエラーが報告された後にnilを返し、コードの実行を中断せずに続行します。

development環境で呼び出された場合、エラーは新しいエラークラスにラップされ(スタックの上位層でrescueされないようにするため)、開発者にデバッグ情報が表示されます。例:

def edit
  if published?
    Rails.error.unexpected("[BUG] Attempting to edit a published article, that shouldn't be possible")
    false
  end
  # ...
end

このメソッドは、production環境で発生する可能性があるが、通常の利用結果では発生することが想定されていないエラーを適切に処理することを目的としています。

1.3 エラー通知のオプション

3つのレポートAPI(#handle#record#report)はすべて以下のオプションをサポートしています。これらのオプションは、すべての登録済みサブスクライバに渡されます。

  • handled: エラーが処理されたかどうかを示すBoolean。 デフォルトはtrueです(ただし#recordのデフォルトはfalseです)。

  • severity: エラーの重大性を表すSymbol。 期待される値は:error:warning:infoのいずれか。 #handleでは:warningに設定されます。 #recordでは:errorに設定されます。

  • context: リクエストやユーザーの詳細など、エラーに関する詳細なコンテキストを提供するHash

  • source: エラーの発生源に関するString。 デフォルトのソースは"application"です。 内部ライブラリから通知されたエラーは他のソースを設定する可能性があります(例: Redisキャッシュライブラリは"redis_cache_store.active_support"を設定する可能性があります)。 サブスクライバは、このソースを利用することで興味のないエラーを無視できます。

Rails.error.handle(context: { user_id: user.id }, severity: :info) do
  # ...
end

1.4 コンテキストをグローバルに設定する

コンテキストは、contextオプションで設定することも、以下のように#set_context APIで設定することもできます。

Rails.error.set_context(section: "checkout", user_id: @user.id)

この方法で設定されたコンテキストは、contextオプションとマージされます。

Rails.error.set_context(a: 1)
Rails.error.handle(context: { b: 2 }) { raise }
# 通知されるコンテキスト: {:a=>1, :b=>2}
Rails.error.handle(context: { b: 3 }) { raise }
# 通知されるコンテキスト: {:a=>1, :b=>3}

1.5 エラークラスでフィルタリングする

Rails.error.handleRails.error.recordでは、以下のように特定のクラスのエラーだけを通知できます。

Rails.error.handle(IOError) do
  1 + "1" # TypeErrorが発生
end
1 + 1 # TypeErrorsはIOErrorsではないので、ここは「実行されない」

上のTypeErrorはRailsのエラー通知レポーターにキャプチャされません。通知されるのは IOErrorおよびその子孫インスタンスだけです。その他のエラーは通常どおりraiseします。

1.6 通知を無効にする

Rails.error.disableを呼び出すことで、ブロック内でサブスクライバにエラーが通知されないようにできます。subscribeunsubscribeの場合と同様に、サブスクライバ自身を渡すことも、サブスクライバのクラスを渡すことも可能です。

Rails.error.disable(ErrorSubscriber) do
  1 + "1" # TypeErrorはErrorSubscriber経由で報告されなくなる
end

これは、サードパーティのエラー通知サービスでエラーを別の方法で処理したい場合や、技術スタックの上位で処理したい場合にも有用です。

1.7 ライブラリで利用する

エラー通知ライブラリは、以下のようにRailtieでライブラリのサブスクライバを登録できます。

module MySdk
  class Railtie < ::Rails::Railtie
    initializer "my_sdk.error_subscribe" do
      Rails.error.subscribe(MyErrorSubscriber.new)
    end
  end
end

エラーサブスクライバを登録すると、Rackミドルウェアのような他のエラー機構がある場合、エラーが繰り返し通知される可能性があります。他のエラー機構を削除するか、レポーターの機能を調整して、通知済みの例外を通知しないようにする必要があります。

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加