jeremyevans-exception_notification 1.0.20080808

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README ADDED
@@ -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(joe@schmoe.com bill@schmoe.com)
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 exception.notifier@default.com
36
+ ExceptionNotifier.sender_address =
37
+ %("Application Error" <app.error@myapp.com>)
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
@@ -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
@@ -0,0 +1 @@
1
+ %w'exception_notifiable exception_notifier exception_notifier_helper'.each{|x| require x}
@@ -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" <exception.notifier@default.com>)
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
@@ -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,0 +1 @@
1
+ require "action_mailer"
@@ -0,0 +1,61 @@
1
+ require 'test_helper'
2
+ require 'exception_notifier_helper'
3
+
4
+ class ExceptionNotifierHelperTest < Test::Unit::TestCase
5
+
6
+ class ExceptionNotifierHelperIncludeTarget
7
+ include ExceptionNotifierHelper
8
+ end
9
+
10
+ def setup
11
+ @helper = ExceptionNotifierHelperIncludeTarget.new
12
+ end
13
+
14
+ # No controller
15
+
16
+ def test_should_not_exclude_raw_post_parameters_if_no_controller
17
+ assert !@helper.exclude_raw_post_parameters?
18
+ end
19
+
20
+ # Controller, no filtering
21
+
22
+ class ControllerWithoutFilterParameters; end
23
+
24
+ def test_should_not_filter_env_values_for_raw_post_data_keys_if_controller_can_not_filter_parameters
25
+ stub_controller(ControllerWithoutFilterParameters.new)
26
+ assert @helper.filter_sensitive_post_data_from_env("RAW_POST_DATA", "secret").include?("secret")
27
+ end
28
+ def test_should_not_exclude_raw_post_parameters_if_controller_can_not_filter_parameters
29
+ stub_controller(ControllerWithoutFilterParameters.new)
30
+ assert !@helper.exclude_raw_post_parameters?
31
+ end
32
+ def test_should_return_params_if_controller_can_not_filter_parameters
33
+ stub_controller(ControllerWithoutFilterParameters.new)
34
+ assert_equal :params, @helper.filter_sensitive_post_data_parameters(:params)
35
+ end
36
+
37
+ # Controller with filtering
38
+
39
+ class ControllerWithFilterParameters
40
+ def filter_parameters(params); :filtered end
41
+ end
42
+
43
+ def test_should_filter_env_values_for_raw_post_data_keys_if_controller_can_filter_parameters
44
+ stub_controller(ControllerWithFilterParameters.new)
45
+ assert !@helper.filter_sensitive_post_data_from_env("RAW_POST_DATA", "secret").include?("secret")
46
+ assert @helper.filter_sensitive_post_data_from_env("SOME_OTHER_KEY", "secret").include?("secret")
47
+ end
48
+ def test_should_exclude_raw_post_parameters_if_controller_can_filter_parameters
49
+ stub_controller(ControllerWithFilterParameters.new)
50
+ assert @helper.exclude_raw_post_parameters?
51
+ end
52
+ def test_should_delegate_param_filtering_to_controller_if_controller_can_filter_parameters
53
+ stub_controller(ControllerWithFilterParameters.new)
54
+ assert_equal :filtered, @helper.filter_sensitive_post_data_parameters(:params)
55
+ end
56
+
57
+ private
58
+ def stub_controller(controller)
59
+ @helper.instance_variable_set(:@controller, controller)
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'active_support'
4
+
5
+ $:.unshift File.join(File.dirname(__FILE__), '../lib')
6
+
7
+ RAILS_ROOT = '.' unless defined?(RAILS_ROOT)
@@ -0,0 +1 @@
1
+ <%= @backtrace.join "\n" %>
@@ -0,0 +1,7 @@
1
+ <% max = @request.env.keys.max { |a,b| a.length <=> b.length } -%>
2
+ <% @request.env.keys.sort.each do |key| -%>
3
+ * <%= "%-*s: %s" % [max.length, key, filter_sensitive_post_data_from_env(key, @request.env[key].to_s.strip)] %>
4
+ <% end -%>
5
+
6
+ * Process: <%= $$ %>
7
+ * Server : <%= `hostname -s`.chomp %>
@@ -0,0 +1,16 @@
1
+ <% if show_attributes -%>
2
+ [attributes]
3
+ <% attrs = inspect_model.attributes -%>
4
+ <% max = attrs.keys.max { |a,b| a.length <=> b.length } -%>
5
+ <% attrs.keys.sort.each do |attr| -%>
6
+ * <%= "%*-s: %s" % [max.length, attr, object_to_yaml(attrs[attr]).gsub(/\n/, "\n ").strip] %>
7
+ <% end -%>
8
+ <% end -%>
9
+
10
+ <% if show_instance_variables -%>
11
+ [instance variables]
12
+ <% inspect_model.instance_variables.sort.each do |variable| -%>
13
+ <%- next if variable == "@attributes" -%>
14
+ * <%= variable %>: <%= inspect_value(inspect_model.instance_variable_get(variable)) %>
15
+ <% end -%>
16
+ <% end -%>
@@ -0,0 +1,4 @@
1
+ * URL : <%= @request.protocol %><%= @host %><%= @request.request_uri %>
2
+ * IP address: <%= @request.env["HTTP_X_FORWARDED_FOR"] || @request.env["REMOTE_ADDR"] %>
3
+ * Parameters: <%= filter_sensitive_post_data_parameters(@request.parameters).inspect %>
4
+ * Rails root: <%= @rails_root %>
@@ -0,0 +1,2 @@
1
+ * session id: <%= @request.session.instance_variable_get(:@session_id).inspect %>
2
+ * data: <%= PP.pp(@request.session.instance_variable_get(:@data),"").gsub(/\n/, "\n ").strip %>
@@ -0,0 +1,3 @@
1
+ -------------------------------
2
+ <%= title.to_s.humanize %>:
3
+ -------------------------------
@@ -0,0 +1,6 @@
1
+ A <%= @exception.class %> occurred in <%= @controller.controller_name %>#<%= @controller.action_name %>:
2
+
3
+ <%= @exception.message %>
4
+ <%= @backtrace.first %>
5
+
6
+ <%= @sections.map { |section| render_section(section) }.join %>
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jeremyevans-exception_notification
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.20080808
5
+ platform: ruby
6
+ authors:
7
+ - Rails Core Team
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-08-08 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: code@jeremyevans.net
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ files:
25
+ - lib/exception_notifiable.rb
26
+ - lib/exception_notification.rb
27
+ - lib/exception_notifier.rb
28
+ - lib/exception_notifier_helper.rb
29
+ - rails/init.rb
30
+ - README
31
+ - test/exception_notifier_helper_test.rb
32
+ - test/test_helper.rb
33
+ - views/exception_notifier/_backtrace.rhtml
34
+ - views/exception_notifier/_environment.rhtml
35
+ - views/exception_notifier/_inspect_model.rhtml
36
+ - views/exception_notifier/_request.rhtml
37
+ - views/exception_notifier/_session.rhtml
38
+ - views/exception_notifier/_title.rhtml
39
+ - views/exception_notifier/exception_notification.rhtml
40
+ has_rdoc: true
41
+ homepage: http://github.com/jeremyevans/exception_notification
42
+ post_install_message:
43
+ rdoc_options:
44
+ - --main
45
+ - README
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.2.0
64
+ signing_key:
65
+ specification_version: 2
66
+ summary: Gemified exception_notification rails plugin, compatible with Rails 2.1
67
+ test_files:
68
+ - test/exception_notifier_helper_test.rb
69
+ - test/test_helper.rb