scout_apm 2.1.29 → 2.1.30

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.
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