newrelic_rpm 9.3.0 → 9.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/.build_ignore +6 -1
  3. data/CHANGELOG.md +193 -6
  4. data/README.md +4 -0
  5. data/Rakefile +1 -1
  6. data/lib/new_relic/agent/attribute_pre_filtering.rb +109 -0
  7. data/lib/new_relic/agent/configuration/default_source.rb +177 -34
  8. data/lib/new_relic/agent/configuration/environment_source.rb +1 -1
  9. data/lib/new_relic/agent/distributed_tracing.rb +1 -1
  10. data/lib/new_relic/agent/http_clients/async_http_wrappers.rb +83 -0
  11. data/lib/new_relic/agent/http_clients/ethon_wrappers.rb +111 -0
  12. data/lib/new_relic/agent/http_clients/httpx_wrappers.rb +93 -0
  13. data/lib/new_relic/agent/instrumentation/action_controller_other_subscriber.rb +1 -1
  14. data/lib/new_relic/agent/instrumentation/active_record_helper.rb +1 -2
  15. data/lib/new_relic/agent/instrumentation/active_support_broadcast_logger/chain.rb +69 -0
  16. data/lib/new_relic/agent/instrumentation/active_support_broadcast_logger/instrumentation.rb +13 -0
  17. data/lib/new_relic/agent/instrumentation/active_support_broadcast_logger/prepend.rb +37 -0
  18. data/lib/new_relic/agent/instrumentation/active_support_broadcast_logger.rb +23 -0
  19. data/lib/new_relic/agent/instrumentation/active_support_logger/instrumentation.rb +4 -0
  20. data/lib/new_relic/agent/instrumentation/active_support_logger.rb +3 -1
  21. data/lib/new_relic/agent/instrumentation/async_http/chain.rb +23 -0
  22. data/lib/new_relic/agent/instrumentation/async_http/instrumentation.rb +37 -0
  23. data/lib/new_relic/agent/instrumentation/async_http/prepend.rb +15 -0
  24. data/lib/new_relic/agent/instrumentation/async_http.rb +26 -0
  25. data/lib/new_relic/agent/instrumentation/bunny/instrumentation.rb +9 -0
  26. data/lib/new_relic/agent/instrumentation/concurrent_ruby/instrumentation.rb +2 -2
  27. data/lib/new_relic/agent/instrumentation/controller_instrumentation.rb +1 -2
  28. data/lib/new_relic/agent/instrumentation/curb/instrumentation.rb +4 -0
  29. data/lib/new_relic/agent/instrumentation/delayed_job/instrumentation.rb +3 -0
  30. data/lib/new_relic/agent/instrumentation/elasticsearch/instrumentation.rb +4 -1
  31. data/lib/new_relic/agent/instrumentation/ethon/chain.rb +39 -0
  32. data/lib/new_relic/agent/instrumentation/ethon/instrumentation.rb +105 -0
  33. data/lib/new_relic/agent/instrumentation/ethon/prepend.rb +35 -0
  34. data/lib/new_relic/agent/instrumentation/ethon.rb +39 -0
  35. data/lib/new_relic/agent/instrumentation/excon/middleware.rb +3 -0
  36. data/lib/new_relic/agent/instrumentation/grape/instrumentation.rb +4 -0
  37. data/lib/new_relic/agent/instrumentation/grpc/client/instrumentation.rb +4 -0
  38. data/lib/new_relic/agent/instrumentation/grpc/server/instrumentation.rb +4 -0
  39. data/lib/new_relic/agent/instrumentation/grpc_client.rb +1 -1
  40. data/lib/new_relic/agent/instrumentation/grpc_server.rb +1 -1
  41. data/lib/new_relic/agent/instrumentation/httpclient/instrumentation.rb +4 -0
  42. data/lib/new_relic/agent/instrumentation/httprb/instrumentation.rb +4 -0
  43. data/lib/new_relic/agent/instrumentation/httpx/chain.rb +20 -0
  44. data/lib/new_relic/agent/instrumentation/httpx/instrumentation.rb +51 -0
  45. data/lib/new_relic/agent/instrumentation/httpx/prepend.rb +15 -0
  46. data/lib/new_relic/agent/instrumentation/httpx.rb +27 -0
  47. data/lib/new_relic/agent/instrumentation/logger/instrumentation.rb +3 -0
  48. data/lib/new_relic/agent/instrumentation/memcache/instrumentation.rb +9 -0
  49. data/lib/new_relic/agent/instrumentation/memcache.rb +2 -2
  50. data/lib/new_relic/agent/instrumentation/mongodb_command_subscriber.rb +1 -3
  51. data/lib/new_relic/agent/instrumentation/net_http/instrumentation.rb +5 -1
  52. data/lib/new_relic/agent/instrumentation/notifications_subscriber.rb +4 -0
  53. data/lib/new_relic/agent/instrumentation/padrino/instrumentation.rb +4 -0
  54. data/lib/new_relic/agent/instrumentation/queue_time.rb +1 -1
  55. data/lib/new_relic/agent/instrumentation/rack/instrumentation.rb +6 -0
  56. data/lib/new_relic/agent/instrumentation/rails3/action_controller.rb +4 -0
  57. data/lib/new_relic/agent/instrumentation/rails_notifications/action_controller.rb +1 -0
  58. data/lib/new_relic/agent/instrumentation/rake/instrumentation.rb +4 -0
  59. data/lib/new_relic/agent/instrumentation/redis/instrumentation.rb +4 -0
  60. data/lib/new_relic/agent/instrumentation/resque/instrumentation.rb +4 -0
  61. data/lib/new_relic/agent/instrumentation/roda/chain.rb +43 -0
  62. data/lib/new_relic/agent/instrumentation/roda/ignorer.rb +45 -0
  63. data/lib/new_relic/agent/instrumentation/roda/instrumentation.rb +68 -0
  64. data/lib/new_relic/agent/instrumentation/roda/prepend.rb +24 -0
  65. data/lib/new_relic/agent/instrumentation/roda/roda_transaction_namer.rb +29 -0
  66. data/lib/new_relic/agent/instrumentation/roda.rb +36 -0
  67. data/lib/new_relic/agent/instrumentation/sequel.rb +1 -1
  68. data/lib/new_relic/agent/instrumentation/sidekiq/client.rb +4 -0
  69. data/lib/new_relic/agent/instrumentation/sidekiq/server.rb +26 -3
  70. data/lib/new_relic/agent/instrumentation/sidekiq.rb +5 -3
  71. data/lib/new_relic/agent/instrumentation/sinatra/instrumentation.rb +4 -0
  72. data/lib/new_relic/agent/instrumentation/sinatra/transaction_namer.rb +1 -3
  73. data/lib/new_relic/agent/instrumentation/stripe.rb +28 -0
  74. data/lib/new_relic/agent/instrumentation/stripe_subscriber.rb +77 -0
  75. data/lib/new_relic/agent/instrumentation/tilt/instrumentation.rb +4 -0
  76. data/lib/new_relic/agent/instrumentation/typhoeus/instrumentation.rb +5 -1
  77. data/lib/new_relic/agent/log_event_attributes.rb +1 -1
  78. data/lib/new_relic/agent/messaging.rb +2 -2
  79. data/lib/new_relic/agent/monitors/synthetics_monitor.rb +12 -1
  80. data/lib/new_relic/agent/new_relic_service.rb +33 -17
  81. data/lib/new_relic/agent/pipe_service.rb +1 -1
  82. data/lib/new_relic/agent/rules_engine.rb +1 -1
  83. data/lib/new_relic/agent/span_event_primitive.rb +16 -4
  84. data/lib/new_relic/agent/system_info.rb +26 -0
  85. data/lib/new_relic/agent/tracer.rb +5 -6
  86. data/lib/new_relic/agent/transaction/abstract_segment.rb +55 -0
  87. data/lib/new_relic/agent/transaction/external_request_segment.rb +5 -2
  88. data/lib/new_relic/agent/transaction/message_broker_segment.rb +1 -2
  89. data/lib/new_relic/agent/transaction/request_attributes.rb +46 -10
  90. data/lib/new_relic/agent/transaction.rb +30 -6
  91. data/lib/new_relic/agent/transaction_error_primitive.rb +16 -0
  92. data/lib/new_relic/agent/transaction_event_primitive.rb +19 -0
  93. data/lib/new_relic/agent/utilization/gcp.rb +1 -3
  94. data/lib/new_relic/agent/utilization/vendor.rb +5 -7
  95. data/lib/new_relic/agent.rb +19 -3
  96. data/lib/new_relic/cli/command.rb +1 -0
  97. data/lib/new_relic/constants.rb +3 -0
  98. data/lib/new_relic/control/class_methods.rb +1 -7
  99. data/lib/new_relic/control/frameworks/rails.rb +14 -2
  100. data/lib/new_relic/control/frameworks/roda.rb +20 -0
  101. data/lib/new_relic/language_support.rb +9 -0
  102. data/lib/new_relic/noticed_error.rb +5 -2
  103. data/lib/new_relic/rack/agent_hooks.rb +1 -1
  104. data/lib/new_relic/rack/agent_middleware.rb +0 -16
  105. data/lib/new_relic/rack/browser_monitoring.rb +1 -1
  106. data/lib/new_relic/supportability_helper.rb +1 -0
  107. data/lib/new_relic/version.rb +1 -1
  108. data/lib/tasks/bump_version.rake +1 -1
  109. data/lib/tasks/config.rake +3 -2
  110. data/lib/tasks/helpers/config.html.erb +93 -0
  111. data/lib/tasks/helpers/format.rb +11 -7
  112. data/lib/tasks/helpers/version_bump.rb +2 -2
  113. data/lib/tasks/instrumentation_generator/instrumentation.thor +3 -3
  114. data/lib/tasks/newrelicyml.rake +1 -1
  115. data/lib/tasks/tests.rake +71 -0
  116. data/newrelic.yml +103 -31
  117. data/newrelic_rpm.gemspec +12 -6
  118. data/test/agent_helper.rb +1027 -0
  119. metadata +63 -8
  120. data/lib/tasks/helpers/removers.rb +0 -33
  121. data/lib/tasks/multiverse.rake +0 -6
  122. data/lib/tasks/multiverse.rb +0 -76
@@ -0,0 +1,1027 @@
1
+ # This file is distributed under New Relic's license terms.
2
+ # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
3
+ # frozen_string_literal: true
4
+
5
+ # These helpers should not have any gem dependencies except on newrelic_rpm
6
+ # itself, and should be usable from within any multiverse suite.
7
+
8
+ require 'json'
9
+ require 'net/http'
10
+ begin
11
+ require 'net/http/status'
12
+ rescue LoadError
13
+ # NOP -- Net::HTTP::STATUS_CODES was introduced in Ruby 2.5
14
+ end
15
+
16
+ class ArrayLogDevice
17
+ def initialize(array = [])
18
+ @array = array
19
+ end
20
+ attr_reader :array
21
+
22
+ def write(message)
23
+ @array << message
24
+ end
25
+
26
+ def close; end
27
+ end
28
+
29
+ def fake_guid(length = 16)
30
+ NewRelic::Agent::GuidGenerator.generate_guid(length)
31
+ end
32
+
33
+ def assert_between(floor, ceiling, value, message = "expected #{floor} <= #{value} <= #{ceiling}")
34
+ assert((floor <= value && value <= ceiling), message)
35
+ end
36
+
37
+ def assert_in_delta(expected, actual, delta)
38
+ assert_between((expected - delta), (expected + delta), actual)
39
+ end
40
+
41
+ def harvest_error_traces!
42
+ NewRelic::Agent.instance.error_collector.error_trace_aggregator.harvest!
43
+ end
44
+
45
+ def reset_error_traces!
46
+ NewRelic::Agent.instance.error_collector.error_trace_aggregator.reset!
47
+ end
48
+
49
+ def assert_has_traced_error(error_class)
50
+ errors = harvest_error_traces!
51
+
52
+ refute_nil errors.find { |e| e.exception_class_name == error_class.name }, \
53
+ "Didn't find error of class #{error_class}"
54
+ end
55
+
56
+ def last_traced_error
57
+ harvest_error_traces!.last
58
+ end
59
+
60
+ def harvest_transaction_events!
61
+ NewRelic::Agent.instance.transaction_event_aggregator.harvest!
62
+ end
63
+
64
+ def last_transaction_event
65
+ harvest_transaction_events!.last.last
66
+ end
67
+
68
+ def harvest_span_events!
69
+ NewRelic::Agent.instance.span_event_aggregator.harvest!
70
+ end
71
+
72
+ def last_span_event
73
+ harvest_span_events!.last.last
74
+ end
75
+
76
+ def harvest_error_events!
77
+ NewRelic::Agent.instance.error_collector.error_event_aggregator.harvest!
78
+ end
79
+
80
+ def last_error_event
81
+ harvest_error_events!.last.last
82
+ end
83
+
84
+ unless defined? assert_includes
85
+ def assert_includes(collection, member, msg = nil)
86
+ msg = "Expected #{collection.inspect} to include #{member.inspect}"
87
+
88
+ assert_includes collection, member, msg
89
+ end
90
+ end
91
+
92
+ unless defined? assert_not_includes
93
+ def assert_not_includes(collection, member, msg = nil)
94
+ msg = "Expected #{collection.inspect} not to include #{member.inspect}"
95
+
96
+ refute_includes collection, member, msg
97
+ end
98
+ end
99
+
100
+ unless defined? assert_empty
101
+ def assert_empty(collection, msg = nil)
102
+ assert_empty collection, msg
103
+ end
104
+ end
105
+
106
+ def assert_equal_unordered(left, right)
107
+ assert_equal(left.length, right.length, "Lengths don't match. #{left.length} != #{right.length}")
108
+ left.each { |element| assert_includes(right, element) }
109
+ end
110
+
111
+ def assert_log_contains(log, message)
112
+ lines = log.array
113
+
114
+ assert (lines.any? { |line| line.match(message) }),
115
+ "Could not find message. Log contained: #{lines.join("\n")}"
116
+ end
117
+
118
+ def assert_audit_log_contains(audit_log_contents, needle)
119
+ # Original request bodies dumped to the log have symbol keys, but once
120
+ # they go through a dump/load, they're strings again, so we strip
121
+ # double-quotes and colons from the log, and the strings we searching for.
122
+ regex = /[:"]/
123
+ needle = needle.gsub(regex, '')
124
+ haystack = audit_log_contents.gsub(regex, '')
125
+
126
+ assert_includes(haystack, needle, "Expected log to contain '#{needle}'")
127
+ end
128
+
129
+ # Because we don't generate a strictly machine-readable representation of
130
+ # request bodies for the audit log, the transformation into strings is
131
+ # effectively one-way. This, combined with the fact that Hash traversal order
132
+ # is arbitrary in Ruby 1.8.x means that it's difficult to directly assert that
133
+ # some object graph made it into the audit log (due to different possible
134
+ # orderings of the key/value pairs in Hashes that were embedded in the request
135
+ # body). So, this method traverses an object graph and only makes assertions
136
+ # about the terminal (non-Array-or-Hash) nodes therein.
137
+ def assert_audit_log_contains_object(audit_log_contents, o, format = :json)
138
+ case o
139
+ when Hash
140
+ o.each do |k, v|
141
+ assert_audit_log_contains_object(audit_log_contents, v, format)
142
+ assert_audit_log_contains_object(audit_log_contents, k, format)
143
+ end
144
+ when Array
145
+
146
+ o.each do |el|
147
+ assert_audit_log_contains_object(audit_log_contents, el, format)
148
+ end
149
+ when NilClass
150
+
151
+ assert_audit_log_contains(audit_log_contents, format == :json ? 'null' : 'nil')
152
+ else
153
+ assert_audit_log_contains(audit_log_contents, o.inspect)
154
+ end
155
+ end
156
+
157
+ def compare_metrics(expected, actual)
158
+ actual.delete_if { |a| a.include?('GC/Transaction/') }
159
+
160
+ assert_equal(expected.to_a.sort, actual.to_a.sort, "extra: #{(actual - expected).to_a.inspect}; missing: #{(expected - actual).to_a.inspect}")
161
+ end
162
+
163
+ def metric_spec_from_specish(specish)
164
+ spec = case specish
165
+ when String then NewRelic::MetricSpec.new(specish)
166
+ when Array then NewRelic::MetricSpec.new(*specish)
167
+ end
168
+ spec
169
+ end
170
+
171
+ def _normalize_metric_expectations(expectations)
172
+ case expectations
173
+ when Array
174
+ hash = {}
175
+ # Just assert that the metric is present, nothing about the attributes
176
+ expectations.each { |k| hash[k] = {} }
177
+ hash
178
+ when String
179
+ {expectations => {}}
180
+ else
181
+ expectations
182
+ end
183
+ end
184
+
185
+ def dump_stats(stats)
186
+ str = +" Call count: #{stats.call_count}\n"
187
+ str << " Total call time: #{stats.total_call_time}\n"
188
+ str << " Total exclusive time: #{stats.total_exclusive_time}\n"
189
+ str << " Min call time: #{stats.min_call_time}\n"
190
+ str << " Max call time: #{stats.max_call_time}\n"
191
+ str << " Sum of squares: #{stats.sum_of_squares}\n"
192
+ str << " Apdex S: #{stats.apdex_s}\n"
193
+ str << " Apdex T: #{stats.apdex_t}\n"
194
+ str << " Apdex F: #{stats.apdex_f}\n"
195
+ str
196
+ end
197
+
198
+ def assert_stats_has_values(stats, expected_spec, expected_attrs)
199
+ expected_attrs.each do |attr, expected_value|
200
+ actual_value = stats.send(attr)
201
+
202
+ msg = "Expected #{attr} for #{expected_spec} to be #{'~' unless attr == :call_count}#{expected_value}, " \
203
+ "got #{actual_value}.\nActual stats:\n#{dump_stats(stats)}"
204
+
205
+ if attr == :call_count
206
+ assert_stats_has_values_with_call_count(expected_value, actual_value, msg)
207
+ else
208
+ assert_in_delta(expected_value, actual_value, 0.0001, msg)
209
+ end
210
+ end
211
+ end
212
+
213
+ def assert_stats_has_values_with_call_count(expected_value, actual_value, msg)
214
+ # >, <, >=, <= comparisons
215
+ if expected_value.to_s =~ /([<>]=?)\s*(\d+)/
216
+ operator = Regexp.last_match(1).to_sym
217
+ count = Regexp.last_match(2).to_i
218
+
219
+ assert_operator(actual_value, operator, count, msg)
220
+ # == comparison
221
+ else
222
+ assert_equal(expected_value, actual_value, msg)
223
+ end
224
+ end
225
+
226
+ def assert_metrics_recorded(expected)
227
+ expected = _normalize_metric_expectations(expected)
228
+ expected.each do |specish, expected_attrs|
229
+ expected_spec = metric_spec_from_specish(specish)
230
+ actual_stats = NewRelic::Agent.instance.stats_engine.to_h[expected_spec]
231
+ if !actual_stats
232
+ all_specs = NewRelic::Agent.instance.stats_engine.to_h.keys.sort
233
+ matches = all_specs.select { |spec| spec.name == expected_spec.name }
234
+ matches.map! { |m| " #{m.inspect}" }
235
+
236
+ msg = "Did not find stats for spec #{expected_spec.inspect}."
237
+ msg += "\nDid find specs: [\n#{matches.join(",\n")}\n]" unless matches.empty?
238
+ msg += "\nAll specs in there were: #{format_metric_spec_list(all_specs)}"
239
+
240
+ assert(actual_stats, msg)
241
+ end
242
+
243
+ assert_stats_has_values(actual_stats, expected_spec, expected_attrs)
244
+ end
245
+ end
246
+
247
+ # Use this to assert that *only* the given set of metrics has been recorded.
248
+ #
249
+ # If you want to scope the search for unexpected metrics to a particular
250
+ # namespace (e.g. metrics matching 'Controller/'), pass a Regex for the
251
+ # :filter option. Only metrics matching the regex will be searched when looking
252
+ # for unexpected metrics.
253
+ #
254
+ # If you want to *allow* unexpected metrics matching certain patterns, use
255
+ # the :ignore_filter option. This will allow you to specify a Regex that
256
+ # allowlists broad swathes of metric territory (e.g. 'Supportability/').
257
+ #
258
+ def assert_metrics_recorded_exclusive(expected, options = {})
259
+ expected = _normalize_metric_expectations(expected)
260
+
261
+ assert_metrics_recorded(expected)
262
+
263
+ recorded_metrics = NewRelic::Agent.instance.stats_engine.to_h.keys
264
+
265
+ if options[:filter]
266
+ recorded_metrics = recorded_metrics.select { |m| m.name.match(options[:filter]) }
267
+ end
268
+ if options[:ignore_filter]
269
+ recorded_metrics.reject! { |m| m.name.match(options[:ignore_filter]) }
270
+ end
271
+
272
+ expected_metrics = expected.keys.map { |s| metric_spec_from_specish(s) }
273
+
274
+ unexpected_metrics = recorded_metrics - expected_metrics
275
+ unexpected_metrics.reject! { |m| m.name.include?('GC/Transaction') }
276
+
277
+ assert_equal(0, unexpected_metrics.size, "Found unexpected metrics: #{format_metric_spec_list(unexpected_metrics)}")
278
+ end
279
+
280
+ def assert_newrelic_metadata_present(metadata)
281
+ assert metadata.key?('newrelic')
282
+ refute_nil metadata['newrelic']
283
+ end
284
+
285
+ def assert_distributed_tracing_payload_created_for_transaction(transaction)
286
+ assert transaction.distributed_tracer.instance_variable_get(:@distributed_trace_payload_created)
287
+ end
288
+
289
+ # The clear_metrics! method prevents metrics from "leaking" between tests by resetting
290
+ # the @stats_hash instance variable in the current instance of NewRelic::Agent::StatsEngine.
291
+
292
+ module NewRelic
293
+ module Agent
294
+ class StatsEngine
295
+ def reset_for_test!
296
+ @stats_hash = StatsHash.new
297
+ end
298
+ end
299
+ end
300
+ end
301
+
302
+ def clear_metrics!
303
+ NewRelic::Agent.instance.stats_engine.reset_for_test!
304
+ end
305
+
306
+ def assert_metrics_not_recorded(not_expected)
307
+ not_expected = _normalize_metric_expectations(not_expected)
308
+ found_but_not_expected = []
309
+ not_expected.each do |specish, _|
310
+ spec = metric_spec_from_specish(specish)
311
+ if NewRelic::Agent.instance.stats_engine.to_h[spec]
312
+ found_but_not_expected << spec
313
+ end
314
+ end
315
+
316
+ assert_empty(found_but_not_expected, "Found unexpected metrics: #{format_metric_spec_list(found_but_not_expected)}")
317
+ end
318
+
319
+ alias :refute_metrics_recorded :assert_metrics_not_recorded
320
+
321
+ def assert_no_metrics_match(regex)
322
+ matching_metrics = []
323
+ NewRelic::Agent.instance.stats_engine.to_h.keys.map(&:to_s).each do |metric|
324
+ matching_metrics << metric if metric.match(regex)
325
+ end
326
+
327
+ assert_empty(
328
+ matching_metrics,
329
+ "Found unexpected metrics:\n" + matching_metrics.map { |m| " '#{m}'" }.join("\n") + "\n\n"
330
+ )
331
+ end
332
+
333
+ alias :refute_metrics_match :assert_no_metrics_match
334
+
335
+ def format_metric_spec_list(specs)
336
+ spec_strings = specs.map do |spec|
337
+ "#{spec.name} (#{spec.scope.empty? ? '<unscoped>' : spec.scope})"
338
+ end
339
+ "[\n #{spec_strings.join(",\n ")}\n]"
340
+ end
341
+
342
+ def assert_truthy(expected, msg = nil)
343
+ msg ||= "Expected #{expected.inspect} to be truthy"
344
+
345
+ refute !expected, msg
346
+ end
347
+
348
+ def assert_falsy(expected, msg = nil)
349
+ msg ||= "Expected #{expected.inspect} to be falsy"
350
+
351
+ refute expected, msg
352
+ end
353
+
354
+ unless defined? assert_false
355
+ def assert_false(expected)
356
+ refute expected
357
+ end
358
+ end
359
+
360
+ unless defined? refute
361
+ alias refute assert_false
362
+ end
363
+
364
+ # Mock up a transaction for testing purposes, optionally specifying a name and
365
+ # transaction category. The given block will be executed within the context of the
366
+ # dummy transaction.
367
+ #
368
+ # Examples:
369
+ #
370
+ # With default name ('dummy') and category (:other):
371
+ # in_transaction { ... }
372
+ #
373
+ # With an explicit transaction name and default category:
374
+ # in_transaction('foobar') { ... }
375
+ #
376
+ # With default name and explicit category:
377
+ # in_transaction(:category => :controller) { ... }
378
+ #
379
+ # With a transaction name plus category:
380
+ # in_transaction('foobar', :category => :controller) { ... }
381
+ #
382
+ def in_transaction(*args, &blk)
383
+ opts = args.last&.is_a?(Hash) ? args.pop : {}
384
+ category = (opts&.delete(:category)) || :other
385
+
386
+ # At least one test passes `:transaction_name => nil`, so handle it gently
387
+ name = opts.key?(:transaction_name) ? opts.delete(:transaction_name) : args.first || 'dummy'
388
+
389
+ state = NewRelic::Agent::Tracer.state
390
+ txn = nil
391
+
392
+ NewRelic::Agent::Tracer.in_transaction(name: name, category: category, options: opts) do
393
+ txn = state.current_transaction
394
+ yield(state.current_transaction)
395
+ end
396
+
397
+ txn
398
+ end
399
+
400
+ # Temporarily disables default transformer so tests with invalid inputs can be tried
401
+ def with_disabled_defaults_transformer(key)
402
+ begin
403
+ transformer = NewRelic::Agent::Configuration::DEFAULTS[key][:transform]
404
+ NewRelic::Agent::Configuration::DEFAULTS[key][:transform] = nil
405
+ yield
406
+ ensure
407
+ NewRelic::Agent::Configuration::DEFAULTS[key][:transform] = transformer
408
+ end
409
+ end
410
+
411
+ # Convenience wrapper to stand up a transaction and provide a segment within
412
+ # that transaction to work with. The same arguments as provided to in_transaction
413
+ # may be supplied.
414
+ def with_segment(*args, &blk)
415
+ segment = nil
416
+ txn = in_transaction(*args) do |t|
417
+ segment = t.current_segment
418
+ yield(segment, t)
419
+ end
420
+ [segment, txn]
421
+ end
422
+
423
+ # building error attributes on segments are deferred until it's time
424
+ # to publish/harvest them as spans, so for testing, we'll explicitly
425
+ # build 'em as appropriate so we can test 'em
426
+ def build_deferred_error_attributes(segment)
427
+ return unless segment.noticed_error
428
+ return if segment.noticed_error_attributes.frozen?
429
+
430
+ segment.noticed_error.build_error_attributes
431
+ end
432
+
433
+ def capture_segment_with_error
434
+ begin
435
+ segment_with_error = nil
436
+ with_segment do |segment|
437
+ segment_with_error = segment
438
+ raise 'oops!'
439
+ end
440
+ rescue Exception => exception
441
+ assert segment_with_error, 'expected to have a segment_with_error'
442
+ build_deferred_error_attributes(segment_with_error)
443
+ return segment_with_error, exception
444
+ end
445
+ end
446
+
447
+ def stub_transaction_guid(guid)
448
+ NewRelic::Agent::Transaction.tl_current.instance_variable_set(:@guid, guid)
449
+ end
450
+
451
+ # Convenience wrapper around in_transaction that sets the category so that it
452
+ # looks like we are in a web transaction
453
+ def in_web_transaction(name = 'dummy')
454
+ in_transaction(name, :category => :controller, :request => stub(:path => '/')) do |txn|
455
+ yield(txn)
456
+ end
457
+ end
458
+
459
+ def in_background_transaction(name = 'silly')
460
+ in_transaction(name, :category => :task) do |txn|
461
+ yield(txn)
462
+ end
463
+ end
464
+
465
+ def refute_contains_request_params(attributes)
466
+ attributes.keys.each do |key|
467
+ refute_match(/^request\.parameters\./, key.to_s)
468
+ end
469
+ end
470
+
471
+ def last_transaction_trace
472
+ return unless last_sample = NewRelic::Agent.agent.transaction_sampler.last_sample
473
+
474
+ NewRelic::Agent::Transaction::TraceBuilder.build_trace(last_sample)
475
+ end
476
+
477
+ def last_transaction_trace_request_params
478
+ agent_attributes = attributes_for(last_transaction_trace, :agent)
479
+ agent_attributes.inject({}) do |memo, (key, value)|
480
+ memo[key] = value if key.to_s.start_with?('request.parameters.')
481
+ memo
482
+ end
483
+ end
484
+
485
+ def find_sql_trace(metric_name)
486
+ NewRelic::Agent.agent.sql_sampler.sql_traces.values.detect do |trace|
487
+ trace.database_metric_name == metric_name
488
+ end
489
+ end
490
+
491
+ def last_sql_trace
492
+ NewRelic::Agent.agent.sql_sampler.sql_traces.values.last
493
+ end
494
+
495
+ def find_last_transaction_node(transaction_sample = nil)
496
+ if transaction_sample
497
+ root_node = transaction_sample.root_node
498
+ else
499
+ root_node = last_transaction_trace.root_node
500
+ end
501
+
502
+ last_node = nil
503
+ root_node.each_node { |s| last_node = s }
504
+
505
+ return last_node
506
+ end
507
+
508
+ def find_node_with_name(transaction_sample, name)
509
+ transaction_sample.root_node.each_node do |node|
510
+ if node.metric_name == name
511
+ return node
512
+ end
513
+ end
514
+
515
+ nil
516
+ end
517
+
518
+ def find_node_with_name_matching(transaction_sample, regex)
519
+ transaction_sample.root_node.each_node do |node|
520
+ if node.metric_name.match(regex)
521
+ return node
522
+ end
523
+ end
524
+
525
+ nil
526
+ end
527
+
528
+ def find_all_nodes_with_name_matching(transaction_sample, regexes)
529
+ regexes = [regexes].flatten
530
+ matching_nodes = []
531
+
532
+ transaction_sample.root_node.each_node do |node|
533
+ regexes.each do |regex|
534
+ if node.metric_name.match(regex)
535
+ matching_nodes << node
536
+ end
537
+ end
538
+ end
539
+
540
+ matching_nodes
541
+ end
542
+
543
+ def with_config(config_hash, at_start = true)
544
+ config = NewRelic::Agent::Configuration::DottedHash.new(config_hash, true)
545
+ NewRelic::Agent.config.add_config_for_testing(config, at_start)
546
+ NewRelic::Agent.instance.refresh_attribute_filter
547
+ begin
548
+ yield
549
+ ensure
550
+ NewRelic::Agent.config.remove_config(config)
551
+ NewRelic::Agent.instance.refresh_attribute_filter
552
+ end
553
+ end
554
+
555
+ def with_server_source(config_hash, at_start = true)
556
+ with_config(config_hash, at_start) do
557
+ NewRelic::Agent.config.notify_server_source_added
558
+ yield
559
+ end
560
+ end
561
+
562
+ def with_config_low_priority(config_hash)
563
+ with_config(config_hash, false) do
564
+ yield
565
+ end
566
+ end
567
+
568
+ def with_transaction_renaming_rules(rule_specs)
569
+ original_engine = NewRelic::Agent.agent.instance_variable_get(:@transaction_rules)
570
+ begin
571
+ new_engine = NewRelic::Agent::RulesEngine.create_transaction_rules('transaction_name_rules' => rule_specs)
572
+ NewRelic::Agent.agent.instance_variable_set(:@transaction_rules, new_engine)
573
+ yield
574
+ ensure
575
+ NewRelic::Agent.agent.instance_variable_set(:@transaction_rules, original_engine)
576
+ end
577
+ end
578
+
579
+ # Need to guard against double-installing this patch because in 1.8.x the same
580
+ # file can be required multiple times under different non-canonicalized paths.
581
+ unless Time.respond_to?(:__original_now)
582
+ Time.instance_eval do
583
+ class << self
584
+ attr_accessor :__frozen_now
585
+ alias_method :__original_now, :now
586
+
587
+ def now
588
+ __frozen_now || __original_now
589
+ end
590
+ end
591
+ end
592
+ end
593
+
594
+ def nr_freeze_time(now = Time.now)
595
+ Time.__frozen_now = now
596
+ end
597
+
598
+ def nr_unfreeze_time
599
+ Time.__frozen_now = nil
600
+ end
601
+
602
+ def advance_time(seconds)
603
+ Time.__frozen_now = Time.now + seconds
604
+ end
605
+
606
+ unless Process.respond_to?(:__original_clock_gettime)
607
+ Process.instance_eval do
608
+ class << self
609
+ attr_accessor :__frozen_clock_gettime
610
+ alias_method :__original_clock_gettime, :clock_gettime
611
+
612
+ def clock_gettime(clock_id, unit = :float_second)
613
+ __frozen_clock_gettime || __original_clock_gettime(clock_id, unit)
614
+ end
615
+ end
616
+ end
617
+ end
618
+
619
+ def advance_process_time(seconds, clock_id = Process::CLOCK_REALTIME)
620
+ Process.__frozen_clock_gettime = Process.clock_gettime(clock_id) + seconds
621
+ end
622
+
623
+ def nr_freeze_process_time(now = Process.clock_gettime(Process::CLOCK_REALTIME))
624
+ Process.__frozen_clock_gettime = now
625
+ end
626
+
627
+ def nr_unfreeze_process_time
628
+ Process.__frozen_clock_gettime = nil
629
+ end
630
+
631
+ def with_constant_defined(constant_symbol, implementation = Module.new)
632
+ const_path = constant_path(constant_symbol.to_s)
633
+
634
+ if const_path
635
+ # Constant is already defined, nothing to do
636
+ return yield
637
+ else
638
+ const_path = constant_path(constant_symbol.to_s, :allow_partial => true)
639
+ parent = const_path[-1]
640
+ constant_symbol = constant_symbol.to_s.split('::').last.to_sym
641
+ end
642
+
643
+ begin
644
+ parent.const_set(constant_symbol, implementation)
645
+ yield
646
+ ensure
647
+ parent.send(:remove_const, constant_symbol)
648
+ end
649
+ end
650
+
651
+ def constant_path(name, opts = {})
652
+ allow_partial = opts[:allow_partial]
653
+ path = [Object]
654
+ parts = name.gsub(/^::/, '').split('::')
655
+ parts.each do |part|
656
+ if !path.last.constants.include?(part.to_sym)
657
+ return allow_partial ? path : nil
658
+ end
659
+
660
+ path << path.last.const_get(part)
661
+ end
662
+ path
663
+ end
664
+
665
+ def get_parent(constant_name)
666
+ parent_name = constant_name.gsub(/::[^:]*$/, '')
667
+ const_path = constant_path(parent_name)
668
+ const_path ? const_path[-1] : nil
669
+ end
670
+
671
+ def undefine_constant(constant_symbol)
672
+ const_str = constant_symbol.to_s
673
+ parent = get_parent(const_str)
674
+ const_name = const_str.gsub(/.*::/, '')
675
+ return yield unless parent&.constants&.include?(const_name.to_sym)
676
+
677
+ removed_constant = parent.send(:remove_const, const_name)
678
+ yield
679
+ ensure
680
+ parent.const_set(const_name, removed_constant) if removed_constant
681
+ end
682
+
683
+ def with_debug_logging
684
+ orig_logger = NewRelic::Agent.logger
685
+ $stderr.puts '', '---', ''
686
+ NewRelic::Agent.logger =
687
+ NewRelic::Agent::AgentLogger.new('', Logger.new($stderr))
688
+
689
+ with_config(:log_level => 'debug') do
690
+ yield
691
+ end
692
+ ensure
693
+ NewRelic::Agent.logger = orig_logger
694
+ end
695
+
696
+ def create_agent_command(args = {})
697
+ NewRelic::Agent::Commands::AgentCommand.new([-1, {'name' => 'command_name', 'arguments' => args}])
698
+ end
699
+
700
+ def wait_for_backtrace_service_poll(opts = {})
701
+ defaults = {
702
+ :timeout => 10.0,
703
+ :service => NewRelic::Agent.agent.instance_variable_get(:@agent_command_router).backtrace_service,
704
+ :iterations => 1
705
+ }
706
+ opts = defaults.merge(opts)
707
+ deadline = Process.clock_gettime(Process::CLOCK_REALTIME) + opts[:timeout]
708
+
709
+ service = opts[:service]
710
+ worker_loop = service.worker_loop
711
+ worker_loop.setup(0, service.method(:poll))
712
+
713
+ until worker_loop.iterations > opts[:iterations]
714
+ sleep(0.01)
715
+ if Process.clock_gettime(Process::CLOCK_REALTIME) > deadline
716
+ raise "Timed out waiting #{opts[:timeout]} s for backtrace service poll\n" +
717
+ "Worker loop ran for #{opts[:service].worker_loop.iterations} iterations\n\n" +
718
+ Thread.list.map { |t|
719
+ "#{t.to_s}: newrelic_label: #{t[:newrelic_label].inspect}\n\n" +
720
+ (t.backtrace || []).join("\n\t")
721
+ }.join("\n\n")
722
+ end
723
+ end
724
+ end
725
+
726
+ def with_array_logger(level = :info)
727
+ orig_logger = NewRelic::Agent.logger
728
+ config = {:log_level => level}
729
+ logdev = ArrayLogDevice.new
730
+ override_logger = Logger.new(logdev)
731
+
732
+ with_config(config) do
733
+ NewRelic::Agent.logger = NewRelic::Agent::AgentLogger.new('', override_logger)
734
+ yield
735
+ end
736
+
737
+ return logdev
738
+ ensure
739
+ NewRelic::Agent.logger = orig_logger
740
+ end
741
+
742
+ # The EnvUpdater was introduced due to random fails in JRuby environment
743
+ # whereby attempting to set ENV[key] = some_value randomly failed.
744
+ # It is conjectured that this is thread related, but may also be
745
+ # a core bug in the JVM implementation of Ruby. Root cause was not
746
+ # discovered, but it was found that a combination of retrying and using
747
+ # mutex lock around the update operation was the only consistently working
748
+ # solution as the error continued to surface without the mutex and
749
+ # retry alone wasn't enough, either.
750
+ #
751
+ # JRUBY: oraclejdk8 + jruby-9.2.6.0
752
+ #
753
+ # NOTE: Singleton pattern to ensure one mutex lock for all threads
754
+ class EnvUpdater
755
+ MAX_RETRIES = 5
756
+
757
+ def initialize
758
+ @mutex = Mutex.new
759
+ end
760
+
761
+ # Will attempt the given block up to MAX_RETRIES before
762
+ # surfacing the exception down the chain.
763
+ def with_retry(retry_limit = MAX_RETRIES)
764
+ retries ||= 0
765
+ sleep(retries)
766
+ yield
767
+ rescue
768
+ (retries += 1) < retry_limit ? retry : raise
769
+ end
770
+
771
+ # Locks and updates the ENV
772
+ def safe_update(env)
773
+ with_retry do
774
+ @mutex.synchronize do
775
+ env.each { |key, val| ENV[key] = val.to_s }
776
+ end
777
+ end
778
+ end
779
+
780
+ # Locks and restores the ENV
781
+ def safe_restore(old_env)
782
+ with_retry do
783
+ @mutex.synchronize do
784
+ old_env.each { |key, val| val ? ENV[key] = val : ENV.delete(key) }
785
+ end
786
+ end
787
+ end
788
+
789
+ # Singleton pattern implemented via @@instance
790
+ def self.instance
791
+ @@instance ||= EnvUpdater.new
792
+ end
793
+
794
+ def self.safe_update(env)
795
+ instance.safe_update(env)
796
+ end
797
+
798
+ def self.safe_restore(old_env)
799
+ instance.safe_restore(old_env)
800
+ end
801
+
802
+ # Effectively saves current ENV settings for given env's key/values,
803
+ # runs given block, then restores ENV to original state before returning.
804
+ def self.inject(env, &block)
805
+ old_env = {}
806
+ env.each { |key, val| old_env[key] = ENV[key] }
807
+ begin
808
+ safe_update(env)
809
+ yield
810
+ ensure
811
+ safe_restore(old_env)
812
+ end
813
+ end
814
+
815
+ # must call instance here to ensure only one @mutex for all threads.
816
+ instance
817
+ end
818
+
819
+ # Changes ENV settings to given and runs given block and restores ENV
820
+ # to original values before returning.
821
+ def with_environment(env, &block)
822
+ EnvUpdater.inject(env) { yield }
823
+ end
824
+
825
+ def with_argv(argv)
826
+ old_argv = ARGV.dup
827
+ ARGV.clear
828
+ ARGV.concat(argv)
829
+
830
+ begin
831
+ yield
832
+ ensure
833
+ ARGV.clear
834
+ ARGV.concat(old_argv)
835
+ end
836
+ end
837
+
838
+ def with_ignore_error_filter(filter, &blk)
839
+ original_filter = NewRelic::Agent.ignore_error_filter
840
+ NewRelic::Agent.ignore_error_filter(&filter)
841
+
842
+ yield
843
+ ensure
844
+ NewRelic::Agent::ErrorCollector.ignore_error_filter = original_filter
845
+ end
846
+
847
+ def json_dump_and_encode(object)
848
+ Base64.encode64(JSON.dump(object))
849
+ end
850
+
851
+ def get_last_analytics_event
852
+ NewRelic::Agent.agent.transaction_event_aggregator.harvest![1].last
853
+ end
854
+
855
+ def swap_instance_method(target, method_name, new_method_implementation, &blk)
856
+ old_method_implementation = target.instance_method(method_name)
857
+ target.send(:define_method, method_name, new_method_implementation)
858
+ yield
859
+ rescue NameError => e
860
+ puts "Your target does not have the instance method #{method_name}"
861
+ puts e.inspect
862
+ ensure
863
+ target.send(:define_method, method_name, old_method_implementation)
864
+ end
865
+
866
+ def cross_agent_tests_dir
867
+ File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'cross_agent_tests'))
868
+ end
869
+
870
+ def load_cross_agent_test(name)
871
+ test_file_path = File.join(cross_agent_tests_dir, "#{name}.json")
872
+ data = File.read(test_file_path)
873
+ data.gsub!('callCount', 'call_count')
874
+ data = JSON.load(data)
875
+ data.each { |testcase| testcase['testname'].tr!(' ', '_') if String === testcase['testname'] }
876
+ data
877
+ end
878
+
879
+ def each_cross_agent_test(options)
880
+ options = {:dir => nil, :pattern => '*'}.update(options)
881
+ path = File.join([cross_agent_tests_dir, options[:dir], options[:pattern]].compact)
882
+ Dir.glob(path).each { |file| yield(file) }
883
+ end
884
+
885
+ def assert_event_attributes(event, test_name, expected_attributes, non_expected_attributes)
886
+ incorrect_attributes = []
887
+
888
+ event_attrs = event[0]
889
+
890
+ expected_attributes.each do |name, expected_value|
891
+ actual_value = event_attrs[name]
892
+ incorrect_attributes << name unless actual_value == expected_value
893
+ end
894
+
895
+ msg = +"Found missing or incorrect attribute values in #{test_name}:\n"
896
+
897
+ incorrect_attributes.each do |name|
898
+ msg << " #{name}: expected = #{expected_attributes[name].inspect}, actual = #{event_attrs[name].inspect}\n"
899
+ end
900
+ msg << "\n"
901
+
902
+ msg << "All event values:\n"
903
+ event_attrs.each do |name, actual_value|
904
+ msg << " #{name}: #{actual_value.inspect}\n"
905
+ end
906
+
907
+ assert_empty(incorrect_attributes, msg)
908
+
909
+ non_expected_attributes.each do |name|
910
+ refute event_attrs[name], "Found value '#{event_attrs[name]}' for attribute '#{name}', but expected nothing in #{test_name}"
911
+ end
912
+ end
913
+
914
+ def attributes_for(sample, type)
915
+ sample.attributes.instance_variable_get("@#{type}_attributes")
916
+ end
917
+
918
+ def uncache_trusted_account_key
919
+ NewRelic::Agent::Transaction::TraceContext::AccountHelpers.instance_variable_set(:@trace_state_entry_key, nil)
920
+ end
921
+
922
+ def reset_buffers_and_caches
923
+ NewRelic::Agent.drop_buffered_data
924
+ uncache_trusted_account_key
925
+ end
926
+
927
+ def message_for_status_code(code)
928
+ # Net::HTTP::STATUS_CODES was introduced in Ruby 2.5
929
+ if defined?(Net::HTTP::STATUS_CODES)
930
+ return Net::HTTP::STATUS_CODES[code]
931
+ end
932
+
933
+ case code
934
+ when 200 then 'OK'
935
+ when 404 then 'Not Found'
936
+ when 403 then 'Forbidden'
937
+ else 'Unknown'
938
+ end
939
+ end
940
+
941
+ # wraps the given headers in a Net::HTTPResponse which has accompanying
942
+ # http status code associated with it.
943
+ # a "status_code" may be passed in the headers to alter the HTTP Status Code
944
+ # that is wrapped in the response.
945
+ def mock_http_response(headers, wrap_it = true)
946
+ status_code = (headers.delete('status_code') || 200).to_i
947
+ net_http_resp = Net::HTTPResponse.new(1.0, status_code, message_for_status_code(status_code))
948
+ headers.each do |key, value|
949
+ net_http_resp.add_field(key.to_s, value)
950
+ end
951
+ return net_http_resp unless wrap_it
952
+
953
+ NewRelic::Agent::HTTPClients::NetHTTPResponse.new(net_http_resp)
954
+ end
955
+
956
+ # +expected+ can be a string or regular expression
957
+ def assert_match_or_equal(expected, value)
958
+ if expected.is_a?(Regexp)
959
+ assert_match expected, value
960
+ else
961
+ assert_equal expected, value
962
+ end
963
+ end
964
+
965
+ # selects the last segment with a noticed_error and checks
966
+ # the expectations against it.
967
+ def assert_segment_noticed_error(txn, segment_name, error_classes, error_message)
968
+ error_segment = txn.segments.reverse.detect { |s| s.noticed_error }
969
+
970
+ assert error_segment, 'Expected at least one segment with a noticed_error'
971
+
972
+ assert_match_or_equal segment_name, error_segment.name
973
+
974
+ noticed_error = error_segment.noticed_error
975
+
976
+ assert_match_or_equal error_classes, noticed_error.exception_class_name
977
+ assert_match_or_equal error_message, noticed_error.message
978
+ end
979
+
980
+ def assert_transaction_noticed_error(txn, error_classes)
981
+ refute_empty txn.exceptions, 'Expected transaction to notice the error'
982
+ assert_match_or_equal error_classes, txn.exceptions.keys.first.class.name
983
+ end
984
+
985
+ def refute_transaction_noticed_error(txn, error_class)
986
+ error_segment = txn.segments.reverse.detect { |s| s.noticed_error }
987
+
988
+ assert error_segment, 'Expected at least one segment with a noticed_error'
989
+ assert_empty txn.exceptions, 'Expected transaction to NOT notice any segment errors'
990
+ end
991
+
992
+ def refute_raises(*exp)
993
+ msg = "#{exp.pop}.\n" if String === exp.last
994
+
995
+ begin
996
+ yield
997
+ rescue MiniTest::Skip => e
998
+ puts "SKIP REPORTS: #{e.inspect}"
999
+ return e if exp.include?(MiniTest::Skip)
1000
+
1001
+ raise e
1002
+ rescue Exception => e
1003
+ puts "EXCEPTION RAISED: #{e.inspect}\n#{e.backtrace}"
1004
+ exp = exp.first if exp.size == 1
1005
+
1006
+ flunk(msg || "unexpected exception raised: #{e}")
1007
+ end
1008
+ end
1009
+
1010
+ def assert_implements(instance, method, *args)
1011
+ fail_message = "expected #{instance.class}##{method} method to be implemented"
1012
+ refute_raises NotImplementedError, fail_message do
1013
+ instance.send(method, *args)
1014
+ end
1015
+ end
1016
+
1017
+ def defer_testing_to_min_supported_rails(test_file, min_rails_version, supports_jruby = true)
1018
+ if defined?(Rails) &&
1019
+ defined?(Rails::VERSION::STRING) &&
1020
+ (Rails::VERSION::STRING.to_f >= min_rails_version) &&
1021
+ (supports_jruby || !NewRelic::LanguageSupport.jruby?)
1022
+
1023
+ yield
1024
+ else
1025
+ puts "Skipping tests in #{File.basename(test_file)} because Rails >= #{min_rails_version} is unavailable" if ENV['VERBOSE_TEST_OUTPUT']
1026
+ end
1027
+ end