scout_apm 1.1.0.pre1 → 1.2.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.markdown +6 -0
  3. data/lib/scout_apm/agent/reporting.rb +67 -77
  4. data/lib/scout_apm/agent.rb +56 -9
  5. data/lib/scout_apm/background_job_integrations/delayed_job.rb +19 -0
  6. data/lib/scout_apm/background_job_integrations/sidekiq.rb +60 -0
  7. data/lib/scout_apm/bucket_name_splitter.rb +2 -2
  8. data/lib/scout_apm/capacity.rb +2 -1
  9. data/lib/scout_apm/context.rb +1 -5
  10. data/lib/scout_apm/environment.rb +16 -1
  11. data/lib/scout_apm/instruments/action_controller_rails_2.rb +13 -3
  12. data/lib/scout_apm/instruments/action_controller_rails_3.rb +20 -20
  13. data/lib/scout_apm/instruments/active_record.rb +5 -8
  14. data/lib/scout_apm/instruments/delayed_job.rb +56 -0
  15. data/lib/scout_apm/instruments/middleware.rb +44 -0
  16. data/lib/scout_apm/instruments/mongoid.rb +1 -1
  17. data/lib/scout_apm/instruments/moped.rb +2 -2
  18. data/lib/scout_apm/instruments/net_http.rb +1 -2
  19. data/lib/scout_apm/instruments/process/process_cpu.rb +5 -1
  20. data/lib/scout_apm/instruments/process/process_memory.rb +5 -1
  21. data/lib/scout_apm/instruments/sinatra.rb +14 -2
  22. data/lib/scout_apm/layaway.rb +33 -79
  23. data/lib/scout_apm/layaway_file.rb +2 -1
  24. data/lib/scout_apm/layer.rb +115 -0
  25. data/lib/scout_apm/layer_converter.rb +196 -0
  26. data/lib/scout_apm/metric_meta.rb +24 -4
  27. data/lib/scout_apm/metric_stats.rb +14 -4
  28. data/lib/scout_apm/request_manager.rb +26 -0
  29. data/lib/scout_apm/request_queue_time.rb +54 -0
  30. data/lib/scout_apm/serializers/payload_serializer.rb +8 -1
  31. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +1 -0
  32. data/lib/scout_apm/slow_transaction.rb +3 -0
  33. data/lib/scout_apm/store.rb +122 -190
  34. data/lib/scout_apm/tracer.rb +54 -83
  35. data/lib/scout_apm/tracked_request.rb +168 -0
  36. data/lib/scout_apm/version.rb +1 -1
  37. data/lib/scout_apm.rb +18 -5
  38. metadata +11 -2
@@ -0,0 +1,44 @@
1
+ module ScoutApm
2
+ module Instruments
3
+ class Middleware
4
+ def initalize(logger=ScoutApm::Agent.instance.logger)
5
+ @logger = logger
6
+ @installed = false
7
+ end
8
+
9
+ def installed?
10
+ @installed
11
+ end
12
+
13
+ def install
14
+ @installed = true
15
+
16
+ if defined?(ActionDispatch) && defined?(ActionDispatch::MiddlewareStack) && defined?(ActionDispatch::MiddlewareStack::Middleware)
17
+ ActionDispatch::MiddlewareStack::Middleware.class_eval do
18
+ def build(app)
19
+ ScoutApm::Agent.instance.logger.info("Building Middleware #{klass.name}")
20
+ new_mw = klass.new(app, *args, &block)
21
+ MiddlewareWrapper.new(new_mw, klass.name)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ class MiddlewareWrapper
28
+ def initialize(app, name)
29
+ @app = app
30
+ @type = "Middleware"
31
+ @name = name
32
+ end
33
+
34
+ def call(env)
35
+ req = ScoutApm::RequestManager.lookup
36
+ req.start_layer( ScoutApm::Layer.new(@type, @name) )
37
+ @app.call(env)
38
+ ensure
39
+ req.stop_layer
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -22,7 +22,7 @@ module ScoutApm
22
22
  ::Mongoid::Collection.class_eval do
23
23
  include ScoutApm::Tracer
24
24
  (::Mongoid::Collections::Operations::ALL - [:<<, :[]]).each do |method|
25
- instrument_method method, :metric_name => "MongoDB/\#{@klass}/#{method}"
25
+ instrument_method method, :type => "MongoDB", :name => '#{@klass}/#{method}'
26
26
  end
27
27
  end
28
28
  end
@@ -23,8 +23,8 @@ module ScoutApm
23
23
  def process_with_scout_instruments(operation, &callback)
24
24
  if operation.respond_to?(:collection)
25
25
  collection = operation.collection
26
- self.class.instrument("MongoDB/Process/#{collection}/#{operation.class.to_s.split('::').last}",
27
- :desc => scout_sanitize_log(operation.log_inspect)) do
26
+ name = "Process/#{collection}/#{operation.class.to_s.split('::').last}"
27
+ self.class.instrument("MongoDB", name, :annotate_layer => { :query => scout_sanitize_log(operation.log_inspect) }) do
28
28
  process_without_scout_instruments(operation, &callback)
29
29
  end
30
30
  end
@@ -22,7 +22,7 @@ module ScoutApm
22
22
  include ScoutApm::Tracer
23
23
 
24
24
  def request_with_scout_instruments(*args,&block)
25
- self.class.instrument("HTTP/request", :desc => "#{(@address+args.first.path.split('?').first)[0..99]}") do
25
+ self.class.instrument("HTTP", "request", :annotate_layer => { :url => "#{(@address+args.first.path.split('?').first)[0..99]}" } ) do
26
26
  request_without_scout_instruments(*args,&block)
27
27
  end
28
28
  end
@@ -30,7 +30,6 @@ module ScoutApm
30
30
  alias request request_with_scout_instruments
31
31
  end
32
32
  end
33
-
34
33
  end
35
34
  end
36
35
  end
@@ -17,8 +17,12 @@ module ScoutApm
17
17
  @last_stime = t.stime
18
18
  end
19
19
 
20
+ def metric_type
21
+ "CPU"
22
+ end
23
+
20
24
  def metric_name
21
- "CPU/Utilization"
25
+ "Utilization"
22
26
  end
23
27
 
24
28
  def human_name
@@ -8,8 +8,12 @@ module ScoutApm
8
8
  @logger = logger
9
9
  end
10
10
 
11
+ def metric_type
12
+ "Memory"
13
+ end
14
+
11
15
  def metric_name
12
- "Memory/Physical"
16
+ "Physical"
13
17
  end
14
18
 
15
19
  def human_name
@@ -29,9 +29,21 @@ module ScoutApm
29
29
 
30
30
  module SinatraInstruments
31
31
  def dispatch_with_scout_instruments!
32
- scout_controller_action = "Controller/Sinatra/#{scout_sinatra_controller_name(@request)}"
33
- self.class.scout_apm_trace(scout_controller_action, :uri => @request.path_info, :ip => @request.ip) do
32
+ scout_controller_action = "Sinatra/#{scout_sinatra_controller_name(@request)}"
33
+
34
+ req = ScoutApm::RequestManager.lookup
35
+ req.annotate_request(:uri => @request.path_info)
36
+ req.context.add_user(:ip => @request.ip)
37
+ req.set_headers(request.headers)
38
+
39
+ req.start_layer( ScoutApm::Layer.new("Controller", scout_controller_action) )
40
+ begin
34
41
  dispatch_without_scout_instruments!
42
+ rescue
43
+ req.error!
44
+ raise
45
+ ensure
46
+ req.stop_layer
35
47
  end
36
48
  end
37
49
 
@@ -1,9 +1,5 @@
1
- # Stores metrics in a file before sending them to the server. Two uses:
1
+ # Stores StoreReportingPeriod objects in a file before sending them to the server.
2
2
  # 1. A centralized store for multiple Agent processes. This way, only 1 checkin is sent to Scout rather than 1 per-process.
3
- # 2. Bundling up reports from multiple timeslices to make updates more efficent server-side.
4
- #
5
- # Data is stored in a Hash, where the keys are Time.to_i on the minute. The value is a Hash {:metrics => Hash, :slow_transactions => Array}.
6
- # When depositing data, the new data is either merged with an existing time or placed in a new key.
7
3
  module ScoutApm
8
4
  class Layaway
9
5
  attr_accessor :file
@@ -12,93 +8,51 @@ module ScoutApm
12
8
  @file = ScoutApm::LayawayFile.new
13
9
  end
14
10
 
15
- def deposit_and_deliver
16
- new_metrics = ScoutApm::Agent.instance.store.metric_hash
17
- log_deposited_metrics(new_metrics)
18
- log_deposited_slow_transactions(ScoutApm::Agent.instance.store.slow_transactions)
19
- to_deliver = {}
20
- file.read_and_write do |old_data|
21
- old_data ||= Hash.new
22
- # merge data
23
- # if (1) there's data in the file and (2) there isn't any data yet for the current minute, this means we've
24
- # collected all metrics for the previous slots and we're ready to deliver.
25
- #
26
- # Example w/2 processes:
27
- #
28
- # 12:00:34 ---
29
- # Process 1: old_data.any? => false, so deposits.
30
- # Process 2: old_data_any? => true and old_data[12:00].nil? => false, so deposits.
31
- #
32
- # 12:01:34 ---
33
- # Process 1: old_data.any? => true and old_data[12:01].nil? => true, so delivers metrics.
34
- # Process 2: old_data.any? => true and old_data[12:01].nil? => false, so deposits.
35
- if old_data.any? and old_data[slot].nil?
36
- to_deliver = old_data
37
- old_data = Hash.new
38
- elsif old_data.any?
39
- ScoutApm::Agent.instance.logger.debug "Not yet time to deliver payload for slot [#{Utils::Time.to_s(old_data.keys.sort.last)}]"
11
+ # We're changing the format, so detect if we're loading an old formatted
12
+ # file, and just drop it if so. There's no important data there, since it's
13
+ # used mostly for just syncronizing between processes
14
+ def verify_layaway_file_contents
15
+ file.read_and_write do |existing_data|
16
+ existing_data ||= {}
17
+ if existing_data.keys.all?{|k| k.is_a? StoreReportingPeriodTimestamp } &&
18
+ existing_data.values.all? {|v| v.is_a? StoreReportingPeriod }
19
+ existing_data
40
20
  else
41
- ScoutApm::Agent.instance.logger.debug "There is no data in the layaway file to deliver."
21
+ {}
42
22
  end
43
- old_data[slot]=ScoutApm::Agent.instance.store.merge_data_and_clear(old_data[slot] || {:metrics => {}, :slow_transactions => []})
44
- log_saved_data(old_data,new_metrics)
45
- old_data
46
23
  end
47
- to_deliver.any? ? validate_data(to_deliver) : {}
48
24
  end
49
25
 
50
- # Ensures the data we're sending to the server isn't stale.
51
- # This can occur if the agent is collecting data, and app server goes down w/data in the local storage.
52
- # When it is restarted later data will remain in local storage but it won't be for the current reporting interval.
53
- #
54
- # If the data is stale, an empty Hash is returned. Otherwise, the data from the most recent slot is returned.
55
- def validate_data(data)
56
- data = data.to_a.sort
57
- now = Time.now
58
- if (most_recent = data.first.first) < now.to_i - 2*60
59
- ScoutApm::Agent.instance.logger.debug "Local Storage is stale (#{Utils::Time.to_s(most_recent)}). Not sending data."
60
- {}
61
- else
62
- data.first.last
26
+ def add_reporting_period(time, reporting_period)
27
+ file.read_and_write do |existing_data|
28
+ existing_data ||= Hash.new
29
+ existing_data.merge!(time => reporting_period) {|key, old_val, new_val|
30
+ old_val.merge_metrics!(new_val.metrics).merge_slow_transactions!(new_val.slow_transactions)
31
+ }
63
32
  end
64
- rescue
65
- ScoutApm::Agent.instance.logger.debug $!.message
66
- ScoutApm::Agent.instance.logger.debug $!.backtrace
67
33
  end
68
34
 
69
- # Data is stored under timestamp-keys, aligned to the beginning of the
70
- # current minute
71
- def slot
72
- t = Time.now
73
- t -= t.sec
74
- t.to_i
75
- end
35
+ REPORTING_INTERVAL = 60 # seconds
36
+ MAX_INTERVALS = 5
76
37
 
77
- def log_deposited_metrics(new_metrics)
78
- request_count = new_metrics.
79
- to_a.
80
- select { |meta, stats| meta.metric_name =~ /\AController/ }.
81
- map { |meta, stats| stats.call_count }.
82
- inject(0) { |total, i| total + i }
38
+ # Returns an array of ReportingPeriod objects that are ready to be pushed to the server
39
+ def periods_ready_for_delivery
40
+ ready_for_delivery = []
83
41
 
84
- ScoutApm::Agent.instance.logger.debug "Depositing #{request_count} requests into #{Utils::Time.to_s(slot)} slot."
85
- end
42
+ file.read_and_write do |existing_data|
43
+ existing_data ||= {}
44
+ ready_for_delivery = existing_data.select {|time, rp| should_send?(rp) } # Select off the values we want
45
+
46
+ # Rewrite anything not plucked out back to the file
47
+ existing_data.reject {|k, v| ready_for_delivery.keys.include?(k) }
48
+ end
86
49
 
87
- def log_deposited_slow_transactions(new_slow_transactions)
88
- ScoutApm::Agent.instance.logger.debug "Depositing #{new_slow_transactions.size} slow transactions into #{Utils::Time.to_s(slot)} slot."
50
+ return ready_for_delivery.values
89
51
  end
90
52
 
91
- def log_saved_data(old_data,new_metrics)
92
- ScoutApm::Agent.instance.logger.debug "Saving the following #{old_data.size} time slots locally:"
93
- old_data.each do |k,v|
94
- controller_count = 0
95
- new_metrics.each do |meta,stats|
96
- if meta.metric_name =~ /\AController/
97
- controller_count += stats.call_count
98
- end
99
- end
100
- ScoutApm::Agent.instance.logger.debug "#{Utils::Time.to_s(k)} => #{controller_count} requests and #{v[:slow_transactions].size} slow transactions"
101
- end
53
+ # We just want to send anything older than X
54
+ def should_send?(reporting_period)
55
+ reporting_period.timestamp.age_in_seconds > REPORTING_INTERVAL
102
56
  end
103
57
  end
104
58
  end
@@ -42,8 +42,9 @@ module ScoutApm
42
42
  "Change the path by setting the `data_file` key in scout_apm.yml"
43
43
  )
44
44
  ScoutApm::Agent.instance.logger.debug(e.backtrace.join("\n\t"))
45
+
45
46
  # ensure the in-memory metric hash is cleared so data doesn't continue to accumulate.
46
- ScoutApm::Agent.instance.store.metric_hash = {}
47
+ # ScoutApm::Agent.instance.store.metric_hash = {}
47
48
  end
48
49
 
49
50
  def get_data(f)
@@ -0,0 +1,115 @@
1
+ module ScoutApm
2
+ class Layer
3
+ # Type: a general name for the kind of thing being tracked.
4
+ # Examples: "Middleware", "ActiveRecord", "Controller", "View"
5
+ #
6
+ attr_reader :type
7
+
8
+ # Name: a more specific name of this single item
9
+ # Examples: "Rack::Cache", "User#find", "users/index", "users/index.html.erb"
10
+ attr_reader :name
11
+
12
+ # An array of children layers, in call order.
13
+ # For instance, if we are in a middleware, there will likely be only a single
14
+ # child, which is another middleware. In a Controller, we may have a handful
15
+ # of children: [ActiveRecord, ActiveRecord, View, HTTP Call].
16
+ #
17
+ # This useful to get actual time spent in this layer vs. children time
18
+ attr_reader :children
19
+
20
+ # Time objects recording the start & stop times of this layer
21
+ attr_reader :start_time, :stop_time
22
+
23
+ # The description of this layer. Will contain additional details specific to the type of layer.
24
+ # For an ActiveRecord metric, it will contain the SQL run
25
+ # For an outoing HTTP call, it will contain the remote URL accessed
26
+ # Leave blank if there is nothing to note
27
+ attr_reader :desc
28
+
29
+ # If this layer took longer than a fixed amount of time, store the
30
+ # backtrace of where it occurred.
31
+ attr_reader :backtrace
32
+
33
+
34
+ def initialize(type, name, start_time = Time.now)
35
+ @type = type
36
+ @name = name
37
+ @start_time = start_time
38
+ @children = [] # In order of calls
39
+ @desc = nil
40
+ end
41
+
42
+ def add_child(child)
43
+ @children << child
44
+ end
45
+
46
+ def record_stop_time!(stop_time = Time.now)
47
+ @stop_time = stop_time
48
+ end
49
+
50
+ def desc=(desc)
51
+ @desc = desc
52
+ end
53
+
54
+ def subscopable!
55
+ @subscopable = true
56
+ end
57
+
58
+ def subscopable?
59
+ @subscopable
60
+ end
61
+
62
+ # This is the old style name. This function is used for now, but should be
63
+ # removed, and the new type & name split should be enforced through the
64
+ # app.
65
+ def legacy_metric_name
66
+ "#{type}/#{name}"
67
+ end
68
+
69
+ def store_backtrace(bt)
70
+ @backtrace = bt
71
+ end
72
+
73
+ ######################################
74
+ # Debugging Helpers
75
+ ######################################
76
+
77
+ # May not be safe to call in every rails app, relies on Time#iso8601
78
+ def to_s
79
+ name_clause = "#{type}/#{name}"
80
+
81
+ total_string = total_call_time == 0 ? nil : "Total: #{total_call_time}"
82
+ self_string = total_exclusive_time == 0 ? nil : "Self: #{total_exclusive_time}"
83
+ timing_string = [total_string, self_string].compact.join(", ")
84
+
85
+ time_clause = "(Start: #{start_time.iso8601} / Stop: #{stop_time.try(:iso8601)} [#{timing_string}])"
86
+ desc_clause = "Description: #{desc.inspect}"
87
+ children_clause = "Children: #{children.length}"
88
+
89
+ "<Layer: #{name_clause} #{time_clause} #{desc_clause} #{children_clause}>"
90
+ end
91
+
92
+ ######################################
93
+ # Time Calculations
94
+ ######################################
95
+
96
+ def total_call_time
97
+ if stop_time
98
+ stop_time - start_time
99
+ else
100
+ # Shouldn't have called this yet. Return 0
101
+ 0
102
+ end
103
+ end
104
+
105
+ def total_exclusive_time
106
+ total_call_time - child_time
107
+ end
108
+
109
+ def child_time
110
+ children.
111
+ map { |child| child.total_call_time }.
112
+ inject(0) { |sum, time| sum + time }
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,196 @@
1
+ module ScoutApm
2
+ class LayerConverterBase
3
+ attr_reader :walker
4
+ attr_reader :request
5
+ attr_reader :root_layer
6
+
7
+ def initialize(request)
8
+ @request = request
9
+ @root_layer = request.root_layer
10
+ @walker = LayerDepthFirstWalker.new(root_layer)
11
+ end
12
+
13
+ # Scope is determined by the first Controller we hit. Most of the time
14
+ # there will only be 1 anyway. But if you have a controller that calls
15
+ # another controller method, we may pick that up:
16
+ # def update
17
+ # show
18
+ # render :update
19
+ # end
20
+ def scope_layer
21
+ @scope_layer ||= walker.walk do |layer|
22
+ if layer.type == "Controller"
23
+ break layer
24
+ end
25
+ end
26
+ end
27
+
28
+ end
29
+
30
+ # Take a TrackedRequest and turn it into a hash of:
31
+ # MetricMeta => MetricStats
32
+ # This will do some merging of metrics as duplicate calls are seen.
33
+ class LayerMetricConverter < LayerConverterBase
34
+ def call
35
+ scope = scope_layer
36
+
37
+ # TODO: Track requests that never reach a Controller (for example, when
38
+ # Middleware decides to return rather than passing onward)
39
+ return {} unless scope
40
+
41
+ create_metrics
42
+ end
43
+
44
+ # Full metrics from this request. These get aggregated in Store for the
45
+ # overview metrics, or stored permanently in a SlowTransaction
46
+ # Some merging of metrics will happen here, so if a request calls the same
47
+ # ActiveRecord or View repeatedly, it'll get merged.
48
+ def create_metrics
49
+ metric_hash = Hash.new
50
+
51
+ walker.walk do |layer|
52
+ meta_options = if layer == scope_layer # We don't scope the controller under itself
53
+ {}
54
+ else
55
+ {:scope => scope_layer.legacy_metric_name}
56
+ end
57
+
58
+ meta_options.merge!(:desc => layer.desc) if layer.desc
59
+
60
+ meta = MetricMeta.new(layer.legacy_metric_name, meta_options)
61
+ meta.extra.merge!(:backtrace => layer.backtrace) if layer.backtrace
62
+ metric_hash[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
63
+
64
+ stat = metric_hash[meta]
65
+ stat.update!(layer.total_call_time, layer.total_exclusive_time)
66
+ end
67
+
68
+ metric_hash
69
+ end
70
+ end
71
+
72
+ class LayerErrorConverter < LayerConverterBase
73
+ def call
74
+ scope = scope_layer
75
+
76
+ # Should we mark a request as errored out if a middleware raises?
77
+ # How does that interact w/ a tool like Sentry or Honeybadger?
78
+ return {} unless scope
79
+ return {} unless request.error?
80
+
81
+ meta = MetricMeta.new("Errors/#{scope.legacy_metric_name}", {})
82
+ stat = MetricStats.new
83
+ stat.update!(1)
84
+
85
+ { meta => stat }
86
+ end
87
+ end
88
+
89
+ # Take a TrackedRequest and turn it into either nil, or a SlowTransaction record
90
+ class LayerSlowTransactionConverter < LayerConverterBase
91
+ SLOW_REQUEST_TIME_THRESHOLD = 2 # seconds
92
+
93
+ def call
94
+ return nil unless should_capture_slow_request?
95
+
96
+ scope = scope_layer
97
+ return nil unless scope
98
+
99
+ uri = request.annotations[:uri] || ""
100
+ SlowTransaction.new(uri,
101
+ scope.legacy_metric_name,
102
+ root_layer.total_call_time,
103
+ create_metrics,
104
+ request.context,
105
+ root_layer.stop_time,
106
+ request.stackprof)
107
+ end
108
+
109
+ # Full metrics from this request. These get aggregated in Store for the
110
+ # overview metrics, or stored permanently in a SlowTransaction
111
+ # Some merging of metrics will happen here, so if a request calls the same
112
+ # ActiveRecord or View repeatedly, it'll get merged.
113
+ def create_metrics
114
+ metric_hash = Hash.new
115
+
116
+ subscope_layers = []
117
+
118
+ walker.before do |layer|
119
+ if layer.subscopable?
120
+ subscope_layers.push(layer)
121
+ end
122
+ end
123
+
124
+ walker.after do |layer|
125
+ if layer.subscopable?
126
+ subscope_layers.pop
127
+ end
128
+ end
129
+
130
+ walker.walk do |layer|
131
+ meta_options = if subscope_layers.first && layer != subscope_layers.first # Don't scope under ourself.
132
+ subscope_name = subscope_layers.first.legacy_metric_name
133
+ {:scope => subscope_name}
134
+ elsif layer == scope_layer # We don't scope the controller under itself
135
+ {}
136
+ else
137
+ {:scope => scope_layer.legacy_metric_name}
138
+ end
139
+
140
+ meta_options.merge!(:desc => layer.desc) if layer.desc
141
+
142
+ meta = MetricMeta.new(layer.legacy_metric_name, meta_options)
143
+ meta.extra.merge!(:backtrace => layer.backtrace) if layer.backtrace
144
+ metric_hash[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
145
+
146
+ stat = metric_hash[meta]
147
+ stat.update!(layer.total_call_time, layer.total_exclusive_time)
148
+ end
149
+
150
+ metric_hash
151
+ end
152
+
153
+ def should_capture_slow_request?
154
+ root_layer.total_call_time > SLOW_REQUEST_TIME_THRESHOLD
155
+ end
156
+ end
157
+
158
+ class LayerDepthFirstWalker
159
+ attr_reader :root_layer
160
+
161
+ def initialize(root_layer)
162
+ @root_layer = root_layer
163
+ end
164
+
165
+ def before(&block)
166
+ @before_block = block
167
+ end
168
+
169
+ def after(&block)
170
+ @after_block = block
171
+ end
172
+
173
+ def walk(layer=root_layer, &block)
174
+ layer.children.each do |child|
175
+ @before_block.call(child) if @before_block
176
+ yield child
177
+ walk(child, &block)
178
+ @after_block.call(child) if @after_block
179
+ end
180
+ nil
181
+ end
182
+
183
+ # Do this w/o using recursion, since it's prone to stack overflows
184
+ # Takes a block to run over each layer
185
+ # def walk
186
+ # layer_stack = [root_layer]
187
+
188
+ # while layer_stack.any?
189
+ # current_layer = layer_stack.pop
190
+ # current_layer.children.reverse.each { |child| layer_stack.push(child) }
191
+
192
+ # yield current_layer
193
+ # end
194
+ # end
195
+ end
196
+ end
@@ -1,11 +1,12 @@
1
1
  # Contains the meta information associated with a metric. Used to lookup Metrics in to Store's metric_hash.
2
- class ScoutApm::MetricMeta
2
+ module ScoutApm
3
+ class MetricMeta
3
4
  include ScoutApm::BucketNameSplitter
4
5
 
5
6
  def initialize(metric_name, options = {})
6
7
  @metric_name = metric_name
7
8
  @metric_id = nil
8
- @scope = Thread::current[:scout_apm_sub_scope] || Thread::current[:scout_apm_scope_name]
9
+ @scope = options[:scope]
9
10
  @desc = options[:desc]
10
11
  @extra = {}
11
12
  end
@@ -14,6 +15,20 @@ class ScoutApm::MetricMeta
14
15
  attr_accessor :client_id
15
16
  attr_accessor :desc, :extra
16
17
 
18
+ # Unsure if type or bucket is a better name.
19
+ def type
20
+ bucket
21
+ end
22
+
23
+ # A key metric is the "core" of a request - either the Rails controller reached, or the background Job executed
24
+ def key_metric?
25
+ self.class.key_metric?(metric_name)
26
+ end
27
+
28
+ def self.key_metric?(metric_name)
29
+ !!(metric_name =~ /\A(Controller|Job)\//)
30
+ end
31
+
17
32
  # To avoid conflicts with different JSON libaries
18
33
  def to_json(*a)
19
34
  %Q[{"metric_id":#{metric_id || 'null'},"metric_name":#{metric_name.to_json},"scope":#{scope.to_json || 'null'}}]
@@ -31,7 +46,11 @@ class ScoutApm::MetricMeta
31
46
  end
32
47
 
33
48
  def eql?(o)
34
- self.class == o.class && metric_name.downcase.eql?(o.metric_name.downcase) && scope == o.scope && client_id == o.client_id && desc == o.desc
49
+ self.class == o.class &&
50
+ metric_name.downcase == o.metric_name.downcase &&
51
+ scope == o.scope &&
52
+ client_id == o.client_id &&
53
+ desc == o.desc
35
54
  end
36
55
 
37
56
  def as_json
@@ -39,4 +58,5 @@ class ScoutApm::MetricMeta
39
58
  # query, stack_trace
40
59
  ScoutApm::AttributeArranger.call(self, json_attributes)
41
60
  end
42
- end # class MetricMeta
61
+ end
62
+ end