lib/honeybadger/agent.rb in honeybadger-2.7.2 vs lib/honeybadger/agent.rb in honeybadger-3.0.0.beta1

- old
+ new

@@ -1,205 +1,363 @@ require 'forwardable' require 'honeybadger/version' require 'honeybadger/config' +require 'honeybadger/context_manager' require 'honeybadger/notice' require 'honeybadger/plugin' require 'honeybadger/logging' +require 'honeybadger/worker' module Honeybadger - # Internal: A broker for the configuration and the workers. + # Public: The Honeybadger agent contains all the methods for interacting with + # the Honeybadger service. It can be used to send notifications to multiple + # projects in large apps. + # + # Context is global by default, meaning agents created via + # `Honeybadger::Agent.new` will share context (added via + # `Honeybadger.context` or `Honeybadger::Agent#context`) with other agents. + # This also includes the Rack environment when using the Honeybadger rack + # middleware. + # + # Examples: + # + # # Standard usage: + # OtherBadger = Honeybadger::Agent.new + # + # # With local context: + # OtherBadger = Honeybadger::Agent.new(local_context: true) + # + # OtherBadger.configure do |config| + # config.api_key = 'project api key' + # end + # + # begin + # # Risky operation + # rescue => e + # OtherBadger.notify(e) + # end class Agent extend Forwardable include Logging::Helper - autoload :Worker, 'honeybadger/agent/worker' - autoload :NullWorker, 'honeybadger/agent/worker' + def self.instance + @instance + end - class << self - extend Forwardable - - def_delegators :callbacks, :exception_filter, :exception_fingerprint, :backtrace_filter - - def callbacks - @callbacks ||= Config::Callbacks.new - end + def self.instance=(instance) + @instance = instance end - private - - def self.load_plugins!(config) - Dir[File.expand_path('../plugins/*.rb', __FILE__)].each do |plugin| - require plugin + def initialize(opts = {}) + if opts.kind_of?(Config) + @config = opts + opts = {} end - Plugin.load!(config) - end - public + @context = opts.delete(:context) + @context ||= ContextManager.new if opts.delete(:local_context) - def self.instance - @instance - end + @config ||= Config.new(opts) - def self.running? - !instance.nil? + init_worker end - def self.start(config = {}) - return true if running? + # Public: Send an exception to Honeybadger. Does not report ignored + # exceptions by default. + # + # exception_or_opts - An Exception object, or a Hash of options which is used + # to build the notice. All other types of objects will + # be converted to a String and used as the `:error_message`. + # opts - The options Hash when the first argument is an + # Exception. (default: {}): + # :error_message - The String error message. + # :error_class - The String class name of the error. (optional) + # :force - Always report the exception, even when + # ignored. (optional) + # + # Examples: + # + # # With an exception: + # begin + # fail 'oops' + # rescue => exception + # Honeybadger.notify(exception, context: { + # my_data: 'value' + # }) # => '-1dfb92ae-9b01-42e9-9c13-31205b70744a' + # end + # + # # Custom notification: + # Honeybadger.notify({ + # error_class: 'MyClass', + # error_message: 'Something went wrong.', + # context: {my_data: 'value'} + # }) # => '06220c5a-b471-41e5-baeb-de247da45a56' + # + # Returns a String UUID reference to the notice within Honeybadger or false + # when ignored. + def notify(exception_or_opts, opts = {}) + return false if config.disabled? - unless config.kind_of?(Config) - config = Config.new(config) + if exception_or_opts.is_a?(Exception) + opts.merge!(exception: exception_or_opts) + elsif exception_or_opts.respond_to?(:to_hash) + opts.merge!(exception_or_opts.to_hash) + else + opts[:error_message] = exception_or_opts.to_s end - if config[:disabled] - config.logger.warn('Unable to start Honeybadger -- disabled by configuration.') + validate_notify_opts!(opts) + + opts.merge!(rack_env: context_manager.get_rack_env) + opts.merge!(global_context: context_manager.get_context) + + notice = Notice.new(config, opts) + + unless notice.api_key =~ NOT_BLANK + error { sprintf('Unable to send error report: API key is missing. id=%s', notice.id) } return false - elsif !config.valid? - config.logger.warn('Unable to start Honeybadger -- api_key is missing or invalid.') - return false end - unless config.ping - config.logger.warn('Failed to connect to Honeybadger service -- please verify that api.honeybadger.io is reachable (connection will be retried).') + if !opts[:force] && notice.ignore? + debug { sprintf('ignore notice feature=notices id=%s', notice.id) } + return false end - config.logger.info("Starting Honeybadger version #{VERSION}") - load_plugins!(config) - @instance = new(config) + info { sprintf('Reporting error id=%s', notice.id) } - true - end + if opts[:sync] + send_now(notice) + else + push(notice) + end - def self.stop(*args) - @instance.stop(*args) if @instance - @instance = nil + notice.id end - def self.fork(*args) - # noop + # Public: Save global context for the current request. + # + # hash - A Hash of data which will be sent to Honeybadger when an error + # occurs. (default: nil) + # + # Examples: + # + # Honeybadger.context({my_data: 'my value'}) + # + # # Inside a Rails controller: + # before_action do + # Honeybadger.context({user_id: current_user.id}) + # end + # + # # Clearing global context: + # Honeybadger.context.clear! + # + # Returns self so that method calls can be chained. + def context(hash = nil) + context_manager.set_context(hash) unless hash.nil? + self end - def self.flush(&block) - if self.instance - self.instance.flush(&block) - elsif !block_given? - false - else - yield - end + # Internal: Used to clear context via `#context.clear!`. + def clear! + context_manager.clear! end - # Internal: Callback to perform after agent has been stopped at_exit. + # Public: Get global context for the current request. # - # block - An optional block to execute. # - # Returns Proc callback. - def self.at_exit(&block) - @at_exit = Proc.new if block_given? - @at_exit + # Examples: + # + # Honeybadger.context({my_data: 'my value'}) + # Honeybadger.get_context #now returns {my_data: 'my value'} + # + # Returns hash or nil. + def get_context + context_manager.get_context end - # Internal: Not for public consumption. :) + # Public: Flushes all data from workers before returning. This is most useful + # in tests when using the test backend, where normally the asynchronous + # nature of this library could create race conditions. # - # Prefer dependency injection over accessing config directly, but some - # cases (such as the delayed_job plugin) necessitate it. + # block - The optional block to execute (exceptions will propagate after data + # is flushed). # - # Returns the Agent's config if running, otherwise default config - def self.config - if running? - instance.send(:config) - else - @config ||= Config.new - end + # Examples: + # + # # Without a block: + # it "sends a notification to Honeybadger" do + # expect { + # Honeybadger.notify(StandardError.new('test backend')) + # Honeybadger.flush + # }.to change(Honeybadger::Backend::Test.notifications[:notices], :size).by(0) + # end + # + # # With a block: + # it "sends a notification to Honeybadger" do + # expect { + # Honeybadger.flush do + # 49.times do + # Honeybadger.notify(StandardError.new('test backend')) + # end + # end + # }.to change(Honeybadger::Backend::Test.notifications[:notices], :size).by(49) + # end + # + # Returns value of block if block is given, otherwise true on success or + # false if Honeybadger isn't running. + def flush + return true unless block_given? + yield + ensure + worker.flush end - attr_reader :workers - - def initialize(config) - @config = config - @mutex = Mutex.new - - unless config.backend.kind_of?(Backend::Server) - warn('Initializing development backend: data will not be reported.') - end - - init_workers - - at_exit do - # Fix for https://bugs.ruby-lang.org/issues/5218 - if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby' && RUBY_VERSION =~ /1\.9/ - exit_status = $!.status if $!.is_a?(SystemExit) - end - - notify_at_exit($!) - stop if config[:'send_data_at_exit'] - self.class.at_exit.call if self.class.at_exit - - exit(exit_status) if exit_status - end - end - + # Public: Stops the Honeybadger service. + # + # Examples: + # + # Honeybadger.stop # => nil + # + # Returns nothing def stop(force = false) - workers.each_pair do |key, worker| - worker.send(force ? :shutdown! : :shutdown) - end - + worker.send(force ? :shutdown! : :shutdown) true end - def notice(opts) - opts.merge!(callbacks: self.class.callbacks) - notice = Notice.new(config, opts) + attr_reader :config - if !opts[:force] && notice.ignore? - debug { sprintf('ignore notice feature=notices id=%s', notice.id) } - false - else - debug { sprintf('notice feature=notices id=%s', notice.id) } - push(:notices, notice) - notice.id - end - end + # Public: Configure the Honeybadger agent via Ruby. + # + # block - The configuration block. + # + # Examples: + # + # Honeybadger.configure do |config| + # config.api_key = 'project api key' + # config.exceptions.ignore += [CustomError] + # end + # + # Yields configuration object. + # Returns nothing. + def_delegator :config, :configure - # Internal: Flush the workers. See Honeybadger#flush. + # Public: Callback to ignore exceptions. # - # block - an option block which is executed before flushing data. + # See public API documentation for Honeybadger::Notice for available attributes. # - # Returns value from block if block is given, otherwise true. - def flush - return true unless block_given? + # block - A block returning TrueClass true (to ignore) or FalseClass false + # (to send). + # + # Examples: + # + # # Ignoring based on error message: + # Honeybadger.exception_filter do |notice| + # notice[:error_message] =~ /sensitive data/ + # end + # + # # Ignore an entire class of exceptions: + # Honeybadger.exception_filter do |notice| + # notice[:exception].class < MyError + # end + # + # Returns nothing. + def_delegator :config, :exception_filter + + # Public: Callback to add a custom grouping strategy for exceptions. The + # return value is hashed and sent to Honeybadger. Errors with the same + # fingerprint will be grouped. + # + # See public API documentation for Honeybadger::Notice for available attributes. + # + # block - A block returning any Object responding to #to_s. + # + # Examples: + # + # Honeybadger.exception_fingerprint do |notice| + # [notice[:error_class], notice[:component], notice[:backtrace].to_s].join(':') + # end + # + # Returns nothing. + def_delegator :config, :exception_fingerprint + + # Public: Callback to filter backtrace lines. One use for this is to make + # additional [PROJECT_ROOT] or [GEM_ROOT] substitutions, which are used by + # Honeybadger when grouping errors and displaying application traces. + # + # block - A block which can be used to modify the Backtrace lines sent to + # Honeybadger. The block expects one argument (line) which is the String line + # from the Backtrace, and must return the String new line. + # + # Examples: + # + # Honeybadger.backtrace_filter do |line| + # line.gsub(/^\/my\/unknown\/bundle\/path/, "[GEM_ROOT]") + # end + # + # Returns nothing. + def_delegator :config, :backtrace_filter + + # Public: Sets the Rack environment which is used to report request data + # with errors. + # + # rack_env - The Hash Rack environment. + # block - A block to call. Errors reported from within the block will + # include request data. + # + # Examples: + # + # Honeybadger.with_rack_env(env) do + # begin + # # Risky operation + # rescue => e + # Honeybadger.notify(e) + # end + # end + # + # Returns the return value of block. + def with_rack_env(rack_env, &block) + context_manager.set_rack_env(rack_env) yield ensure - workers.values.each(&:flush) + context_manager.set_rack_env(nil) end - private + # Internal + attr_reader :worker - attr_reader :config, :mutex + # Internal + def_delegators :config, :init! - def push(feature, object) - unless config.feature?(feature) - debug { sprintf('agent dropping feature=%s reason=ping', feature) } - return false - end + private - workers[feature].push(object) + def validate_notify_opts!(opts) + return if opts.has_key?(:exception) + return if opts.has_key?(:error_message) + msg = sprintf('`Honeybadger.notify` was called with invalid arguments. You must pass either an Exception or options Hash containing the `:error_message` key. location=%s', caller[caller.size-1]) + raise ArgumentError.new(msg) if config.dev? + warn(msg) + end + def context_manager + return @context if @context + ContextManager.current + end + + def push(object) + worker.push(object) true end - def init_workers - @workers = Hash.new(NullWorker.new) - workers[:notices] = Worker.new(config, :notices) + def send_now(object) + worker.send_now(object) + true end - def notify_at_exit(ex) - return unless ex - return unless config[:'exceptions.notify_at_exit'] - return if ex.is_a?(SystemExit) - - notice(exception: ex, component: 'at_exit') + def init_worker + @worker = Worker.new(config) end + + @instance = new(Config.new) end end