Skip to content

Commit 3b17c8d

Browse files
author
Tom Copeland
committed
Implementing ExceptionNotifier
1 parent 015261b commit 3b17c8d

16 files changed

+470
-0
lines changed

Diff for: app/controllers/application_controller.rb

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ class ApplicationController < ActionController::Base
66
before_filter :require_logged_in, :require_not_overeager
77
around_filter :record_api_request
88

9+
include ExceptionNotifiable
10+
911
def current_user
1012
@current_user ||= authenticate_or_request_with_http_basic("RubyForge API") do |username, password|
1113
User.authenticate(username, password)

Diff for: config/environment.rb

+3
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@
4242
require 'digest/md5'
4343

4444
require 'pp'
45+
46+
ExceptionNotifier.exception_recipients = %w([email protected])
47+
ExceptionNotifier.sender_address = %("RubyForge API Error" <[email protected]>)

Diff for: vendor/plugins/exception_notification/README

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
= Exception Notifier Plugin for Rails
2+
3+
The Exception Notifier plugin provides a mailer object and a default set of
4+
templates for sending email notifications when errors occur in a Rails
5+
application. The plugin is configurable, allowing programmers to specify:
6+
7+
* the sender address of the email
8+
* the recipient addresses
9+
* the text used to prefix the subject line
10+
11+
The email includes information about the current request, session, and
12+
environment, and also gives a backtrace of the exception.
13+
14+
== Usage
15+
16+
First, include the ExceptionNotifiable mixin in whichever controller you want
17+
to generate error emails (typically ApplicationController):
18+
19+
class ApplicationController < ActionController::Base
20+
include ExceptionNotifiable
21+
...
22+
end
23+
24+
Then, specify the email recipients in your environment:
25+
26+
ExceptionNotifier.exception_recipients = %w([email protected] [email protected])
27+
28+
And that's it! The defaults take care of the rest.
29+
30+
== Configuration
31+
32+
You can tweak other values to your liking, as well. In your environment file,
33+
just set any or all of the following values:
34+
35+
# defaults to [email protected]
36+
ExceptionNotifier.sender_address =
37+
%("Application Error" <[email protected]>)
38+
39+
# defaults to "[ERROR] "
40+
ExceptionNotifier.email_prefix = "[APP] "
41+
42+
Email notifications will only occur when the IP address is determined not to
43+
be local. You can specify certain addresses to always be local so that you'll
44+
get a detailed error instead of the generic error page. You do this in your
45+
controller (or even per-controller):
46+
47+
consider_local "64.72.18.143", "14.17.21.25"
48+
49+
You can specify subnet masks as well, so that all matching addresses are
50+
considered local:
51+
52+
consider_local "64.72.18.143/24"
53+
54+
The address "127.0.0.1" is always considered local. If you want to completely
55+
reset the list of all addresses (for instance, if you wanted "127.0.0.1" to
56+
NOT be considered local), you can simply do, somewhere in your controller:
57+
58+
local_addresses.clear
59+
60+
== Customization
61+
62+
By default, the notification email includes four parts: request, session,
63+
environment, and backtrace (in that order). You can customize how each of those
64+
sections are rendered by placing a partial named for that part in your
65+
app/views/exception_notifier directory (e.g., _session.rhtml). Each partial has
66+
access to the following variables:
67+
68+
* @controller: the controller that caused the error
69+
* @request: the current request object
70+
* @exception: the exception that was raised
71+
* @host: the name of the host that made the request
72+
* @backtrace: a sanitized version of the exception's backtrace
73+
* @rails_root: a sanitized version of RAILS_ROOT
74+
* @data: a hash of optional data values that were passed to the notifier
75+
* @sections: the array of sections to include in the email
76+
77+
You can reorder the sections, or exclude sections completely, by altering the
78+
ExceptionNotifier.sections variable. You can even add new sections that
79+
describe application-specific data--just add the section's name to the list
80+
(whereever you'd like), and define the corresponding partial. Then, if your
81+
new section requires information that isn't available by default, make sure
82+
it is made available to the email using the exception_data macro:
83+
84+
class ApplicationController < ActionController::Base
85+
...
86+
protected
87+
exception_data :additional_data
88+
89+
def additional_data
90+
{ :document => @document,
91+
:person => @person }
92+
end
93+
...
94+
end
95+
96+
In the above case, @document and @person would be made available to the email
97+
renderer, allowing your new section(s) to access and display them. See the
98+
existing sections defined by the plugin for examples of how to write your own.
99+
100+
== Advanced Customization
101+
102+
By default, the email notifier will only notify on critical errors. For
103+
ActiveRecord::RecordNotFound and ActionController::UnknownAction, it will
104+
simply render the contents of your public/404.html file. Other exceptions
105+
will render public/500.html and will send the email notification. If you want
106+
to use different rules for the notification, you will need to implement your
107+
own rescue_action_in_public method. You can look at the default implementation
108+
in ExceptionNotifiable for an example of how to go about that.
109+
110+
111+
Copyright (c) 2005 Jamis Buck, released under the MIT license

Diff for: vendor/plugins/exception_notification/init.rb

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
require "action_mailer"
2+
require "exception_notifier"
3+
require "exception_notifiable"
4+
require "exception_notifier_helper"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
require 'ipaddr'
2+
3+
# Copyright (c) 2005 Jamis Buck
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining
6+
# a copy of this software and associated documentation files (the
7+
# "Software"), to deal in the Software without restriction, including
8+
# without limitation the rights to use, copy, modify, merge, publish,
9+
# distribute, sublicense, and/or sell copies of the Software, and to
10+
# permit persons to whom the Software is furnished to do so, subject to
11+
# the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be
14+
# included in all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23+
module ExceptionNotifiable
24+
def self.included(target)
25+
target.extend(ClassMethods)
26+
end
27+
28+
module ClassMethods
29+
def consider_local(*args)
30+
local_addresses.concat(args.flatten.map { |a| IPAddr.new(a) })
31+
end
32+
33+
def local_addresses
34+
addresses = read_inheritable_attribute(:local_addresses)
35+
unless addresses
36+
addresses = [IPAddr.new("127.0.0.1")]
37+
write_inheritable_attribute(:local_addresses, addresses)
38+
end
39+
addresses
40+
end
41+
42+
def exception_data(deliverer=self)
43+
if deliverer == self
44+
read_inheritable_attribute(:exception_data)
45+
else
46+
write_inheritable_attribute(:exception_data, deliverer)
47+
end
48+
end
49+
50+
def exceptions_to_treat_as_404
51+
exceptions = [ActiveRecord::RecordNotFound,
52+
ActionController::UnknownController,
53+
ActionController::UnknownAction]
54+
exceptions << ActionController::RoutingError if ActionController.const_defined?(:RoutingError)
55+
exceptions
56+
end
57+
end
58+
59+
private
60+
61+
def local_request?
62+
remote = IPAddr.new(request.remote_ip)
63+
!self.class.local_addresses.detect { |addr| addr.include?(remote) }.nil?
64+
end
65+
66+
def render_404
67+
respond_to do |type|
68+
type.html { render :file => "#{RAILS_ROOT}/public/404.html", :status => "404 Not Found" }
69+
type.all { render :nothing => true, :status => "404 Not Found" }
70+
end
71+
end
72+
73+
def render_500
74+
respond_to do |type|
75+
type.html { render :file => "#{RAILS_ROOT}/public/500.html", :status => "500 Error" }
76+
type.all { render :nothing => true, :status => "500 Error" }
77+
end
78+
end
79+
80+
def rescue_action_in_public(exception)
81+
case exception
82+
when *self.class.exceptions_to_treat_as_404
83+
render_404
84+
85+
else
86+
render_500
87+
88+
deliverer = self.class.exception_data
89+
data = case deliverer
90+
when nil then {}
91+
when Symbol then send(deliverer)
92+
when Proc then deliverer.call(self)
93+
end
94+
95+
ExceptionNotifier.deliver_exception_notification(exception, self,
96+
request, data)
97+
end
98+
end
99+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
require 'pathname'
2+
3+
# Copyright (c) 2005 Jamis Buck
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining
6+
# a copy of this software and associated documentation files (the
7+
# "Software"), to deal in the Software without restriction, including
8+
# without limitation the rights to use, copy, modify, merge, publish,
9+
# distribute, sublicense, and/or sell copies of the Software, and to
10+
# permit persons to whom the Software is furnished to do so, subject to
11+
# the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be
14+
# included in all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23+
class ExceptionNotifier < ActionMailer::Base
24+
@@sender_address = %("Exception Notifier" <[email protected]>)
25+
cattr_accessor :sender_address
26+
27+
@@exception_recipients = []
28+
cattr_accessor :exception_recipients
29+
30+
@@email_prefix = "[ERROR] "
31+
cattr_accessor :email_prefix
32+
33+
@@sections = %w(request session environment backtrace)
34+
cattr_accessor :sections
35+
36+
self.template_root = "#{File.dirname(__FILE__)}/../views"
37+
38+
def self.reloadable?() false end
39+
40+
def exception_notification(exception, controller, request, data={})
41+
content_type "text/plain"
42+
43+
subject "#{email_prefix}#{controller.controller_name}##{controller.action_name} (#{exception.class}) #{exception.message.inspect}"
44+
45+
recipients exception_recipients
46+
from sender_address
47+
48+
body data.merge({ :controller => controller, :request => request,
49+
:exception => exception, :host => (request.env["HTTP_X_FORWARDED_HOST"] || request.env["HTTP_HOST"]),
50+
:backtrace => sanitize_backtrace(exception.backtrace),
51+
:rails_root => rails_root, :data => data,
52+
:sections => sections })
53+
end
54+
55+
private
56+
57+
def sanitize_backtrace(trace)
58+
re = Regexp.new(/^#{Regexp.escape(rails_root)}/)
59+
trace.map { |line| Pathname.new(line.gsub(re, "[RAILS_ROOT]")).cleanpath.to_s }
60+
end
61+
62+
def rails_root
63+
@rails_root ||= Pathname.new(RAILS_ROOT).cleanpath.to_s
64+
end
65+
66+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
require 'pp'
2+
3+
# Copyright (c) 2005 Jamis Buck
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining
6+
# a copy of this software and associated documentation files (the
7+
# "Software"), to deal in the Software without restriction, including
8+
# without limitation the rights to use, copy, modify, merge, publish,
9+
# distribute, sublicense, and/or sell copies of the Software, and to
10+
# permit persons to whom the Software is furnished to do so, subject to
11+
# the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be
14+
# included in all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23+
module ExceptionNotifierHelper
24+
VIEW_PATH = "views/exception_notifier"
25+
APP_PATH = "#{RAILS_ROOT}/app/#{VIEW_PATH}"
26+
PARAM_FILTER_REPLACEMENT = "[FILTERED]"
27+
28+
def render_section(section)
29+
RAILS_DEFAULT_LOGGER.info("rendering section #{section.inspect}")
30+
summary = render_overridable(section).strip
31+
unless summary.blank?
32+
title = render_overridable(:title, :locals => { :title => section }).strip
33+
"#{title}\n\n#{summary.gsub(/^/, " ")}\n\n"
34+
end
35+
end
36+
37+
def render_overridable(partial, options={})
38+
if File.exist?(path = "#{APP_PATH}/_#{partial}.rhtml")
39+
render(options.merge(:file => path, :use_full_path => false))
40+
elsif File.exist?(path = "#{File.dirname(__FILE__)}/../#{VIEW_PATH}/_#{partial}.rhtml")
41+
render(options.merge(:file => path, :use_full_path => false))
42+
else
43+
""
44+
end
45+
end
46+
47+
def inspect_model_object(model, locals={})
48+
render_overridable(:inspect_model,
49+
:locals => { :inspect_model => model,
50+
:show_instance_variables => true,
51+
:show_attributes => true }.merge(locals))
52+
end
53+
54+
def inspect_value(value)
55+
len = 512
56+
result = object_to_yaml(value).gsub(/\n/, "\n ").strip
57+
result = result[0,len] + "... (#{result.length-len} bytes more)" if result.length > len+20
58+
result
59+
end
60+
61+
def object_to_yaml(object)
62+
object.to_yaml.sub(/^---\s*/m, "")
63+
end
64+
65+
def exclude_raw_post_parameters?
66+
@controller && @controller.respond_to?(:filter_parameters)
67+
end
68+
69+
def filter_sensitive_post_data_parameters(parameters)
70+
exclude_raw_post_parameters? ? @controller.__send__(:filter_parameters, parameters) : parameters
71+
end
72+
73+
def filter_sensitive_post_data_from_env(env_key, env_value)
74+
return env_value unless exclude_raw_post_parameters?
75+
return PARAM_FILTER_REPLACEMENT if (env_key =~ /RAW_POST_DATA/i)
76+
return @controller.__send__(:filter_parameters, {env_key => env_value}).values[0]
77+
end
78+
end

0 commit comments

Comments
 (0)