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,40 @@
1
+ module ScoutApm
2
+ module Instruments
3
+ module Resque
4
+ def around_perform_with_scout_instruments(*args)
5
+ job_name = self.to_s
6
+ queue = find_queue
7
+
8
+ if job_name == "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper"
9
+ job_name = args.first["job_class"] rescue job_name
10
+ queue = args.first["queue_name"] rescue queue_name
11
+ end
12
+
13
+ req = ScoutApm::RequestManager.lookup
14
+ req.job!
15
+
16
+ begin
17
+ req.start_layer(ScoutApm::Layer.new('Queue', queue))
18
+ started_queue = true
19
+ req.start_layer(ScoutApm::Layer.new('Job', job_name))
20
+ started_job = true
21
+
22
+ yield
23
+ rescue => e
24
+ req.error!
25
+ raise
26
+ ensure
27
+ req.stop_layer if started_job
28
+ req.stop_layer if started_queue
29
+ end
30
+ end
31
+
32
+ def find_queue
33
+ return @queue if @queue
34
+ return queue if self.respond_to?(:queue)
35
+ return "unknown"
36
+ end
37
+ end
38
+ end
39
+ end
40
+
@@ -27,18 +27,23 @@ module ScoutApm
27
27
  private :children
28
28
 
29
29
  def initialize(unique_cutoff = DEFAULT_UNIQUE_CUTOFF)
30
- @children = Hash.new { |hash, key| hash[key] = Set.new }
30
+ @children = Hash.new
31
31
  @limited_layers = nil # populated when needed
32
32
  @unique_cutoff = unique_cutoff
33
33
  end
34
34
 
35
+ def child_set(metric_type)
36
+ children[metric_type] = Set.new if !children.has_key?(metric_type)
37
+ children[metric_type]
38
+ end
39
+
35
40
  # Add a new layer into this set
36
41
  # Only add completed layers - otherwise this will collect up incorrect info
37
42
  # into the created LimitedLayer, since it will "freeze" any current data for
38
43
  # total_call_time and similar methods.
39
44
  def <<(child)
40
45
  metric_type = child.type
41
- set = children[metric_type]
46
+ set = child_set(metric_type)
42
47
 
43
48
  if set.size >= unique_cutoff
44
49
  # find limited_layer
@@ -0,0 +1,26 @@
1
+ module ScoutApm
2
+ module Rack
3
+ def self.install!
4
+ ScoutApm::Agent.instance.start(:skip_app_server_check => true)
5
+ ScoutApm::Agent.instance.start_background_worker
6
+ end
7
+
8
+ def self.transaction(endpoint_name, env)
9
+ req = ScoutApm::RequestManager.lookup
10
+ req.annotate_request(:uri => env["REQUEST_PATH"]) rescue nil
11
+ req.context.add_user(:ip => env["REMOTE_ADDR"]) rescue nil
12
+
13
+ req.web!
14
+ req.start_layer(ScoutApm::Layer.new('Controller', endpoint_name))
15
+
16
+ begin
17
+ yield
18
+ rescue
19
+ req.error!
20
+ raise
21
+ ensure
22
+ req.stop_layer
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ module ScoutApm
2
+ module Remote
3
+ class Message
4
+ attr_reader :type
5
+ attr_reader :command
6
+ attr_reader :args
7
+
8
+ def initialize(type, command, *args)
9
+ @type = type
10
+ @command = command
11
+ @args = args
12
+ end
13
+
14
+ def self.decode(msg)
15
+ Marshal.load(msg)
16
+ end
17
+
18
+ def encode
19
+ Marshal.dump(self)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ module ScoutApm
2
+ module Remote
3
+ class Recorder
4
+ attr_reader :logger
5
+ attr_reader :remote_agent_host
6
+ attr_reader :remote_agent_port
7
+
8
+ def initialize(remote_agent_host, remote_agent_port, logger)
9
+ @remote_agent_host = remote_agent_host
10
+ @remote_agent_port = remote_agent_port
11
+ @logger = logger
12
+ end
13
+
14
+ def start
15
+ # nothing to do
16
+ self
17
+ end
18
+
19
+ def stop
20
+ # nothing to do
21
+ end
22
+
23
+ def record!(request)
24
+ begin
25
+ t1 = Time.now
26
+ # Mark this request as recorded, so the next lookup on this thread, it
27
+ # can be recreated
28
+ request.recorded!
29
+
30
+ # Only send requests that we actually want. Incidental http &
31
+ # background thread stuff can just be dropped
32
+ unless request.job? || request.web?
33
+ return
34
+ end
35
+
36
+ request.prepare_to_dump!
37
+ message = ScoutApm::Remote::Message.new('record', 'record!', request)
38
+ encoded = message.encode
39
+ logger.debug "Remote Agent: Posting a message of length: #{encoded.length}"
40
+ post(encoded)
41
+ t2 = Time.now
42
+
43
+ logger.debug("Remote Recording took: #{t2.to_f - t1.to_f} seconds")
44
+ rescue => e
45
+ logger.debug "Remote: Error while sending to collector: #{e.inspect}, #{e.backtrace.join("\n")}"
46
+ end
47
+ end
48
+
49
+ def post(encoded_message)
50
+ http = Net::HTTP.new(remote_agent_host, remote_agent_port)
51
+ request = Net::HTTP::Post.new("/users")
52
+ request.body = encoded_message
53
+ response = http.request(request)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,49 @@
1
+ module ScoutApm
2
+ module Remote
3
+ class Router
4
+ attr_reader :logger
5
+ attr_reader :routes
6
+
7
+ # If/When we add different types, this signature should change to a hash
8
+ # of {type => Object}, rather than building it in the initializer here.
9
+ #
10
+ # Keys of routes should be strings
11
+ def initialize(recorder, logger)
12
+ @routes = {
13
+ 'record' => recorder
14
+ }
15
+
16
+ @logger = logger
17
+ end
18
+
19
+ # A message is a 2 element array [:type, :command, [args]].
20
+ # For this first creation, this should be ['record', 'record', [TrackedRequest]] (the args arg should always be an array, even w/ only 1 item)
21
+ #
22
+ # Where
23
+ # type: ['recorder']
24
+ # command: any function supported on that type of object
25
+ # args: any array of arguments
26
+ #
27
+ # Raises on unknown message
28
+ #
29
+ # Returns whatever the recipient object returns
30
+ def handle(msg)
31
+ message = Remote::Message.decode(msg)
32
+ assert_type(message)
33
+ call_route(message)
34
+ end
35
+
36
+ private
37
+
38
+ def assert_type(message)
39
+ if ! routes.keys.include?(message.type.to_s)
40
+ raise "Unknown type: #{message.type.to_s}"
41
+ end
42
+ end
43
+
44
+ def call_route(message)
45
+ routes[message.type].send(message.command, *message.args)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,58 @@
1
+ # Web Server bound to localhost that listens for remote agent reports. Forwards
2
+ # onto the router
3
+ module ScoutApm
4
+ module Remote
5
+ class Server
6
+ attr_reader :router
7
+ attr_reader :bind
8
+ attr_reader :port
9
+ attr_reader :logger
10
+
11
+ def initialize(bind, port, router, logger)
12
+ @router = router
13
+ @logger = logger
14
+ @bind = bind
15
+ @port = port
16
+ @server = nil
17
+ end
18
+
19
+ def start
20
+ @server = WEBrick::HTTPServer.new(
21
+ :BindAddress => bind,
22
+ :Port => port,
23
+ :AccessLog => [],
24
+ :Logger => @logger
25
+ )
26
+
27
+ @server.mount_proc '/' do |request, response|
28
+ router.handle(request.body)
29
+
30
+ # arbitrary response, client doesn't expect anything in particular
31
+ response.body = 'Ok'
32
+ end
33
+
34
+ @thread = Thread.new do
35
+ begin
36
+ logger.debug("Remote: Starting Server on #{bind}:#{port}")
37
+
38
+ @server.start
39
+
40
+ logger.debug("Remote: Server returned after #start call, thread exiting")
41
+ rescue => e
42
+ logger.debug("Remote: Server Exception, #{e}")
43
+ end
44
+ end
45
+ end
46
+
47
+ def running?
48
+ @thread.alive?
49
+ @server && @server.status == :Running
50
+ end
51
+
52
+ def stop
53
+ @server.stop
54
+ @thread.kill
55
+ end
56
+ end
57
+ end
58
+ end
@@ -11,7 +11,7 @@ module ScoutApm
11
11
  def self.find
12
12
  req = Thread.current[:scout_request]
13
13
 
14
- if req && req.recorded?
14
+ if req && (req.stopping? || req.recorded?)
15
15
  nil
16
16
  else
17
17
  req
@@ -0,0 +1,26 @@
1
+ # Provide a synchronous approach to recording TrackedRequests
2
+ # Doesn't attempt to background the work, or do it elsewhere. It happens
3
+ # inline, in the caller thread right when record! is called
4
+
5
+ module ScoutApm
6
+ class SynchronousRecorder
7
+ attr_reader :logger
8
+
9
+ def initialize(logger)
10
+ @logger = logger
11
+ end
12
+
13
+ def start
14
+ # nothing to do
15
+ self
16
+ end
17
+
18
+ def stop
19
+ # nothing to do
20
+ end
21
+
22
+ def record!(request)
23
+ request.record!
24
+ end
25
+ end
26
+ end
@@ -42,6 +42,9 @@ module ScoutApm
42
42
  # Whereas the instant_key gets set per-request in reponse to a URL param, dev_trace is set in the config file
43
43
  attr_accessor :dev_trace
44
44
 
45
+ # An object that responds to `record!(TrackedRequest)` to store this tracked request
46
+ attr_reader :recorder
47
+
45
48
  def initialize(store)
46
49
  @store = store #this is passed in so we can use a real store (normal operation) or fake store (instant mode only)
47
50
  @layers = []
@@ -51,12 +54,19 @@ module ScoutApm
51
54
  @context = Context.new
52
55
  @root_layer = nil
53
56
  @error = false
57
+ @stopping = false
54
58
  @instant_key = nil
55
59
  @mem_start = mem_usage
56
- @dev_trace = ScoutApm::Agent.instance.config.value('dev_trace') && Rails.env.development?
60
+ @dev_trace = ScoutApm::Agent.instance.config.value('dev_trace') && ScoutApm::Agent.instance.environment.env == "development"
61
+ @recorder = ScoutApm::Agent.instance.recorder
62
+
63
+ ignore_request! if @recorder.nil?
57
64
  end
58
65
 
59
66
  def start_layer(layer)
67
+ # If we're already stopping, don't do additional layers
68
+ return if stopping?
69
+
60
70
  return if ignoring_children?
61
71
 
62
72
  return ignoring_start_layer if ignoring_request?
@@ -68,6 +78,9 @@ module ScoutApm
68
78
  end
69
79
 
70
80
  def stop_layer
81
+ # If we're already stopping, don't do additional layers
82
+ return if stopping?
83
+
71
84
  return if ignoring_children?
72
85
 
73
86
  return ignoring_stop_layer if ignoring_request?
@@ -79,7 +92,7 @@ module ScoutApm
79
92
  # lined up correctly. If stop_layer gets called twice, when it should
80
93
  # only have been called once you'll end up with this error.
81
94
  if layer.nil?
82
- ScoutApm::Agent.instance.logger.warn("Error stopping layer, was nil. Root Layer: #{@root_layer.inspect}")
95
+ logger.warn("Error stopping layer, was nil. Root Layer: #{@root_layer.inspect}")
83
96
  stop_request
84
97
  return
85
98
  end
@@ -181,11 +194,20 @@ module ScoutApm
181
194
  #
182
195
  # * Send the request off to be stored
183
196
  def stop_request
197
+ @stopping = true
198
+
184
199
  if ScoutApm::Agent.instance.config.value('profile')
185
200
  ScoutApm::Instruments::Stacks.stop_sampling(true)
186
201
  ScoutApm::Instruments::Stacks.update_indexes(0, 0)
187
202
  end
188
- record!
203
+
204
+ if recorder
205
+ recorder.record!(self)
206
+ end
207
+ end
208
+
209
+ def stopping?
210
+ @stopping
189
211
  end
190
212
 
191
213
  # Enable ScoutProf for this thread
@@ -250,13 +272,21 @@ module ScoutApm
250
272
  # Persist the Request
251
273
  ###################################
252
274
 
275
+ def recorded!
276
+ @recorded = true
277
+ end
278
+
253
279
  # Convert this request to the appropriate structure, then report it into
254
280
  # the peristent Store object
255
281
  def record!
256
- @recorded = true
282
+ recorded!
257
283
 
258
284
  return if ignoring_request?
259
285
 
286
+ # If we didn't have store, but we're trying to record anyway, go
287
+ # figure that out. (this happens in Remote Agent scenarios)
288
+ restore_store if @store.nil?
289
+
260
290
  # Bail out early if the user asked us to ignore this uri
261
291
  return if ScoutApm::Agent.instance.ignored_uris.ignore?(annotations[:uri])
262
292
 
@@ -301,7 +331,6 @@ module ScoutApm
301
331
 
302
332
  allocation_metrics = LayerConverters::AllocationMetricConverter.new(self).call
303
333
  @store.track!(allocation_metrics)
304
-
305
334
  end
306
335
 
307
336
  # Only call this after the request is complete
@@ -415,5 +444,24 @@ module ScoutApm
415
444
  def ignoring_recorded?
416
445
  @ignoring_depth <= 0
417
446
  end
447
+
448
+ def logger
449
+ ScoutApm::Agent.instance.logger
450
+ end
451
+
452
+ # Actually go fetch & make-real any lazily created data.
453
+ # Clean up any cleverness in objects.
454
+ # Makes this object ready to be Marshal Dumped (or otherwise serialized)
455
+ def prepare_to_dump!
456
+ @call_set = nil
457
+ @store = nil
458
+ @recorder = nil
459
+ end
460
+
461
+ # Go re-fetch the store based on what the Agent's official one is. Used
462
+ # after hydrating a dumped TrackedRequest
463
+ def restore_store
464
+ @store = ScoutApm::Agent.instance.store
465
+ end
418
466
  end
419
467
  end