Class: NewRelic::Agent::ErrorCollector

Inherits:
Object
  • Object
show all
Defined in:
lib/new_relic/agent/error_collector.rb

Overview

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

Constant Summary collapse

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

20
EXCEPTION_TAG_IVAR =
:'@__nr_seen_exception'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(events) ⇒ ErrorCollector

Returns a new error collector



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 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

Instance Attribute Details

#error_event_aggregatorObject (readonly)

Returns the value of attribute error_event_aggregator.



19
20
21
# File 'lib/new_relic/agent/error_collector.rb', line 19

def error_event_aggregator
  @error_event_aggregator
end

#error_trace_aggregatorObject (readonly)

Returns the value of attribute error_trace_aggregator.



19
20
21
# File 'lib/new_relic/agent/error_collector.rb', line 19

def error_trace_aggregator
  @error_trace_aggregator
end

Class Method Details

.ignore_error_filterObject



71
72
73
# 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) ⇒ Object

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.)



61
62
63
64
65
66
67
68
69
# 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

Instance Method Details

#aggregated_metric_names(txn) ⇒ Object



181
182
183
184
185
186
187
188
189
190
191
192
# 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) ⇒ Object



173
174
175
176
177
178
179
# 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) ⇒ Object



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# 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?Boolean

Returns:

  • (Boolean)


42
43
44
# File 'lib/new_relic/agent/error_collector.rb', line 42

def disabled?
  !enabled?
end

#drop_buffered_dataObject



328
329
330
331
332
# 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?Boolean

Returns:

  • (Boolean)


38
39
40
# 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) ⇒ Boolean

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

Returns:

  • (Boolean)


125
126
127
128
129
130
131
132
133
134
# 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) ⇒ Boolean

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

Returns:

  • (Boolean)


106
107
108
109
110
111
# 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) ⇒ Boolean

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

Returns:

  • (Boolean)


143
144
145
# 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) ⇒ Boolean

Returns:

  • (Boolean)


147
148
149
150
151
# 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) ⇒ Object



83
84
85
# File 'lib/new_relic/agent/error_collector.rb', line 83

def expect(errors)
  @error_filter.expect(errors)
end

#expected?(ex, status_code = nil) ⇒ Boolean

Returns:

  • (Boolean)


87
88
89
# 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) ⇒ Object

extracts a stack trace from the exception for debugging purposes



230
231
232
233
234
235
236
237
# 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) ⇒ Object



75
76
77
# File 'lib/new_relic/agent/error_collector.rb', line 75

def ignore(errors)
  @error_filter.ignore(errors)
end

#ignore?(ex, status_code = nil) ⇒ Boolean

Returns:

  • (Boolean)


79
80
81
# 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) ⇒ Boolean

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

Returns:

  • (Boolean)


101
102
103
# 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 = {}) ⇒ Object

Increments a statistic that tracks total error rate



195
196
197
198
199
200
201
202
203
204
205
206
# 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) ⇒ Object



208
209
210
211
212
213
# 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_filtersObject



91
92
93
# File 'lib/new_relic/agent/error_collector.rb', line 91

def load_error_filters
  @error_filter.load_all
end

#notice_agent_error(exception) ⇒ Object

*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.



324
325
326
# 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) ⇒ Object

See NewRelic::Agent.notice_error for options and commentary



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# 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 = {}) ⇒ Object



239
240
241
242
243
244
245
246
247
248
249
250
# 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_filtersObject



95
96
97
# File 'lib/new_relic/agent/error_collector.rb', line 95

def reset_error_filters
  @error_filter.reset
end

#sense_method(object, method) ⇒ Object

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



225
226
227
# 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) ⇒ Boolean

Returns:

  • (Boolean)


215
216
217
218
219
220
# 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) ⇒ Object



163
164
165
166
167
168
169
170
171
# 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) ⇒ Object



153
154
155
156
157
158
159
160
161
# 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) ⇒ Object



276
277
278
279
280
281
282
283
284
285
286
287
288
# 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