lib/airbrake-ruby.rb
require 'net/https'
require 'logger'
require 'json'
require 'set'
require 'socket'
require 'time'
require 'airbrake-ruby/version'
require 'airbrake-ruby/loggable'
require 'airbrake-ruby/stashable'
require 'airbrake-ruby/mergeable'
require 'airbrake-ruby/grouppable'
require 'airbrake-ruby/config'
require 'airbrake-ruby/config/validator'
require 'airbrake-ruby/config/processor'
require 'airbrake-ruby/remote_settings/callback'
require 'airbrake-ruby/remote_settings/settings_data'
require 'airbrake-ruby/remote_settings'
require 'airbrake-ruby/promise'
require 'airbrake-ruby/thread_pool'
require 'airbrake-ruby/response'
require 'airbrake-ruby/sync_sender'
require 'airbrake-ruby/async_sender'
require 'airbrake-ruby/nested_exception'
require 'airbrake-ruby/ignorable'
require 'airbrake-ruby/inspectable'
require 'airbrake-ruby/notice'
require 'airbrake-ruby/backtrace'
require 'airbrake-ruby/truncator'
require 'airbrake-ruby/filters/keys_filter'
require 'airbrake-ruby/filters/keys_allowlist'
require 'airbrake-ruby/filters/keys_blocklist'
require 'airbrake-ruby/filters/gem_root_filter'
require 'airbrake-ruby/filters/system_exit_filter'
require 'airbrake-ruby/filters/root_directory_filter'
require 'airbrake-ruby/filters/thread_filter'
require 'airbrake-ruby/filters/context_filter'
require 'airbrake-ruby/filters/exception_attributes_filter'
require 'airbrake-ruby/filters/dependency_filter'
require 'airbrake-ruby/filters/git_revision_filter'
require 'airbrake-ruby/filters/git_repository_filter'
require 'airbrake-ruby/filters/git_last_checkout_filter'
require 'airbrake-ruby/filters/sql_filter'
require 'airbrake-ruby/filter_chain'
require 'airbrake-ruby/code_hunk'
require 'airbrake-ruby/file_cache'
require 'airbrake-ruby/hash_keyable'
require 'airbrake-ruby/performance_notifier'
require 'airbrake-ruby/notice_notifier'
require 'airbrake-ruby/deploy_notifier'
require 'airbrake-ruby/stat'
require 'airbrake-ruby/time_truncate'
require 'airbrake-ruby/tdigest'
require 'airbrake-ruby/query'
require 'airbrake-ruby/request'
require 'airbrake-ruby/performance_breakdown'
require 'airbrake-ruby/benchmark'
require 'airbrake-ruby/monotonic_time'
require 'airbrake-ruby/timed_trace'
require 'airbrake-ruby/queue'
require 'airbrake-ruby/context'
require 'airbrake-ruby/backlog'
# Airbrake is a thin wrapper around instances of the notifier classes (such as
# notice, performance & deploy notifiers). It creates a way to access them via a
# consolidated global interface.
#
# Prior to using it, you must {configure} it.
#
# @example
# Airbrake.configure do |c|
# c.project_id = 113743
# c.project_key = 'fd04e13d806a90f96614ad8e529b2822'
# end
#
# Airbrake.notify('Oops!')
#
# @since v1.0.0
# @api public
# rubocop:disable Metrics/ModuleLength
module Airbrake
# The general error that this library uses when it wants to raise.
Error = Class.new(StandardError)
# @return [String] the label to be prepended to the log output
LOG_LABEL = '**Airbrake:'.freeze
# @return [Boolean] true if current Ruby is JRuby. The result is used for
# special cases where we need to work around older implementations
JRUBY = (RUBY_ENGINE == 'jruby')
# @return [Boolean] true if this Ruby supports safe levels and tainting,
# to guard against using deprecated or unsupported features.
HAS_SAFE_LEVEL = (
RUBY_ENGINE == 'ruby' &&
Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7')
)
class << self
# @since v4.2.3
# @api private
attr_writer :performance_notifier
# @since v4.2.3
# @api private
attr_writer :notice_notifier
# @since v4.2.3
# @api private
attr_writer :deploy_notifier
# Configures the Airbrake notifier.
#
# @example
# Airbrake.configure do |c|
# c.project_id = 113743
# c.project_key = 'fd04e13d806a90f96614ad8e529b2822'
# end
#
# @yield [config]
# @yieldparam config [Airbrake::Config]
# @return [void]
def configure
yield config = Airbrake::Config.instance
Airbrake::Loggable.instance = config.logger
config_processor = Airbrake::Config::Processor.new(config)
config_processor.process_blocklist(notice_notifier)
config_processor.process_allowlist(notice_notifier)
@remote_settings ||= config_processor.process_remote_configuration
config_processor.add_filters(notice_notifier)
end
# @since v4.2.3
# @api private
def performance_notifier
@performance_notifier ||= PerformanceNotifier.new
end
# @since v4.2.3
# @api private
def notice_notifier
@notice_notifier ||= NoticeNotifier.new
end
# @since v4.2.3
# @api private
def deploy_notifier
@deploy_notifier ||= DeployNotifier.new
end
# @return [Boolean] true if the notifier was configured, false otherwise
# @since v2.3.0
def configured?
@notice_notifier && @notice_notifier.configured?
end
# Sends an exception to Airbrake asynchronously.
#
# @example Sending an exception
# Airbrake.notify(RuntimeError.new('Oops!'))
# @example Sending a string
# # Converted to RuntimeError.new('Oops!') internally
# Airbrake.notify('Oops!')
# @example Sending a Notice
# notice = airbrake.build_notice(RuntimeError.new('Oops!'))
# airbrake.notify(notice)
#
# @param [Exception, String, Airbrake::Notice] exception The exception to be
# sent to Airbrake
# @param [Hash] params The additional payload to be sent to Airbrake. Can
# contain any values. The provided values will be displayed in the Params
# tab in your project's dashboard
# @yield [notice] The notice to filter
# @yieldparam [Airbrake::Notice]
# @yieldreturn [void]
# @return [Airbrake::Promise]
# @see .notify_sync
def notify(exception, params = {}, &block)
notice_notifier.notify(exception, params, &block)
end
# Sends an exception to Airbrake synchronously.
#
# @example
# Airbrake.notify_sync('App crashed!')
# #=> {"id"=>"123", "url"=>"https://airbrake.io/locate/321"}
#
# @param [Exception, String, Airbrake::Notice] exception The exception to be
# sent to Airbrake
# @param [Hash] params The additional payload to be sent to Airbrake. Can
# contain any values. The provided values will be displayed in the Params
# tab in your project's dashboard
# @yield [notice] The notice to filter
# @yieldparam [Airbrake::Notice]
# @yieldreturn [void]
# @return [Airbrake::Promise] the reponse from the server
# @see .notify
def notify_sync(exception, params = {}, &block)
notice_notifier.notify_sync(exception, params, &block)
end
# Runs a callback before {.notify} or {.notify_sync} kicks in. This is
# useful if you want to ignore specific notices or filter the data the
# notice contains.
#
# @example Ignore all notices
# Airbrake.add_filter(&:ignore!)
# @example Ignore based on some condition
# Airbrake.add_filter do |notice|
# notice.ignore! if notice[:error_class] == 'StandardError'
# end
# @example Ignore with help of a class
# class MyFilter
# def call(notice)
# # ...
# end
# end
#
# Airbrake.add_filter(MyFilter.new)
#
# @param [#call] filter The filter object
# @yield [notice] The notice to filter
# @yieldparam [Airbrake::Notice]
# @yieldreturn [void]
# @return [void]
def add_filter(filter = nil, &block)
notice_notifier.add_filter(filter, &block)
end
# Deletes a filter added via {Airbrake#add_filter}.
#
# @example
# # Add a MyFilter filter (we pass an instance here).
# Airbrake.add_filter(MyFilter.new)
#
# # Delete the filter (we pass class name here).
# Airbrake.delete_filter(MyFilter)
#
# @param [Class] filter_class The class of the filter you want to delete
# @return [void]
# @since v3.1.0
# @note This method cannot delete filters assigned via the Proc form.
def delete_filter(filter_class)
notice_notifier.delete_filter(filter_class)
end
# Builds an Airbrake notice. This is useful, if you want to add or modify a
# value only for a specific notice. When you're done modifying the notice,
# send it with {.notify} or {.notify_sync}.
#
# @example
# notice = airbrake.build_notice('App crashed!')
# notice[:params][:username] = user.name
# airbrake.notify_sync(notice)
#
# @param [Exception] exception The exception on top of which the notice
# should be built
# @param [Hash] params The additional params attached to the notice
# @return [Airbrake::Notice] the notice built with help of the given
# arguments
def build_notice(exception, params = {})
notice_notifier.build_notice(exception, params)
end
# Makes the notice notifier a no-op, which means you cannot use the
# {.notify} and {.notify_sync} methods anymore. It also stops the notice
# notifier's worker threads.
#
# @example
# Airbrake.close
# Airbrake.notify('App crashed!') #=> raises Airbrake::Error
#
# @return [nil]
# rubocop:disable Style/IfUnlessModifier
def close
if defined?(@notice_notifier) && @notice_notifier
@notice_notifier.close
end
if defined?(@performance_notifier) && @performance_notifier
@performance_notifier.close
end
if defined?(@remote_settings) && @remote_settings
@remote_settings.stop_polling
end
nil
end
# rubocop:enable Style/IfUnlessModifier
# Pings the Airbrake Deploy API endpoint about the occurred deploy.
#
# @param [Hash{Symbol=>String}] deploy_info The params for the API
# @option deploy_info [Symbol] :environment
# @option deploy_info [Symbol] :username
# @option deploy_info [Symbol] :repository
# @option deploy_info [Symbol] :revision
# @option deploy_info [Symbol] :version
# @return [void]
def notify_deploy(deploy_info)
deploy_notifier.notify(deploy_info)
end
# Merges +context+ with the current context.
#
# The context will be attached to the notice object upon a notify call and
# cleared after it's attached. The context data is attached to the
# `params/airbrake_context` key.
#
# @example
# class MerryGrocer
# def load_fruits(fruits)
# Airbrake.merge_context(fruits: fruits)
# end
#
# def deliver_fruits
# Airbrake.notify('fruitception')
# end
#
# def load_veggies(veggies)
# Airbrake.merge_context(veggies: veggies)
# end
#
# def deliver_veggies
# Airbrake.notify('veggieboom!')
# end
# end
#
# grocer = MerryGrocer.new
#
# # Load some fruits to the context.
# grocer.load_fruits(%w(mango banana apple))
#
# # Deliver the fruits. Note that we are not passing anything,
# # `deliver_fruits` knows that we loaded something.
# grocer.deliver_fruits
#
# # Load some vegetables and deliver them to Airbrake. Note that the
# # fruits have been delivered and therefore the grocer doesn't have them
# # anymore. We merge veggies with the new context.
# grocer.load_veggies(%w(cabbage carrot onion))
# grocer.deliver_veggies
#
# # The context is empty again, feel free to load more.
#
# @param [Hash{Symbol=>Object}] context
# @return [void]
def merge_context(context)
notice_notifier.merge_context(context)
end
# Increments request statistics of a certain +route+ invoked with +method+,
# which returned +status_code+.
#
# After a certain amount of time (n seconds) the aggregated route
# information will be sent to Airbrake.
#
# @example
# Airbrake.notify_request(
# method: 'POST',
# route: '/thing/:id/create',
# status_code: 200,
# timing: 123.45 # ms
# )
#
# @param [Hash{Symbol=>Object}] request_info
# @option request_info [String] :method The HTTP method that was invoked
# @option request_info [String] :route The route that was invoked
# @option request_info [Integer] :status_code The response code that the
# route returned
# @option request_info [Float] :timing How much time it took to process the
# request (in ms)
# @param [Hash] stash What needs to be appeneded to the stash, so it's
# available in filters
# @return [void]
# @since v3.0.0
# @see Airbrake::PerformanceNotifier#notify
def notify_request(request_info, stash = {})
request = Request.new(**request_info)
request.stash.merge!(stash)
performance_notifier.notify(request)
end
# Synchronously increments request statistics of a certain +route+ invoked
# with +method+, which returned +status_code+.
# @since v4.10.0
# @see .notify_request
def notify_request_sync(request_info, stash = {})
request = Request.new(**request_info)
request.stash.merge!(stash)
performance_notifier.notify_sync(request)
end
# Increments SQL statistics of a certain +query+. When +method+ and +route+
# are provided, the query is grouped by these parameters.
#
# After a certain amount of time (n seconds) the aggregated query
# information will be sent to Airbrake.
#
# @example
# Airbrake.notify_query(
# method: 'GET',
# route: '/things',
# query: 'SELECT * FROM things',
# func: 'do_stuff',
# file: 'app/models/foo.rb',
# line: 452,
# timing: 123.45 # ms
# )
#
# @param [Hash{Symbol=>Object}] query_info
# @option query_info [String] :method The HTTP method that triggered this
# SQL query (optional)
# @option query_info [String] :route The route that triggered this SQL
# query (optional)
# @option query_info [String] :query The query that was executed
# @option request_info [String] :func The function that called the query
# (optional)
# @option request_info [String] :file The file that has the function that
# called the query (optional)
# @option request_info [Integer] :line The line that executes the query
# (optional)
# @option query_info [Float] :timing How much time it took to process the
# query (in ms)
# @param [Hash] stash What needs to be appeneded to the stash, so it's
# available in filters
# @return [void]
# @since v3.2.0
# @see Airbrake::PerformanceNotifier#notify
def notify_query(query_info, stash = {})
query = Query.new(**query_info)
query.stash.merge!(stash)
performance_notifier.notify(query)
end
# Synchronously increments SQL statistics of a certain +query+. When
# +method+ and +route+ are provided, the query is grouped by these
# parameters.
# @since v4.10.0
# @see .notify_query
def notify_query_sync(query_info, stash = {})
query = Query.new(**query_info)
query.stash.merge!(stash)
performance_notifier.notify_sync(query)
end
# Increments performance breakdown statistics of a certain route.
#
# @example
# Airbrake.notify_performance_breakdown(
# method: 'POST',
# route: '/thing/:id/create',
# response_type: 'json',
# groups: { db: 24.0, view: 0.4 }, # ms
# timing: 123.45 # ms
# )
#
# @param [Hash{Symbol=>Object}] breakdown_info
# @option breakdown_info [String] :method HTTP method
# @option breakdown_info [String] :route
# @option breakdown_info [String] :response_type
# @option breakdown_info [Array<Hash{Symbol=>Float}>] :groups
# @option breakdown_info [Float] :timing How much time it took to process
# the performance breakdown (in ms)
# @param [Hash] stash What needs to be appeneded to the stash, so it's
# available in filters
# @return [void]
# @since v4.2.0
def notify_performance_breakdown(breakdown_info, stash = {})
performance_breakdown = PerformanceBreakdown.new(**breakdown_info)
performance_breakdown.stash.merge!(stash)
performance_notifier.notify(performance_breakdown)
end
# Increments performance breakdown statistics of a certain route
# synchronously.
# @since v4.10.0
# @see .notify_performance_breakdown
def notify_performance_breakdown_sync(breakdown_info, stash = {})
performance_breakdown = PerformanceBreakdown.new(**breakdown_info)
performance_breakdown.stash.merge!(stash)
performance_notifier.notify_sync(performance_breakdown)
end
# Increments statistics of a certain queue (worker).
#
# @example
# Airbrake.notify_queue(
# queue: 'emails',
# error_count: 1,
# groups: { redis: 24.0, sql: 0.4 } # ms
# )
#
# @param [Hash{Symbol=>Object}] queue_info
# @option queue_info [String] :queue The name of the queue/worker
# @option queue_info [Integer] :error_count How many times this worker
# failed
# @option queue_info [Array<Hash{Symbol=>Float}>] :groups Where the job
# spent its time
# @option breakdown_info [Float] :timing How much time it took to process
# the queue (in ms)
# @param [Hash] stash What needs to be appended to the stash, so it's
# available in filters
# @return [void]
# @since v4.9.0
# @see .notify_queue_sync
def notify_queue(queue_info, stash = {})
queue = Queue.new(**queue_info)
queue.stash.merge!(stash)
performance_notifier.notify(queue)
end
# Increments statistics of a certain queue (worker) synchronously.
# @since v4.10.0
# @see .notify_queue
def notify_queue_sync(queue_info, stash = {})
queue = Queue.new(**queue_info)
queue.stash.merge!(stash)
performance_notifier.notify_sync(queue)
end
# Runs a callback before {.notify_request}, {.notify_query}, {.notify_queue}
# or {.notify_performance_breakdown} kicks in. This is useful if you want to
# ignore specific metrics or filter the data the metric contains.
#
# @example Ignore all metrics
# Airbrake.add_performance_filter(&:ignore!)
# @example Filter sensitive data
# Airbrake.add_performance_filter do |metric|
# case metric
# when Airbrake::Query
# metric.route = '[Filtered]'
# when Airbrake::Request
# metric.query = '[Filtered]'
# end
# end
# @example Filter with help of a class
# class MyFilter
# def call(metric)
# # ...
# end
# end
#
# Airbrake.add_performance_filter(MyFilter.new)
#
# @param [#call] filter The filter object
# @yield [metric] The metric to filter
# @yieldparam [Airbrake::Query, Airbrake::Request]
# @yieldreturn [void]
# @return [void]
# @since v3.2.0
# @see Airbrake::PerformanceNotifier#add_filter
def add_performance_filter(filter = nil, &block)
performance_notifier.add_filter(filter, &block)
end
# Deletes a filter added via {Airbrake#add_performance_filter}.
#
# @example
# # Add a MyFilter filter (we pass an instance here).
# Airbrake.add_performance_filter(MyFilter.new)
#
# # Delete the filter (we pass class name here).
# Airbrake.delete_performance_filter(MyFilter)
#
# @param [Class] filter_class The class of the filter you want to delete
# @return [void]
# @since v3.2.0
# @note This method cannot delete filters assigned via the Proc form.
# @see Airbrake::PerformanceNotifier#delete_filter
def delete_performance_filter(filter_class)
performance_notifier.delete_filter(filter_class)
end
# Resets all notifiers, including its filters
# @return [void]
# @since v4.2.2
def reset
close
self.performance_notifier = PerformanceNotifier.new
self.notice_notifier = NoticeNotifier.new
self.deploy_notifier = DeployNotifier.new
end
end
end
# rubocop:enable Metrics/ModuleLength