scout_apm 3.0.0.pre10 → 3.0.0.pre11

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.markdown +47 -0
  4. data/Guardfile +42 -0
  5. data/lib/scout_apm.rb +14 -0
  6. data/lib/scout_apm/agent.rb +58 -1
  7. data/lib/scout_apm/agent/logging.rb +6 -1
  8. data/lib/scout_apm/app_server_load.rb +21 -11
  9. data/lib/scout_apm/background_job_integrations/resque.rb +85 -0
  10. data/lib/scout_apm/background_job_integrations/sidekiq.rb +19 -3
  11. data/lib/scout_apm/background_recorder.rb +43 -0
  12. data/lib/scout_apm/background_worker.rb +6 -6
  13. data/lib/scout_apm/config.rb +14 -3
  14. data/lib/scout_apm/environment.rb +14 -0
  15. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +112 -70
  16. data/lib/scout_apm/instruments/action_view.rb +49 -0
  17. data/lib/scout_apm/instruments/active_record.rb +2 -2
  18. data/lib/scout_apm/instruments/mongoid.rb +10 -2
  19. data/lib/scout_apm/instruments/resque.rb +40 -0
  20. data/lib/scout_apm/layer_children_set.rb +7 -2
  21. data/lib/scout_apm/rack.rb +26 -0
  22. data/lib/scout_apm/remote/message.rb +23 -0
  23. data/lib/scout_apm/remote/recorder.rb +57 -0
  24. data/lib/scout_apm/remote/router.rb +49 -0
  25. data/lib/scout_apm/remote/server.rb +58 -0
  26. data/lib/scout_apm/request_manager.rb +1 -1
  27. data/lib/scout_apm/synchronous_recorder.rb +26 -0
  28. data/lib/scout_apm/tracked_request.rb +53 -5
  29. data/lib/scout_apm/utils/backtrace_parser.rb +3 -3
  30. data/lib/scout_apm/utils/scm.rb +14 -0
  31. data/lib/scout_apm/version.rb +1 -1
  32. data/scout_apm.gemspec +2 -0
  33. data/test/unit/remote/test_message.rb +13 -0
  34. data/test/unit/remote/test_router.rb +33 -0
  35. data/test/unit/remote/test_server.rb +15 -0
  36. data/test/unit/test_tracked_request.rb +87 -0
  37. data/test/unit/utils/backtrace_parser_test.rb +5 -0
  38. data/test/unit/utils/scm.rb +17 -0
  39. metadata +52 -2
@@ -0,0 +1,43 @@
1
+ # Provide a background thread queue to do the processing of
2
+ # TrackedRequest objects, to remove it from the hot-path of returning a
3
+ # web response
4
+
5
+ module ScoutApm
6
+ class BackgroundRecorder
7
+ attr_reader :queue
8
+ attr_reader :thread
9
+ attr_reader :logger
10
+
11
+ def initialize(logger)
12
+ @logger = logger
13
+ @queue = Queue.new
14
+ end
15
+
16
+ def start
17
+ logger.info("Starting BackgroundRecorder")
18
+ @thread = Thread.new(&method(:thread_func))
19
+ self
20
+ end
21
+
22
+ def stop
23
+ @thread.kill
24
+ end
25
+
26
+ def record!(request)
27
+ start unless @thread.alive?
28
+ @queue.push(request)
29
+ end
30
+
31
+ def thread_func
32
+ while req = queue.pop
33
+ begin
34
+ logger.debug("recording in thread. Queue size: #{queue.size}")
35
+ # For now, just proxy right back into the TrackedRequest object's record function
36
+ req.record!
37
+ rescue => e
38
+ logger.warn("Error in BackgroundRecorder - #{e.message} : #{e.backtrace}")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -36,12 +36,6 @@ module ScoutApm
36
36
 
37
37
  loop do
38
38
  begin
39
- # Bail out if @keep_running is false
40
- unless @keep_running
41
- ScoutApm::Agent.instance.logger.debug "Background Worker: breaking from loop"
42
- break
43
- end
44
-
45
39
  now = Time.now
46
40
 
47
41
  # Sleep the correct amount of time to reach next_time
@@ -51,6 +45,12 @@ module ScoutApm
51
45
  now = Time.now
52
46
  end
53
47
 
48
+ # Bail out if @keep_running is false
49
+ unless @keep_running
50
+ ScoutApm::Agent.instance.logger.debug "Background Worker: breaking from loop"
51
+ break
52
+ end
53
+
54
54
  @task.call
55
55
 
56
56
  # Adjust the next time to run forward by @periods until it is in the future
@@ -25,7 +25,10 @@ require 'scout_apm/environment'
25
25
  # profile - turn on/off scoutprof (only applicable in Gem versions including scoutprof)
26
26
  # proxy - an http proxy
27
27
  # report_format - 'json' or 'marshal'. Marshal is legacy and will be removed.
28
+ # scm_subdirectory - if the app root lives in source management in a subdirectory. E.g. #{SCM_ROOT}/src
28
29
  # uri_reporting - 'path' or 'full_path' default is 'full_path', which reports URL params as well as the path.
30
+ # remote_agent_host - Internal: What host to bind to, and also send messages to for remote. Default: 127.0.0.1.
31
+ # remote_agent_port - What port to bind the remote webserver to
29
32
  #
30
33
  # Any of these config settings can be set with an environment variable prefixed
31
34
  # by SCOUT_ and uppercasing the key: SCOUT_LOG_LEVEL for instance.
@@ -34,6 +37,7 @@ module ScoutApm
34
37
  class Config
35
38
  KNOWN_CONFIG_OPTIONS = [
36
39
  'application_root',
40
+ 'async_recording',
37
41
  'compress_payload',
38
42
  'config_file',
39
43
  'data_file',
@@ -52,7 +56,10 @@ module ScoutApm
52
56
  'name',
53
57
  'profile',
54
58
  'proxy',
59
+ 'remote_agent_host',
60
+ 'remote_agent_port',
55
61
  'report_format',
62
+ 'scm_subdirectory',
56
63
  'uri_reporting',
57
64
  ]
58
65
 
@@ -127,11 +134,12 @@ module ScoutApm
127
134
 
128
135
 
129
136
  SETTING_COERCIONS = {
130
- "monitor" => BooleanCoercion.new,
131
- "enable_background_jobs" => BooleanCoercion.new,
132
- "dev_trace" => BooleanCoercion.new,
137
+ "async_recording" => BooleanCoercion.new,
133
138
  "detailed_middleware" => BooleanCoercion.new,
139
+ "dev_trace" => BooleanCoercion.new,
140
+ "enable_background_jobs" => BooleanCoercion.new,
134
141
  "ignore" => JsonCoercion.new,
142
+ "monitor" => BooleanCoercion.new,
135
143
  }
136
144
 
137
145
 
@@ -215,7 +223,10 @@ module ScoutApm
215
223
  'log_level' => 'info',
216
224
  'profile' => true, # for scoutprof
217
225
  'report_format' => 'json',
226
+ 'scm_subdirectory' => '',
218
227
  'uri_reporting' => 'full_path',
228
+ 'remote_agent_host' => '127.0.0.1',
229
+ 'remote_agent_port' => 7721, # picked at random
219
230
  }.freeze
220
231
 
221
232
  def value(key)
@@ -24,6 +24,7 @@ module ScoutApm
24
24
  ]
25
25
 
26
26
  BACKGROUND_JOB_INTEGRATIONS = [
27
+ ScoutApm::BackgroundJobIntegrations::Resque.new,
27
28
  ScoutApm::BackgroundJobIntegrations::Sidekiq.new,
28
29
  ScoutApm::BackgroundJobIntegrations::DelayedJob.new,
29
30
  ]
@@ -84,6 +85,14 @@ module ScoutApm
84
85
  end
85
86
  end
86
87
 
88
+ def scm_subdirectory
89
+ @scm_subdirectory ||= if Agent.instance.config.value('scm_subdirectory').empty?
90
+ ''
91
+ else
92
+ Agent.instance.config.value('scm_subdirectory').sub(/^\//, '') # Trim any leading slash
93
+ end
94
+ end
95
+
87
96
  def root
88
97
  @root ||= framework_root
89
98
  end
@@ -144,6 +153,11 @@ module ScoutApm
144
153
  background_job_integration && background_job_integration.name
145
154
  end
146
155
 
156
+ # If both stdin & stdout are interactive and the Rails::Console constant is defined
157
+ def interactive?
158
+ defined?(::Rails::Console) && $stdout.isatty && $stdin.isatty
159
+ end
160
+
147
161
  ### ruby checks
148
162
 
149
163
  def rubinius?
@@ -1,6 +1,6 @@
1
1
  module ScoutApm
2
2
  module Instruments
3
- # instrumentation for Rails 3 and Rails 4 is the same.
3
+ # instrumentation for Rails 3, 4, and 5 is the same.
4
4
  class ActionControllerRails3Rails4
5
5
  attr_reader :logger
6
6
 
@@ -20,86 +20,89 @@ module ScoutApm
20
20
  # before and after filter timing. Instrumenting Base includes those
21
21
  # filters, at the expense of missing out on controllers that don't use
22
22
  # the full Rails stack.
23
- if defined?(::ActionController) && defined?(::ActionController::Base)
24
- ScoutApm::Agent.instance.logger.info "Instrumenting ActionController::Base"
25
- ::ActionController::Base.class_eval do
26
- # include ScoutApm::Tracer
27
- include ScoutApm::Instruments::ActionControllerRails3Rails4Instruments
23
+ if defined?(::ActionController)
24
+ if defined?(::ActionController::Base)
25
+ ScoutApm::Agent.instance.logger.info "Instrumenting ActionController::Base"
26
+ ::ActionController::Base.class_eval do
27
+ # include ScoutApm::Tracer
28
+ include ScoutApm::Instruments::ActionControllerBaseInstruments
29
+ end
28
30
  end
29
- ScoutApm::Agent.instance.logger.info "Installing ScoutProf profiling" if ScoutApm::Agent.instance.config.value('profile')
30
- end
31
-
32
- if defined?(::ActionView) && defined?(::ActionView::PartialRenderer)
33
- ScoutApm::Agent.instance.logger.info "Instrumenting ActionView::PartialRenderer"
34
- ::ActionView::PartialRenderer.class_eval do
35
- include ScoutApm::Tracer
36
-
37
- instrument_method :render_partial,
38
- :type => "View",
39
- :name => '#{@template.virtual_path rescue "Unknown Partial"}/Rendering',
40
- :scope => true
41
31
 
42
- instrument_method :collection_with_template,
43
- :type => "View",
44
- :name => '#{@template.virtual_path rescue "Unknown Collection"}/Rendering',
45
- :scope => true
32
+ if defined?(::ActionController::Metal)
33
+ ScoutApm::Agent.instance.logger.info "Instrumenting ActionController::Metal"
34
+ ::ActionController::Metal.class_eval do
35
+ include ScoutApm::Instruments::ActionControllerMetalInstruments
36
+ end
46
37
  end
47
38
 
48
- ScoutApm::Agent.instance.logger.info "Instrumenting ActionView::TemplateRenderer"
49
- ::ActionView::TemplateRenderer.class_eval do
50
- include ScoutApm::Tracer
51
- instrument_method :render_template,
52
- :type => "View",
53
- :name => '#{args[0].virtual_path rescue "Unknown"}/Rendering',
54
- :scope => true
39
+ if defined?(::ActionController::API)
40
+ ScoutApm::Agent.instance.logger.info "Instrumenting ActionController::Api"
41
+ ::ActionController::API.class_eval do
42
+ include ScoutApm::Instruments::ActionControllerAPIInstruments
43
+ end
55
44
  end
56
45
  end
57
- end
58
- end
59
46
 
60
- module ActionControllerRails3Rails4Instruments
61
- def process_action(*args)
62
- req = ScoutApm::RequestManager.lookup
63
- req.annotate_request(:uri => scout_transaction_uri(request))
64
-
65
- # IP Spoofing Protection can throw an exception, just move on w/o remote ip
66
- req.context.add_user(:ip => request.remote_ip) rescue nil
67
-
68
- req.set_headers(request.headers)
69
-
70
- # Check if this this request is to be reported instantly
71
- if instant_key = request.cookies['scoutapminstant']
72
- Agent.instance.logger.info "Instant trace request with key=#{instant_key} for path=#{path}"
73
- req.instant_key = instant_key
74
- end
75
-
76
- req.web!
77
-
78
- layer = ScoutApm::Layer.new("Controller", "#{controller_path}/#{action_name}")
47
+ end
79
48
 
80
- if ScoutApm::Agent.instance.config.value('profile') && ScoutApm::Instruments::Stacks::ENABLED
81
- if defined?(ScoutApm::Instruments::Stacks::INSTALLED) && ScoutApm::Instruments::Stacks::INSTALLED
82
- # Capture ScoutProf if we can
83
- req.enable_profiled_thread!
84
- layer.set_root_class(self.class)
85
- layer.traced!
49
+ # Returns a new anonymous module each time it is called. So
50
+ # we can insert this multiple times into the ancestors
51
+ # stack. Otherwise it only exists the first time you include it
52
+ # (under Metal, instead of under API) and we miss instrumenting
53
+ # before_action callbacks
54
+ def self.build_instrument_module
55
+ Module.new do
56
+ def process_action(*args)
57
+ req = ScoutApm::RequestManager.lookup
58
+ current_layer = req.current_layer
59
+
60
+ # Check if this this request is to be reported instantly
61
+ if instant_key = request.cookies['scoutapminstant']
62
+ Agent.instance.logger.info "Instant trace request with key=#{instant_key} for path=#{path}"
63
+ req.instant_key = instant_key
64
+ end
65
+
66
+ if current_layer && current_layer.type == "Controller"
67
+ # Don't start a new layer if ActionController::API or ActionController::Base handled it already.
68
+ super
69
+ else
70
+ req.annotate_request(:uri => ScoutApm::Instruments::ActionControllerRails3Rails4.scout_transaction_uri(request))
71
+
72
+ # IP Spoofing Protection can throw an exception, just move on w/o remote ip
73
+ req.context.add_user(:ip => request.remote_ip) rescue nil
74
+ req.set_headers(request.headers)
75
+
76
+ req.web!
77
+
78
+ resolved_name = scout_action_name(*args)
79
+ layer = ScoutApm::Layer.new("Controller", "#{controller_path}/#{resolved_name}")
80
+
81
+ if enable_scoutprof? && ScoutApm::Agent.instance.config.value('profile') && ScoutApm::Instruments::Stacks::ENABLED
82
+ if defined?(ScoutApm::Instruments::Stacks::INSTALLED) && ScoutApm::Instruments::Stacks::INSTALLED
83
+ # Capture ScoutProf if we can
84
+ req.enable_profiled_thread!
85
+ layer.set_root_class(self.class)
86
+ layer.traced!
87
+ end
88
+ end
89
+
90
+ req.start_layer(layer)
91
+ begin
92
+ super
93
+ rescue
94
+ req.error!
95
+ raise
96
+ ensure
97
+ req.stop_layer
98
+ end
99
+ end
86
100
  end
87
101
  end
88
-
89
- # Start the layer, then execute the user's code
90
- req.start_layer(layer)
91
- begin
92
- super
93
- rescue
94
- req.error!
95
- raise
96
- ensure
97
- req.stop_layer
98
- end
99
- end # process_action
102
+ end
100
103
 
101
104
  # Given an +ActionDispatch::Request+, formats the uri based on config settings.
102
- def scout_transaction_uri(request)
105
+ def self.scout_transaction_uri(request)
103
106
  case ScoutApm::Agent.instance.config.value("uri_reporting")
104
107
  when 'path'
105
108
  request.path # strips off the query string for more security
@@ -107,8 +110,47 @@ module ScoutApm
107
110
  request.filtered_path
108
111
  end
109
112
  end
113
+ end
110
114
 
111
- end # ActionControllerRails3Rails4Instruments
115
+ module ActionControllerBaseInstruments
116
+ include ScoutApm::Instruments::ActionControllerRails3Rails4.build_instrument_module
117
+
118
+ def scout_action_name(*args)
119
+ action_name
120
+ end
121
+
122
+ def enable_scoutprof?
123
+ true
124
+ end
125
+ end
126
+
127
+ module ActionControllerMetalInstruments
128
+ include ScoutApm::Instruments::ActionControllerRails3Rails4.build_instrument_module
129
+
130
+ def scout_action_name(*args)
131
+ action_name = args[0]
132
+ end
133
+
134
+ def enable_scoutprof?
135
+ false
136
+ end
137
+ end
138
+
139
+ module ActionControllerAPIInstruments
140
+ include ScoutApm::Instruments::ActionControllerRails3Rails4.build_instrument_module
141
+
142
+ def scout_action_name(*args)
143
+ action_name
144
+ end
145
+
146
+ def enable_scoutprof?
147
+ false
148
+ end
149
+ end
150
+
151
+ # Empty, noop module to provide compatibility w/ previous manual instrumentation
152
+ module ActionControllerRails3Rails4Instruments
153
+ end
112
154
  end
113
155
  end
114
156
 
@@ -0,0 +1,49 @@
1
+ module ScoutApm
2
+ module Instruments
3
+ # instrumentation for Rails 3 and Rails 4 is the same.
4
+ class ActionView
5
+ attr_reader :logger
6
+
7
+ def initalize(logger=ScoutApm::Agent.instance.logger)
8
+ @logger = logger
9
+ @installed = false
10
+ end
11
+
12
+ def installed?
13
+ @installed
14
+ end
15
+
16
+ def install
17
+ @installed = true
18
+
19
+ if defined?(::ActionView) && defined?(::ActionView::PartialRenderer)
20
+ ScoutApm::Agent.instance.logger.info "Instrumenting ActionView::PartialRenderer"
21
+ ::ActionView::PartialRenderer.class_eval do
22
+ include ScoutApm::Tracer
23
+
24
+ instrument_method :render_partial,
25
+ :type => "View",
26
+ :name => '#{@template.virtual_path rescue "Unknown Partial"}/Rendering',
27
+ :scope => true
28
+
29
+ instrument_method :collection_with_template,
30
+ :type => "View",
31
+ :name => '#{@template.virtual_path rescue "Unknown Collection"}/Rendering',
32
+ :scope => true
33
+ end
34
+
35
+ ScoutApm::Agent.instance.logger.info "Instrumenting ActionView::TemplateRenderer"
36
+ ::ActionView::TemplateRenderer.class_eval do
37
+ include ScoutApm::Tracer
38
+ instrument_method :render_template,
39
+ :type => "View",
40
+ :name => '#{args[0].virtual_path rescue "Unknown"}/Rendering',
41
+ :scope => true
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+
@@ -122,7 +122,7 @@ module ScoutApm
122
122
  current_layer.desc = desc
123
123
  end
124
124
 
125
- log_without_scout_instruments(sql, name, &block)
125
+ log_without_scout_instruments(*args, &block)
126
126
 
127
127
  # OR: Start a new layer, we didn't pick up instrumentation earlier in the stack.
128
128
  else
@@ -130,7 +130,7 @@ module ScoutApm
130
130
  layer.desc = desc
131
131
  req.start_layer(layer)
132
132
  begin
133
- log_without_scout_instruments(sql, name, &block)
133
+ log_without_scout_instruments(*args, &block)
134
134
  ensure
135
135
  req.stop_layer
136
136
  end
@@ -32,8 +32,8 @@ module ScoutApm
32
32
  ### See moped instrument for Moped driven deploys
33
33
 
34
34
  ### 5.x Mongoid
35
- if mongoid_v5? && defined?(::Mongoid::Contextual::Mongo)
36
- ScoutApm::Agent.instance.logger.info "Instrumenting Mongoid 5.x"
35
+ if (mongoid_v5? || mongoid_v6?) && defined?(::Mongoid::Contextual::Mongo)
36
+ ScoutApm::Agent.instance.logger.info "Instrumenting Mongoid 5.x/6.x"
37
37
  # All the public methods from Mongoid::Contextual::Mongo.
38
38
  # TODO: Geo and MapReduce support (?). They are in other Contextual::* classes
39
39
  methods = [
@@ -99,6 +99,14 @@ module ScoutApm
99
99
  end
100
100
  end
101
101
 
102
+ def mongoid_v6?
103
+ if defined?(::Mongoid::VERSION)
104
+ ::Mongoid::VERSION =~ /\A6/
105
+ else
106
+ false
107
+ end
108
+ end
109
+
102
110
 
103
111
  # Example of what a filter looks like: => {"founded"=>{"$gte"=>"1980-1-1"}, "name"=>{"$in"=>["Tool", "Deftones", "Melvins"]}}
104
112
  # Approach: find every leaf-node, clear it. inspect the whole thing when done.