scout_apm 3.0.0.pre10 → 3.0.0.pre11

Sign up to get free protection for your applications and to get access to all the features.
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.