class NewRelic::Agent::ErrorCollector

This class collects errors from the parent application, storing them until they are harvested and transmitted to the server

Constants

EXCEPTION_TAG_IVAR
MAX_ERROR_QUEUE_LENGTH

Maximum possible length of the queue - defaults to 20, may be made configurable in the future. This is a tradeoff between memory and data retention

Attributes

error_event_aggregator[R]
error_trace_aggregator[R]

Public Class Methods

ignore_error_filter() click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 71
def self.ignore_error_filter
  defined?(@ignore_filter) ? @ignore_filter : nil
end
ignore_error_filter=(block) click to toggle source

We store the passed block in both an ivar on the class, and implicitly within the body of the ignore_filter_proc method intentionally here. The define_method trick is needed to get around the fact that users may call ‘return’ from within their filter blocks, which would otherwise result in a LocalJumpError.

The raw block is also stored in an instance variable so that we can return it later in its original form.

This is all done at the class level in order to avoid the case where the user sets up an ignore filter on one instance of the ErrorCollector, and then that instance subsequently gets discarded during agent startup. (For example, if the agent is initially disabled, and then gets enabled via a call to manual_start later on.)

# File lib/new_relic/agent/error_collector.rb, line 61
def self.ignore_error_filter=(block)
  @ignore_filter = block
  if block
    define_method(:ignore_filter_proc, &block)
  elsif method_defined?(:ignore_filter_proc)
    remove_method(:ignore_filter_proc)
  end
  @ignore_filter
end
new(events) click to toggle source

Returns a new error collector

# File lib/new_relic/agent/error_collector.rb, line 22
def initialize(events)
  @error_trace_aggregator = ErrorTraceAggregator.new(MAX_ERROR_QUEUE_LENGTH)
  @error_event_aggregator = ErrorEventAggregator.new(events)

  @error_filter = NewRelic::Agent::ErrorFilter.new

  %w[
    ignore_classes ignore_messages ignore_status_codes
    expected_classes expected_messages expected_status_codes
  ].each do |w|
    Agent.config.register_callback(:"error_collector.#{w}") do |value|
      @error_filter.load_from_config(w, value)
    end
  end
end

Public Instance Methods

aggregated_metric_names(txn) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 181
def aggregated_metric_names(txn)
  metric_names = ['Errors/all']
  return metric_names unless txn

  if txn.recording_web_transaction?
    metric_names << 'Errors/allWeb'
  else
    metric_names << 'Errors/allOther'
  end

  metric_names
end
blamed_metric_name(txn, options) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 173
def blamed_metric_name(txn, options)
  if options[:metric] && options[:metric] != ::NewRelic::Agent::UNKNOWN_METRIC
    "Errors/#{options[:metric]}"
  else
    "Errors/#{txn.best_name}" if txn
  end
end
create_noticed_error(exception, options) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 290
def create_noticed_error(exception, options)
  error_metric = options.delete(:metric) || NewRelic::EMPTY_STR

  noticed_error = NewRelic::NoticedError.new(error_metric, exception)
  noticed_error.request_uri = options.delete(:uri) || NewRelic::EMPTY_STR
  noticed_error.request_port = options.delete(:port)
  noticed_error.attributes = options.delete(:attributes)

  noticed_error.file_name = sense_method(exception, :file_name)
  noticed_error.line_number = sense_method(exception, :line_number)
  noticed_error.stack_trace = truncate_trace(extract_stack_trace(exception))

  noticed_error.expected = !!options.delete(:expected) || expected?(exception) # rubocop:disable Style/DoubleNegation

  noticed_error.attributes_from_notice_error = options.delete(:custom_params) || {}

  # Any options that are passed to notice_error which aren't known keys
  # get treated as custom attributes, so merge them into that hash.
  noticed_error.attributes_from_notice_error.merge!(options)

  update_error_group_name(noticed_error, exception, options)

  noticed_error
end
disabled?() click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 42
def disabled?
  !enabled?
end
drop_buffered_data() click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 328
def drop_buffered_data
  @error_trace_aggregator.reset!
  @error_event_aggregator.reset!
  nil
end
enabled?() click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 38
def enabled?
  error_trace_aggregator.enabled? || error_event_aggregator.enabled?
end
error_affects_apdex?(error, options) click to toggle source

Neither ignored nor expected errors impact apdex.

Ignored errors are checked via ‘#error_is_ignored?` Expected errors are checked in 2 separate ways:

1. The presence of an `expected: true` attribute key/value pair in the
   options hash, which will be set if that key/value pair was used in
   the `notice_error` public API.
2. By calling `#expected?` which in turn calls `ErrorFilter#expected?`
   which checks for 3 things:
     - A match for user-defined HTTP status codes to expect
     - A match for user-defined error classes to expect
     - A match for user-defined error messages to expect
# File lib/new_relic/agent/error_collector.rb, line 125
def error_affects_apdex?(error, options)
  return false if error_is_ignored?(error)
  return false if options[:expected]

  !expected?(error, ::NewRelic::Agent::Tracer.state.current_transaction&.http_response_code)
rescue => e
  NewRelic::Agent.logger.error("Could not determine if error '#{error}' should impact Apdex - " \
                               "#{e.class}: #{e.message}. Defaulting to 'true' (it should impact Apdex).")
  true
end
error_is_ignored?(error, status_code = nil) click to toggle source

an error is ignored if it is nil or if it is filtered

# File lib/new_relic/agent/error_collector.rb, line 106
def error_is_ignored?(error, status_code = nil)
  error && (@error_filter.ignore?(error, status_code) || ignored_by_filter_proc?(error))
rescue => e
  NewRelic::Agent.logger.error("Error '#{error}' will NOT be ignored. Exception '#{e}' while determining whether to ignore or not.", e)
  false
end
exception_is_java_object?(exception) click to toggle source

Calling instance_variable_set on a wrapped Java object in JRuby will generate a warning unless that object’s class has already been marked as persistent, so we skip tagging of exception objects that are actually wrapped Java objects on JRuby.

See github.com/jruby/jruby/wiki/Persistence

# File lib/new_relic/agent/error_collector.rb, line 143
def exception_is_java_object?(exception)
  NewRelic::LanguageSupport.jruby? && exception.respond_to?(:java_class)
end
exception_tagged_with?(ivar, exception) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 147
def exception_tagged_with?(ivar, exception)
  return false if exception_is_java_object?(exception)

  exception.instance_variable_defined?(ivar)
end
expect(errors) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 83
def expect(errors)
  @error_filter.expect(errors)
end
expected?(ex, status_code = nil) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 87
def expected?(ex, status_code = nil)
  @error_filter.expected?(ex, status_code)
end
extract_stack_trace(exception) click to toggle source

extracts a stack trace from the exception for debugging purposes

# File lib/new_relic/agent/error_collector.rb, line 230
def extract_stack_trace(exception)
  actual_exception = if defined?(Rails::VERSION::MAJOR) && Rails::VERSION::MAJOR < 5
    sense_method(exception, :original_exception) || exception
  else
    exception
  end
  sense_method(actual_exception, :backtrace) || '<no stack trace>'
end
ignore(errors) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 75
def ignore(errors)
  @error_filter.ignore(errors)
end
ignore?(ex, status_code = nil) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 79
def ignore?(ex, status_code = nil)
  @error_filter.ignore?(ex, status_code)
end
ignored_by_filter_proc?(error) click to toggle source

Checks the provided error against the error filter, if there is an error filter

# File lib/new_relic/agent/error_collector.rb, line 101
def ignored_by_filter_proc?(error)
  respond_to?(:ignore_filter_proc) && !ignore_filter_proc(error)
end
increment_error_count!(state, exception, options = {}) click to toggle source

Increments a statistic that tracks total error rate

# File lib/new_relic/agent/error_collector.rb, line 195
def increment_error_count!(state, exception, options = {})
  txn = state.current_transaction

  metric_names = aggregated_metric_names(txn)
  blamed_metric = blamed_metric_name(txn, options)
  metric_names << blamed_metric if blamed_metric

  stats_engine = NewRelic::Agent.agent.stats_engine
  stats_engine.record_unscoped_metrics(state, metric_names) do |stats|
    stats.increment_count
  end
end
increment_expected_error_count!(state, exception) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 208
def increment_expected_error_count!(state, exception)
  stats_engine = NewRelic::Agent.agent.stats_engine
  stats_engine.record_unscoped_metrics(state, ['ErrorsExpected/all']) do |stats|
    stats.increment_count
  end
end
load_error_filters() click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 91
def load_error_filters
  @error_filter.load_all
end
notice_agent_error(exception) click to toggle source

*Use sparingly for difficult to track bugs.*

Track internal agent errors for communication back to New Relic. To use, make a specific subclass of NewRelic::Agent::InternalAgentError, then pass an instance of it to this method when your problem occurs.

Limits are treated differently for these errors. We only gather one per class per harvest, disregarding (and not impacting) the app error queue limit.

# File lib/new_relic/agent/error_collector.rb, line 324
def notice_agent_error(exception)
  @error_trace_aggregator.notice_agent_error(exception)
end
notice_error(exception, options = {}, span_id = nil) click to toggle source

See NewRelic::Agent.notice_error for options and commentary

# File lib/new_relic/agent/error_collector.rb, line 253
def notice_error(exception, options = {}, span_id = nil)
  status_code = process_http_status_code(exception, options)
  return if skip_notice_error?(exception, status_code)

  tag_exception(exception)

  state = ::NewRelic::Agent::Tracer.state
  if options[:expected]
    increment_expected_error_count!(state, exception)
  else
    increment_error_count!(state, exception, options)
  end

  noticed_error = create_noticed_error(exception, options)
  error_trace_aggregator.add_to_error_queue(noticed_error)
  span_id ||= state.current_transaction&.current_segment&.guid
  error_event_aggregator.record(noticed_error, state.current_transaction&.payload, span_id)
  exception
rescue => e
  ::NewRelic::Agent.logger.warn("Failure when capturing error '#{exception}':", e)
  nil
end
notice_segment_error(segment, exception, options = {}) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 239
def notice_segment_error(segment, exception, options = {})
  status_code = process_http_status_code(exception, options)
  return if skip_notice_error?(exception, status_code)

  options.merge!(segment.llm_event.error_attributes(exception)) if segment.llm_event

  segment.set_noticed_error(create_noticed_error(exception, options))
  exception
rescue => e
  ::NewRelic::Agent.logger.warn("Failure when capturing segment error '#{exception}':", e)
  nil
end
reset_error_filters() click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 95
def reset_error_filters
  @error_filter.reset
end
sense_method(object, method) click to toggle source

calls a method on an object, if it responds to it - used for detection and soft fail-safe. Returns nil if the method does not exist

# File lib/new_relic/agent/error_collector.rb, line 225
def sense_method(object, method)
  object.__send__(method) if object.respond_to?(method)
end
skip_notice_error?(exception, status_code = nil) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 215
def skip_notice_error?(exception, status_code = nil)
  disabled? ||
    exception.nil? ||
    exception_tagged_with?(EXCEPTION_TAG_IVAR, exception) ||
    error_is_ignored?(exception, status_code)
end
tag_exception(exception) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 163
def tag_exception(exception)
  return if exception_is_java_object?(exception) || exception.frozen?

  begin
    exception.instance_variable_set(EXCEPTION_TAG_IVAR, true)
  rescue => e
    NewRelic::Agent.logger.warn("Failed to tag exception: #{exception}: ", e)
  end
end
tag_exception_using(ivar, exception) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 153
def tag_exception_using(ivar, exception)
  return if exception_is_java_object?(exception) || exception.frozen?

  begin
    exception.instance_variable_set(ivar, true)
  rescue => e
    NewRelic::Agent.logger.warn("Failed to tag exception: #{exception}: ", e)
  end
end
truncate_trace(trace, keep_frames = nil) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 276
def truncate_trace(trace, keep_frames = nil)
  keep_frames ||= Agent.config[:'error_collector.max_backtrace_frames']
  return trace if !keep_frames || trace.length < keep_frames || trace.length == 0

  # If keep_frames is odd, we will split things up favoring the top of the trace
  keep_top = (keep_frames / 2.0).ceil
  keep_bottom = (keep_frames / 2.0).floor

  truncate_frames = trace.length - keep_frames

  truncated_trace = trace[0...keep_top].concat(["<truncated #{truncate_frames.to_s} additional frames>"]).concat(trace[-keep_bottom..-1])
  truncated_trace
end

Private Instance Methods

build_customer_callback_hash(noticed_error, exception, options) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 346
def build_customer_callback_hash(noticed_error, exception, options)
  {error: exception,
   customAttributes: noticed_error.custom_attributes,
   'request.uri': noticed_error.request_uri,
   'http.statusCode': noticed_error.agent_attributes[:'http.statusCode'],
   'http.method': noticed_error.intrinsic_attributes[:'http.method'],
   'error.expected': noticed_error.expected,
   options: options}
end
error_group_callback() click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 356
def error_group_callback
  NewRelic::Agent.error_group_callback
end
process_http_status_code(exception, options) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 360
def process_http_status_code(exception, options)
  status_code = ::NewRelic::Agent::Tracer.state.current_transaction&.http_response_code
  options[:expected] = true if !options[:expected] && @error_filter.expected?(exception, status_code)

  status_code
end
update_error_group_name(noticed_error, exception, options) click to toggle source
# File lib/new_relic/agent/error_collector.rb, line 336
def update_error_group_name(noticed_error, exception, options)
  return unless error_group_callback

  callback_hash = build_customer_callback_hash(noticed_error, exception, options)
  result = error_group_callback.call(callback_hash)
  noticed_error.error_group = result
rescue StandardError => e
  NewRelic::Agent.logger.error("Failed to obtain error group from customer callback: #{e.class} - #{e.message}")
end