exception_notification-5.0.1/0000755000004100000410000000000015045670240016314 5ustar www-datawww-dataexception_notification-5.0.1/exception_notification.gemspec0000644000004100000410000000333315045670240024427 0ustar www-datawww-data# frozen_string_literal: true require File.expand_path("lib/exception_notification/version", __dir__) Gem::Specification.new do |s| s.name = "exception_notification" s.version = ExceptionNotification::VERSION s.authors = ["Jamis Buck", "Josh Peek", "Sebastián Martínez", "Kevin McPhillips"] s.summary = "Exception notification for Ruby applications" s.homepage = "https://kmcphillips.github.io/exception_notification/" s.email = "github@kevinmcphillips.ca" s.license = "MIT" s.metadata = {"changelog_uri" => "https://github.com/kmcphillips/exception_notification/blob/master/CHANGELOG.rdoc"} s.required_ruby_version = ">= 3.2" s.files = Dir[ "lib/**/*", "docs/**/*", "MIT-LICENSE", "exception_notification.gemspec", "Rakefile", "*.md", "*.rdoc" ].reject { |f| File.directory?(f) } s.require_path = "lib" s.add_dependency("actionmailer", ">= 7.1", "< 9") s.add_dependency("activesupport", ">= 7.1", "< 9") s.add_development_dependency "aws-sdk-sns", "~> 1" s.add_development_dependency "carrier-pigeon", ">= 0.7.0" s.add_development_dependency "dogapi", ">= 1.23.0" s.add_development_dependency "hipchat", ">= 1.0.0" s.add_development_dependency "httparty", "~> 0.10.2" s.add_development_dependency "mocha", ">= 0.13.0" s.add_development_dependency "mock_redis", "~> 0.19.0" s.add_development_dependency "net-smtp" s.add_development_dependency "ostruct" s.add_development_dependency "rails", ">= 7.1", "< 9" s.add_development_dependency "resque", "~> 1.8.0" s.add_development_dependency "sidekiq", ">= 5.0.4" s.add_development_dependency "slack-notifier", ">= 1.0.0" s.add_development_dependency "standard" s.add_development_dependency "timecop", "~> 0.9.0" end exception_notification-5.0.1/CONTRIBUTING.md0000644000004100000410000000160215045670240020544 0ustar www-datawww-data# How to contribute Pull requests welcome! Please try to make them as complete and clear as possible, with reproduction steps and good tests. ## Issues Issues are monitored and responded to, but pull requests are much more likely to be merged. Make sure the issue includes version information, reproduction steps, and any other relevant information. You can use the `examples/sample_app.rb` to help reproduce the issue. ## Pull Requests All PRs with changes will be reviewed and merged if possible. Thank you for taking the time to contribute. Changes must include tests. Please include a description of the problem and why the change is needed. Same with issues, reproduction steps are helpful. ### Running the tests ```bash bundle install bundle exec rake test ``` And running the linting with [standard](https://github.com/standardrb/standard): ```bash bundle exec standardrb ``` exception_notification-5.0.1/lib/0000755000004100000410000000000015045670240017062 5ustar www-datawww-dataexception_notification-5.0.1/lib/generators/0000755000004100000410000000000015045670240021233 5ustar www-datawww-dataexception_notification-5.0.1/lib/generators/exception_notification/0000755000004100000410000000000015045670240025777 5ustar www-datawww-dataexception_notification-5.0.1/lib/generators/exception_notification/templates/0000755000004100000410000000000015045670240027775 5ustar www-datawww-data././@LongLink0000644000000000000000000000015300000000000011602 Lustar rootrootexception_notification-5.0.1/lib/generators/exception_notification/templates/exception_notification.rb.erbexception_notification-5.0.1/lib/generators/exception_notification/templates/exception_notification.0000644000004100000410000000436015045670240034545 0ustar www-datawww-data# Move this require to your `config/application.rb` if you want to be notified from runner commands too. require "exception_notification/rails" require "exception_notification/rake" <% if options.sidekiq? %> require "exception_notification/sidekiq"<% end %><% if options.resque? %> require "resque/failure/multiple" require "resque/failure/redis" require "exception_notification/resque" Resque::Failure::Multiple.classes = [ Resque::Failure::Redis, ExceptionNotification::Resque ] Resque::Failure.backend = Resque::Failure::Multiple<% end %> ExceptionNotification.configure do |config| # Ignore additional exception types. The default list of exception classes is: # ActiveRecord::RecordNotFound Mongoid::Errors::DocumentNotFound AbstractController::ActionNotFound # ActionController::RoutingError ActionController::UnknownFormat ActionController::UrlGenerationError # ActionDispatch::Http::MimeNegotiation::InvalidType Rack::Utils::InvalidParameterError # config.ignored_exceptions += %w[ActionView::TemplateError CustomError] # Adds a condition to decide when an exception must be ignored or not. # The ignore_if method can be invoked multiple times to add extra conditions. # config.ignore_if do |exception, options| # Rails.env.local? # end # Ignore exceptions generated by crawlers # config.ignore_crawlers %w[Googlebot bingbot] # Notifiers ================================================================= # Email notifier sends notifications by email. config.add_notifier :email, { email_prefix: "[ERROR] ", sender_address: %("Notifier" ), exception_recipients: %w[exceptions@example.com] } # Campfire notifier sends notifications to your Campfire room. Requires "tinder" gem. # config.add_notifier :campfire, { # subdomain: "my_subdomain", # token: "my_token", # room_name: "my_room" # } # HipChat notifier sends notifications to your HipChat room. Requires "hipchat" gem. # config.add_notifier :hipchat, { # api_token: "my_token", # room_name: "my_room" # } # Webhook notifier sends notifications over HTTP protocol. Requires "httparty" gem. # config.add_notifier :webhook, { # url: "http://example.com:5555/hubot/path", # http_method: :post # } end exception_notification-5.0.1/lib/generators/exception_notification/install_generator.rb0000644000004100000410000000125715045670240032045 0ustar www-datawww-data# frozen_string_literal: true module ExceptionNotification module Generators class InstallGenerator < ::Rails::Generators::Base desc "Creates a ExceptionNotification initializer." source_root File.expand_path("templates", __dir__) class_option :resque, type: :boolean, desc: "Add support for sending notifications when errors occur in Resque jobs." class_option :sidekiq, type: :boolean, desc: "Add support for sending notifications when errors occur in Sidekiq jobs." def copy_initializer template "exception_notification.rb.erb", "config/initializers/exception_notification.rb" end end end end exception_notification-5.0.1/lib/exception_notifier/0000755000004100000410000000000015045670240022757 5ustar www-datawww-dataexception_notification-5.0.1/lib/exception_notifier/views/0000755000004100000410000000000015045670240024114 5ustar www-datawww-dataexception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/0000755000004100000410000000000015045670240030011 5ustar www-datawww-dataexception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/_backtrace.text.erb0000644000004100000410000000004115045670240033537 0ustar www-datawww-data<%= raw @backtrace.join("\n") %> exception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/_request.text.erb0000644000004100000410000000065215045670240033320 0ustar www-datawww-data* URL : <%= raw safe_encode @request.url %> * HTTP Method: <%= raw @request.request_method %> * IP address : <%= raw @request.remote_ip %> * Parameters : <%= raw safe_encode @request.filtered_parameters.inspect %> * Timestamp : <%= raw @timestamp %> * Server : <%= raw Socket.gethostname %> <% if defined?(Rails) && Rails.respond_to?(:root) %> * Rails root : <%= raw Rails.root %> <% end %> * Process: <%= raw $$ %> exception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/_environment.html.erb0000644000004100000410000000041215045670240034146 0ustar www-datawww-data<% filtered_env = @request.filtered_env -%> exception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/_data.text.erb0000644000004100000410000000004515045670240032535 0ustar www-datawww-data* data: <%= raw PP.pp(@data, +"") %> ././@LongLink0000644000000000000000000000017000000000000011601 Lustar rootrootexception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/background_exception_notification.html.erbexception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/background_exception_no0000644000004100000410000000307215045670240034627 0ustar www-datawww-data Exception <% sections_content = @sections.map do |section| begin summary = render(section).strip unless summary.blank? title = render("title", :title => section).strip [title, summary] end rescue Exception => e title = render("title", :title => section).strip summary = ["ERROR: Failed to generate exception summary:", [e.class.to_s, e.message].join(": "), e.backtrace && e.backtrace.join("\n")].compact.join("\n\n") [title, summary] end end %>
<% sections_content.each do |title, summary| %> <% end %>

<%= @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' %> <%= @exception.class %> occurred in background at <%= @timestamp %> :

<%= @exception.message %>

<%= @backtrace.first %>
          
<%= raw title %>
<%= raw summary %>
././@LongLink0000644000000000000000000000015500000000000011604 Lustar rootrootexception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/exception_notification.text.erbexception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/exception_notification.0000644000004100000410000000147515045670240034565 0ustar www-datawww-data<%= @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' %> <%= @exception.class %> occurred in <%= @kontroller.controller_name %>#<%= @kontroller.action_name %>: <%= raw @exception.message %> <%= raw @backtrace.first %> <% sections = @sections.map do |section| begin summary = render(section).strip unless summary.blank? title = render("title", title: section).strip "#{title}\n\n#{summary.gsub(/^/, " ")}\n\n" end rescue Exception => e title = render("title", title: section).strip summary = ["ERROR: Failed to generate exception summary:", [e.class.to_s, e.message].join(": "), e.backtrace && e.backtrace.join("\n")].compact.join("\n\n") [title, summary.gsub(/^/, " "), nil].join("\n\n") end end.join %> <%= raw sections %> exception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/_session.text.erb0000644000004100000410000000037515045670240033315 0ustar www-datawww-data* session id: <%= @request.ssl? ? "[FILTERED]" : (raw (@request.session['session_id'] || (@request.env["rack.session.options"] and @request.env["rack.session.options"][:id])).inspect.html_safe) %> * data: <%= raw PP.pp(@request.session.to_hash, +"") %> ././@LongLink0000644000000000000000000000015500000000000011604 Lustar rootrootexception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/exception_notification.html.erbexception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/exception_notification.0000644000004100000410000000312415045670240034556 0ustar www-datawww-data Exception <% sections_content = @sections.map do |section| begin summary = render(section).strip unless summary.blank? title = render("title", title: section).strip [title, summary] end rescue Exception => e title = render("title", title: section).strip summary = ["ERROR: Failed to generate exception summary:", [e.class.to_s, e.message].join(": "), e.backtrace && e.backtrace.join("\n")].compact.join("\n\n") [title, summary] end end %>
<% sections_content.each do |title, summary| %> <% end %>

<%= @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' %> <%= @exception.class %> occurred in <%= @kontroller.controller_name %>#<%= @kontroller.action_name %>:

<%= @exception.message %>

<%= @backtrace.first %>
          
<%= raw title %>
<%= raw summary %>
exception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/_title.text.erb0000644000004100000410000000014015045670240032741 0ustar www-datawww-data------------------------------- <%= raw title.to_s.humanize %>: ------------------------------- exception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/_session.html.erb0000644000004100000410000000056515045670240033276 0ustar www-datawww-data exception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/_title.html.erb0000644000004100000410000000005015045670240032721 0ustar www-datawww-data

<%= title.to_s.humanize %>

exception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb0000644000004100000410000000021015045670240033515 0ustar www-datawww-data
  <%= @backtrace.join("\n") %>
exception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/_request.html.erb0000644000004100000410000000145215045670240033277 0ustar www-datawww-data exception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/_environment.text.erb0000644000004100000410000000044315045670240034172 0ustar www-datawww-data<% filtered_env = @request.filtered_env -%> <% max = filtered_env.keys.map(&:to_s).max { |a, b| a.length <=> b.length } -%> <% filtered_env.keys.map(&:to_s).sort.each do |key| -%> * <%= raw safe_encode("%-*s: %s" % [max.length, key, inspect_object(filtered_env[key])]).strip %> <% end -%> ././@LongLink0000644000000000000000000000017000000000000011601 Lustar rootrootexception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erbexception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/background_exception_no0000644000004100000410000000070315045670240034625 0ustar www-datawww-data<%= @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' %> <%= @exception.class %> occurred in background at <%= raw @timestamp %> : <%= @exception.message %> <%= @backtrace.first %> <% sections = @sections.map do |section| summary = render(section).strip unless summary.blank? title = render("title", :title => section).strip "#{title}\n\n#{summary.gsub(/^/, " ")}\n\n" end end.join %> <%= raw sections %> exception_notification-5.0.1/lib/exception_notifier/views/exception_notifier/_data.html.erb0000644000004100000410000000017015045670240032514 0ustar www-datawww-data exception_notification-5.0.1/lib/exception_notifier/notifier.rb0000644000004100000410000000133115045670240025121 0ustar www-datawww-data# frozen_string_literal: true require "active_support/deprecation" module ExceptionNotifier class Notifier def self.exception_notification(env, exception, options = {}) ActiveSupport::Deprecation.warn( "Please use ExceptionNotifier.notify_exception(exception, options.merge(env: env))." ) ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options.merge(env: env)) end def self.background_exception_notification(exception, options = {}) ActiveSupport::Deprecation.warn "Please use ExceptionNotifier.notify_exception(exception, options)." ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options) end end end exception_notification-5.0.1/lib/exception_notifier/modules/0000755000004100000410000000000015045670240024427 5ustar www-datawww-dataexception_notification-5.0.1/lib/exception_notifier/modules/error_grouping.rb0000644000004100000410000000532015045670240030017 0ustar www-datawww-data# frozen_string_literal: true require "active_support" require "active_support/core_ext/numeric/time" require "active_support/concern" module ExceptionNotifier module ErrorGrouping extend ActiveSupport::Concern included do mattr_accessor :error_grouping self.error_grouping = false mattr_accessor :error_grouping_period self.error_grouping_period = 5.minutes mattr_accessor :notification_trigger mattr_accessor :error_grouping_cache end module ClassMethods # Fallback to the memory store while the specified cache store doesn't work # def fallback_cache_store @fallback_cache_store ||= ActiveSupport::Cache::MemoryStore.new end def error_count(error_key) count = begin error_grouping_cache.read(error_key) rescue => e log_cache_error(error_grouping_cache, e, :read) fallback_cache_store.read(error_key) end count&.to_i end def save_error_count(error_key, count) error_grouping_cache.write(error_key, count, expires_in: error_grouping_period) rescue => e log_cache_error(error_grouping_cache, e, :write) fallback_cache_store.write(error_key, count, expires_in: error_grouping_period) end def group_error!(exception, options) message_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\nmessage:#{exception.message}")}" accumulated_errors_count = 1 if (count = error_count(message_based_key)) accumulated_errors_count = count + 1 save_error_count(message_based_key, accumulated_errors_count) else backtrace_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\npath:#{exception.backtrace.try(:first)}")}" if (count = error_grouping_cache.read(backtrace_based_key)) accumulated_errors_count = count + 1 save_error_count(backtrace_based_key, accumulated_errors_count) else save_error_count(backtrace_based_key, accumulated_errors_count) save_error_count(message_based_key, accumulated_errors_count) end end options[:accumulated_errors_count] = accumulated_errors_count end def send_notification?(exception, count) if notification_trigger.respond_to?(:call) notification_trigger.call(exception, count) else factor = Math.log2(count) factor.to_i == factor end end private def log_cache_error(cache, exception, action) "#{cache.inspect} failed to #{action}, reason: #{exception.message}. Falling back to memory cache store." end end end end exception_notification-5.0.1/lib/exception_notifier/modules/backtrace_cleaner.rb0000644000004100000410000000047515045670240030372 0ustar www-datawww-data# frozen_string_literal: true module ExceptionNotifier module BacktraceCleaner def clean_backtrace(exception) if defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) Rails.backtrace_cleaner.send(:filter, exception.backtrace) else exception.backtrace end end end end exception_notification-5.0.1/lib/exception_notifier/modules/formatter.rb0000644000004100000410000000563015045670240026763 0ustar www-datawww-data# frozen_string_literal: true require "active_support/core_ext/time" require "action_dispatch" module ExceptionNotifier class Formatter include ExceptionNotifier::BacktraceCleaner attr_reader :app_name def initialize(exception, opts = {}) @exception = exception @env = opts[:env] @errors_count = opts[:accumulated_errors_count].to_i @app_name = opts[:app_name] || rails_app_name end # # :warning: Error occurred in production :warning: # :warning: Error occurred :warning: # def title env = Rails.env if defined?(::Rails) && ::Rails.respond_to?(:env) if env "⚠️ Error occurred in #{env} ⚠️" else "⚠️ Error occurred ⚠️" end end # # A *NoMethodError* occurred. # 3 *NoMethodError* occurred. # A *NoMethodError* occurred in *home#index*. # def subtitle errors_text = if errors_count > 1 errors_count else /^[aeiou]/i.match?(exception.class.to_s) ? "An" : "A" end in_action = " in *#{controller_and_action}*" if controller "#{errors_text} *#{exception.class}* occurred#{in_action}." end # # # *Request:* # ``` # * url : https://www.example.com/ # * http_method : GET # * ip_address : 127.0.0.1 # * parameters : {"controller"=>"home", "action"=>"index"} # * timestamp : 2019-01-01 00:00:00 UTC # ``` # def request_message request = ActionDispatch::Request.new(env) if env return unless request [ "```", "* url : #{request.original_url}", "* http_method : #{request.method}", "* ip_address : #{request.remote_ip}", "* parameters : #{request.filtered_parameters}", "* timestamp : #{Time.current}", "```" ].join("\n") end # # # *Backtrace:* # ``` # * app/controllers/my_controller.rb:99:in `specific_function' # * app/controllers/my_controller.rb:70:in `specific_param' # * app/controllers/my_controller.rb:53:in `my_controller_params' # ``` # def backtrace_message backtrace = exception.backtrace ? clean_backtrace(exception) : nil return unless backtrace text = [] text << "```" backtrace.first(3).each { |line| text << "* #{line}" } text << "```" text.join("\n") end # # home#index # def controller_and_action "#{controller.controller_name}##{controller.action_name}" if controller end private attr_reader :exception, :env, :errors_count def rails_app_name return unless defined?(::Rails) && ::Rails.respond_to?(:application) if Rails::VERSION::MAJOR >= 6 Rails.application.class.module_parent_name.underscore else Rails.application.class.parent_name.underscore end end def controller env["action_controller.instance"] if env end end end exception_notification-5.0.1/lib/exception_notifier/teams_notifier.rb0000644000004100000410000001251715045670240026322 0ustar www-datawww-data# frozen_string_literal: true require "action_dispatch" require "active_support/core_ext/time" require "json" module ExceptionNotifier class TeamsNotifier < BaseNotifier include ExceptionNotifier::BacktraceCleaner class MissingController def method_missing(*args, &block) end def respond_to_missing?(*args) end end attr_accessor :httparty def initialize(options = {}) super @default_options = options @httparty = HTTParty end def call(exception, options = {}) @options = options.merge(@default_options) @exception = exception @backtrace = exception.backtrace ? clean_backtrace(exception) : nil @env = @options.delete(:env) @application_name = @options.delete(:app_name) || rails_app_name @gitlab_url = @options.delete(:git_url) @jira_url = @options.delete(:jira_url) @webhook_url = @options.delete(:webhook_url) raise ArgumentError, "You must provide 'webhook_url' parameter." unless @webhook_url if @env.nil? @controller = @request_items = nil else @controller = @env["action_controller.instance"] || MissingController.new @additional_exception_data = @env["exception_notifier.exception_data"] request = ActionDispatch::Request.new(@env) @request_items = {url: request.original_url, http_method: request.method, ip_address: request.remote_ip, parameters: request.filtered_parameters, timestamp: Time.current} end payload = message_text @options[:body] = payload.to_json @options[:headers] ||= {} @options[:headers]["Content-Type"] = "application/json" @options[:debug_output] = $stdout @httparty.post(@webhook_url, @options) end private def message_text text = { "@type" => "MessageCard", "@context" => "http://schema.org/extensions", "summary" => "#{@application_name} Exception Alert", "title" => "⚠️ Exception Occurred in #{env_name} ⚠️", "sections" => [ { "activityTitle" => activity_title, "activitySubtitle" => @exception.message.to_s } ], "potentialAction" => [] } text["sections"].push details text["potentialAction"].push gitlab_view_link unless @gitlab_url.nil? text["potentialAction"].push gitlab_issue_link unless @gitlab_url.nil? text["potentialAction"].push jira_issue_link unless @jira_url.nil? text end def details details = { "title" => "Details", "facts" => [] } details["facts"].push message_request unless @request_items.nil? details["facts"].push message_backtrace unless @backtrace.nil? details["facts"].push additional_exception_data unless @additional_exception_data.nil? details end def activity_title errors_count = @options[:accumulated_errors_count].to_i "#{(errors_count > 1) ? errors_count : "A"} *#{@exception.class}* occurred" + (@controller ? " in *#{controller_and_method}*." : ".") end def message_request { "name" => "Request", "value" => "#{hash_presentation(@request_items)}\n " } end def message_backtrace(size = 3) text = [] size = (@backtrace.size < size) ? @backtrace.size : size text << "```" size.times { |i| text << "* " + @backtrace[i] } text << "```" { "name" => "Backtrace", "value" => text.join(" \n").to_s } end def additional_exception_data { "name" => "Data", "value" => "`#{@additional_exception_data}`\n " } end def gitlab_view_link { "@type" => "ViewAction", "name" => "\u{1F98A} View in GitLab", "target" => [ "#{@gitlab_url}/#{@application_name}" ] } end def gitlab_issue_link link = [@gitlab_url, @application_name, "issues", "new"].join("/") params = { "issue[title]" => ["[BUG] Error 500 :", controller_and_method, "(#{@exception.class})", @exception.message].compact.join(" ") }.to_query { "@type" => "ViewAction", "name" => "\u{1F98A} Create Issue in GitLab", "target" => [ "#{link}/?#{params}" ] } end def jira_issue_link { "@type" => "ViewAction", "name" => "🐞 Create Issue in Jira", "target" => [ "#{@jira_url}/secure/CreateIssue!default.jspa" ] } end def controller_and_method if @controller "#{@controller.controller_name}##{@controller.action_name}" else "" end end def hash_presentation(hash) text = [] hash.each do |key, value| text << "* **#{key}** : `#{value}`" end text.join(" \n") end def rails_app_name return unless defined?(Rails) && Rails.respond_to?(:application) if ::Gem::Version.new(Rails.version) >= ::Gem::Version.new("6.0") Rails.application.class.module_parent_name.underscore else Rails.application.class.parent_name.underscore end end def env_name Rails.env if defined?(Rails) && Rails.respond_to?(:env) end end end exception_notification-5.0.1/lib/exception_notifier/base_notifier.rb0000644000004100000410000000165415045670240026123 0ustar www-datawww-data# frozen_string_literal: true module ExceptionNotifier class BaseNotifier attr_accessor :base_options def initialize(options = {}) @base_options = options end def send_notice(exception, options, message, message_opts = nil) _pre_callback(exception, options, message, message_opts) result = yield(message, message_opts) _post_callback(exception, options, message, message_opts) result end def _pre_callback(exception, options, message, message_opts) return unless @base_options[:pre_callback].respond_to?(:call) @base_options[:pre_callback].call(options, self, exception.backtrace, message, message_opts) end def _post_callback(exception, options, message, message_opts) return unless @base_options[:post_callback].respond_to?(:call) @base_options[:post_callback].call(options, self, exception.backtrace, message, message_opts) end end end exception_notification-5.0.1/lib/exception_notifier/hipchat_notifier.rb0000644000004100000410000000305015045670240026621 0ustar www-datawww-data# frozen_string_literal: true module ExceptionNotifier class HipchatNotifier < BaseNotifier attr_accessor :from attr_accessor :room attr_accessor :message_options def initialize(options) super begin api_token = options.delete(:api_token) room_name = options.delete(:room_name) opts = { api_version: options.delete(:api_version) || "v1" } opts[:server_url] = options.delete(:server_url) if options[:server_url] @from = options.delete(:from) || "Exception" @room = HipChat::Client.new(api_token, opts)[room_name] @message_template = options.delete(:message_template) || lambda { |exception, errors_count| msg = if errors_count > 1 "The exception occurred #{errors_count} times: '#{Rack::Utils.escape_html(exception.message)}'" else "A new exception occurred: '#{Rack::Utils.escape_html(exception.message)}'" end msg += " on '#{exception.backtrace.first}'" if exception.backtrace msg } @message_options = options @message_options[:color] ||= "red" rescue @room = nil end end def call(exception, options = {}) return unless active? message = @message_template.call(exception, options[:accumulated_errors_count].to_i) send_notice(exception, options, message, @message_options) do |msg, message_opts| @room.send(@from, msg, message_opts) end end private def active? !@room.nil? end end end exception_notification-5.0.1/lib/exception_notifier/webhook_notifier.rb0000644000004100000410000000306715045670240026647 0ustar www-datawww-data# frozen_string_literal: true require "action_dispatch" require "active_support/core_ext/time" module ExceptionNotifier class WebhookNotifier < BaseNotifier def initialize(options) super @default_options = options end def call(exception, options = {}) env = options[:env] options = options.reverse_merge(@default_options) url = options.delete(:url) http_method = options.delete(:http_method) || :post options[:body] ||= {} options[:body][:server] = Socket.gethostname options[:body][:process] = $PROCESS_ID options[:body][:rails_root] = Rails.root if defined?(Rails) && Rails.respond_to?(:root) options[:body][:exception] = { error_class: exception.class.to_s, message: exception.message.inspect, backtrace: exception.backtrace } options[:body][:data] = (env && env["exception_notifier.exception_data"] || {}).merge(options[:data] || {}) unless env.nil? request = ActionDispatch::Request.new(env) request_items = { url: request.original_url, http_method: request.method, ip_address: request.remote_ip, parameters: request.filtered_parameters, timestamp: Time.current } options[:body][:request] = request_items options[:body][:session] = request.session options[:body][:environment] = request.filtered_env end send_notice(exception, options, nil, @default_options) do |_, _| HTTParty.send(http_method, url, options) end end end end exception_notification-5.0.1/lib/exception_notifier/mattermost_notifier.rb0000644000004100000410000000413315045670240027403 0ustar www-datawww-data# frozen_string_literal: true require "httparty" module ExceptionNotifier class MattermostNotifier < BaseNotifier def call(exception, opts = {}) options = opts.merge(base_options) @exception = exception @formatter = Formatter.new(exception, options) @gitlab_url = options[:git_url] @env = options[:env] || {} payload = { text: message_text.compact.join("\n"), username: options[:username] || "Exception Notifier" } payload[:icon_url] = options[:avatar] if options[:avatar] payload[:channel] = options[:channel] if options[:channel] httparty_options = options.except( :avatar, :channel, :username, :git_url, :webhook_url, :env, :accumulated_errors_count, :app_name ) httparty_options[:body] = payload.to_json httparty_options[:headers] ||= {} httparty_options[:headers]["Content-Type"] = "application/json" HTTParty.post(options[:webhook_url], httparty_options) end private attr_reader :formatter def message_text text = [ "@channel", "### #{formatter.title}", formatter.subtitle, "*#{@exception.message}*" ] if (request = formatter.request_message.presence) text << "### Request" text << request end if (backtrace = formatter.backtrace_message.presence) text << "### Backtrace" text << backtrace end if (exception_data = @env["exception_notifier.exception_data"]) text << "### Data" data_string = exception_data.map { |k, v| "* #{k} : #{v}" }.join("\n") text << "```\n#{data_string}\n```" end text << message_issue_link if @gitlab_url text end def message_issue_link link = [@gitlab_url, formatter.app_name, "issues", "new"].join("/") params = { "issue[title]" => ["[BUG] Error 500 :", formatter.controller_and_action || "", "(#{@exception.class})", @exception.message].compact.join(" ") }.to_query "[Create an issue](#{link}/?#{params})" end end end exception_notification-5.0.1/lib/exception_notifier/datadog_notifier.rb0000644000004100000410000000742615045670240026617 0ustar www-datawww-data# frozen_string_literal: true require "action_dispatch" module ExceptionNotifier class DatadogNotifier < BaseNotifier attr_reader :client, :default_options def initialize(options) super @client = options.fetch(:client) @default_options = options end def call(exception, options = {}) client.emit_event( datadog_event(exception, options) ) end def datadog_event(exception, options = {}) DatadogExceptionEvent.new( exception, options.reverse_merge(default_options) ).event end class DatadogExceptionEvent include ExceptionNotifier::BacktraceCleaner MAX_TITLE_LENGTH = 120 MAX_VALUE_LENGTH = 300 MAX_BACKTRACE_SIZE = 3 ALERT_TYPE = "error" attr_reader :exception, :options def initialize(exception, options) @exception = exception @options = options end def request @request ||= ActionDispatch::Request.new(options[:env]) if options[:env] end def controller @controller ||= options[:env] && options[:env]["action_controller.instance"] end def backtrace @backtrace ||= exception.backtrace ? clean_backtrace(exception) : [] end def tags options[:tags] || [] end def title_prefix options[:title_prefix] || "" end def event title = formatted_title body = formatted_body Dogapi::Event.new( body, msg_title: title, alert_type: ALERT_TYPE, tags: tags, aggregation_key: [title] ) end def formatted_title title = "#{title_prefix}#{controller_subtitle} (#{exception.class}) #{exception.message.inspect}" truncate(title, MAX_TITLE_LENGTH) end def formatted_body text = [] text << "%%%" text << formatted_request if request text << formatted_session if request text << formatted_backtrace text << "%%%" text.join("\n") end def formatted_key_value(key, value) "**#{key}:** #{value}" end def formatted_request text = [] text << "### **Request**" text << formatted_key_value("URL", request.url) text << formatted_key_value("HTTP Method", request.request_method) text << formatted_key_value("IP Address", request.remote_ip) text << formatted_key_value("Parameters", request.filtered_parameters.inspect) text << formatted_key_value("Timestamp", Time.current) text << formatted_key_value("Server", Socket.gethostname) text << formatted_key_value("Rails root", Rails.root) if defined?(Rails) && Rails.respond_to?(:root) text << formatted_key_value("Process", $PROCESS_ID) text << "___" text.join("\n") end def formatted_session text = [] text << "### **Session**" text << formatted_key_value("Data", request.session.to_hash) text << "___" text.join("\n") end def formatted_backtrace size = [backtrace.size, MAX_BACKTRACE_SIZE].min text = [] text << "### **Backtrace**" text << "````" size.times { |i| text << backtrace[i] } text << "````" text << "___" text.join("\n") end def truncate(string, max) (string.length > max) ? "#{string[0...max]}..." : string end def inspect_object(object) case object when Hash, Array truncate(object.inspect, MAX_VALUE_LENGTH) else object.to_s end end private def controller_subtitle "#{controller.controller_name} #{controller.action_name}" if controller end end end end exception_notification-5.0.1/lib/exception_notifier/email_notifier.rb0000644000004100000410000001504315045670240026275 0ustar www-datawww-data# frozen_string_literal: true require "active_support/core_ext/time" require "action_mailer" require "action_dispatch" require "pp" module ExceptionNotifier class EmailNotifier < BaseNotifier DEFAULT_OPTIONS = { sender_address: %("Exception Notifier" ), exception_recipients: [], email_prefix: "[ERROR] ", email_format: :text, sections: %w[request session environment backtrace], background_sections: %w[backtrace data], verbose_subject: true, normalize_subject: false, include_controller_and_action_names_in_subject: true, delivery_method: nil, mailer_settings: nil, email_headers: {}, mailer_parent: "ActionMailer::Base", template_path: "exception_notifier", deliver_with: nil }.freeze module Mailer class MissingController def method_missing(*args, &block) end def respond_to_missing?(*args) end end def self.extended(base) base.class_eval do send(:include, ExceptionNotifier::BacktraceCleaner) # Append application view path to the ExceptionNotifier lookup context. append_view_path "#{File.dirname(__FILE__)}/views" def exception_notification(env, exception, options = {}, default_options = {}) load_custom_views @env = env @exception = exception env_options = env["exception_notifier.options"] || {} @options = default_options.merge(env_options).merge(options) @kontroller = env["action_controller.instance"] || MissingController.new @request = ActionDispatch::Request.new(env) @backtrace = exception.backtrace ? clean_backtrace(exception) : [] @timestamp = Time.current @sections = @options[:sections] @data = (env["exception_notifier.exception_data"] || {}).merge(options[:data] || {}) @sections += %w[data] unless @data.empty? compose_email end def background_exception_notification(exception, options = {}, default_options = {}) load_custom_views @exception = exception @options = default_options.merge(options).symbolize_keys @backtrace = exception.backtrace || [] @timestamp = Time.current @sections = @options[:background_sections] @data = options[:data] || {} @env = @kontroller = nil compose_email end private def compose_subject subject = @options[:email_prefix].to_s.dup subject << "(#{@options[:accumulated_errors_count]} times)" if @options[:accumulated_errors_count].to_i > 1 subject << "#{@kontroller.controller_name}##{@kontroller.action_name}" if include_controller? subject << " (#{@exception.class})" subject << " #{@exception.message.inspect}" if @options[:verbose_subject] subject = EmailNotifier.normalize_digits(subject) if @options[:normalize_subject] (subject.length > 120) ? subject[0...120] + "..." : subject end def include_controller? @kontroller && @options[:include_controller_and_action_names_in_subject] end def set_data_variables @data.each do |name, value| instance_variable_set(:"@#{name}", value) end end helper_method :inspect_object def truncate(string, max) (string.length > max) ? "#{string[0...max]}..." : string end def inspect_object(object) case object when Hash, Array truncate(object.inspect, 300) else object.to_s end end helper_method :safe_encode def safe_encode(value) value.encode("utf-8", invalid: :replace, undef: :replace, replace: "_") end def html_mail? @options[:email_format] == :html end def compose_email set_data_variables subject = compose_subject name = @env.nil? ? "background_exception_notification" : "exception_notification" exception_recipients = maybe_call(@options[:exception_recipients]) headers = { delivery_method: @options[:delivery_method], to: exception_recipients, from: @options[:sender_address], subject: subject, template_name: name }.merge(@options[:email_headers]) mail = mail(headers) do |format| format.text format.html if html_mail? end mail.delivery_method.settings.merge!(@options[:mailer_settings]) if @options[:mailer_settings] mail end def load_custom_views return unless defined?(Rails) && Rails.respond_to?(:root) prepend_view_path Rails.root.nil? ? "app/views" : "#{Rails.root}/app/views" end def maybe_call(maybe_proc) maybe_proc.respond_to?(:call) ? maybe_proc.call : maybe_proc end end end end def initialize(options) super delivery_method = options[:delivery_method] || :smtp mailer_settings_key = :"#{delivery_method}_settings" options[:mailer_settings] = options.delete(mailer_settings_key) @base_options = DEFAULT_OPTIONS.merge(options) end def call(exception, options = {}) message = create_email(exception, options) message.send(base_options[:deliver_with] || default_deliver_with(message)) end def create_email(exception, options = {}) env = options[:env] send_notice(exception, options, nil, base_options) do |_, default_opts| if env.nil? mailer.background_exception_notification(exception, options, default_opts) else mailer.exception_notification(env, exception, options, default_opts) end end end def self.normalize_digits(string) string.gsub(/[0-9]+/, "N") end private def mailer @mailer ||= Class.new(base_options[:mailer_parent].constantize).tap do |mailer| mailer.extend(EmailNotifier::Mailer) mailer.mailer_name = base_options[:template_path] end end def default_deliver_with(message) # FIXME: use `if Gem::Version.new(ActionMailer::VERSION::STRING) < Gem::Version.new('4.1')` message.respond_to?(:deliver_now) ? :deliver_now : :deliver end end end exception_notification-5.0.1/lib/exception_notifier/sns_notifier.rb0000644000004100000410000000525315045670240026013 0ustar www-datawww-data# frozen_string_literal: true module ExceptionNotifier class SnsNotifier < BaseNotifier def initialize(options) super raise ArgumentError, "You must provide 'region' option" unless options[:region] raise ArgumentError, "You must provide 'access_key_id' option" unless options[:access_key_id] raise ArgumentError, "You must provide 'secret_access_key' option" unless options[:secret_access_key] @notifier = Aws::SNS::Client.new( region: options[:region], access_key_id: options[:access_key_id], secret_access_key: options[:secret_access_key] ) @options = default_options.merge(options) end def call(exception, custom_opts = {}) custom_options = options.merge(custom_opts) subject = build_subject(exception, custom_options) message = build_message(exception, custom_options) notifier.publish( topic_arn: custom_options[:topic_arn], message: message, subject: subject ) end private attr_reader :notifier, :options def build_subject(exception, options) subject = "#{options[:sns_prefix]} - #{accumulated_exception_name(exception, options)} occurred" (subject.length > 120) ? subject[0...120] + "..." : subject end def build_message(exception, options) exception_name = accumulated_exception_name(exception, options) if options[:env].nil? text = "#{exception_name} occured in background\n" data = options[:data] || {} else env = options[:env] kontroller = env["action_controller.instance"] data = (env["exception_notifier.exception_data"] || {}).merge(options[:data] || {}) request = "#{env["REQUEST_METHOD"]} <#{env["REQUEST_URI"]}>" text = "#{exception_name} occurred while #{request}" text += " was processed by #{kontroller.controller_name}##{kontroller.action_name}\n" if kontroller end text += "Exception: #{exception.message}\n" text += "Hostname: #{Socket.gethostname}\n" text += "Data: #{data}\n" return unless exception.backtrace formatted_backtrace = exception.backtrace.first(options[:backtrace_lines]).join("\n").to_s text + "Backtrace:\n#{formatted_backtrace}\n" end def accumulated_exception_name(exception, options) errors_count = options[:accumulated_errors_count].to_i measure_word = if errors_count > 1 errors_count else /^[aeiou]/i.match?(exception.class.to_s) ? "An" : "A" end "#{measure_word} #{exception.class}" end def default_options { sns_prefix: "[ERROR]", backtrace_lines: 10 } end end end exception_notification-5.0.1/lib/exception_notifier/google_chat_notifier.rb0000644000004100000410000000174515045670240027465 0ustar www-datawww-data# frozen_string_literal: true require "httparty" module ExceptionNotifier class GoogleChatNotifier < BaseNotifier def call(exception, opts = {}) options = base_options.merge(opts) formatter = Formatter.new(exception, options) HTTParty.post( options[:webhook_url], body: {text: body(exception, formatter)}.to_json, headers: {"Content-Type" => "application/json"} ) end private def body(exception, formatter) text = [ "\nApplication: *#{formatter.app_name}*", formatter.subtitle, "", formatter.title, "*#{exception.message.tr("`", "'")}*" ] if (request = formatter.request_message.presence) text << "" text << "*Request:*" text << request end if (backtrace = formatter.backtrace_message.presence) text << "" text << "*Backtrace:*" text << backtrace end text.compact.join("\n") end end end exception_notification-5.0.1/lib/exception_notifier/irc_notifier.rb0000644000004100000410000000325015045670240025760 0ustar www-datawww-data# frozen_string_literal: true module ExceptionNotifier class IrcNotifier < BaseNotifier def initialize(options) super @config = OpenStruct.new parse_options(options) end def call(exception, options = {}) errors_count = options[:accumulated_errors_count].to_i occurrences = "(#{errors_count} times)" if errors_count > 1 message = "#{occurrences}'#{exception.message}'" message += " on '#{exception.backtrace.first}'" if exception.backtrace return unless active? send_notice(exception, options, message) do |msg, _| send_message([*@config.prefix, *msg].join(" ")) end end def send_message(message) CarrierPigeon.send @config.irc.merge(message: message) end private def parse_options(options) nick = options.fetch(:nick, "ExceptionNotifierBot") password = options[:password] ? ":#{options[:password]}" : nil domain = options.fetch(:domain, nil) port = options[:port] ? ":#{options[:port]}" : nil channel = options.fetch(:channel, "#log") notice = options.fetch(:notice, false) ssl = options.fetch(:ssl, false) join = options.fetch(:join, false) uri = "irc://#{nick}#{password}@#{domain}#{port}/#{channel}" prefix = options.fetch(:prefix, nil) recipients = options[:recipients] ? options[:recipients].join(", ") + ":" : nil @config.prefix = [*prefix, *recipients].join(" ") @config.irc = {uri: uri, ssl: ssl, notice: notice, join: join} end def active? valid_uri? @config.irc[:uri] end def valid_uri?(uri) URI.parse(uri) rescue URI::InvalidURIError false end end end exception_notification-5.0.1/lib/exception_notifier/slack_notifier.rb0000644000004100000410000000656115045670240026310 0ustar www-datawww-data# frozen_string_literal: true module ExceptionNotifier class SlackNotifier < BaseNotifier include ExceptionNotifier::BacktraceCleaner attr_accessor :notifier def initialize(options) super begin @ignore_data_if = options[:ignore_data_if] @backtrace_lines = options.fetch(:backtrace_lines, 10) @additional_fields = options[:additional_fields] webhook_url = options.fetch(:webhook_url) @message_opts = options.fetch(:additional_parameters, {}) @color = @message_opts.delete(:color) { "danger" } @notifier = Slack::Notifier.new webhook_url, options rescue @notifier = nil end end def call(exception, options = {}) clean_message = exception.message.tr("`", "'") attchs = attchs(exception, clean_message, options) return unless valid? args = [exception, options, clean_message, @message_opts.merge(attachments: attchs)] send_notice(*args) do |_msg, message_opts| message_opts[:channel] = options[:channel] if options.key?(:channel) @notifier.ping "", message_opts end end protected def valid? !@notifier.nil? end def deep_reject(hash, block) hash.each do |k, v| deep_reject(v, block) if v.is_a?(Hash) hash.delete(k) if block.call(k, v) end end private def attchs(exception, clean_message, options) text, data = information_from_options(exception.class, options) backtrace = clean_backtrace(exception) if exception.backtrace fields = fields(clean_message, backtrace, data) [color: @color, text: text, fields: fields, mrkdwn_in: %w[text fields]] end def information_from_options(exception_class, options) errors_count = options[:accumulated_errors_count].to_i measure_word = if errors_count > 1 errors_count else /^[aeiou]/i.match?(exception_class.to_s) ? "An" : "A" end exception_name = "*#{measure_word}* `#{exception_class}`" env = options[:env] options[:headers] ||= {} options[:headers]["Content-Type"] = "application/json" if env.nil? data = options[:data] || {} text = "#{exception_name} *occured in background*\n" else data = (env["exception_notifier.exception_data"] || {}).merge(options[:data] || {}) kontroller = env["action_controller.instance"] request = "#{env["REQUEST_METHOD"]} <#{env["REQUEST_URI"]}>" text = "#{exception_name} *occurred while* `#{request}`" text += " *was processed by* `#{kontroller.controller_name}##{kontroller.action_name}`" if kontroller text += "\n" end [text, data] end def fields(clean_message, backtrace, data) fields = [ {title: "Exception", value: clean_message}, {title: "Hostname", value: Socket.gethostname} ] if backtrace formatted_backtrace = "```#{backtrace.first(@backtrace_lines).join("\n")}```" fields << {title: "Backtrace", value: formatted_backtrace} end unless data.empty? deep_reject(data, @ignore_data_if) if @ignore_data_if.is_a?(Proc) data_string = data.map { |k, v| "#{k}: #{v}" }.join("\n") fields << {title: "Data", value: "```#{data_string}```"} end fields.concat(@additional_fields) if @additional_fields fields end end end exception_notification-5.0.1/lib/exception_notification/0000755000004100000410000000000015045670240023626 5ustar www-datawww-dataexception_notification-5.0.1/lib/exception_notification/rake.rb0000644000004100000410000000330215045670240025073 0ustar www-datawww-data# frozen_string_literal: true # Copied/adapted from https://github.com/airbrake/airbrake/blob/master/lib/airbrake/rake.rb Rake::TaskManager.record_task_metadata = true if Rake.const_defined?(:TaskManager) module ExceptionNotification module RakeTaskExtensions # A wrapper around the original +#execute+, that catches all errors and # passes them on to ExceptionNotifier. def execute(args = nil) super rescue Exception => e # standard:disable Lint/RescueException ExceptionNotifier.notify_exception(e, data: data_for_exception_notifier(e)) unless e.is_a?(SystemExit) raise e end private def data_for_exception_notifier(exception = nil) data = {} data[:error_class] = exception.class.name if exception data[:error_message] = exception.message if exception data[:rake] = {} data[:rake][:rake_command_line] = reconstruct_command_line data[:rake][:name] = name data[:rake][:timestamp] = timestamp.to_s # data[:investigation] = investigation data[:rake][:full_comment] = full_comment if full_comment data[:rake][:arg_names] = arg_names if arg_names.any? data[:rake][:arg_description] = arg_description if arg_description data[:rake][:locations] = locations if locations.any? data[:rake][:sources] = sources if sources.any? if prerequisite_tasks.any? data[:rake][:prerequisite_tasks] = prerequisite_tasks.map do |p| p.__send__(:data_for_exception_notifier)[:rake] end end data end def reconstruct_command_line "rake #{ARGV.join(" ")}" end end end module Rake class Task prepend ExceptionNotification::RakeTaskExtensions end end exception_notification-5.0.1/lib/exception_notification/resque.rb0000644000004100000410000000100015045670240025446 0ustar www-datawww-data# frozen_string_literal: true require "resque/failure/base" module ExceptionNotification class Resque < Resque::Failure::Base def self.count ::Resque::Stat[:failed] end def save data = { error_class: exception.class.name, error_message: exception.message, failed_at: Time.now.to_s, payload: payload, queue: queue, worker: worker.to_s } ExceptionNotifier.notify_exception(exception, data: {resque: data}) end end end exception_notification-5.0.1/lib/exception_notification/rails.rb0000644000004100000410000000167115045670240025272 0ustar www-datawww-data# frozen_string_literal: true # Warning: This must be required after rails but before initializers have been run. If you require # it from config/initializers/exception_notification.rb, then the rails and rake_task callbacks # registered here will have no effect, because Rails will have already invoked all registered rails # and rake_tasks handlers. module ExceptionNotification class Engine < ::Rails::Engine config.exception_notification = ExceptionNotifier config.exception_notification.logger = Rails.logger config.exception_notification.error_grouping_cache = Rails.cache config.app_middleware.use ExceptionNotification::Rack rake_tasks do # Report exceptions occurring in Rake tasks. require "exception_notification/rake" end runner do # Report exceptions occurring in runner commands. require "exception_notification/rails/runner_tie" Rails::RunnerTie.new.call end end end exception_notification-5.0.1/lib/exception_notification/sidekiq.rb0000644000004100000410000000064515045670240025611 0ustar www-datawww-data# frozen_string_literal: true require "sidekiq" ::Sidekiq.configure_server do |config| config.error_handlers << proc do |ex, context, config| # Before Sidekiq 7.1.5 the config was not passed to the proc if config ExceptionNotifier.notify_exception(ex, data: {sidekiq: {context: context, config: config}}) else ExceptionNotifier.notify_exception(ex, data: {sidekiq: context}) end end end exception_notification-5.0.1/lib/exception_notification/version.rb0000644000004100000410000000012415045670240025635 0ustar www-datawww-data# frozen_string_literal: true module ExceptionNotification VERSION = "5.0.1" end exception_notification-5.0.1/lib/exception_notification/rack.rb0000644000004100000410000000461215045670240025076 0ustar www-datawww-data# frozen_string_literal: true module ExceptionNotification class Rack class CascadePassException < RuntimeError; end def initialize(app, options = {}) @app = app ExceptionNotifier.tap do |en| en.ignored_exceptions = options.delete(:ignore_exceptions) if options.key?(:ignore_exceptions) en.error_grouping = options.delete(:error_grouping) if options.key?(:error_grouping) en.error_grouping_period = options.delete(:error_grouping_period) if options.key?(:error_grouping_period) en.notification_trigger = options.delete(:notification_trigger) if options.key?(:notification_trigger) if options.key?(:error_grouping_cache) en.error_grouping_cache = options.delete(:error_grouping_cache) elsif defined?(Rails) && Rails.respond_to?(:cache) en.error_grouping_cache = Rails.cache end end if options.key?(:ignore_if) rack_ignore = options.delete(:ignore_if) ExceptionNotifier.ignore_if do |exception, opts| opts.key?(:env) && rack_ignore.call(opts[:env], exception) end end if options.key?(:ignore_notifier_if) rack_ignore_by_notifier = options.delete(:ignore_notifier_if) rack_ignore_by_notifier.each do |notifier, proc| ExceptionNotifier.ignore_notifier_if(notifier) do |exception, opts| opts.key?(:env) && proc.call(opts[:env], exception) end end end ExceptionNotifier.ignore_crawlers(options.delete(:ignore_crawlers)) if options.key?(:ignore_crawlers) @ignore_cascade_pass = options.delete(:ignore_cascade_pass) { true } options.each do |notifier_name, opts| ExceptionNotifier.register_exception_notifier(notifier_name, opts) end end def call(env) _, headers, = response = @app.call(env) if !@ignore_cascade_pass && headers["X-Cascade"] == "pass" msg = "This exception means that the preceding Rack middleware set the 'X-Cascade' header to 'pass' -- in " \ "Rails, this often means that the route was not found (404 error)." raise CascadePassException, msg end response rescue Exception => e # standard:disable Lint/RescueException env["exception_notifier.delivered"] = true if ExceptionNotifier.notify_exception(e, env: env) raise e unless e.is_a?(CascadePassException) response end end end exception_notification-5.0.1/lib/exception_notification/rails/0000755000004100000410000000000015045670240024740 5ustar www-datawww-dataexception_notification-5.0.1/lib/exception_notification/rails/runner_tie.rb0000644000004100000410000000174415045670240027445 0ustar www-datawww-data# frozen_string_literal: true module ExceptionNotification module Rails class RunnerTie # Registers an at_exit callback, which checks if there was an exception. This is a pretty # crude way to detect exceptions from runner commands, but Rails doesn't provide a better API. # # This should only be called from a runner callback in your Rails config; otherwise you may # register the at_exit callback in more places than you need or want it. def call at_exit do exception = $ERROR_INFO if exception && !exception.is_a?(SystemExit) ExceptionNotifier.notify_exception(exception, data: data_for_exception_notifier(exception)) end end end private def data_for_exception_notifier(exception = nil) data = {} data[:error_class] = exception.class.name if exception data[:error_message] = exception.message if exception data end end end end exception_notification-5.0.1/lib/exception_notification.rb0000644000004100000410000000062115045670240024152 0ustar www-datawww-data# frozen_string_literal: true require "exception_notifier" require "exception_notification/rack" require "exception_notification/version" module ExceptionNotification # Alternative way to setup ExceptionNotification. # Run 'rails generate exception_notification:install' to create # a fresh initializer with all configuration values. def self.configure yield ExceptionNotifier end end exception_notification-5.0.1/lib/exception_notifier.rb0000644000004100000410000001457615045670240023321 0ustar www-datawww-data# frozen_string_literal: true require "logger" require "active_support/core_ext/string/inflections" require "active_support/core_ext/module/attribute_accessors" require "exception_notifier/base_notifier" require "exception_notifier/modules/error_grouping" module ExceptionNotifier include ErrorGrouping autoload :BacktraceCleaner, "exception_notifier/modules/backtrace_cleaner" autoload :Formatter, "exception_notifier/modules/formatter" autoload :Notifier, "exception_notifier/notifier" autoload :EmailNotifier, "exception_notifier/email_notifier" autoload :HipchatNotifier, "exception_notifier/hipchat_notifier" autoload :WebhookNotifier, "exception_notifier/webhook_notifier" autoload :IrcNotifier, "exception_notifier/irc_notifier" autoload :SlackNotifier, "exception_notifier/slack_notifier" autoload :MattermostNotifier, "exception_notifier/mattermost_notifier" autoload :TeamsNotifier, "exception_notifier/teams_notifier" autoload :SnsNotifier, "exception_notifier/sns_notifier" autoload :GoogleChatNotifier, "exception_notifier/google_chat_notifier" autoload :DatadogNotifier, "exception_notifier/datadog_notifier" class UndefinedNotifierError < StandardError; end # Define logger mattr_accessor :logger @@logger = Logger.new($stdout) # Define a set of exceptions to be ignored, ie, dont send notifications when any of them are raised. mattr_accessor :ignored_exceptions @@ignored_exceptions = %w[ ActiveRecord::RecordNotFound Mongoid::Errors::DocumentNotFound AbstractController::ActionNotFound ActionController::RoutingError ActionController::UnknownFormat ActionController::UrlGenerationError ActionDispatch::Http::MimeNegotiation::InvalidType Rack::Utils::InvalidParameterError ] mattr_accessor :testing_mode @@testing_mode = false class << self # Store conditions that decide when exceptions must be ignored or not. @@ignores = [] # Store by-notifier conditions that decide when exceptions must be ignored or not. @@by_notifier_ignores = {} # Store notifiers that send notifications when exceptions are raised. @@notifiers = {} def testing_mode! self.testing_mode = true end def notify_exception(exception, options = {}, &block) return false if ignored_exception?(options[:ignore_exceptions], exception) return false if ignored?(exception, options) if error_grouping errors_count = group_error!(exception, options) return false unless send_notification?(exception, errors_count) end notification_fired = false selected_notifiers = options.delete(:notifiers) || notifiers [*selected_notifiers].each do |notifier| unless notifier_ignored?(exception, options, notifier: notifier) fire_notification(notifier, exception, options.dup, &block) notification_fired = true end end notification_fired end def register_exception_notifier(name, notifier_or_options) if notifier_or_options.respond_to?(:call) @@notifiers[name] = notifier_or_options elsif notifier_or_options.is_a?(Hash) create_and_register_notifier(name, notifier_or_options) else raise ArgumentError, "Invalid notifier '#{name}' defined as #{notifier_or_options.inspect}" end end alias_method :add_notifier, :register_exception_notifier def unregister_exception_notifier(name) @@notifiers.delete(name) end def registered_exception_notifier(name) @@notifiers[name] end def notifiers @@notifiers.keys end # Adds a condition to decide when an exception must be ignored or not. # # ExceptionNotifier.ignore_if do |exception, options| # not Rails.env.production? # end def ignore_if(&block) @@ignores << block end def ignore_notifier_if(notifier, &block) @@by_notifier_ignores[notifier] = block end def ignore_crawlers(crawlers) ignore_if do |_exception, opts| opts.key?(:env) && from_crawler(opts[:env], crawlers) end end def clear_ignore_conditions! @@ignores.clear @@by_notifier_ignores.clear end private def ignored?(exception, options) @@ignores.any? { |condition| condition.call(exception, options) } rescue Exception => e # standard:disable Lint/RescueException raise e if @@testing_mode logger.warn( "An error occurred when evaluating an ignore condition. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}" ) false end def notifier_ignored?(exception, options, notifier:) return false unless @@by_notifier_ignores.key?(notifier) condition = @@by_notifier_ignores[notifier] condition.call(exception, options) rescue Exception => e # standard:disable Lint/RescueException raise e if @@testing_mode logger.warn(<<~"MESSAGE") An error occurred when evaluating a by-notifier ignore condition. #{e.class}: #{e.message} #{e.backtrace.join("\n")} MESSAGE false end def ignored_exception?(ignore_array, exception) all_ignored_exceptions = (Array(ignored_exceptions) + Array(ignore_array)).map(&:to_s) exception_ancestors = exception.singleton_class.ancestors.map(&:to_s) !(all_ignored_exceptions & exception_ancestors).empty? end def fire_notification(notifier_name, exception, options, &block) notifier = registered_exception_notifier(notifier_name) notifier.call(exception, options, &block) rescue Exception => e # standard:disable Lint/RescueException raise e if @@testing_mode logger.warn( "An error occurred when sending a notification using '#{notifier_name}' notifier." \ "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}" ) false end def create_and_register_notifier(name, options) notifier_classname = "#{name}_notifier".camelize notifier_class = ExceptionNotifier.const_get(notifier_classname) notifier = notifier_class.new(options) register_exception_notifier(name, notifier) rescue NameError => e raise UndefinedNotifierError, "No notifier named '#{name}' was found. Please, revise your configuration options. Cause: #{e.message}" end def from_crawler(env, ignored_crawlers) agent = env["HTTP_USER_AGENT"] Array(ignored_crawlers).any? do |crawler| agent =~ Regexp.new(crawler) end end end end exception_notification-5.0.1/Rakefile0000644000004100000410000000070615045670240017764 0ustar www-datawww-data# frozen_string_literal: true require "rubygems" require "bundler/setup" require "irb" Bundler::GemHelper.install_tasks require "rake/testtask" task default: [:test] Rake::TestTask.new(:test) do |t| t.libs << "lib" t.libs << "test" t.pattern = "test/**/*_test.rb" t.warning = false end desc "Start a console with the gem" task :console do ARGV.clear puts "ExceptionNotification #{ExceptionNotification::VERSION} loaded." IRB.start end exception_notification-5.0.1/CODE_OF_CONDUCT.md0000644000004100000410000000353615045670240021122 0ustar www-datawww-data# Contributor Code of Conduct As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery * Personal attacks * Trolling or insulting/derogatory comments * Public or private harassment * Publishing other's private information, such as physical or electronic addresses, without explicit permission * Other unethical or unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant, version 1.2.0](http://contributor-covenant.org/version/1/2/0/). exception_notification-5.0.1/CHANGELOG.rdoc0000644000004100000410000001753415045670240020466 0ustar www-datawww-data== Unreleased nil == 5.0.1 * Change packaged gem files. Remove unnecessary files which cause warnings in some envs. #9 (@brianclinkenbeard) == 5.0.0 * Switch linter to Standardrb. Remove Rubocop. * Change default ruby version to 3.4.2. * Drop support for old Rails and Ruby versions. Require Rails 7.1 and Ruby 3.2 at least. * Add two Rails exceptions to the ignore list: ActionDispatch::Http::MimeNegotiation::InvalidType Rack::Utils::InvalidParameterError * Fix some warnings and patches around sidekiq. * General repo tidying and updating. (All by @kmcphillips) == 4.6.0 https://github.com/smartinez87/exception_notification/releases/tag/v4.6.0 * Rails 8 compatibility (@kmcphillips) * Exception data for teams channel notification (@rachitpant) * Report exceptions occurring in Rake tasks and runner commands (@TylerRick) * suggest Rails.env.local? in ignore_if block (@glaszig) * Improve compatibility with frozen string literal (@Throne3d) * Remove unnecessary :channel option from Slack guide doc (@westonganger) * Add Content-Type header to Slack notifier - req. for discord (@cdadityang) == 4.5.0 * enhancements * Added Rails 7 compatibility (by @fwininger) * Added support for the optional `data` attribute to the SNS notifier (@TomK32) * Addressed a deprecation warning for `module_parent_name` which was thrown for users using Rails > 6.x (@quorak) * Restored the hash separator for `controller#action` in the email notifier (@garethrees) * removals * Dropped support for Tinder (gem is no longer maintained) (by @fwininger) * Dropped support for Ruby on Rails versions below 5.2 == 4.4.3 * big fixes * Remove using configured default from address from custom mailer_parent class == 4.4.2 (yanked) * bug fixes * Fix `sender_address` being overwritten == 4.4.1 * enhancements * Enhance `ignore_if` option to allow by-notifier customization (by @fursich) * Ignore extended modules of ignored exceptions (by @elengine) * Add `exception_data` to Mattermost notifier (by @camillof) * bug fixes * Fix Rubocop offenses (by @nicolasferraro) == 4.4.0 * enhancements * Rails 6 compatibility (by @shanecav) * Add Datadog notifier (by @ajain0184) * Use backtrace cleaner for Slack notifications (by @pomier) * Add slack channel name override option (by @chaadow) * Addition of sample application for testing purposes (by @ampeigonet) * bug fixes * Fix error in Resque failure backend (by @EmilioCristalli) * Remove sqlite dependency (by @EmilioCristalli) * Configure ignore_crawlers from Rails initializer (by @EmilioCristalli) == 4.3.0 * enhancements * Add Microsoft Teams Notifier (by @phaelin) * Add SNS notifier (by @FLarra) * Add Google Chats notifier (by @renatolond) * Align output of section-headers consistently (by @kronn) * ExceptionNotifier.notify_exception receives block & pass it to each notifier (by @pocke) * Update Travis to latest rubies (by @lostapathy) * bug fixes * Replace all before_filter to before_action on readme (by @pastullo) * Fix error when using error grouping outside of rails (by @garethcokell) * Fix missing MissingController Mattermost class (by @n-rodriguez) == 4.2.2 * enhancements * Error grouping (by @Martin91) * Additional fields for Slack support (by @schurig) * Enterprise HipChat support (by @seanhuber) == 4.2.1 * enhancements * Allow customizable backtrace for Slack (by @aried3r) * Add `Mongoid::Errors::DocumentNotFound` to ignored_exceptions (by @nazarok) * Improved text in Slack notifier (by @vojtad) * bug fixes * Fix data being sent on webhook notifier == 4.2.0 * enhancements * update URL in gemspec (by @ktdreyer) * Add `hostname` to Slack notifier (by @juanazam) * Allow `exception_recipients` to be a proc (by @kellyjosephprice) * Add Mattermost integration (by @Aschen) * Rails 5 compatible * bug fixes * Fix error when showing timestamp on non Rails apps * Fix delivery failure when deliver_with specified (by @grzuy) == 4.1.4 * bug fixes * HTML-escape exception messages sent to hipchat (by @gburt) * Send the correct options in send_notice (by @pcboy) == 4.1.3 * enhancements * Add a way to have a backtrace callback on notifiers (by @pcboy) * bug fixes * Fix incompatible character encodings error (by @san650) == 4.1.2 * enhancements * Change format of Slack notifications (by @eldano) == 4.1.1 * bug fixes * Alternate way to monkeypatch (by @joshco) * Fix BacktraceCleaner namespacing (by @esdlocomb) == 4.1.0 * enhancements * Add support for Sidekiq 3.0 (by @mbrictson) * Add IRC notifier (by @nathanjsharpe) * Add ActionController::UnknownFormat to default ignored exceptions (by @rezwyi) * Add message_template option to HipChat notifier (by @makimoto) * Add support for HipChat APIv2 (by @michaelherold) * Add Slack notifier (by @martin1keogh) * Add option for notifying on `X-Cascade` header (by @etipton) * Improve backtrace data (by @munkius) * bug fixes * Fix `Rails.root` exception (by @hovatterz) * Fix email notifier in Sinatra (by @betesh) == 4.0.1 * enhancements * Add HipChat notifier (by @j15e) * Log backtrace when notification fails * Send more info in Webhook notifier * Add HTTP method to request section == 4.0.0 * enhancements * Be able to override delivery_method (by @jweslley) * Add logger to log when notifications cannot be shiped (by @jweslley) * Add Rails generator to create an initializer file (by @jweslley) * Add rails engine (by @jweslley) * Add sidekiq support (by @jweslley) * Add resque support (by @jweslley) * Better style for html views (by @jweslley) * Support customizable Mailer class (by @Bishop) * Turn ExceptionNotification Rails agnostic (by @jweslley) * Support custom ignore exceptions for background notifications (by @jweslley) * Be able to implement custom notifiers (by @jweslley) * Add Webhook notifier (by @jweslley) * Rails 4 compatible * bug fixes * Don't error if Rails isn't defined. (by @dpogue) * Fix call to #normalize_digits (by @ghiculescu) == 3.0.1 * enhancements * Custom Headers (by @DouweM) * Make Tinder a soft-dependency (by @fgrehm) * bug fixes * Fix `code converter not found` (by @alanjds) == 3.0.0 * enhancements * Campfire integration * Support for HTML notifications (by @Xenofex) * Be able to override SMTP settings (by @piglop and @Macint) * bug fixes * Fix encoding issues * Allow default sections to be overridden (by @jfarmer) * Don't automatically deliver background notifications == 2.6.1 * bug fixes * Fix finding custom sections on Background notifications. Fixes [#68] == 2.6.0 * enhancements * Avoid raising exception on dev mode * Add ignore_if option to avoid sending certain notifications. * Add normalize_subject option to remove numbers from email so that they thread (by @jjb) * Allow the user to provide a custom message and hash of data (by @jjb) * Add support for configurable background sections and a data partial (by @jeffrafter) * Include timestamp of exception in notification body * Add support for rack based session management (by @phoet) * Add ignore_crawlers option to ignore exceptions generated by crawlers * Add verbode_subject option to exclude exception message from subject (by @amishyn) * bug fixes * Correctly set view path at the right time so that new sections are properly available (by @scrozier) * Fix handling exceptions with no backtrace * Fix issue on Solaris with hostname (by @bluescripts) * Ensure exceptions in view templates doesn't cause problems, allowing the notification to be sent anyway (by @sce) == 2.5.1 * bug fixes * Fix lib references on gemspec == 2.5.0 * enhancements * Add Background Notifications * bug fixes * Filter session_id on secure requests == 2.4.1 * enhancements * Use values set for the middleware as defaults * bug fixes * Avoid sending emails with large subjects * Avoid having to add 'require' option on gem configuration exception_notification-5.0.1/docs/0000755000004100000410000000000015045670240017244 5ustar www-datawww-dataexception_notification-5.0.1/docs/notifiers/0000755000004100000410000000000015045670240021246 5ustar www-datawww-dataexception_notification-5.0.1/docs/notifiers/irc.md0000644000004100000410000000503715045670240022352 0ustar www-datawww-data### IRC notifier This notifier sends notifications to an IRC channel using the carrier-pigeon gem. #### Usage Just add the [carrier-pigeon](https://github.com/portertech/carrier-pigeon) gem to your `Gemfile`: ```ruby gem 'carrier-pigeon' ``` To configure it, you need to set at least the 'domain' option, like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, irc: { domain: 'irc.example.com' } ``` There are several other options, which are described below. For example, to use ssl and a password, add a prefix, post to the '#log' channel, and include recipients in the message (so that they will be notified), your configuration might look like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, irc: { domain: 'irc.example.com', nick: 'BadNewsBot', password: 'secret', port: 6697, channel: '#log', ssl: true, prefix: '[Exception Notification]', recipients: ['peter', 'michael', 'samir'] } ``` #### Options ##### domain *String, required* The domain name of your IRC server. ##### nick *String, optional* The message will appear from this nick. Default : 'ExceptionNotifierBot'. ##### password *String, optional* Password for your IRC server. ##### port *String, optional* Port your IRC server is listening on. Default : 6667. ##### channel *String, optional* Message will appear in this channel. Default : '#log'. ##### notice *Boolean, optional* Send a notice. Default : false. ##### ssl *Boolean, optional* Whether to use SSL. Default : false. ##### join *Boolean, optional* Join a channel. Default : false. ##### recipients *Array of strings, optional* Nicks to include in the message. Default: [] exception_notification-5.0.1/docs/notifiers/campfire.md0000644000004100000410000000263115045670240023360 0ustar www-datawww-data### Campfire notifier This notifier sends notifications to your Campfire room. #### Usage Just add the [tinder](https://github.com/collectiveidea/tinder) gem to your `Gemfile`: ```ruby gem 'tinder' ``` To configure it, you need to set the `subdomain`, `token` and `room_name` options, like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, campfire: { subdomain: 'my_subdomain', token: 'my_token', room_name: 'my_room' } ``` #### Options ##### subdomain *String, required* Your subdomain at Campfire. ##### room_name *String, required* The Campfire room where the notifications must be published to. ##### token *String, required* The API token to allow access to your Campfire account. For more options to set Campfire, like _ssl_, check [here](https://github.com/collectiveidea/tinder/blob/master/lib/tinder/campfire.rb#L17). exception_notification-5.0.1/docs/notifiers/google_chat.md0000644000004100000410000000154415045670240024047 0ustar www-datawww-data### Google Chat Notifier Post notifications in a Google Chats channel via [incoming webhook](https://developers.google.com/hangouts/chat/how-tos/webhooks) Add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`: ```ruby gem 'httparty' ``` To configure it, you **need** to set the `webhook_url` option. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, google_chat: { webhook_url: 'https://chat.googleapis.com/v1/spaces/XXXXXXXX/messages?key=YYYYYYYYYYYYY&token=ZZZZZZZZZZZZ' } ``` ##### webhook_url *String, required* The Incoming WebHook URL on Google Chats. ##### app_name *String, optional* Your application name, shown in the notification. Defaults to `Rails.application.class.module_parent_name.underscore` for Rails versions >= 6; `Rails.application.class.parent_name.underscore` otherwise. exception_notification-5.0.1/docs/notifiers/webhook.md0000644000004100000410000000536015045670240023232 0ustar www-datawww-data### WebHook notifier This notifier ships notifications over the HTTP protocol. #### Usage Just add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`: ```ruby gem 'httparty' ``` To configure it, you need to set the `url` option, like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, webhook: { url: 'http://domain.com:5555/hubot/path' } ``` By default, the WebhookNotifier will call the URLs using the POST method. But, you can change this using the `http_method` option. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, webhook: { url: 'http://domain.com:5555/hubot/path', http_method: :get } ``` Besides the `url` and `http_method` options, all the other options are passed directly to HTTParty. Thus, if the HTTP server requires authentication, you can include the following options: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, webhook: { url: 'http://domain.com:5555/hubot/path', basic_auth: { username: 'alice', password: 'password' } } ``` For more HTTParty options, check out the [documentation](https://github.com/jnunemaker/httparty). exception_notification-5.0.1/docs/notifiers/mattermost.md0000644000004100000410000001115415045670240023771 0ustar www-datawww-data### Mattermost notifier Post notification in a mattermost channel via [incoming webhook](http://docs.mattermost.com/developer/webhooks-incoming.html) Just add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`: ```ruby gem 'httparty' ``` To configure it, you **need** to set the `webhook_url` option. You can also specify an other channel with `channel` option. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, mattermost: { webhook_url: 'http://your-mattermost.com/hooks/blablabla', channel: 'my-channel' } ``` If you are using Github or Gitlab for issues tracking, you can specify `git_url` as follow to add a *Create issue* link in you notification. By default this will use your Rails application name to match the git repository. If yours differ you can specify `app_name`. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, mattermost: { webhook_url: 'http://your-mattermost.com/hooks/blablabla', git_url: 'github.com/aschen' } ``` You can also specify the bot name and avatar with `username` and `avatar` options. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: 'PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, mattermost: { webhook_url: 'http://your-mattermost.com/hooks/blablabla', avatar: 'http://example.com/your-image.png', username: 'Fail bot' } ``` Finally since the notifier use HTTParty, you can include all HTTParty options, like basic_auth for example. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, mattermost: { webhook_url: 'http://your-mattermost.com/hooks/blablabla', basic_auth: { username: 'clara', password: 'password' } } ``` #### Options ##### webhook_url *String, required* The Incoming WebHook URL on mattermost. ##### channel *String, optional* Message will appear in this channel. Defaults to the channel you set as such on mattermost. ##### username *String, optional* Username of the bot. Defaults to "Incoming Webhook" ##### avatar *String, optional* Avatar of the bot. Defaults to incoming webhook icon. ##### git_url *String, optional* Url of your gitlab or github with your organisation name for issue creation link (Eg: `github.com/aschen`). Defaults to nil and don't add link to the notification. ##### app_name *String, optional* Your application name used for issue creation link. Defaults to `Rails.application.class.module_parent_name.underscore` for Rails versions >= 6; `Rails.application.class.parent_name.underscore` otherwise. exception_notification-5.0.1/docs/notifiers/datadog.md0000644000004100000410000000200315045670240023166 0ustar www-datawww-data### Datadog notifier This notifier sends error events to Datadog using the [Dogapi](https://github.com/DataDog/dogapi-rb) gem. #### Usage Just add the [Dogapi](https://github.com/DataDog/dogapi-rb) gem to your `Gemfile`: ```ruby gem 'dogapi' ``` To use datadog notifier, you first need to create a `Dogapi::Client` with your datadog api and application keys, like this: ```ruby client = Dogapi::Client.new(api_key, application_key) ``` You then need to set the `client` option, like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: "[PREFIX] ", sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, datadog: { client: client } ``` #### Options ##### client *DogApi::Client, required* The API client to send events to Datadog. ##### title_prefix *String, optional* Prefix for event title in Datadog. ##### tags *Array of Strings, optional* Optional tags for events in Datadog. exception_notification-5.0.1/docs/notifiers/hipchat.md0000644000004100000410000000311215045670240023205 0ustar www-datawww-data### HipChat notifier This notifier sends notifications to your Hipchat room. #### Usage Just add the [hipchat](https://github.com/hipchat/hipchat-rb) gem to your `Gemfile`: ```ruby gem 'hipchat' ``` To configure it, you need to set the `token` and `room_name` options, like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, hipchat: { api_token: 'my_token', room_name: 'my_room' } ``` #### Options ##### room_name *String, required* The HipChat room where the notifications must be published to. ##### api_token *String, required* The API token to allow access to your HipChat account. ##### notify *Boolean, optional* Notify users. Default : false. ##### color *String, optional* Color of the message. Default : 'red'. ##### from *String, optional, maximum length : 15* Message will appear from this nickname. Default : 'Exception'. ##### server_url *String, optional* Custom Server URL for self-hosted, Enterprise HipChat Server For all options & possible values see [Hipchat API](https://www.hipchat.com/docs/api/method/rooms/message). exception_notification-5.0.1/docs/notifiers/slack.md0000644000004100000410000001207715045670240022674 0ustar www-datawww-data### Slack notifier This notifier sends notifications to a slack channel using the slack-notifier gem. #### Usage Just add the [slack-notifier](https://github.com/stevenosloan/slack-notifier) gem to your `Gemfile`: ```ruby gem 'slack-notifier' ``` To configure it, you need to set at least the 'webhook_url' option, like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, slack: { webhook_url: '[Your webhook url]', additional_parameters: { icon_url: 'http://image.jpg', mrkdwn: true } } ``` The slack notification will include any data saved under `env['exception_notifier.exception_data']`. An example of how to send the server name to Slack in Rails (put this code in application_controller.rb): ```ruby before_action :set_notification def set_notification request.env['exception_notifier.exception_data'] = { 'server' => request.env['SERVER_NAME'] } # can be any key-value pairs end ``` If you find this too verbose, you can determine to exclude certain information by doing the following: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, slack: { webhook_url: '[Your webhook url]', channel: '#exceptions', additional_parameters: { icon_url: 'http://image.jpg', mrkdwn: true }, ignore_data_if: lambda {|key, value| "#{key}" == 'key_to_ignore' || value.is_a?(ClassToBeIgnored) } } ``` Any evaluation to `true` will cause the key / value pair not be be sent along to Slack. the `slack-notifier` gem allows to override the channel default value, if you ever need to send a notification to a different slack channel. Simply add the `channel` option when calling `.notify_exception` ```ruby ExceptionNotifier.notify_exception( exception, env: request.env, channel: '#my-custom-channel', # Make sure the channel name starts with `#` data: { error: error_variable, server: server_name } ) ``` If you ever need to add more `slack-notifier` specific options, and particularly to the `#ping` method of the slack notifier, you can use the `pre_callback` option when defining the middleware. ```ruby pre_callback: proc { |opts, _notifier, _backtrace, _message, message_opts| message_opts[:channel] = opts[:channel] if opts.key?(:channel) } ``` - `message_opts` is the hash you want to append to if you need to add an option. - `options` is the hash containing the values when you call `ExceptionNotification.notify_exception` An example implementation would be: ```ruby config.middleware.use ExceptionNotification::Rack, slack: { webhook_url: '[Your webhook url]', pre_callback: proc { |opts, _notifier, _backtrace, _message, message_opts| message_opts[:ping_option] = opts[:ping_option] if opts.key?(:ping_option) } }, error_grouping: true ``` Then when calling from within your application code: ```ruby ExceptionNotifier.notify_exception( exception, env: request.env, ping_option: 'value', # this will be passed to the slack notifier's `#ping` # method, as a parameter. The `:pre_callback` hook will catch it # and do that for you. # Helpful, if the API evolves, you only need to update # the `slack-notifier` gem data: { error: error_variable, server: server_name } ) ``` #### Options ##### webhook_url *String, required* The Incoming WebHook URL on slack. ##### username *String, optional* Username of the bot. Defaults to the name you set as such on slack ##### custom_hook *String, optional* Custom hook name. See [slack-notifier](https://github.com/stevenosloan/slack-notifier#custom-hook-name) for more information. Default: 'incoming-webhook' ##### additional_parameters *Hash of strings, optional* Contains additional payload for a message (e.g avatar, attachments, etc). See [slack-notifier](https://github.com/stevenosloan/slack-notifier#additional-parameters) for more information.. Default: '{}' ##### additional_fields *Array of Hashes, optional* Contains additional fields that will be added to the attachement. See [Slack documentation](https://api.slack.com/docs/message-attachments). exception_notification-5.0.1/docs/notifiers/custom.md0000644000004100000410000000326115045670240023104 0ustar www-datawww-data### Custom notifier Simply put, notifiers are objects which respond to `#call(exception, options)` method. Thus, a lambda can be used as a notifier as follow: ```ruby ExceptionNotifier.add_notifier :custom_notifier_name, ->(exception, options) { puts "Something goes wrong: #{exception.message}"} ``` More advanced users or third-party framework developers, also can create notifiers to be shipped in gems and take advantage of ExceptionNotification's Notifier API to standardize the [various](https://github.com/airbrake/airbrake) [solutions](https://www.honeybadger.io) [out](http://www.exceptional.io) [there](https://bugsnag.com). For this, beyond the `#call(exception, options)` method, the notifier class MUST BE defined under the ExceptionNotifier namespace and its name sufixed by `Notifier`, e.g: ExceptionNotifier::SimpleNotifier. #### Example Define the custom notifier: ```ruby module ExceptionNotifier class SimpleNotifier def initialize(options) # do something with the options... end def call(exception, options={}) # send the notification end end end ``` Using it: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, simple: { # simple notifier options } ``` exception_notification-5.0.1/docs/notifiers/teams.md0000644000004100000410000000353715045670240022711 0ustar www-datawww-data### Teams notifier Post notification in a Microsoft Teams channel via [Incoming Webhook Connector](https://docs.microsoft.com/en-us/outlook/actionable-messages/actionable-messages-via-connectors) Just add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`: ```ruby gem 'httparty' ``` To configure it, you **need** to set the `webhook_url` option. If you are using GitLab for issue tracking, you can specify `git_url` as follows to add a *Create issue* button in your notification. By default this will use your Rails application name to match the git repository. If yours differs, you can specify `app_name`. By that same notion, you may also set a `jira_url` to get a button that will send you to the New Issue screen in Jira. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: "[PREFIX] ", sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, teams: { webhook_url: 'https://outlook.office.com/webhook/your-guid/IncomingWebhook/team-guid', git_url: 'https://your-gitlab.com/Group/Project', jira_url: 'https://your-jira.com' } ``` #### Options ##### webhook_url *String, required* The Incoming WebHook URL on Teams. ##### git_url *String, optional* Url of your gitlab or github with your organisation name for issue creation link (Eg: `github.com/aschen`). Defaults to nil and doesn't add link to the notification. ##### jira_url *String, optional* Url of your Jira instance, adds button for Create Issue screen. Defaults to nil and doesn't add a button to the card. ##### app_name *String, optional* Your application name used for git issue creation link. Defaults to `Rails.application.class.module_parent_name.underscore` for Rails versions >= 6; `Rails.application.class.parent_name.underscore` otherwise. exception_notification-5.0.1/docs/notifiers/email.md0000644000004100000410000002070715045670240022665 0ustar www-datawww-data### Email notifier The Email notifier sends notifications by email. The notifications/emails sent includes information about the current request, session, and environment, and also gives a backtrace of the exception. After an exception notification has been delivered the rack environment variable `exception_notifier.delivered` will be set to true. #### ActionMailer configuration For the email to be sent, there must be a default ActionMailer `delivery_method` setting configured. If you do not have one, you can use the following code (assuming your app server machine has `sendmail`). Depending on the environment you want ExceptionNotification to run in, put the following code in your `config/environments/production.rb` and/or `config/environments/development.rb`: ```ruby config.action_mailer.delivery_method = :sendmail # Defaults to: # config.action_mailer.sendmail_settings = { # location: '/usr/sbin/sendmail', # arguments: '-i -t' # } config.action_mailer.perform_deliveries = true config.action_mailer.raise_delivery_errors = true ``` #### Options ##### sender_address *String, default: %("Exception Notifier" )* Who the message is from. ##### exception_recipients *String/Array of strings/Proc, default: []* Who the message is destined for, can be a string of addresses, an array of addresses, or it can be a proc that returns a string of addresses or an array of addresses. The proc will be evaluated when the mail is sent. ##### email_prefix *String, default: [ERROR]* The subject's prefix of the message. ##### sections *Array of strings, default: %w(request session environment backtrace)* By default, the notification email includes four parts: request, session, environment, and backtrace (in that order). You can customize how each of those sections are rendered by placing a partial named for that part in your `app/views/exception_notifier` directory (e.g., `_session.rhtml`). Each partial has access to the following variables: ```ruby @kontroller # the controller that caused the error @request # the current request object @exception # the exception that was raised @backtrace # a sanitized version of the exception's backtrace @data # a hash of optional data values that were passed to the notifier @sections # the array of sections to include in the email ``` You can reorder the sections, or exclude sections completely, by using `sections` option. You can even add new sections that describe application-specific data--just add the section's name to the list (wherever you'd like), and define the corresponding partial. Like the following example with two new added sections: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com}, sections: %w{my_section1 my_section2} } ``` Place your custom sections under `./app/views/exception_notifier/` with the suffix `.text.erb`, e.g. `./app/views/exception_notifier/_my_section1.text.erb`. If your new section requires information that isn't available by default, make sure it is made available to the email using the `exception_data` macro: ```ruby class ApplicationController < ActionController::Base before_action :log_additional_data ... protected def log_additional_data request.env['exception_notifier.exception_data'] = { document: @document, person: @person } end ... end ``` In the above case, `@document` and `@person` would be made available to the email renderer, allowing your new section(s) to access and display them. See the existing sections defined by the plugin for examples of how to write your own. ##### background_sections *Array of strings, default: %w(backtrace data)* When using [background notifications](#background-notifications) some variables are not available in the views, like `@kontroller` and `@request`. Thus, you may want to include different sections for background notifications: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com}, background_sections: %w{my_section1 my_section2 backtrace data} } ``` ##### email_headers *Hash of strings, default: {}* Additionally, you may want to set customized headers on the outcoming emails. To do so, simply use the `:email_headers` option: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: "[PREFIX] ", sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com}, email_headers: { "X-Custom-Header" => "foobar" } } ``` ##### verbose_subject *Boolean, default: true* If enabled, include the exception message in the subject. Use `verbose_subject: false` to exclude it. ##### normalize_subject *Boolean, default: false* If enabled, remove numbers from subject so they thread as a single one. Use `normalize_subject: true` to enable it. ##### include_controller_and_action_names_in_subject *Boolean, default: true* If enabled, include the controller and action names in the subject. Use `include_controller_and_action_names_in_subject: false` to exclude them. ##### email_format *Symbol, default: :text* By default, ExceptionNotification sends emails in plain text, in order to sends multipart notifications (aka HTML emails) use `email_format: :html`. ##### delivery_method *Symbol, default: :smtp* By default, ExceptionNotification sends emails using the ActionMailer configuration of the application. In order to send emails by another delivery method, use the `delivery_method` option: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com}, delivery_method: :postmark, postmark_settings: { api_key: ENV['POSTMARK_API_KEY'] } } ``` Besides the `delivery_method` option, you also can customize the mailer settings by passing a hash under an option named `DELIVERY_METHOD_settings`. Thus, you can use override specific SMTP settings for notifications using: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com}, delivery_method: :smtp, smtp_settings: { user_name: 'bob', password: 'password', } } ``` A complete list of `smtp_settings` options can be found in the [ActionMailer Configuration documentation](http://api.rubyonrails.org/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Configuration+options). ##### mailer_parent *String, default: ActionMailer::Base* The parent mailer which ExceptionNotification mailer inherit from. ##### deliver_with *Symbol, default: :deliver_now The method name to send emalis using ActionMailer. exception_notification-5.0.1/docs/notifiers/sns.md0000644000004100000410000000261315045670240022375 0ustar www-datawww-data### Amazon SNS Notifier Notify all exceptions Amazon - Simple Notification Service: [SNS](https://aws.amazon.com/sns/). #### Usage Add the [aws-sdk-sns](https://github.com/aws/aws-sdk-ruby/tree/master/gems/aws-sdk-sns) gem to your `Gemfile`: ```ruby gem 'aws-sdk-sns', '~> 1.5' ``` To configure it, you **need** to set 3 required options for aws: `region`, `access_key_id` and `secret_access_key`, and one more option for sns: `topic_arn`. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, sns: { region: 'us-east-x', access_key_id: 'access_key_id', secret_access_key: 'secret_access_key', topic_arn: 'arn:aws:sns:us-east-x:XXXX:my-topic' } ``` ##### sns_prefix *String, optional * Prefix in the notification subject, by default: "[Error]" ##### backtrace_lines *Integer, optional * Number of backtrace lines to be displayed in the notification message. By default: 10 #### Note: * You may need to update your previous `aws-sdk-*` gems in order to setup `aws-sdk-sns` correctly. * If you need any further information about the available regions or any other SNS related topic consider: [SNS faqs](https://aws.amazon.com/sns/faqs/) exception_notification-5.0.1/MIT-LICENSE0000644000004100000410000000214515045670240017752 0ustar www-datawww-dataCopyright (c) 2011-2016 Sebastian Martinez Copyright (c) 2005-2010 Jamis Buck The MIT License (MIT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. exception_notification-5.0.1/README.md0000644000004100000410000004054015045670240017576 0ustar www-datawww-data# Exception Notification [![Gem Version](https://badge.fury.io/rb/exception_notification.svg)](https://badge.fury.io/rb/exception_notification) [![Build Status](https://github.com/kmcphillips/exception_notification/actions/workflows/ci.yml/badge.svg)](https://github.com/kmcphillips/exception_notification/actions/workflows/ci.yml) --- The Exception Notification gem provides a set of [notifiers](#notifiers) for sending notifications when errors occur in a Rack/Rails application. The built-in notifiers can deliver notifications by [email](docs/notifiers/email.md), [HipChat](docs/notifiers/hipchat.md), [Slack](docs/notifiers/slack.md), [Mattermost](docs/notifiers/mattermost.md), [Teams](docs/notifiers/teams.md), [IRC](docs/notifiers/irc.md), [Amazon SNS](docs/notifiers/sns.md), [Google Chat](docs/notifiers/google_chat.md), [Datadog](docs/notifiers/datadog.md) or via custom [WebHooks](docs/notifiers/webhook.md). There's a [Railscast (2011) about Exception Notification](http://railscasts.com/episodes/104-exception-notifications-revised) you can see that may help you getting started. ## Gem status This gem is not under active development, but is maintained. There are more robust and modern solutions for exception handling. But this code was [extracted from Rails about 15+ years ago](https://github.com/rails/exception_notification) and still has lots of value for some applications. ## Requirements * Ruby 3.2 or greater * If using Rails, version 7.1 or greater. (Sinatra or other Rack-based applications are supported.) ## Getting Started Add the following line to your application's Gemfile: ```ruby gem "exception_notification" ``` ### Rails In order to install ExceptionNotification as an [engine](https://api.rubyonrails.org/classes/Rails/Engine.html), just run the following command from the terminal: ```bash rails g exception_notification:install ``` This generates an initializer file, `config/initializers/exception_notification.rb` with some default configuration, which you should modify as needed. Make sure the gem is not listed solely under the `production` group in your `Gemfile`, since this initializer will be loaded regardless of environment. If you want it to only be enabled in production, you can add this to your configuration: ```ruby config.ignore_if do |exception, options| !!Rails.env.local? end ``` The generated initializer file will include this require: ```ruby require "exception_notification/rails" ``` which automatically adds the ExceptionNotification middleware to the Rails middleware stack. This middleware is what watches for unhandled exceptions from your Rails app (except for [background jobs](#background-jobs)) and notifies you when they occur. The generated file adds an `email` notifier: ```ruby config.add_notifier :email, { email_prefix: "[ERROR] ", sender_address: %{"Notifier" }, exception_recipients: %w{exceptions@example.com} } ``` **Note**: In order to enable delivery notifications by email, make sure you have [ActionMailer configured](docs/notifiers/email.md#actionmailer-configuration). #### Adding middleware manually Alternatively, if for some reason you don't want to `require "exception_notification/rails"`, you can manually add the middleware, like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: "[PREFIX] ", sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} } ``` This is the older way of configuring ExceptionNotification (which prior to version 4 was the _only_ way to configure it), and is still the way used in some of the examples. Options passed to the `ExceptionNotification::Rack` middleware in this way are translated to the equivalent configuration options for the `ExceptionNotification.configure` of configuring (compare to the [Rails](#rails) example above). ### Rack/Sinatra In order to use ExceptionNotification with Sinatra, please take a look in the [example application](examples/sinatra). ### Custom Data, e.g. Current User Save the current user in the `request` using a controller callback. ```ruby class ApplicationController < ActionController::Base before_action :prepare_exception_notifier private def prepare_exception_notifier request.env["exception_notifier.exception_data"] = { current_user: current_user } end end ``` The current user will show up in your email, in a new section titled "Data". ``` ------------------------------- Data: * data: {:current_user=> # Options -> sections" below. ### Filtering parameters Since the error notification contains the full request parameters, you may want to filter out sensitive information. The `filter_parameters` in Rails can be used to filter out sensitive information from the request parameters. ```ruby config.filter_parameters += [:secret_details, :credit_card_number] ``` See the Rails documentation for more information: https://guides.rubyonrails.org/configuring.html#config-filter-parameters ## Notifiers ExceptionNotification relies on notifiers to deliver notifications when errors occur in your applications. By default the following notifiers are available: * [Datadog notifier](docs/notifiers/datadog.md) * [Email notifier](docs/notifiers/email.md) * [HipChat notifier](docs/notifiers/hipchat.md) * [IRC notifier](docs/notifiers/irc.md) * [Slack notifier](docs/notifiers/slack.md) * [Mattermost notifier](docs/notifiers/mattermost.md) * [Teams notifier](docs/notifiers/teams.md) * [Amazon SNS](docs/notifiers/sns.md) * [Google Chat notifier](docs/notifiers/google_chat.md) * [WebHook notifier](docs/notifiers/webhook.md) You also can implement your own [custom notifier](docs/notifiers/custom.md). ## Error Grouping In general, ExceptionNotification will send a notification when every error occurs, which may result in a problem: if your site has a high throughput and a particular error is raised frequently, you will receive too many notifications. During a short period of time, your mail box may be filled with thousands of exception mails, or your mail server may even become slow. To prevent this, you can choose to group errors by setting the `:error_grouping` option to `true`. Error grouping uses a default formula of `Math.log2(errors_count)` to determine whether to send the notification, based on the accumulated error count for each specific exception. This makes the notifier only send a notification when the count is: 1, 2, 4, 8, 16, 32, 64, 128, ..., (2**n). You can use `:notification_trigger` to override this default formula. The following code shows the available options to configure error grouping: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, ignore_exceptions: ['ActionView::TemplateError'] + ExceptionNotifier.ignored_exceptions, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, error_grouping: true, # error_grouping_period: 5.minutes, # the time before an error is regarded as fixed # error_grouping_cache: Rails.cache, # for other applications such as Sinatra, use one instance of ActiveSupport::Cache::Store # # notification_trigger: specify a callback to determine when a notification should be sent, # the callback will be invoked with two arguments: # exception: the exception raised # count: accumulated errors count for this exception # # notification_trigger: lambda { |exception, count| count % 10 == 0 } ``` ## Ignore Exceptions You can choose to ignore certain exceptions, which will make ExceptionNotification avoid sending notifications for those specified. There are three ways of specifying which exceptions to ignore: * `:ignore_exceptions` - By exception class (i.e. ignore RecordNotFound ones) * `:ignore_crawlers` - From crawler (i.e. ignore ones originated by Googlebot) * `:ignore_if` - Custom (i.e. ignore exceptions that satisfy some condition) * `:ignore_notifer_if` - Custom (i.e. let each notifier ignore exceptions if by-notifier condition is satisfied) ### :ignore_exceptions *Array of strings, default: %w{ActiveRecord::RecordNotFound Mongoid::Errors::DocumentNotFound AbstractController::ActionNotFound ActionController::RoutingError ActionController::UnknownFormat ActionDispatch::Http::MimeNegotiation::InvalidType Rack::Utils::InvalidParameterError}* Ignore specified exception types. To achieve that, you should use the `:ignore_exceptions` option, like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, ignore_exceptions: ['ActionView::TemplateError'] + ExceptionNotifier.ignored_exceptions, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} } ``` The above will make ExceptionNotifier ignore a *TemplateError* exception, plus the ones ignored by default. ### :ignore_crawlers *Array of strings, default: []* In some cases you may want to avoid getting notifications from exceptions made by crawlers. To prevent sending those unwanted notifications, use the `:ignore_crawlers` option like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, ignore_crawlers: %w{Googlebot bingbot}, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} } ``` ### :ignore_if *Lambda, default: nil* You can ignore exceptions based on a condition. Take a look: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, ignore_if: ->(env, exception) { exception.message =~ /^Couldn't find Page with ID=/ }, email: { email_prefix: '[PREFIX] ', sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com}, } ``` You can make use of both the environment and the exception inside the lambda to decide wether to avoid or not sending the notification. ### :ignore_notifier_if * Hash of Lambda, default: nil* In case you want a notifier to ignore certain exceptions, but don't want other notifiers to skip them, you can set by-notifier ignore options. By setting below, each notifier will ignore exceptions when its corresponding condition is met. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, ignore_notifier_if: { email: ->(env, exception) { !Rails.env.production? }, slack: ->(env, exception) { exception.message =~ /^Couldn't find Page with ID=/ } }, email: { sender_address: %{"notifier" }, exception_recipients: %w{exceptions@example.com} }, slack: { webhook_url: '[Your webhook url]', channel: '#exceptions', } ``` To customize each condition, you can make use of environment and the exception object inside the lambda. ## Rack X-Cascade Header Some rack apps (Rails in particular) utilize the "X-Cascade" header to pass the request-handling responsibility to the next middleware in the stack. Rails' routing middleware uses this strategy, rather than raising an exception, to handle routing errors (e.g. 404s); to be notified whenever a 404 occurs, set this option to "false." ### :ignore_cascade_pass *Boolean, default: true* Set to false to trigger notifications when another rack middleware sets the "X-Cascade" header to "pass." ## Background Jobs The ExceptionNotification middleware can only detect notifications that occur during web requests (controller actions). If you have any Ruby code that gets run _outside_ of a normal web request (hereafter referred to as a "background job" or "background process"), exceptions must be detected a different way (the middleware won't even be running in this context). Examples of background jobs include jobs triggered from a cron file or from a queue. ExceptionNotificatior can be configured to automatically notify of exceptions occurring in most common types of Rails background jobs such as [rake tasks](#rake-tasks). Additionally, it provides optional integrations for some 3rd-party libraries such as [Resque and Sidekiq](#resquesidekiq). And of course you can manually trigger a notification if no integration is provided. ### Rails runner To enable exception notification for your runner commands, add this line to your `config/application.rb` _below_ the `Bundler.require` line (ensuring that `exception_notification` and `rails` gems will have already been required): ```ruby require 'exception_notification/rails' ``` (Requiring it from an initializer is too late, because this depends on the `runner` callback, and that will have already been fired _before_ any initializers run.) ### Rake tasks If you've already added `require 'exception_notification/rails'` to your `config/application.rb` as described [above](#rails-runner), then there's nothing further you need to do. (That Engine has a `rake_tasks` callback which automatically requires the file below.) Alternatively, you can add this line to your `config/initializers/exception_notification.rb`: ```ruby require 'exception_notification/rake' ``` ### Manually notify of exceptions If you want to manually send a notifications from a background process that is not _automatically_ handled by ExceptionNotification, then you need to manually call the `notify_exception` method like this: ```ruby begin # some code... rescue => e ExceptionNotifier.notify_exception(e) end ``` You can include information about the background process that created the error by including a `data` parameter: ```ruby begin # some code... rescue => e ExceptionNotifier.notify_exception( e, data: { worker: worker.to_s, queue: queue, payload: payload} ) end ``` ### Resque/Sidekiq Instead of manually calling background notifications for each job/worker, you can configure ExceptionNotification to do this automatically. For this, run: ```bash rails g exception_notification:install --resque ``` or ```bash rails g exception_notification:install --sidekiq ``` As above, make sure the gem is not listed solely under the `production` group, since this initializer will be loaded regardless of environment. ## Manually notify of exceptions from `rescue_from` handler If your controller rescues and handles an error, the middleware won't be able to see that there was an exception, and the notifier will never be run. To manually notify of an error after rescuing it, you can do something like the following: ```ruby class SomeController < ApplicationController rescue_from Exception, with: :server_error def server_error(exception) # Whatever code that handles the exception ExceptionNotifier.notify_exception( exception, env: request.env, data: { message: 'was doing something wrong' } ) end end ``` ## Development and support Pull requests are very welcome! Issues too. You can always debug the gem by running `rake console`. Please read first the [Contributing Guide](CONTRIBUTING.md). And always follow the [code of conduct](CODE_OF_CONDUCT.md). ## License Copyright (c) 2005 Jamis Buck, released under the [MIT license](http://www.opensource.org/licenses/MIT). Maintainer: [Kevin McPhillips](https://github.com/kmcphillips)