Commit d9a1468ad518423b7212fa9c584b7a6628cdad3f

added exception_notification plugin

Commit diff

app/controllers/application.rb

 
44class ApplicationController < ActionController::Base
55 session :session_key => '_ks1_session_id', :secret => GitoriousConfig["cookie_secret"]
66 include AuthenticatedSystem
7 include ExceptionNotifiable
78
89 protected
910 def require_user_has_ssh_keys
toggle raw diff

config/environment.rb

 
5151 config.active_record.default_timezone = :utc
5252
5353 # See Rails::Configuration for more options
54
55 GitoriousConfig = YAML::load_file(File.join(RAILS_ROOT, "config/gitorious.yml"))
5456
5557 # Application configuration should go into files in config/initializers
5658 # -- all .rb files in that directory are automatically loaded
toggle raw diff

config/environments/production.rb

 
1919
2020# Disable delivery errors, bad email addresses will be ignored
2121# config.action_mailer.raise_delivery_errors = false
22
23ExceptionNotifier.exception_recipients = GitoriousConfig["exception_notification_emails"]
toggle raw diff

config/initializers/gitorious_config.rb

 
0GitoriousConfig = YAML::load_file(File.join(RAILS_ROOT, "config/gitorious.yml"))
toggle raw diff

vendor/plugins/exception_notification/README

 
1= Exception Notifier Plugin for Rails
2
3The Exception Notifier plugin provides a mailer object and a default set of
4templates for sending email notifications when errors occur in a Rails
5application. 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
11The email includes information about the current request, session, and
12environment, and also gives a backtrace of the exception.
13
14== Usage
15
16First, include the ExceptionNotifiable mixin in whichever controller you want
17to generate error emails (typically ApplicationController):
18
19 class ApplicationController < ActionController::Base
20 include ExceptionNotifiable
21 ...
22 end
23
24Then, specify the email recipients in your environment:
25
26 ExceptionNotifier.exception_recipients = %w(joe@schmoe.com bill@schmoe.com)
27
28And that's it! The defaults take care of the rest.
29
30== Configuration
31
32You can tweak other values to your liking, as well. In your environment file,
33just 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
42Email notifications will only occur when the IP address is determined not to
43be local. You can specify certain addresses to always be local so that you'll
44get a detailed error instead of the generic error page. You do this in your
45controller (or even per-controller):
46
47 consider_local "64.72.18.143", "14.17.21.25"
48
49You can specify subnet masks as well, so that all matching addresses are
50considered local:
51
52 consider_local "64.72.18.143/24"
53
54The address "127.0.0.1" is always considered local. If you want to completely
55reset the list of all addresses (for instance, if you wanted "127.0.0.1" to
56NOT be considered local), you can simply do, somewhere in your controller:
57
58 local_addresses.clear
59
60== Customization
61
62By default, the notification email includes four parts: request, session,
63environment, and backtrace (in that order). You can customize how each of those
64sections are rendered by placing a partial named for that part in your
65app/views/exception_notifier directory (e.g., _session.rhtml). Each partial has
66access 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
77You can reorder the sections, or exclude sections completely, by altering the
78ExceptionNotifier.sections variable. You can even add new sections that
79describe 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
81new section requires information that isn't available by default, make sure
82it 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
96In the above case, @document and @person would be made available to the email
97renderer, allowing your new section(s) to access and display them. See the
98existing sections defined by the plugin for examples of how to write your own.
99
100== Advanced Customization
101
102By default, the email notifier will only notify on critical errors. For
103ActiveRecord::RecordNotFound and ActionController::UnknownAction, it will
104simply render the contents of your public/404.html file. Other exceptions
105will render public/500.html and will send the email notification. If you want
106to use different rules for the notification, you will need to implement your
107own rescue_action_in_public method. You can look at the default implementation
108in ExceptionNotifiable for an example of how to go about that.
109
110
111Copyright (c) 2005 Jamis Buck, released under the MIT license
toggle raw diff

vendor/plugins/exception_notification/init.rb

 
1require "action_mailer"
toggle raw diff

vendor/plugins/exception_notification/lib/exception_notifiable.rb

 
1require '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.
23module 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
99end
toggle raw diff

vendor/plugins/exception_notification/lib/exception_notifier.rb

 
1require '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.
23class 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 def self.reloadable?; false; end
37
38 def exception_notification(exception, controller, request, data={})
39 content_type "text/plain"
40
41 subject "#{email_prefix}#{controller.controller_name}##{controller.action_name} (#{exception.class}) #{exception.message.inspect}"
42
43 recipients exception_recipients
44 from sender_address
45
46 body data.merge({ :controller => controller, :request => request,
47 :exception => exception, :host => (request.env["HTTP_X_FORWARDED_HOST"] || request.env["HTTP_HOST"]),
48 :backtrace => sanitize_backtrace(exception.backtrace),
49 :rails_root => rails_root, :data => data,
50 :sections => sections })
51 end
52
53 def template_root
54 "#{File.dirname(__FILE__)}/../views"
55 end
56
57 private
58
59 def sanitize_backtrace(trace)
60 re = Regexp.new(/^#{Regexp.escape(rails_root)}/)
61 trace.map { |line| Pathname.new(line.gsub(re, "[RAILS_ROOT]")).cleanpath.to_s }
62 end
63
64 def rails_root
65 @rails_root ||= Pathname.new(RAILS_ROOT).cleanpath.to_s
66 end
67
68end
toggle raw diff

vendor/plugins/exception_notification/lib/exception_notifier_helper.rb

 
1require '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.
23module 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.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.filter_parameters({env_key => env_value}).values[0]
77 end
78end
toggle raw diff

vendor/plugins/exception_notification/test/exception_notifier_helper_test.rb

 
1require 'test_helper'
2require 'exception_notifier_helper'
3
4class 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
61end
toggle raw diff

vendor/plugins/exception_notification/test/test_helper.rb

 
1require 'test/unit'
2require 'rubygems'
3require 'active_support'
4
5$:.unshift File.join(File.dirname(__FILE__), '../lib')
6
7RAILS_ROOT = '.' unless defined?(RAILS_ROOT)
toggle raw diff

vendor/plugins/exception_notification/views/exception_notifier/_backtrace.rhtml

 
1<%= @backtrace.join "\n" %>
toggle raw diff

vendor/plugins/exception_notification/views/exception_notifier/_environment.rhtml

 
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 %>
toggle raw diff

vendor/plugins/exception_notification/views/exception_notifier/_inspect_model.rhtml

 
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 -%>
toggle raw diff

vendor/plugins/exception_notification/views/exception_notifier/_request.rhtml

 
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 %>
toggle raw diff

vendor/plugins/exception_notification/views/exception_notifier/_session.rhtml

 
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 %>
toggle raw diff

vendor/plugins/exception_notification/views/exception_notifier/_title.rhtml

 
1-------------------------------
2<%= title.to_s.humanize %>:
3-------------------------------
toggle raw diff

vendor/plugins/exception_notification/views/exception_notifier/exception_notification.rhtml

 
1A <%= @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 %>
toggle raw diff