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