Rails Advent Calendar 12 日目です。
特になにもしていないと、運用中の Rails アプリケーションで例外が起こっても、気づくのは困難です。
New Relic とか使うとそのあたりは解決しそうな気もしますが (あんまり使ってないので知らない)、簡単に導入できるものとして exception_notification を紹介します。
exception_notification
exception_notification は Rack ミドルウェアで、捕捉されない例外が起こったときに、あらかじめ設定したメールアドレスにメールを送信します。
メール送信後は同じ例外をもう一度 raise するので、そのあとの処理に影響を及ぼすことはありません。
導入
歴史的経緯により notifi__cation__ の箇所と notifi__er__ の箇所があるので注意。
gem 入れて...
# Gemfile
gem "exception_notification"
config/initializers 下に設定ファイルをつくります。
# 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{送信先メールアドレス}
}
)