Rails で捕捉されない例外が発生したらメールを送る


Rails Advent Calendar 12 日目です。

特になにもしていないと、運用中の Rails アプリケーションで例外が起こっても、気づくのは困難です。

New Relic とか使うとそのあたりは解決しそうな気もしますが (あんまり使ってないので知らない)、簡単に導入できるものとして exception_notification を紹介します。

# exception_notification

https://github.com/smartinez87/exception_notification

exception_notification は Rack ミドルウェアで、捕捉されない例外が起こったときに、あらかじめ設定したメールアドレスにメールを送信します。

メール送信後は同じ例外をもう一度 raise するので、そのあとの処理に影響を及ぼすことはありません。

# 導入

歴史的経緯により notifi__cation__ の箇所と notifi__er__ の箇所があるので注意。

gem 入れて...

```ruby
# Gemfile
gem "exception_notification"
```

config/initializers 下に設定ファイルをつくります。

```ruby
# config/initializers/exception_notification.rb
Rails.application.config.middleware.use(
  ExceptionNotifier,
  :email_prefix => "[アプリケーション名] ",
  :sender_address => %{"送信者名" <送信元メールアドレス>},
  :exception_recipients => %w{送信先メールアドレス}
)
```

メールサーバの設定は ActionMailer のものをそのまま使うので、ActionMailer が設定されていれば、これで完了です。

ほかにもいろいろ設定できるようですが、そのへんは README.md 参照してください。

# サンプル

実際に例外が起きると下記のようなメールが送られてきます。

例外の種類とメッセージ、リクエスト、セッション、Rack の env、バックトレースが含まれています。

```
Date: Wed, 12 Sep 2012 20:24:33 +0900
From: Exception Notifier <exception@example.com>
To: admin@example.com
Message-ID: <505070f19a852_55923fc4508c93b8729f@Foobar.local.mail>
Subject: [Todo app] home#show (RuntimeError) "Unexpected Error"
Mime-Version: 1.0
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

A RuntimeError occurred in home#show:

  Unexpected Error
  app/controllers/home_controller.rb:3:in `show'

-------------------------------
Request:
-------------------------------

  * URL       : http://localhost:3000/
  * IP address: 127.0.0.1
  * Parameters: {"controller"=>"home", "action"=>"show"}
  * Rails root: /Users/foo/todo

-------------------------------
Session:
-------------------------------

  * session id: "f59bf3b7aa7c9ace32c939766e32d656"
  * data: {"session_id"=>"f59bf3b7aa7c9ace32c939766e32d656",
   "_csrf_token"=>"no97EC4mADf02hVV3OfR+XqBl73Fk4Tp8jBegWnlwWY="}

-------------------------------
Environment:
-------------------------------

  * GATEWAY_INTERFACE                              : CGI/1.1
  * HTTP_ACCEPT                                    : text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  * HTTP_ACCEPT_CHARSET                            : Shift_JIS,utf-8;q=0.7,*;q=0.3
  * HTTP_ACCEPT_ENCODING                           : gzip,deflate,sdch
  * HTTP_ACCEPT_LANGUAGE                           : ja,en-US;q=0.8,en;q=0.6
  * HTTP_CACHE_CONTROL                             : max-age=0
  * HTTP_CONNECTION                                : keep-alive
  * HTTP_COOKIE                                    : _todo_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg%3D%3D--c34c5fc6de928cde391cccd2b710547c7aab1d06
  * HTTP_HOST                                      : localhost:3000
  * HTTP_IF_NONE_MATCH                             : "132bbc1cdcfdacc705ba8346e527d0bb"
  * HTTP_USER_AGENT                                : Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_1) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1
  * HTTP_VERSION                                   : HTTP/1.1
  * ORIGINAL_FULLPATH                              : /
  * PATH_INFO                                      : /
  * QUERY_STRING                                   : 
  * REMOTE_ADDR                                    : 127.0.0.1
  * REMOTE_HOST                                    : localhost
  * REQUEST_METHOD                                 : GET
  * REQUEST_PATH                                   : /
  * REQUEST_URI                                    : http://localhost:3000/
  * SCRIPT_NAME                                    : 
  * SERVER_NAME                                    : localhost
  * SERVER_PORT                                    : 3000
  * SERVER_PROTOCOL                                : HTTP/1.1
  * SERVER_SOFTWARE                                : WEBrick/1.3.1 (Ruby/1.9.2/2012-04-20)
  * action_controller.instance                     : home#show
  * action_dispatch.backtrace_cleaner              : #<Rails::BacktraceCleaner:0x007f88a48a3d40>
  * action_dispatch.cookies                        : #<ActionDispatch::Cookies::CookieJar:0x007f88a1171430>
  * action_dispatch.logger                         : #<ActiveSupport::TaggedLogging:0x007f88a2da9508>
  * action_dispatch.parameter_filter               : [:password, /RAW_POST_DATA/]
  * action_dispatch.remote_ip                      : 127.0.0.1
  * action_dispatch.request.content_type           : 
  * action_dispatch.request.formats                : [text/html]
  * action_dispatch.request.parameters             : {"controller"=>"home", "action"=>"show"}
  * action_dispatch.request.path_parameters        : {:controller=>"home", :action=>"show"}
  * action_dispatch.request.query_parameters       : {}
  * action_dispatch.request.request_parameters     : {}
  * action_dispatch.request.unsigned_session_cookie: {"session_id"=>"f59bf3b7aa7c9ace32c939766e32d656", "_csrf_token"=>"no97EC4mADf02hVV3OfR+XqBl73Fk4Tp8jBegWnlwWY="}
  * action_dispatch.request_id                     : aae2c8b270a34ce9323289f38e1ff41e
  * action_dispatch.routes                         : #<ActionDispatch::Routing::RouteSet:0x007f88a15a50d8>
  * action_dispatch.secret_token                   : 609b10fdec535f80b97fed445f930500b43d5e5a6b5212f731e09e4b5c6fbfe99c7350f64875e004706e318cc5fe032258af1596d33f303b7b3cb9ea0958c127
  * action_dispatch.show_detailed_exceptions       : true
  * action_dispatch.show_exceptions                : true
  * exception_notifier.options                     : {:sender_address=>"\"Exception Notifier\" <exception@example.com>", :exception_recipients=>["admin@example.com"], :email_prefix=>"[Todo app] ", :sections=>["request", "session", "environment", "backtrace"], :ignore_exceptions=>[AbstractController::ActionNotFound, ActionController::RoutingError]}
  * rack.errors                                    : #<IO:0x007f88a08718a8>
  * rack.input                                     : #<StringIO:0x007f88a1183798>
  * rack.multiprocess                              : false
  * rack.multithread                               : false
  * rack.request.cookie_hash                       : {"_todo_session"=>"BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg==--c34c5fc6de928cde391cccd2b710547c7aab1d06"}
  * rack.request.cookie_string                     : _todo_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg%3D%3D--c34c5fc6de928cde391cccd2b710547c7aab1d06
  * rack.request.query_hash                        : {}
  * rack.request.query_string                      : 
  * rack.run_once                                  : false
  * rack.session                                   : {"session_id"=>"f59bf3b7aa7c9ace32c939766e32d656", "_csrf_token"=>"no97EC4mADf02hVV3OfR+XqBl73Fk4Tp8jBegWnlwWY="}
  * rack.session.options                           : {:path=>"/", :domain=>nil, :expire_after=>nil, :secure=>false, :httponly=>true, :defer=>false, :renew=>false, :coder=>#<Rack::Session::Cookie::Base64::Marshal:0x007f88a3c54df0>, :id=>"f59bf3b7aa7c9ace32c939766e32d656"}
  * rack.url_scheme                                : http
  * rack.version                                   : [1, 1]
  
  * Process: 21906
  * Server : Foobar

-------------------------------
Backtrace:
-------------------------------

  app/controllers/home_controller.rb:3:in `show'
  actionpack (3.2.8) lib/action_controller/metal/implicit_render.rb:4:in `send_action'
  actionpack (3.2.8) lib/abstract_controller/base.rb:167:in `process_action'
  actionpack (3.2.8) lib/action_controller/metal/rendering.rb:10:in `process_action'
  actionpack (3.2.8) lib/abstract_controller/callbacks.rb:18:in `block in process_action'
  activesupport (3.2.8) lib/active_support/callbacks.rb:414:in `_run__1225659172420690068__process_action__357961545621717090__callbacks'
  activesupport (3.2.8) lib/active_support/callbacks.rb:405:in `__run_callback'
  activesupport (3.2.8) lib/active_support/callbacks.rb:385:in `_run_process_action_callbacks'
  activesupport (3.2.8) lib/active_support/callbacks.rb:81:in `run_callbacks'
  actionpack (3.2.8) lib/abstract_controller/callbacks.rb:17:in `process_action'
  actionpack (3.2.8) lib/action_controller/metal/rescue.rb:29:in `process_action'
  actionpack (3.2.8) lib/action_controller/metal/instrumentation.rb:30:in `block in process_action'
  activesupport (3.2.8) lib/active_support/notifications.rb:123:in `block in instrument'
  activesupport (3.2.8) lib/active_support/notifications/instrumenter.rb:20:in `instrument'
  activesupport (3.2.8) lib/active_support/notifications.rb:123:in `instrument'
  actionpack (3.2.8) lib/action_controller/metal/instrumentation.rb:29:in `process_action'
  actionpack (3.2.8) lib/action_controller/metal/params_wrapper.rb:207:in `process_action'
  activerecord (3.2.8) lib/active_record/railties/controller_runtime.rb:18:in `process_action'
  actionpack (3.2.8) lib/abstract_controller/base.rb:121:in `process'
  actionpack (3.2.8) lib/abstract_controller/rendering.rb:45:in `process'
  actionpack (3.2.8) lib/action_controller/metal.rb:203:in `dispatch'
  actionpack (3.2.8) lib/action_controller/metal/rack_delegation.rb:14:in `dispatch'
  actionpack (3.2.8) lib/action_controller/metal.rb:246:in `block in action'
  actionpack (3.2.8) lib/action_dispatch/routing/route_set.rb:73:in `call'
  actionpack (3.2.8) lib/action_dispatch/routing/route_set.rb:73:in `dispatch'
  actionpack (3.2.8) lib/action_dispatch/routing/route_set.rb:36:in `call'
  journey (1.0.4) lib/journey/router.rb:68:in `block in call'
  journey (1.0.4) lib/journey/router.rb:56:in `each'
  journey (1.0.4) lib/journey/router.rb:56:in `call'
  actionpack (3.2.8) lib/action_dispatch/routing/route_set.rb:600:in `call'
  exception_notification (2.5.2) lib/exception_notifier.rb:25:in `call'
  actionpack (3.2.8) lib/action_dispatch/middleware/best_standards_support.rb:17:in `call'
  rack (1.4.1) lib/rack/etag.rb:23:in `call'
  rack (1.4.1) lib/rack/conditionalget.rb:25:in `call'
  actionpack (3.2.8) lib/action_dispatch/middleware/head.rb:14:in `call'
  actionpack (3.2.8) lib/action_dispatch/middleware/params_parser.rb:21:in `call'
  actionpack (3.2.8) lib/action_dispatch/middleware/flash.rb:242:in `call'
  rack (1.4.1) lib/rack/session/abstract/id.rb:205:in `context'
  rack (1.4.1) lib/rack/session/abstract/id.rb:200:in `call'
  actionpack (3.2.8) lib/action_dispatch/middleware/cookies.rb:339:in `call'
  activerecord (3.2.8) lib/active_record/query_cache.rb:64:in `call'
  activerecord (3.2.8) lib/active_record/connection_adapters/abstract/connection_pool.rb:473:in `call'
  actionpack (3.2.8) lib/action_dispatch/middleware/callbacks.rb:28:in `block in call'
  activesupport (3.2.8) lib/active_support/callbacks.rb:405:in `_run__3608295848955106148__call__1508032645417640755__callbacks'
  activesupport (3.2.8) lib/active_support/callbacks.rb:405:in `__run_callback'
  activesupport (3.2.8) lib/active_support/callbacks.rb:385:in `_run_call_callbacks'
  activesupport (3.2.8) lib/active_support/callbacks.rb:81:in `run_callbacks'
  actionpack (3.2.8) lib/action_dispatch/middleware/callbacks.rb:27:in `call'
  actionpack (3.2.8) lib/action_dispatch/middleware/reloader.rb:65:in `call'
  actionpack (3.2.8) lib/action_dispatch/middleware/remote_ip.rb:31:in `call'
  actionpack (3.2.8) lib/action_dispatch/middleware/debug_exceptions.rb:16:in `call'
  actionpack (3.2.8) lib/action_dispatch/middleware/show_exceptions.rb:56:in `call'
  railties (3.2.8) lib/rails/rack/logger.rb:26:in `call_app'
  railties (3.2.8) lib/rails/rack/logger.rb:16:in `call'
  actionpack (3.2.8) lib/action_dispatch/middleware/request_id.rb:22:in `call'
  rack (1.4.1) lib/rack/methodoverride.rb:21:in `call'
  rack (1.4.1) lib/rack/runtime.rb:17:in `call'
  activesupport (3.2.8) lib/active_support/cache/strategy/local_cache.rb:72:in `call'
  rack (1.4.1) lib/rack/lock.rb:15:in `call'
  actionpack (3.2.8) lib/action_dispatch/middleware/static.rb:62:in `call'
  railties (3.2.8) lib/rails/engine.rb:479:in `call'
  railties (3.2.8) lib/rails/rack/log_tailer.rb:17:in `call'
  rack (1.4.1) lib/rack/handler/webrick.rb:59:in `service'
  /Users/foo/.rbenv/versions/1.9.2-p320/lib/ruby/1.9.1/webrick/httpserver.rb
:111:in `service'
  /Users/foo/.rbenv/versions/1.9.2-p320/lib/ruby/1.9.1/webrick/httpserver.rb:70:in `run'
  /Users/foo/.rbenv/versions/1.9.2-p320/lib/ruby/1.9.1/webrick/server.rb:183:in `block in start_thread'
```

# 既知の問題

exception_notification がメールを送るときにさらに例外が起こると、元の例外は raise されず、それがどんなものだったかわからなくなってしまいます (実は、この問題を修正するパッチを書いてたんですが、まだ pull request 送れてません...)。

運用時は、実際に例外を起こしてみて、ちゃんとメールが送られてくるか、必ず確認しておきましょう。

# 追記: Rails 4 対応

Rails 4 では config/initializers 下の設定ファイルの書き方が変わっています。

参考: https://github.com/smartinez87/exception_notification

```
# config/initializers/exception_notification.rb
Rails.application.config.middleware.use(
  ExceptionNotification::Rack,
  :email => {
    :email_prefix => "[アプリケーション名] ",
    :sender_address => %{"送信者名" <送信元メールアドレス>},
    :exception_recipients => %w{送信先メールアドレス}
  }
)
```