require 'rollbar/configuration' require 'rollbar/lazy_store' require 'rollbar/util' require 'rollbar/json' require 'rollbar/exceptions' require 'rollbar/language_support' require 'rollbar/delay/girl_friday' require 'rollbar/delay/thread' require 'rollbar/logger_proxy' require 'rollbar/item' module Rollbar # The notifier class. It has the core functionality # for sending reports to the API. class Notifier attr_accessor :configuration attr_accessor :last_report attr_accessor :scope_object @file_semaphore = Mutex.new def initialize(parent_notifier = nil, payload_options = nil, scope = nil) if parent_notifier self.configuration = parent_notifier.configuration.clone self.scope_object = parent_notifier.scope_object.clone Rollbar::Util.deep_merge(scope_object, scope) if scope else self.configuration = ::Rollbar::Configuration.new self.scope_object = ::Rollbar::LazyStore.new(scope) end Rollbar::Util.deep_merge(configuration.payload_options, payload_options) if payload_options end def reset! self.scope_object = ::Rollbar::LazyStore.new({}) end # Similar to configure below, but used only internally within the gem # to configure it without initializing any of the third party hooks def preconfigure yield(configuration) end # Configures the notifier instance def configure configuration.enabled = true if configuration.enabled.nil? yield(configuration) end def reconfigure self.configuration = Configuration.new configuration.enabled = true yield(configuration) end def unconfigure self.configuration = nil end def scope(scope_overrides = {}, config_overrides = {}) new_notifier = self.class.new(self, nil, scope_overrides) new_notifier.configuration = configuration.merge(config_overrides) new_notifier end def scope!(options = {}, config_overrides = {}) Rollbar::Util.deep_merge(scope_object, options) configuration.merge!(config_overrides) self end # Returns a new notifier with same configuration options # but it sets Configuration#safely to true. # We are using this flag to avoid having inifite loops # when evaluating some custom user methods. def safely new_notifier = scope new_notifier.configuration.safely = true new_notifier end # Turns off reporting for the given block. # # @example # Rollbar.silenced { raise } # # @yield Block which exceptions won't be reported. def silenced yield rescue => e e.instance_variable_set(:@_rollbar_do_not_report, true) raise end # Sends a report to Rollbar. # # Accepts any number of arguments. The last String argument will become # the message or description of the report. The last Exception argument # will become the associated exception for the report. The last hash # argument will be used as the extra data for the report. # # @example # begin # foo = bar # rescue => e # Rollbar.log(e) # end # # @example # Rollbar.log('This is a simple log message') # # @example # Rollbar.log(e, 'This is a description of the exception') # def log(level, *args) return 'disabled' unless configuration.enabled message, exception, extra = extract_arguments(args) use_exception_level_filters = extra && extra.delete(:use_exception_level_filters) == true return 'ignored' if ignored?(exception, use_exception_level_filters) begin call_before_process(:level => level, :exception => exception, :message => message, :extra => extra) rescue Rollbar::Ignore return 'ignored' end level = lookup_exception_level(level, exception, use_exception_level_filters) begin report(level, message, exception, extra) rescue StandardError, SystemStackError => e report_internal_error(e) 'error' end end # See log() above def debug(*args) log('debug', *args) end # See log() above def info(*args) log('info', *args) end # See log() above def warn(*args) log('warning', *args) end # See log() above def warning(*args) log('warning', *args) end # See log() above def error(*args) log('error', *args) end # See log() above def critical(*args) log('critical', *args) end def process_item(item) if configuration.write_to_file if configuration.use_async @file_semaphore.synchronize do write_item(item) end else write_item(item) end else send_item(item) end rescue => e log_error("[Rollbar] Error processing the item: #{e.class}, #{e.message}. Item: #{item.payload.inspect}") raise e end # We will reraise exceptions in this method so async queues # can retry the job or, in general, handle an error report some way. # # At same time that exception is silenced so we don't generate # infinite reports. This example is what we want to avoid: # # 1. New exception in a the project is raised # 2. That report enqueued to Sidekiq queue. # 3. The Sidekiq job tries to send the report to our API # 4. The report fails, for example cause a network failure, # and a exception is raised # 5. We report an internal error for that exception # 6. We reraise the exception so Sidekiq job fails and # Sidekiq can retry the job reporting the original exception # 7. Because the job failed and Sidekiq can be managed by rollbar we'll # report a new exception. # 8. Go to point 2. # # We'll then push to Sidekiq queue indefinitely until the network failure # is fixed. # # Using Rollbar.silenced we avoid the above behavior but Sidekiq # will have a chance to retry the original job. def process_from_async_handler(payload) payload = Rollbar::JSON.load(payload) if payload.is_a?(String) item = Item.build_with(payload, :notifier => self, :configuration => configuration, :logger => logger) Rollbar.silenced do begin process_item(item) rescue => e report_internal_error(e) raise end end end def send_failsafe(message, exception, uuid = nil, host = nil) exception_reason = failsafe_reason(message, exception) log_error "[Rollbar] Sending failsafe response due to #{exception_reason}" body = failsafe_body(exception_reason) failsafe_data = { :level => 'error', :environment => configuration.environment.to_s, :body => { :message => { :body => body } }, :notifier => { :name => 'rollbar-gem', :version => VERSION }, :custom => { :orig_uuid => uuid, :orig_host => host }, :internal => true, :failsafe => true } failsafe_payload = { 'access_token' => configuration.access_token, 'data' => failsafe_data } begin item = Item.build_with(failsafe_payload, :notifier => self, :configuration => configuration, :logger => logger) schedule_item(item) rescue => e log_error "[Rollbar] Error sending failsafe : #{e}" end failsafe_payload end ## Logging %w(debug info warn error).each do |level| define_method(:"log_#{level}") do |message| logger.send(level, message) end end private def call_before_process(options) options = { :level => options[:level], :scope => scope_object, :exception => options[:exception], :message => options[:message], :extra => options[:extra] } handlers = configuration.before_process handlers.each do |handler| begin handler.call(options) rescue Rollbar::Ignore raise rescue => e log_error("[Rollbar] Error calling the `before_process` hook: #{e}") break end end end def extract_arguments(args) message = nil exception = nil extra = nil args.each do |arg| if arg.is_a?(String) message = arg elsif arg.is_a?(Exception) exception = arg elsif arg.is_a?(Hash) extra = arg end end [message, exception, extra] end def lookup_exception_level(orig_level, exception, use_exception_level_filters) return orig_level unless use_exception_level_filters exception_level = filtered_level(exception) return exception_level if exception_level orig_level end def ignored?(exception, use_exception_level_filters = false) return false unless exception return true if use_exception_level_filters && filtered_level(exception) == 'ignore' return true if exception.instance_variable_get(:@_rollbar_do_not_report) false end def filtered_level(exception) return unless exception filter = configuration.exception_level_filters[exception.class.name] if filter.respond_to?(:call) filter.call(exception) else filter end end def report(level, message, exception, extra) unless message || exception || extra log_error '[Rollbar] Tried to send a report with no message, exception or extra data.' return 'error' end item = build_item(level, message, exception, extra) return 'ignored' if item.ignored? schedule_item(item) data = item['data'] log_instance_link(data) Rollbar.last_report = data data end # Reports an internal error in the Rollbar library. This will be reported within the configured # Rollbar project. We'll first attempt to provide a report including the exception traceback. # If that fails, we'll fall back to a more static failsafe response. def report_internal_error(exception) log_error '[Rollbar] Reporting internal error encountered while sending data to Rollbar.' begin item = build_item('error', nil, exception, :internal => true) rescue => e send_failsafe('build_item in exception_data', e) return end begin process_item(item) rescue => e send_failsafe('error in process_item', e) return end begin log_instance_link(item['data']) rescue => e send_failsafe('error logging instance link', e) return end end ## Payload building functions def build_item(level, message, exception, extra) options = { :level => level, :message => message, :exception => exception, :extra => extra, :configuration => configuration, :logger => logger, :scope => scope_object, :notifier => self } item = Item.new(options) item.build item end ## Delivery functions def send_item_using_eventmachine(item) body = item.dump return unless body headers = { 'X-Rollbar-Access-Token' => item['access_token'] } req = EventMachine::HttpRequest.new(configuration.endpoint).post(:body => body, :head => headers) req.callback do if req.response_header.status == 200 log_info '[Rollbar] Success' else log_warning "[Rollbar] Got unexpected status code from Rollbar.io api: #{req.response_header.status}" log_info "[Rollbar] Response: #{req.response}" end end req.errback do log_warning "[Rollbar] Call to API failed, status code: #{req.response_header.status}" log_info "[Rollbar] Error's response: #{req.response}" end end def send_item(item) log_info '[Rollbar] Sending item' if configuration.use_eventmachine send_item_using_eventmachine(item) return end body = item.dump return unless body uri = URI.parse(configuration.endpoint) handle_response(do_post(uri, body, item['access_token'])) end def do_post(uri, body, access_token) http = Net::HTTP.new(uri.host, uri.port) http.open_timeout = configuration.open_timeout http.read_timeout = configuration.request_timeout if uri.scheme == 'https' http.use_ssl = true # This is needed to have 1.8.7 passing tests http.ca_file = ENV['ROLLBAR_SSL_CERT_FILE'] if ENV.key?('ROLLBAR_SSL_CERT_FILE') http.verify_mode = ssl_verify_mode end request = Net::HTTP::Post.new(uri.request_uri) request.body = body request.add_field('X-Rollbar-Access-Token', access_token) handle_net_retries { http.request(request) } end def handle_net_retries return yield if skip_retries? retries = configuration.net_retries - 1 begin yield rescue *LanguageSupport.timeout_exceptions raise if retries <= 0 retries -= 1 retry end end def skip_retries? Rollbar::LanguageSupport.ruby_18? || Rollbar::LanguageSupport.ruby_19? end def handle_response(response) if response.code == '200' log_info '[Rollbar] Success' else log_warning "[Rollbar] Got unexpected status code from Rollbar api: #{response.code}" log_info "[Rollbar] Response: #{response.body}" end end def ssl_verify_mode if configuration.verify_ssl_peer OpenSSL::SSL::VERIFY_PEER else OpenSSL::SSL::VERIFY_NONE end end def write_item(item) if configuration.use_async @file_semaphore.synchronize do do_write_item(item) end else do_write_item(item) end end def do_write_item(item) log_info '[Rollbar] Writing item to file' body = item.dump return unless body begin @file ||= File.open(configuration.filepath, 'a') @file.puts(body) @file.flush log_info '[Rollbar] Success' rescue IOError => e log_error "[Rollbar] Error opening/writing to file: #{e}" end end def failsafe_reason(message, exception) body = '' if exception begin backtrace = exception.backtrace || [] nearest_frame = backtrace[0] exception_info = exception.class.name # #to_s and #message defaults to class.to_s. Add message only if add valuable info. exception_info += %(: "#{exception.message}") if exception.message != exception.class.to_s exception_info += " in #{nearest_frame}" if nearest_frame body += "#{exception_info}: #{message}" rescue log_error('[Rollbar] Error building failsafe exception message') end else begin body += message.to_s rescue log_error('[Rollbar] Error building failsafe message') end end body end def failsafe_body(reason) "Failsafe from rollbar-gem. #{reason}" end def schedule_item(item) return unless item log_info '[Rollbar] Scheduling item' if configuration.use_async process_async_item(item) else process_item(item) end end def default_async_handler return Rollbar::Delay::GirlFriday if defined?(GirlFriday) Rollbar::Delay::Thread end def process_async_item(item) configuration.async_handler ||= default_async_handler configuration.async_handler.call(item.payload) rescue if configuration.failover_handlers.empty? log_error '[Rollbar] Async handler failed, and there are no failover handlers configured. See the docs for "failover_handlers"' return end async_failover(item) end def async_failover(item) log_warning '[Rollbar] Primary async handler failed. Trying failovers...' failover_handlers = configuration.failover_handlers failover_handlers.each do |handler| begin handler.call(item.payload) rescue next unless handler == failover_handlers.last log_error "[Rollbar] All failover handlers failed while processing item: #{Rollbar::JSON.dump(item.payload)}" end end end alias_method :log_warning, :log_warn def log_instance_link(data) return unless data[:uuid] uuid_url = Util.uuid_rollbar_url(data, configuration) log_info "[Rollbar] Details: #{uuid_url} (only available if report was successful)" end def logger @logger ||= LoggerProxy.new(configuration.logger) end end end