scout_apm 2.6.6 → 4.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +49 -0
  3. data/.rubocop.yml +2 -5
  4. data/.travis.yml +3 -7
  5. data/CHANGELOG.markdown +45 -0
  6. data/Gemfile +1 -8
  7. data/gems/rails6.gemfile +1 -1
  8. data/lib/scout_apm.rb +22 -1
  9. data/lib/scout_apm/agent.rb +22 -0
  10. data/lib/scout_apm/agent_context.rb +14 -2
  11. data/lib/scout_apm/background_job_integrations/sidekiq.rb +2 -2
  12. data/lib/scout_apm/config.rb +17 -2
  13. data/lib/scout_apm/detailed_trace.rb +2 -1
  14. data/lib/scout_apm/environment.rb +16 -1
  15. data/lib/scout_apm/error.rb +27 -0
  16. data/lib/scout_apm/error_service.rb +32 -0
  17. data/lib/scout_apm/error_service/error_buffer.rb +39 -0
  18. data/lib/scout_apm/error_service/error_record.rb +211 -0
  19. data/lib/scout_apm/error_service/ignored_exceptions.rb +66 -0
  20. data/lib/scout_apm/error_service/middleware.rb +32 -0
  21. data/lib/scout_apm/error_service/notifier.rb +33 -0
  22. data/lib/scout_apm/error_service/payload.rb +47 -0
  23. data/lib/scout_apm/error_service/periodic_work.rb +17 -0
  24. data/lib/scout_apm/error_service/railtie.rb +11 -0
  25. data/lib/scout_apm/error_service/sidekiq.rb +80 -0
  26. data/lib/scout_apm/extensions/transaction_callback_payload.rb +1 -1
  27. data/lib/scout_apm/instrument_manager.rb +1 -0
  28. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +47 -26
  29. data/lib/scout_apm/instruments/action_view.rb +21 -8
  30. data/lib/scout_apm/instruments/active_record.rb +17 -28
  31. data/lib/scout_apm/instruments/typhoeus.rb +88 -0
  32. data/lib/scout_apm/layer.rb +1 -1
  33. data/lib/scout_apm/middleware.rb +1 -1
  34. data/lib/scout_apm/remote/server.rb +13 -1
  35. data/lib/scout_apm/reporter.rb +8 -3
  36. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +28 -10
  37. data/lib/scout_apm/slow_policy/age_policy.rb +33 -0
  38. data/lib/scout_apm/slow_policy/percent_policy.rb +22 -0
  39. data/lib/scout_apm/slow_policy/percentile_policy.rb +24 -0
  40. data/lib/scout_apm/slow_policy/policy.rb +21 -0
  41. data/lib/scout_apm/slow_policy/speed_policy.rb +16 -0
  42. data/lib/scout_apm/slow_request_policy.rb +18 -77
  43. data/lib/scout_apm/utils/sql_sanitizer.rb +1 -0
  44. data/lib/scout_apm/utils/sql_sanitizer_regex.rb +3 -3
  45. data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +1 -0
  46. data/lib/scout_apm/version.rb +1 -1
  47. data/scout_apm.gemspec +6 -6
  48. data/test/unit/agent_context_test.rb +29 -0
  49. data/test/unit/environment_test.rb +2 -2
  50. data/test/unit/error_service/error_buffer_test.rb +25 -0
  51. data/test/unit/error_service/ignored_exceptions_test.rb +49 -0
  52. data/test/unit/serializers/payload_serializer_test.rb +36 -0
  53. data/test/unit/slow_request_policy_test.rb +41 -13
  54. data/test/unit/sql_sanitizer_test.rb +38 -0
  55. metadata +26 -61
  56. data/lib/scout_apm/slow_job_policy.rb +0 -111
  57. data/test/unit/slow_job_policy_test.rb +0 -6
@@ -0,0 +1,11 @@
1
+ module ScoutApm
2
+ module ErrorService
3
+ class Railtie < Rails::Railtie
4
+ initializer "scoutapm_error_service.middleware" do |app|
5
+ next if ScoutApm::Agent.instance.config.value("error_service")
6
+
7
+ app.config.middleware.insert_after ActionDispatch::DebugExceptions, ScoutApm::ErrorService::Rack
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,80 @@
1
+ module ScoutApm
2
+ module ErrorService
3
+ class Sidekiq
4
+ def initialize
5
+ @context = ScoutApm::Agent.instance.context
6
+ end
7
+
8
+ def install
9
+ return false unless defined?(::Sidekiq)
10
+
11
+ if ::Sidekiq::VERSION < "3"
12
+ install_sidekiq_with_middleware
13
+ else
14
+ install_sidekiq_with_error_handler
15
+ end
16
+
17
+ true
18
+ end
19
+
20
+ def install_sidekiq_with_middleware
21
+ # old behavior
22
+ ::Sidekiq.configure_server do |config|
23
+ config.server_middleware do |chain|
24
+ chain.add ScoutApm::ErrorService::Sidekiq::SidekiqExceptionMiddleware
25
+ end
26
+ end
27
+ end
28
+
29
+ def install_sidekiq_with_error_handler
30
+ ::Sidekiq.configure_server do |config|
31
+ config.error_handlers << proc { |exception, job_info|
32
+ context = ScoutApm::Agent.instance.context
33
+
34
+ # Bail out early, and reraise if the error is not interesting.
35
+ if context.ignored_exceptions.ignored?(exception)
36
+ raise
37
+ end
38
+
39
+ job_class =
40
+ begin
41
+ job_class = job_info[:job]["class"]
42
+ job_class = job_info[:job]["args"][0]["job_class"] if job_class == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
43
+ job_class
44
+ rescue
45
+ "UnknownJob"
46
+ end
47
+
48
+ # Capture the error for further processing and shipping
49
+ context.error_buffer.capture(exception, job_info.merge(:custom_controller => job_class))
50
+ }
51
+ end
52
+ end
53
+
54
+ class SidekiqExceptionMiddleware
55
+ def call(worker, msg, queue)
56
+ yield
57
+ rescue => exception
58
+ context = ScoutApm::Agent.instance.context
59
+
60
+ # Bail out early, and reraise if the error is not interesting.
61
+ if context.ignored_exceptions.ignored?(exception)
62
+ raise
63
+ end
64
+
65
+ # Capture the error for further processing and shipping
66
+ context.error_buffer.capture(
67
+ exception,
68
+ {
69
+ :custom_params => msg,
70
+ :custom_controller => msg["class"]
71
+ }
72
+ )
73
+
74
+ # Finally, reraise
75
+ raise exception
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -26,7 +26,7 @@ module ScoutApm
26
26
  # The time in queue of the transaction in ms. If not present, +nil+ is returned as this is unknown.
27
27
  def queue_time_ms
28
28
  # Controller logic
29
- if converter_results[:queue_time] && converter_results[:queue].any?
29
+ if converter_results[:queue_time] && converter_results[:queue_time].any?
30
30
  converter_results[:queue_time].values.first.total_call_time*1000 # ms
31
31
  # Job logic
32
32
  elsif converter_results[:job]
@@ -30,6 +30,7 @@ module ScoutApm
30
30
  install_instrument(ScoutApm::Instruments::Moped)
31
31
  install_instrument(ScoutApm::Instruments::Mongoid)
32
32
  install_instrument(ScoutApm::Instruments::NetHttp)
33
+ install_instrument(ScoutApm::Instruments::Typhoeus)
33
34
  install_instrument(ScoutApm::Instruments::HttpClient)
34
35
  install_instrument(ScoutApm::Instruments::Memcached)
35
36
  install_instrument(ScoutApm::Instruments::Redis)
@@ -17,46 +17,73 @@ module ScoutApm
17
17
  @installed
18
18
  end
19
19
 
20
+ def installed!
21
+ @installed = true
22
+ end
23
+
20
24
  def install
21
- # We previously instrumented ActionController::Metal, which missed
22
- # before and after filter timing. Instrumenting Base includes those
23
- # filters, at the expense of missing out on controllers that don't use
24
- # the full Rails stack.
25
- if defined?(::ActionController)
26
- @installed = true
25
+ if !defined?(::ActiveSupport)
26
+ return
27
+ end
28
+
29
+ # The block below runs with `self` equal to the ActionController::Base or ::API module, not this class we're in now. By saving an instance of ourselves into the `this` variable, we can continue accessing what we need.
30
+ this = self
27
31
 
32
+ ActiveSupport.on_load(:action_controller) do
33
+ if this.installed?
34
+ this.logger.info("Skipping ActionController - Already Ran")
35
+ next
36
+ else
37
+ this.logger.info("Instrumenting ActionController (on_load)")
38
+ this.installed!
39
+ end
40
+
41
+ # We previously instrumented ActionController::Metal, which missed
42
+ # before and after filter timing. Instrumenting Base includes those
43
+ # filters, at the expense of missing out on controllers that don't use
44
+ # the full Rails stack.
28
45
  if defined?(::ActionController::Base)
29
- logger.info "Instrumenting ActionController::Base"
46
+ this.logger.info "Instrumenting ActionController::Base"
30
47
  ::ActionController::Base.class_eval do
31
- # include ScoutApm::Tracer
32
48
  include ScoutApm::Instruments::ActionControllerBaseInstruments
33
49
  end
34
50
  end
35
51
 
36
52
  if defined?(::ActionController::Metal)
37
- logger.info "Instrumenting ActionController::Metal"
53
+ this.logger.info "Instrumenting ActionController::Metal"
38
54
  ::ActionController::Metal.class_eval do
39
55
  include ScoutApm::Instruments::ActionControllerMetalInstruments
40
56
  end
41
57
  end
42
58
 
43
59
  if defined?(::ActionController::API)
44
- logger.info "Instrumenting ActionController::Api"
60
+ this.logger.info "Instrumenting ActionController::Api"
45
61
  ::ActionController::API.class_eval do
46
62
  include ScoutApm::Instruments::ActionControllerAPIInstruments
47
63
  end
48
64
  end
49
65
  end
50
66
 
51
- # Returns a new anonymous module each time it is called. So
52
- # we can insert this multiple times into the ancestors
53
- # stack. Otherwise it only exists the first time you include it
54
- # (under Metal, instead of under API) and we miss instrumenting
55
- # before_action callbacks
67
+ ScoutApm::Agent.instance.context.logger.info("Instrumenting ActionController (hook installed)")
56
68
  end
57
69
 
70
+ # Returns a new anonymous module each time it is called. So
71
+ # we can insert this multiple times into the ancestors
72
+ # stack. Otherwise it only exists the first time you include it
73
+ # (under Metal, instead of under API) and we miss instrumenting
74
+ # before_action callbacks
58
75
  def self.build_instrument_module
59
76
  Module.new do
77
+ # Determine the URI of this request to capture. Overridable by users in their controller.
78
+ def scout_transaction_uri(config=ScoutApm::Agent.instance.context.config)
79
+ case config.value("uri_reporting")
80
+ when 'path'
81
+ request.path # strips off the query string for more security
82
+ else # default handles filtered params
83
+ request.filtered_path
84
+ end
85
+ end
86
+
60
87
  def process_action(*args)
61
88
  req = ScoutApm::RequestManager.lookup
62
89
  current_layer = req.current_layer
@@ -72,7 +99,11 @@ module ScoutApm
72
99
  # Don't start a new layer if ActionController::API or ActionController::Base handled it already.
73
100
  super
74
101
  else
75
- req.annotate_request(:uri => ScoutApm::Instruments::ActionControllerRails3Rails4.scout_transaction_uri(request))
102
+ begin
103
+ uri = scout_transaction_uri
104
+ req.annotate_request(:uri => uri)
105
+ rescue
106
+ end
76
107
 
77
108
  # IP Spoofing Protection can throw an exception, just move on w/o remote ip
78
109
  if agent_context.config.value('collect_remote_ip')
@@ -95,16 +126,6 @@ module ScoutApm
95
126
  end
96
127
  end
97
128
 
98
- # Given an +ActionDispatch::Request+, formats the uri based on config settings.
99
- # XXX: Don't lookup context like this - find a way to pass it through
100
- def self.scout_transaction_uri(request, config=ScoutApm::Agent.instance.context.config)
101
- case config.value("uri_reporting")
102
- when 'path'
103
- request.path # strips off the query string for more security
104
- else # default handles filtered params
105
- request.filtered_path
106
- end
107
- end
108
129
  end
109
130
 
110
131
  module ActionControllerMetalInstruments
@@ -75,25 +75,34 @@ module ScoutApm
75
75
  end
76
76
 
77
77
  module ActionViewPartialRendererInstruments
78
- def render_partial(*args)
78
+ # In Rails 6, the signature changed to pass the view & template args directly, as opposed to through the instance var
79
+ # New signature is: def render_partial(view, template)
80
+ def render_partial(*args, **kwargs)
79
81
  req = ScoutApm::RequestManager.lookup
80
82
 
81
- template_name = @template.virtual_path rescue "Unknown Partial"
83
+ maybe_template = args[1]
84
+
85
+ template_name = @template.virtual_path rescue nil # Works on Rails 3.2 -> end of Rails 5 series
86
+ template_name ||= maybe_template.virtual_path rescue nil # Works on Rails 6 -> 6.0.3
82
87
  template_name ||= "Unknown Partial"
83
- layer_name = template_name + "/Rendering"
84
88
 
89
+ layer_name = template_name + "/Rendering"
85
90
  layer = ScoutApm::Layer.new("View", layer_name)
86
91
  layer.subscopable!
87
92
 
88
93
  begin
89
94
  req.start_layer(layer)
90
- super(*args)
95
+ if ScoutApm::Agent.instance.context.environment.supports_kwarg_delegation?
96
+ super(*args, **kwargs)
97
+ else
98
+ super(*args)
99
+ end
91
100
  ensure
92
101
  req.stop_layer
93
102
  end
94
103
  end
95
104
 
96
- def collection_with_template(*args)
105
+ def collection_with_template(*args, **kwargs)
97
106
  req = ScoutApm::RequestManager.lookup
98
107
 
99
108
  template_name = @template.virtual_path rescue "Unknown Collection"
@@ -105,7 +114,11 @@ module ScoutApm
105
114
 
106
115
  begin
107
116
  req.start_layer(layer)
108
- super(*args)
117
+ if ScoutApm::Agent.instance.context.environment.supports_kwarg_delegation?
118
+ super(*args, **kwargs)
119
+ else
120
+ super(*args)
121
+ end
109
122
  ensure
110
123
  req.stop_layer
111
124
  end
@@ -113,7 +126,7 @@ module ScoutApm
113
126
  end
114
127
 
115
128
  module ActionViewTemplateRendererInstruments
116
- def render_template(*args)
129
+ def render_template(*args, **kwargs)
117
130
  req = ScoutApm::RequestManager.lookup
118
131
 
119
132
  template_name = args[0].virtual_path rescue "Unknown"
@@ -125,7 +138,7 @@ module ScoutApm
125
138
 
126
139
  begin
127
140
  req.start_layer(layer)
128
- super(*args)
141
+ super(*args, **kwargs)
129
142
  ensure
130
143
  req.stop_layer
131
144
  end
@@ -82,15 +82,8 @@ module ScoutApm
82
82
 
83
83
  # Install #log tracing
84
84
  if Utils::KlassHelper.defined?("ActiveRecord::ConnectionAdapters::AbstractAdapter")
85
- if Module.respond_to?(:prepend)
86
- ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecordInstruments)
87
- ::ActiveRecord::ConnectionAdapters::AbstractAdapter.include(Tracer)
88
- else
89
- ::ActiveRecord::ConnectionAdapters::AbstractAdapter.module_eval do
90
- include ::ScoutApm::Instruments::ActiveRecordAliasMethodInstruments
91
- include ::ScoutApm::Tracer
92
- end
93
- end
85
+ ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecordInstruments)
86
+ ::ActiveRecord::ConnectionAdapters::AbstractAdapter.include(Tracer)
94
87
  end
95
88
 
96
89
  if Utils::KlassHelper.defined?("ActiveRecord::Base")
@@ -172,20 +165,12 @@ module ScoutApm
172
165
  # to the real SQL, and an AR generated "name" for the Query
173
166
  #
174
167
  ################################################################################
175
- #
176
- # Note, if you change this instrumentation, you also need to change ActiveRecordInstruments.
177
- module ActiveRecordAliasMethodInstruments
178
- def self.included(instrumented_class)
168
+ module ActiveRecordInstruments
169
+ def self.prepended(instrumented_class)
179
170
  ScoutApm::Agent.instance.context.logger.info "Instrumenting #{instrumented_class.inspect}"
180
- instrumented_class.class_eval do
181
- unless instrumented_class.method_defined?(:log_without_scout_instruments)
182
- alias_method :log_without_scout_instruments, :log
183
- alias_method :log, :log_with_scout_instruments
184
- end
185
- end
186
171
  end
187
172
 
188
- def log_with_scout_instruments(*args, &block)
173
+ def log(*args, &block)
189
174
  # Extract data from the arguments
190
175
  sql, name = args
191
176
  metric_name = Utils::ActiveRecordMetricName.new(sql, name)
@@ -216,7 +201,7 @@ module ScoutApm
216
201
  end
217
202
  current_layer.desc.merge(desc)
218
203
 
219
- log_without_scout_instruments(*args, &block)
204
+ super(*args, &block)
220
205
 
221
206
  # OR: Start a new layer, we didn't pick up instrumentation earlier in the stack.
222
207
  else
@@ -224,7 +209,7 @@ module ScoutApm
224
209
  layer.desc = desc
225
210
  req.start_layer(layer)
226
211
  begin
227
- log_without_scout_instruments(*args, &block)
212
+ super(*args, &block)
228
213
  ensure
229
214
  req.stop_layer
230
215
  end
@@ -323,14 +308,18 @@ module ScoutApm
323
308
  end
324
309
  end
325
310
 
326
- def find_by_sql_with_scout_instruments(*args, &block)
311
+ def find_by_sql_with_scout_instruments(*args, **kwargs, &block)
327
312
  req = ScoutApm::RequestManager.lookup
328
313
  layer = ScoutApm::Layer.new("ActiveRecord", Utils::ActiveRecordMetricName::DEFAULT_METRIC)
329
314
  layer.annotate_layer(:ignorable => true)
330
315
  req.start_layer(layer)
331
316
  req.ignore_children!
332
317
  begin
333
- find_by_sql_without_scout_instruments(*args, &block)
318
+ if ScoutApm::Agent.instance.context.environment.supports_kwarg_delegation?
319
+ find_by_sql_without_scout_instruments(*args, **kwargs, &block)
320
+ else
321
+ find_by_sql_without_scout_instruments(*args, &block)
322
+ end
334
323
  ensure
335
324
  req.acknowledge_children!
336
325
  req.stop_layer
@@ -408,7 +397,7 @@ module ScoutApm
408
397
  end
409
398
 
410
399
  module ActiveRecordUpdateInstruments
411
- def save(*args, &block)
400
+ def save(*args, **options, &block)
412
401
  model = self.class.name
413
402
  operation = self.persisted? ? "Update" : "Create"
414
403
 
@@ -418,14 +407,14 @@ module ScoutApm
418
407
  req.start_layer(layer)
419
408
  req.ignore_children!
420
409
  begin
421
- super(*args, &block)
410
+ super(*args, **options, &block)
422
411
  ensure
423
412
  req.acknowledge_children!
424
413
  req.stop_layer
425
414
  end
426
415
  end
427
416
 
428
- def save!(*args, &block)
417
+ def save!(*args, **options, &block)
429
418
  model = self.class.name
430
419
  operation = self.persisted? ? "Update" : "Create"
431
420
 
@@ -434,7 +423,7 @@ module ScoutApm
434
423
  req.start_layer(layer)
435
424
  req.ignore_children!
436
425
  begin
437
- super(*args, &block)
426
+ super(*args, **options, &block)
438
427
  ensure
439
428
  req.acknowledge_children!
440
429
  req.stop_layer
@@ -0,0 +1,88 @@
1
+ module ScoutApm
2
+ module Instruments
3
+ class Typhoeus
4
+ attr_reader :context
5
+
6
+ def initialize(context)
7
+ @context = context
8
+ @installed = false
9
+ end
10
+
11
+ def logger
12
+ context.logger
13
+ end
14
+
15
+ def installed?
16
+ @installed
17
+ end
18
+
19
+ def install
20
+ if defined?(::Typhoeus)
21
+ @installed = true
22
+
23
+ logger.info "Instrumenting Typhoeus"
24
+
25
+ ::Typhoeus::Request.send(:prepend, TyphoeusInstrumentation)
26
+ ::Typhoeus::Hydra.send(:prepend, TyphoeusHydraInstrumentation)
27
+ end
28
+ end
29
+
30
+ module TyphoeusHydraInstrumentation
31
+ def run(*args, &block)
32
+ req = ScoutApm::RequestManager.lookup
33
+ req.start_layer(ScoutApm::Layer.new("HTTP", "Hydra"))
34
+ current_layer = req.current_layer
35
+ current_layer.desc = scout_desc if current_layer
36
+
37
+ begin
38
+ super(*args, &block)
39
+ ensure
40
+ req.stop_layer
41
+ end
42
+ end
43
+
44
+ def scout_desc
45
+ "#{self.queued_requests.count} requests"
46
+ rescue
47
+ ""
48
+ end
49
+ end
50
+
51
+ module TyphoeusInstrumentation
52
+ def run(*args, &block)
53
+ req = ScoutApm::RequestManager.lookup
54
+ req.start_layer(ScoutApm::Layer.new("HTTP", scout_request_verb))
55
+ current_layer = req.current_layer
56
+ current_layer.desc = scout_desc(scout_request_verb, scout_request_url) if current_layer
57
+
58
+ begin
59
+ super(*args, &block)
60
+ ensure
61
+ req.stop_layer
62
+ end
63
+ end
64
+
65
+ def scout_desc(verb, uri)
66
+ max_length = ScoutApm::Agent.instance.context.config.value('instrument_http_url_length')
67
+ (String(uri).split('?').first)[0..(max_length - 1)]
68
+ rescue
69
+ ""
70
+ end
71
+
72
+ def scout_request_url
73
+ self.url
74
+ rescue
75
+ ""
76
+ end
77
+
78
+ def scout_request_verb
79
+ self.options[:method].to_s
80
+ rescue
81
+ ""
82
+ end
83
+
84
+ end
85
+
86
+ end
87
+ end
88
+ end