scout_apm 2.1.29 → 2.1.30

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6a05b91b0cb840bd278bee2f470494b5e5a85a32
4
- data.tar.gz: a6d75703bd477570ceb40c31dd230f0ca57657ba
3
+ metadata.gz: 94cee593281d02b80d99aa3bf55a66fb4ef1adf9
4
+ data.tar.gz: a5aff2d47256429910ff7eb2e4f0c145bdca69f7
5
5
  SHA512:
6
- metadata.gz: 7677505dc87d23ce4cbe362bcf6ceced27cd484af214e8563e7b37f215885c8fdb314e70c56252e4968cbf11892aec9e9003e04e37947ba9512d3a6ce8e2daa2
7
- data.tar.gz: 0cb53c39bd75b62a931b8c55125554cd364982231b94c89657eb6d4f89baa8ff46fc8dbe91aa7c24aed6027128c68cd4568ac4b26eaf1c7b9c2f8c190d2f218f
6
+ metadata.gz: 4a38d11380c58574636bf7ed6e086f890989edd7e7ef5a395e4d9cfecd11dc12c30206a85ea4f25324b28c4d92e49cad7be3e2e0ccd332a75f93497cc03b96d8
7
+ data.tar.gz: dd758e13ce68396da594a7d6bc8a04dc9f3f300d7bda905cb3676ca36a74b347ea9ace9f818b652b92150dfdb85d23656ad802c713660098f095b14d8ab88c11
data/.gitignore CHANGED
@@ -17,3 +17,4 @@ test/tmp/*coverage/*
17
17
  coverage/*
18
18
  lib/*.bundle
19
19
  lib/*.so
20
+ log/scout_apm.log
data/CHANGELOG.markdown CHANGED
@@ -1,3 +1,7 @@
1
+ # 2.1.30
2
+
3
+ * Add Resque support.
4
+
1
5
  # 2.1.29
2
6
 
3
7
  * Add `scm_subdirectory` option. Useful for when your app code does not live in your SCM root directory.
data/Guardfile ADDED
@@ -0,0 +1,42 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ ## Uncomment and set this to only include directories you want to watch
5
+ # directories %w(app lib config test spec features) \
6
+ # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
7
+
8
+ ## Note: if you are using the `directories` clause above and you are not
9
+ ## watching the project directory ('.'), then you will want to move
10
+ ## the Guardfile to a watched dir and symlink it back, e.g.
11
+ #
12
+ # $ mkdir config
13
+ # $ mv Guardfile config/
14
+ # $ ln -s config/Guardfile .
15
+ #
16
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
17
+
18
+ guard :minitest do
19
+ # with Minitest::Unit
20
+ watch(%r{^test/(.*)\/?test_(.*)\.rb$})
21
+ watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
22
+ watch(%r{^test/test_helper\.rb$}) { 'test' }
23
+
24
+ # with Minitest::Spec
25
+ # watch(%r{^spec/(.*)_spec\.rb$})
26
+ # watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
27
+ # watch(%r{^spec/spec_helper\.rb$}) { 'spec' }
28
+
29
+ # Rails 4
30
+ # watch(%r{^app/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
31
+ # watch(%r{^app/controllers/application_controller\.rb$}) { 'test/controllers' }
32
+ # watch(%r{^app/controllers/(.+)_controller\.rb$}) { |m| "test/integration/#{m[1]}_test.rb" }
33
+ # watch(%r{^app/views/(.+)_mailer/.+}) { |m| "test/mailers/#{m[1]}_mailer_test.rb" }
34
+ # watch(%r{^lib/(.+)\.rb$}) { |m| "test/lib/#{m[1]}_test.rb" }
35
+ # watch(%r{^test/.+_test\.rb$})
36
+ # watch(%r{^test/test_helper\.rb$}) { 'test' }
37
+
38
+ # Rails < 4
39
+ # watch(%r{^app/controllers/(.*)\.rb$}) { |m| "test/functional/#{m[1]}_test.rb" }
40
+ # watch(%r{^app/helpers/(.*)\.rb$}) { |m| "test/helpers/#{m[1]}_test.rb" }
41
+ # watch(%r{^app/models/(.*)\.rb$}) { |m| "test/unit/#{m[1]}_test.rb" }
42
+ end
@@ -6,7 +6,12 @@ module ScoutApm
6
6
  "#{environment.root}/log"
7
7
  end
8
8
 
9
- def init_logger
9
+ def init_logger(opts={})
10
+ if opts[:force]
11
+ @log_file = nil
12
+ @logger = nil
13
+ end
14
+
10
15
  begin
11
16
  @log_file ||= determine_log_destination
12
17
  rescue => e
@@ -10,6 +10,7 @@ module ScoutApm
10
10
 
11
11
  # Accessors below are for associated classes
12
12
  attr_accessor :store
13
+ attr_reader :recorder
13
14
  attr_accessor :layaway
14
15
  attr_accessor :config
15
16
  attr_accessor :logger
@@ -41,6 +42,9 @@ module ScoutApm
41
42
  @process_start_time = Time.now
42
43
  @options ||= options
43
44
 
45
+ # until the agent is started, there's no recorder
46
+ @recorder = nil
47
+
44
48
  # Start up without attempting to load a configuration file. We need to be
45
49
  # able to lookup configuration options like "application_root" which would
46
50
  # then in turn influence where the configuration file came from.
@@ -54,6 +58,7 @@ module ScoutApm
54
58
  @request_histograms_by_time = Hash.new { |h, k| h[k] = ScoutApm::RequestHistograms.new }
55
59
 
56
60
  @store = ScoutApm::Store.new
61
+
57
62
  @layaway = ScoutApm::Layaway.new(config, environment)
58
63
  @metric_lookup = Hash.new
59
64
 
@@ -111,13 +116,14 @@ module ScoutApm
111
116
  def start(options = {})
112
117
  @options.merge!(options)
113
118
 
114
-
115
119
  @config = ScoutApm::Config.with_file(@config.value("config_file"))
116
120
  layaway.config = config
117
121
 
118
122
  init_logger
119
123
  logger.info "Attempting to start Scout Agent [#{ScoutApm::VERSION}] on [#{environment.hostname}]"
120
124
 
125
+ @recorder = create_recorder
126
+
121
127
  @config.log_settings
122
128
 
123
129
  @ignored_uris = ScoutApm::IgnoredUris.new(config.value('ignore'))
@@ -134,7 +140,6 @@ module ScoutApm
134
140
  @started = true
135
141
  logger.info "Starting monitoring for [#{environment.application_name}]. Framework [#{environment.framework}] App Server [#{environment.app_server}] Background Job Framework [#{environment.background_job_name}]."
136
142
 
137
-
138
143
  [ ScoutApm::Instruments::Process::ProcessCpu.new(environment.processors, logger),
139
144
  ScoutApm::Instruments::Process::ProcessMemory.new(logger),
140
145
  ScoutApm::Instruments::PercentileSampler.new(logger, request_histograms_by_time),
@@ -256,6 +261,9 @@ module ScoutApm
256
261
 
257
262
  install_exit_handler
258
263
 
264
+ @recorder = create_recorder
265
+ logger.info("recorder is now: #{@recorder.class}")
266
+
259
267
  @background_worker = ScoutApm::BackgroundWorker.new
260
268
  @background_worker_thread = Thread.new do
261
269
  @background_worker.start {
@@ -336,5 +344,47 @@ module ScoutApm
336
344
  def background_job_missing?(options = {})
337
345
  environment.background_job_integration.nil? && !options[:skip_background_job_check]
338
346
  end
347
+
348
+ def clear_recorder
349
+ @recorder = nil
350
+ end
351
+
352
+ def create_recorder
353
+ if @recorder
354
+ return @recorder
355
+ end
356
+
357
+ if config.value("async_recording")
358
+ logger.debug("Using asynchronous recording")
359
+ ScoutApm::BackgroundRecorder.new(logger).start
360
+ else
361
+ logger.debug("Using synchronous recording")
362
+ ScoutApm::SynchronousRecorder.new(logger).start
363
+ end
364
+ end
365
+
366
+ def start_remote_server(bind, port)
367
+ return if @remote_server && @remote_server.running?
368
+
369
+ logger.info("Starting Remote Agent Server")
370
+
371
+ # Start the listening web server only in parent process.
372
+ @remote_server = ScoutApm::Remote::Server.new(
373
+ bind,
374
+ port,
375
+ ScoutApm::Remote::Router.new(ScoutApm::SynchronousRecorder.new(logger), logger),
376
+ logger
377
+ )
378
+
379
+ @remote_server.start
380
+ end
381
+
382
+ # Execute this in the child process of a remote agent. The parent is
383
+ # expected to have its accepting webserver up and running
384
+ def use_remote_recorder(host, port)
385
+ logger.debug("Becoming Remote Agent (reporting to: #{host}:#{port})")
386
+ @recorder = ScoutApm::Remote::Recorder.new(host, port, logger)
387
+ @store = ScoutApm::FakeStore.new
388
+ end
339
389
  end
340
390
  end
@@ -0,0 +1,85 @@
1
+ module ScoutApm
2
+ module BackgroundJobIntegrations
3
+ class Resque
4
+ def name
5
+ :resque
6
+ end
7
+
8
+ def present?
9
+ defined?(::Resque) &&
10
+ ::Resque.respond_to?(:before_first_fork) &&
11
+ ::Resque.respond_to?(:after_fork)
12
+ end
13
+
14
+ # Lies. This forks really aggressively, but we have to do handling
15
+ # of it manually here, rather than via any sort of automatic
16
+ # background worker starting
17
+ def forking?
18
+ false
19
+ end
20
+
21
+ def install
22
+ install_before_fork
23
+ install_after_fork
24
+ end
25
+
26
+ def install_before_fork
27
+ ::Resque.before_first_fork do
28
+ begin
29
+ ScoutApm::Agent.instance.start(:skip_app_server_check => true)
30
+ ScoutApm::Agent.instance.start_background_worker
31
+ ScoutApm::Agent.instance.start_remote_server(bind, port)
32
+ rescue Errno::EADDRINUSE
33
+ ScoutApm::Agent.instance.logger.warn "Error while Installing Resque Instruments, Port #{port} already in use. Set via the `remote_agent_port` configuration option"
34
+ rescue => e
35
+ ScoutApm::Agent.instance.logger.warn "Error while Installing Resque before_first_fork: #{e.inspect}"
36
+ end
37
+ end
38
+ end
39
+
40
+ def install_after_fork
41
+ ::Resque.after_fork do
42
+ begin
43
+ ScoutApm::Agent.instance.use_remote_recorder(bind, port)
44
+ inject_job_instrument
45
+ rescue => e
46
+ ScoutApm::Agent.instance.logger.warn "Error while Installing Resque after_fork: #{e.inspect}"
47
+ end
48
+ end
49
+ end
50
+
51
+ # Insert ourselves into the point when resque turns a string "TestJob"
52
+ # into the class constant TestJob, and insert our instrumentation plugin
53
+ # into that constantized class
54
+ #
55
+ # This automates away any need for the user to insert our instrumentation into
56
+ # each of their jobs
57
+ def inject_job_instrument
58
+ ::Resque::Job.class_eval do
59
+ def payload_class_with_scout_instruments
60
+ klass = payload_class_without_scout_instruments
61
+ klass.extend(ScoutApm::Instruments::Resque)
62
+ klass
63
+ end
64
+ alias_method :payload_class_without_scout_instruments, :payload_class
65
+ alias_method :payload_class, :payload_class_with_scout_instruments
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def bind
72
+ config.value("remote_agent_host")
73
+ end
74
+
75
+ def port
76
+ config.value("remote_agent_port")
77
+ end
78
+
79
+ def config
80
+ @config || ScoutApm::Agent.instance.config
81
+ end
82
+ end
83
+ end
84
+ end
85
+
@@ -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
@@ -27,6 +27,8 @@ require 'scout_apm/environment'
27
27
  # report_format - 'json' or 'marshal'. Marshal is legacy and will be removed.
28
28
  # scm_subdirectory - if the app root lives in source management in a subdirectory. E.g. #{SCM_ROOT}/src
29
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
30
32
  #
31
33
  # Any of these config settings can be set with an environment variable prefixed
32
34
  # by SCOUT_ and uppercasing the key: SCOUT_LOG_LEVEL for instance.
@@ -35,6 +37,7 @@ module ScoutApm
35
37
  class Config
36
38
  KNOWN_CONFIG_OPTIONS = [
37
39
  'application_root',
40
+ 'async_recording',
38
41
  'compress_payload',
39
42
  'config_file',
40
43
  'data_file',
@@ -53,6 +56,8 @@ module ScoutApm
53
56
  'name',
54
57
  'profile',
55
58
  'proxy',
59
+ 'remote_agent_host',
60
+ 'remote_agent_port',
56
61
  'report_format',
57
62
  'scm_subdirectory',
58
63
  'uri_reporting',
@@ -129,11 +134,12 @@ module ScoutApm
129
134
 
130
135
 
131
136
  SETTING_COERCIONS = {
132
- "monitor" => BooleanCoercion.new,
133
- "enable_background_jobs" => BooleanCoercion.new,
134
- "dev_trace" => BooleanCoercion.new,
137
+ "async_recording" => BooleanCoercion.new,
135
138
  "detailed_middleware" => BooleanCoercion.new,
139
+ "dev_trace" => BooleanCoercion.new,
140
+ "enable_background_jobs" => BooleanCoercion.new,
136
141
  "ignore" => JsonCoercion.new,
142
+ "monitor" => BooleanCoercion.new,
137
143
  }
138
144
 
139
145
 
@@ -219,6 +225,8 @@ module ScoutApm
219
225
  'report_format' => 'json',
220
226
  'scm_subdirectory' => '',
221
227
  'uri_reporting' => 'full_path',
228
+ 'remote_agent_host' => '127.0.0.1',
229
+ 'remote_agent_port' => 7721, # picked at random
222
230
  }.freeze
223
231
 
224
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
  ]
@@ -0,0 +1,30 @@
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 = @queue
7
+
8
+ req = ScoutApm::RequestManager.lookup
9
+ req.job!
10
+ # req.annotate_request(:queue_latency => latency(msg))
11
+
12
+ begin
13
+ req.start_layer(ScoutApm::Layer.new('Queue', queue))
14
+ started_queue = true
15
+ req.start_layer(ScoutApm::Layer.new('Job', job_name))
16
+ started_job = true
17
+
18
+ yield
19
+ rescue => e
20
+ req.error!
21
+ raise
22
+ ensure
23
+ req.stop_layer if started_job
24
+ req.stop_layer if started_queue
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
@@ -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,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
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?
@@ -66,6 +76,9 @@ module ScoutApm
66
76
  end
67
77
 
68
78
  def stop_layer
79
+ # If we're already stopping, don't do additional layers
80
+ return if stopping?
81
+
69
82
  return if ignoring_children?
70
83
 
71
84
  return ignoring_stop_layer if ignoring_request?
@@ -77,7 +90,7 @@ module ScoutApm
77
90
  # lined up correctly. If stop_layer gets called twice, when it should
78
91
  # only have been called once you'll end up with this error.
79
92
  if layer.nil?
80
- ScoutApm::Agent.instance.logger.warn("Error stopping layer, was nil. Root Layer: #{@root_layer.inspect}")
93
+ logger.warn("Error stopping layer, was nil. Root Layer: #{@root_layer.inspect}")
81
94
  stop_request
82
95
  return
83
96
  end
@@ -169,7 +182,15 @@ module ScoutApm
169
182
  #
170
183
  # * Send the request off to be stored
171
184
  def stop_request
172
- record!
185
+ @stopping = true
186
+
187
+ if recorder
188
+ recorder.record!(self)
189
+ end
190
+ end
191
+
192
+ def stopping?
193
+ @stopping
173
194
  end
174
195
 
175
196
  ###################################
@@ -224,13 +245,21 @@ module ScoutApm
224
245
  # Persist the Request
225
246
  ###################################
226
247
 
248
+ def recorded!
249
+ @recorded = true
250
+ end
251
+
227
252
  # Convert this request to the appropriate structure, then report it into
228
253
  # the peristent Store object
229
254
  def record!
230
- @recorded = true
255
+ recorded!
231
256
 
232
257
  return if ignoring_request?
233
258
 
259
+ # If we didn't have store, but we're trying to record anyway, go
260
+ # figure that out. (this happens in Remote Agent scenarios)
261
+ restore_store if @store.nil?
262
+
234
263
  # Bail out early if the user asked us to ignore this uri
235
264
  return if ScoutApm::Agent.instance.ignored_uris.ignore?(annotations[:uri])
236
265
 
@@ -275,7 +304,6 @@ module ScoutApm
275
304
 
276
305
  allocation_metrics = LayerConverters::AllocationMetricConverter.new(self).call
277
306
  @store.track!(allocation_metrics)
278
-
279
307
  end
280
308
 
281
309
  # Only call this after the request is complete
@@ -389,5 +417,24 @@ module ScoutApm
389
417
  def ignoring_recorded?
390
418
  @ignoring_depth <= 0
391
419
  end
420
+
421
+ def logger
422
+ ScoutApm::Agent.instance.logger
423
+ end
424
+
425
+ # Actually go fetch & make-real any lazily created data.
426
+ # Clean up any cleverness in objects.
427
+ # Makes this object ready to be Marshal Dumped (or otherwise serialized)
428
+ def prepare_to_dump!
429
+ @call_set = nil
430
+ @store = nil
431
+ @recorder = nil
432
+ end
433
+
434
+ # Go re-fetch the store based on what the Agent's official one is. Used
435
+ # after hydrating a dumped TrackedRequest
436
+ def restore_store
437
+ @store = ScoutApm::Agent.instance.store
438
+ end
392
439
  end
393
440
  end
@@ -1,4 +1,4 @@
1
1
  module ScoutApm
2
- VERSION = "2.1.29"
2
+ VERSION = "2.1.30"
3
3
  end
4
4
 
data/lib/scout_apm.rb CHANGED
@@ -14,6 +14,7 @@ require 'socket'
14
14
  require 'thread'
15
15
  require 'time'
16
16
  require 'yaml'
17
+ require 'webrick'
17
18
 
18
19
  #####################################
19
20
  # Gem Requires
@@ -53,6 +54,7 @@ require 'scout_apm/server_integrations/null'
53
54
 
54
55
  require 'scout_apm/background_job_integrations/sidekiq'
55
56
  require 'scout_apm/background_job_integrations/delayed_job'
57
+ require 'scout_apm/background_job_integrations/resque'
56
58
 
57
59
  require 'scout_apm/framework_integrations/rails_2'
58
60
  require 'scout_apm/framework_integrations/rails_3_or_4'
@@ -118,6 +120,8 @@ require 'scout_apm/fake_store'
118
120
  require 'scout_apm/tracer'
119
121
  require 'scout_apm/context'
120
122
  require 'scout_apm/instant_reporting'
123
+ require 'scout_apm/background_recorder'
124
+ require 'scout_apm/synchronous_recorder'
121
125
 
122
126
  require 'scout_apm/metric_meta'
123
127
  require 'scout_apm/metric_stats'
@@ -147,6 +151,12 @@ require 'scout_apm/instant/middleware'
147
151
 
148
152
  require 'scout_apm/rack'
149
153
 
154
+ require 'scout_apm/remote/server'
155
+ require 'scout_apm/remote/router'
156
+ require 'scout_apm/remote/message'
157
+ require 'scout_apm/remote/recorder'
158
+ require 'scout_apm/instruments/resque'
159
+
150
160
  if defined?(Rails) && defined?(Rails::VERSION) && defined?(Rails::VERSION::MAJOR) && Rails::VERSION::MAJOR >= 3 && defined?(Rails::Railtie)
151
161
  module ScoutApm
152
162
  class Railtie < Rails::Railtie
data/scout_apm.gemspec CHANGED
@@ -28,4 +28,6 @@ Gem::Specification.new do |s|
28
28
  s.add_development_dependency "simplecov"
29
29
  s.add_development_dependency "rake-compiler"
30
30
  s.add_development_dependency "addressable"
31
+ s.add_development_dependency "guard"
32
+ s.add_development_dependency "guard-minitest"
31
33
  end
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+
3
+ class MessageTest < Minitest::Test
4
+ def test_message_encode_decode_roundtrip
5
+ message = ScoutApm::Remote::Message.new('type', 'command', ['arg'])
6
+ encoded = message.encode
7
+ decoded = ScoutApm::Remote::Message.decode(encoded)
8
+ assert_equal message.type, decoded.type
9
+ assert_equal message.command, decoded.command
10
+ assert_equal message.args, decoded.args
11
+ end
12
+ end
13
+
@@ -0,0 +1,33 @@
1
+ require 'test_helper'
2
+
3
+ class RouterTest < Minitest::Test
4
+ def test_router_handles_record
5
+ recorder = stub
6
+ router = ScoutApm::Remote::Router.new(recorder, logger)
7
+ message = ScoutApm::Remote::Message.new("record", "foo", 1, 2).encode
8
+
9
+ recorder.expects(:foo).with(1, 2)
10
+
11
+ router.handle(message)
12
+ end
13
+
14
+ def test_router_raises_on_unknown_types
15
+ recorder = stub
16
+ router = ScoutApm::Remote::Router.new(recorder, logger)
17
+ message = ScoutApm::Remote::Message.new("something_else", "foo", 1, 2).encode
18
+
19
+ recorder.expects(:foo).never
20
+ assert_raises do
21
+ router.handle(message)
22
+ end
23
+ end
24
+
25
+ def logger
26
+ @logger ||= Logger.new(logger_io)
27
+ end
28
+
29
+ def logger_io
30
+ @logger_io ||= StringIO.new
31
+ end
32
+ end
33
+
@@ -0,0 +1,15 @@
1
+ require 'test_helper'
2
+
3
+ class TestRemoteServer < Minitest::Test
4
+ def test_start_and_bind
5
+ bind = "127.0.0.1"
6
+ port = 8938
7
+ router = stub(:router)
8
+ logger_io = StringIO.new
9
+ server = ScoutApm::Remote::Server.new(bind, port, router, Logger.new(logger_io))
10
+
11
+ server.start
12
+ sleep 0.01 # Let the server finish starting. The assert should instead allow a time
13
+ assert server.running?
14
+ end
15
+ end
@@ -0,0 +1,87 @@
1
+ require 'test_helper'
2
+
3
+ class TrackedRequestDumpAndLoadTest < Minitest::Test
4
+ # TrackedRequest must be marshalable
5
+ def test_marshal_dump_load
6
+ tr = ScoutApm::TrackedRequest.new(ScoutApm::FakeStore.new)
7
+ tr.prepare_to_dump!
8
+
9
+ dumped = Marshal.dump(tr)
10
+ loaded = Marshal.load(dumped)
11
+ assert_false loaded.nil?
12
+ end
13
+
14
+ def test_restore_store
15
+ faux = ScoutApm::FakeStore.new
16
+ tr = ScoutApm::TrackedRequest.new(faux)
17
+ assert_equal faux, tr.instance_variable_get("@store")
18
+
19
+ tr.prepare_to_dump!
20
+ assert_nil tr.instance_variable_get("@store")
21
+
22
+ tr.restore_store
23
+ assert_equal ScoutApm::Agent.instance.store, tr.instance_variable_get("@store")
24
+ end
25
+ end
26
+
27
+ class TrackedRequestFlagsTest < Minitest::Test
28
+ def test_set_web
29
+ tr = ScoutApm::TrackedRequest.new(ScoutApm::FakeStore.new)
30
+ assert_false tr.web?
31
+ tr.web!
32
+ assert tr.web?
33
+ end
34
+
35
+ def test_set_job
36
+ tr = ScoutApm::TrackedRequest.new(ScoutApm::FakeStore.new)
37
+ assert ! tr.job?
38
+ tr.job!
39
+ assert tr.job?
40
+ end
41
+
42
+ def test_set_error
43
+ tr = ScoutApm::TrackedRequest.new(ScoutApm::FakeStore.new)
44
+ assert_false tr.error?
45
+ tr.error!
46
+ assert tr.error?
47
+ end
48
+
49
+ def test_set_error_and_web
50
+ tr = ScoutApm::TrackedRequest.new(ScoutApm::FakeStore.new)
51
+ assert_false tr.error?
52
+ assert_false tr.web?
53
+
54
+ tr.web!
55
+ assert_false tr.error?
56
+ assert tr.web?
57
+
58
+ tr.error!
59
+ assert tr.error?
60
+ assert tr.web?
61
+ end
62
+ end
63
+
64
+ class TrackedRequestLayerManipulationTest < Minitest::Test
65
+ def test_start_layer
66
+ tr = ScoutApm::TrackedRequest.new(ScoutApm::FakeStore.new)
67
+ tr.start_layer(ScoutApm::Layer.new("Foo", "Bar"))
68
+
69
+ assert_equal "Foo", tr.current_layer.type
70
+ end
71
+
72
+ def test_start_several_layers
73
+ # layers are Controller -> ActiveRecord
74
+ controller_layer = ScoutApm::Layer.new("Controller", "users/index")
75
+ ar_layer = ScoutApm::Layer.new("ActiveRecord", "Users#find")
76
+
77
+ tr = ScoutApm::TrackedRequest.new(ScoutApm::FakeStore.new)
78
+ tr.start_layer(controller_layer)
79
+ tr.start_layer(ar_layer)
80
+
81
+ assert_equal "ActiveRecord", tr.current_layer.type
82
+
83
+ tr.stop_layer
84
+
85
+ assert_equal "Controller", tr.current_layer.type
86
+ end
87
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_apm
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.29
4
+ version: 2.1.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Haynes
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-08-03 00:00:00.000000000 Z
12
+ date: 2017-08-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: minitest
@@ -109,6 +109,34 @@ dependencies:
109
109
  - - ">="
110
110
  - !ruby/object:Gem::Version
111
111
  version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: guard
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: guard-minitest
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
112
140
  description: Monitors Ruby apps and reports detailed metrics on performance to Scout.
113
141
  email:
114
142
  - support@scoutapp.com
@@ -122,6 +150,7 @@ files:
122
150
  - ".rubocop.yml"
123
151
  - CHANGELOG.markdown
124
152
  - Gemfile
153
+ - Guardfile
125
154
  - LICENSE.md
126
155
  - README.markdown
127
156
  - Rakefile
@@ -138,7 +167,9 @@ files:
138
167
  - lib/scout_apm/app_server_load.rb
139
168
  - lib/scout_apm/attribute_arranger.rb
140
169
  - lib/scout_apm/background_job_integrations/delayed_job.rb
170
+ - lib/scout_apm/background_job_integrations/resque.rb
141
171
  - lib/scout_apm/background_job_integrations/sidekiq.rb
172
+ - lib/scout_apm/background_recorder.rb
142
173
  - lib/scout_apm/background_worker.rb
143
174
  - lib/scout_apm/bucket_name_splitter.rb
144
175
  - lib/scout_apm/call_set.rb
@@ -176,6 +207,7 @@ files:
176
207
  - lib/scout_apm/instruments/process/process_memory.rb
177
208
  - lib/scout_apm/instruments/rails_router.rb
178
209
  - lib/scout_apm/instruments/redis.rb
210
+ - lib/scout_apm/instruments/resque.rb
179
211
  - lib/scout_apm/instruments/sinatra.rb
180
212
  - lib/scout_apm/job_record.rb
181
213
  - lib/scout_apm/layaway.rb
@@ -200,6 +232,10 @@ files:
200
232
  - lib/scout_apm/platform_integrations/heroku.rb
201
233
  - lib/scout_apm/platform_integrations/server.rb
202
234
  - lib/scout_apm/rack.rb
235
+ - lib/scout_apm/remote/message.rb
236
+ - lib/scout_apm/remote/recorder.rb
237
+ - lib/scout_apm/remote/router.rb
238
+ - lib/scout_apm/remote/server.rb
203
239
  - lib/scout_apm/reporter.rb
204
240
  - lib/scout_apm/request_histograms.rb
205
241
  - lib/scout_apm/request_manager.rb
@@ -225,6 +261,7 @@ files:
225
261
  - lib/scout_apm/slow_transaction.rb
226
262
  - lib/scout_apm/stack_item.rb
227
263
  - lib/scout_apm/store.rb
264
+ - lib/scout_apm/synchronous_recorder.rb
228
265
  - lib/scout_apm/tracer.rb
229
266
  - lib/scout_apm/tracked_request.rb
230
267
  - lib/scout_apm/utils/active_record_metric_name.rb
@@ -259,12 +296,16 @@ files:
259
296
  - test/unit/layer_children_set_test.rb
260
297
  - test/unit/limited_layer_test.rb
261
298
  - test/unit/metric_set_test.rb
299
+ - test/unit/remote/test_message.rb
300
+ - test/unit/remote/test_router.rb
301
+ - test/unit/remote/test_server.rb
262
302
  - test/unit/scored_item_set_test.rb
263
303
  - test/unit/serializers/payload_serializer_test.rb
264
304
  - test/unit/slow_job_policy_test.rb
265
305
  - test/unit/slow_request_policy_test.rb
266
306
  - test/unit/sql_sanitizer_test.rb
267
307
  - test/unit/store_test.rb
308
+ - test/unit/test_tracked_request.rb
268
309
  - test/unit/utils/active_record_metric_name_test.rb
269
310
  - test/unit/utils/backtrace_parser_test.rb
270
311
  - test/unit/utils/numbers_test.rb
@@ -290,8 +331,39 @@ required_rubygems_version: !ruby/object:Gem::Requirement
290
331
  version: '0'
291
332
  requirements: []
292
333
  rubyforge_project: scout_apm
293
- rubygems_version: 2.2.2
334
+ rubygems_version: 2.4.5.2
294
335
  signing_key:
295
336
  specification_version: 4
296
337
  summary: Ruby application performance monitoring
297
- test_files: []
338
+ test_files:
339
+ - test/data/config_test_1.yml
340
+ - test/test_helper.rb
341
+ - test/unit/agent_test.rb
342
+ - test/unit/background_job_integrations/sidekiq_test.rb
343
+ - test/unit/config_test.rb
344
+ - test/unit/context_test.rb
345
+ - test/unit/environment_test.rb
346
+ - test/unit/git_revision_test.rb
347
+ - test/unit/histogram_test.rb
348
+ - test/unit/ignored_uris_test.rb
349
+ - test/unit/instruments/active_record_instruments_test.rb
350
+ - test/unit/instruments/net_http_test.rb
351
+ - test/unit/instruments/percentile_sampler_test.rb
352
+ - test/unit/layaway_test.rb
353
+ - test/unit/layer_children_set_test.rb
354
+ - test/unit/limited_layer_test.rb
355
+ - test/unit/metric_set_test.rb
356
+ - test/unit/remote/test_message.rb
357
+ - test/unit/remote/test_router.rb
358
+ - test/unit/remote/test_server.rb
359
+ - test/unit/scored_item_set_test.rb
360
+ - test/unit/serializers/payload_serializer_test.rb
361
+ - test/unit/slow_job_policy_test.rb
362
+ - test/unit/slow_request_policy_test.rb
363
+ - test/unit/sql_sanitizer_test.rb
364
+ - test/unit/store_test.rb
365
+ - test/unit/test_tracked_request.rb
366
+ - test/unit/utils/active_record_metric_name_test.rb
367
+ - test/unit/utils/backtrace_parser_test.rb
368
+ - test/unit/utils/numbers_test.rb
369
+ - test/unit/utils/scm.rb