目次
Rails で、エラーが出たときにメールが送信されるようにしました。
環境
- Ubuntu 15.05 (64bit)
- Ruby 2.2.2
- Rails 4.1.8
昔のやり方
Rails 3 のときは、 ApplicationController
で rescue_in_public
にメール送信のコードを見たことがあります。
1 2 3 4 5 6 7 8 9 10 11 12 |
class ApplicationController < ActionController::Base protected def rescue_action_in_public(exception) SystemMailer.system_error( current_user, self, request, exception).deliver super(exception) end def local_request? false end end |
current_user
は ログインしているユーザです。 devise
を利用しています。
しかし同じことをやっても意図した通りにメールは送信されませんでした。
解決法
Rails 3 のときにもあった rescue_from
を使いました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class ApplicationController < ActionController::Base rescue_from Exception, :handle_exception private # handle exception # ==== Parameter # * +exception+ def handle_exception(exception) send_error_mail(exception) raise exception end # send error mail # ==== Parameter # * +exception+ def send_error_mail(exception) if !Rails.env.development? SystemMailer.error_mail( exception, current_user, self, request) end end end |
rescue_from Exception
とすることで、コントローラ内の処理で発生したすべての例外を拾います。 これが rescue_from StandardError
だと 単純に raise "exception!"
のように例外を起こしたときの RuntimeError
が拾えません。 また、0 = 1
の時に起きるような syntax error は拾いません。 拾うのはプログラムとして動いた上でのエラーです。 そして、コントローラに入る前の処理は拾いません。
重要なのは handle_exception
の中の raise exception
です。 handle_exception
ではエラーをもみ消したいわけではなく、メールを送りたいだけです。 AOP の考え方です。 今回は簡易的に application_controller.rb
にメソッドを追加しています。
send_error_mail
の中で使われている current_user
は gem devise で用意されているもので、ログインしているユーザを返します。 適宜変更してください。 self
はどのコントローラで例外が発生したのかをメールに載せるために渡しています。 request
はリクエストのURLなどを表示するのに使います。 また !Rails.env.development?
で評価を行っているところがありますが、 config
に書くようにできれば一番いいです。 Rails 4.2 になったらカスタムコンフィギュレーションが使えるそうなので、アップグレード後の課題としました。
注意点
コントローラに入った後の例外しか処理しないので、 コントローラに移る前の例外は別の方法で補足する必要があります。
それならばコントローラの中ではなく外側で例外処理を書けばいいという考え方もあるのですが、コントローラの中でしか参照できない値や、コントローラの中で使いやすくなっているオブジェクトなどがあるので、 applicatoin_controller.rb
に処理を書きました。
送信メールの作成
送信メールは次のようにして作ります。
メーラクラス
まずは、 rails g mailer SystemMailer system_error
を実行して SystemMailer
クラス を作ります。 作成された メールのクラスは次のように変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class SystemMailer < ActionMailer::Base default from: 'sender@mail.com', to: 'recipient@mail.com' # ==== Parameter # * +user+ # * +controller+ # * +request+ # * +exception+ def system_error(user, controller, request, exception) @user = user @controller = controller @request = request @exception = exception subject = "[ApplicationName #{Rails.env}][error] System Error has occurred!!" mail(subject: subject) end end |
単純にインスタンス変数に渡しているだけです。 インスタンス変数の情報をメール本文に盛り込みます。
デフォルトの送信先は recipient@email.com
にしています。 これはメールごとに変更することができます。 実際の送信先は、 開発チームが Google のメールサービスを使っていれば Google グループ でメーリングリストを作成してそちらに送信するのもいいですし、 さくらレンタルサーバを使って入ればそちらでメーリングリストを作成して送るのがいいと思います。 送信先をデータベースで管理して、必要に応じて変えるというやり方もあります。
メール本文
上で実行したコマンドによって、 app/views/system_mailer/system_error.text.erb
ができているのでそれを編集します。 html 形式 がいいなら .html.erb
となっているものを使いましょう。 私は html 形式 のビューは消しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
============================================================ base info ============================================================ [user id] <%=@user.nil? ? '(without access token or logging in)' : @user.id %> [time] <%=Time.now%> [host] <%=`hostname`.strip%> [source] <%=@controller.controller_name%>#<%=@controller.action_name%> ============================================================ request info ============================================================ [URL] <%=@request.original_url%> [method] <%=@request.request_method%> [header] <%@request.headers.each{|k,v|%> <%=k%> : <%=v.to_s.force_encoding('utf-8')%> <%}%> [parameter] <%=@request.parameters.inspect%> [referrer] <%=@request.referrer.blank? ? '(no referrer)' : @request.referrer%> ============================================================ exception info ============================================================ [class name] <%=@exception.class%> [message] <%=@exception.message%> [backtrace] <%@exception.backtrace.each{|trace|%> <%=trace%> <%}%> |
header の値を出力しているところで force_encoding
を使っています。 force_encoding
を行わないと、 リクエストパラメータに日本語が含まれている場合に 次のようなエラーが出ます。
ActionView::Template::Error (incompatible character encodings: ASCII-8BIT and UTF-8)
ビューでの処理は避けるべきだという考え方からすると、 Time.now
のところと
のところはまだまだ改善が必要です。hostname
.strip
またこのメールでは、ユーザが入力したパスワードなども見えてしまうので、(メールの送信経路が危険な場合は)部分的にヘッダデータを隠すか SSL を用いてメールを送信するか、 メールに詳細を記述せずにメール本文を連絡のみに留めるなどして対策を行ってください。
私の場合は、メール通知に加えてロギングを行うため、 本記事のメソッドをもう少し変えて実装しました。