Ruby / Rails のエラー処理をそのままの状態でリリースすると色々と問題があるので、
それに対してどういうことをやったりしているかのまとめ。
Ruby: 2.3.1p112
Rails: 4.2.7
特定の場所のエラーを拾う
何らかのエラーが発生した場合、
そのまま Rails の英語のエラー画面を出してしまうと、
ユーザーに無駄に悪い印象を与えてしまったりするので、
表側に出す用のエラー画面を用意して遷移させておく。
begin
# 普通の処理
rescue => e
# エラー時の処理
end
のようにすると、普通の処理部分でエラーが発生したら、エラー時の例外処理を実行できる。
エラー時の処理では、ログ出力したり、表側用のエラー画面を出したりする。
logger.error e
logger.error e.backtrace.join("\n")
flash[:alert] += '更新に失敗しました'
render :edit
ログ出力が不十分だと、どんなエラーが起こったかを捕捉できなくなり、
障害発生時に苦労したり詰むハメになるので、
ログに十分な情報を出すようにしなければならない。
メソッド中身全部から拾いたい場合は
def method
begin
# 普通の処理
rescue => e
# エラー時の処理
end
end
↓
def method
# 普通の処理
rescue => e
# エラー時の処理
end
のように省略できる。
rescue => e
というのは
rescue StandardError => e
の略みたいなものらしく、
通常のプログラムで起こる一般的なエラー群のStandardErrorを拾うようになっているので、
通常はこのままでいい。
あえて拾いたいエラーが他にある場合は
begin
# 普通の処理
rescue ActiveRecord::RecordInvalid => e # 他のエラーを指定
# ActiveRecord::RecordInvalid が発火した時の処理
rescue => e
# 上で拾われなかった StandardError が発火したの時の処理
end
のように、StandardErrorの前に指定して拾う。
なお、このActiveRecord::RecordInvalidは.save!のバリデーションエラーで発火するので、
他のシステムエラーと区別したい時にこういう風に書いたりする。
アプリケーション全体でエラーを拾う
application_controller.rb で
rescue_from ActiveRecord::RecordNotFound, with: :render_404
def render_404(e = nil)
if e
logger.error e
logger.error e.backtrace.join("\n")
end
render template: 'errors/error404', status: 404, layout: 'application', content_type: 'text/html'
end
のように書くと、
ActionController::RecordNotFoundがどこで起こってもrender_404メソッドが実行され、
404用のエラー画面を出したり、エラーをログに出力したりするようになる。
要は、エラーを拾いたい場所に、いちいちエラー処理を書かなくても拾えるようになる。
この全体で拾うエラー処理は、場所ごとで拾うエラー処理より優先度が低いので、
場所ごとに書いたエラー処理を上書きしてしまう心配はない(はず)。
エラーログを整理
上記のに加えて、application_controller.rb に
def log_app_error(e, log_level = :error)
logger.send(log_level, <<LOG)
# {e.class} (#{e.message}):
#{Rails.backtrace_cleaner.clean(e.backtrace).join("\n").indent(1)}
LOG
end
のようなエラーログ出力メソッドを書き、
ログを出したいシーンで
log_app_error e
を書くと、
NoMethodError (undefined method `nai_method' for nil:NilClass):
app/controllers/companies_controller.rb:6:in `index`
のような、無駄を省いた感じの backtrace を出せるようになる。
development 環境では表側用のエラー画面に移動させない
表側用のエラー画面は、本番環境では出した方がいいが、
ローカルやステージング環境では出す意義が薄く、
普通に Rails のエラー画面を出した方がエラーに気づきやすいので、
development 環境では出さないようにしておくといい。
rescue_from ActiveRecord::RecordNotFound, with: :render_404 unless Rails.env.development?
のようにunless Rails.env.development?のような条件を付けると、
ローカルでは出ないようになる。
複数の rescue_from を書く
rescue_from StandardError, with: :render_500 unless Rails.env.development?
rescue_from ActiveRecord::RecordNotFound, with: :render_404 unless Rails.env.development?
のように並べて書くと、
ActiveRecord::RecordNotFoundが起こった場合は 404 画面、
StandardErrorが起こった場合は 500 画面を出すといった出し分けができる。
rescue_fromは、どういうわけか下のものほど優先度が高いらしいので、
上記の例だと、仮に両方起こった場合、
下にあるrender_404のエラー処理が実行されることになる。
rescue の対象には広すぎるクラスを指定しないようにする
Ruby の例外クラスの構造はこんな感じになっている。
( https://docs.ruby-lang.org/ja/latest/library/_builtin.html の「例外クラス」より )
- Exception
- NoMemoryError
- ScriptError
- LoadError
- NotImplementedError
- SyntaxError
- SecurityError
- SignalException
- Interrupt
- StandardError
- ArgumentError
- (大量)
- ZeroDivisionError
- SystemExit
- SystemStackError
- fatal
※ Rails も含めるともっと増え、ActiveRecord関連等の大量のクラスがStandardErrorに入る。
例えばStandardErrorを指定すると、その子のZeroDivisionError等も拾うようになる。
そのため、Exceptionのような、あまり広い範囲のエラーを拾うようにしてしまうと、
Interrupt(Ctrl+C 等の割り込み)のように、拾うとまずい系も拾ってしまうので、
拾っても大丈夫なのだけを拾うようにしなければならない。
どの例外クラスを拾うべきか
考えてみた結果がこちら。
(もちろんただの一例で、こうするべきという話ではないです)
拾いたい
StandardError
通常のプログラムで発生する可能性の高い例外クラスを集めたものとのことなので、拾うべき。
gem が追加した例外クラスも、ほとんどがここに入る模様。
拾いたくない
Interrupt, SystemExit
割り込みを邪魔されないようにしたいため。
拾えない
fatal
そもそも通常の方法ではアクセス自体できないとのこと。
自分には判断できない
NoMemoryError, ScriptError, SecurityError, SystemStackError
これらは拾っても問題なさそうな気がしていたが、
調べてみると、拾わない方がいい説も割とあるので、
自分的には拾うべきかが分からなくなってきたというもの。
今回の例では入れないでおく。
(Exceptionは、バージョンアップ等で余計なものが入りうるので指定していない)
rescue 節内から raise で脱出
http://qiita.com/ktarow/items/9d8f3217bb148f2e51d2#raise%E3%81%AE%E8%BF%BD%E5%8A%A0
によると、
raiseが発生した時点で実行中のrescue節が終了し、呼び出し元に戻っています。
とのことで、
rescue節内でraiseを引数なしで実行すると、
rescueで拾っているエラーを再発生させ、呼び出し元に戻るらしい。
これを利用すると、
rescueで特定のエラーを拾い、それ用の処理(ログ出力等)を追加して、
本来のエラー処理へ戻すことができる。
Interruptとかを止めたくないけど、それらを捕捉しなくても本当に大丈夫なのか心配だから、
一応念のためログ出力だけしておいたという処理がこんな感じ。
rescue_from NoMemoryError, ScriptError, Interrupt, SecurityError, SignalException, SystemExit, SystemStackError, with: :only_logging unless Rails.env.development?
def only_logging(e = nil)
log_app_error e if e
raise
end
まとめ
こうして application_controller.rb にできたエラー処理がこんな感じ。
rescue_from StandardError, with: :render_500 unless Rails.env.development?
rescue_from NoMemoryError, ScriptError, Interrupt, SecurityError, SignalException, SystemExit, SystemStackError, with: :only_logging unless Rails.env.development?
rescue_from ActiveRecord::RecordNotFound, ActionController::RoutingError,with: :render_404 unless Rails.env.development?
def render_404(e = nil)
log_app_error e if e
render template: 'errors/error404', status: 404, layout: 'application', content_type: 'text/html'
end
def render_500(e = nil)
log_app_error e if e
render template: 'errors/error500', status: 500, layout: 'application', content_type: 'text/html'
end
def only_logging(e = nil)
log_app_error e if e
raise
end
その他
ログに出るだけでは、通知の意味では不十分なので、
サーバー側で起こった深刻な種類のエラーを確実に通知したければ、
特定の種類のエラーのレポートを、システム管理者のメアドに送るようにもできる。
(筆者はその方法をちゃんと調べていないので、ここには書かない)
あまりどうでもいいエラーを送るようにしてしまっていると、
見る側の時間を浪費したり、いずれ重要なのごとスルーされるようになってしまうので、
どうでもいいエラーは送らないようにすることも重要と思われる。