scout_apm 1.1.0.pre1 → 1.2.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +6 -0
- data/lib/scout_apm/agent/reporting.rb +67 -77
- data/lib/scout_apm/agent.rb +56 -9
- data/lib/scout_apm/background_job_integrations/delayed_job.rb +19 -0
- data/lib/scout_apm/background_job_integrations/sidekiq.rb +60 -0
- data/lib/scout_apm/bucket_name_splitter.rb +2 -2
- data/lib/scout_apm/capacity.rb +2 -1
- data/lib/scout_apm/context.rb +1 -5
- data/lib/scout_apm/environment.rb +16 -1
- data/lib/scout_apm/instruments/action_controller_rails_2.rb +13 -3
- data/lib/scout_apm/instruments/action_controller_rails_3.rb +20 -20
- data/lib/scout_apm/instruments/active_record.rb +5 -8
- data/lib/scout_apm/instruments/delayed_job.rb +56 -0
- data/lib/scout_apm/instruments/middleware.rb +44 -0
- data/lib/scout_apm/instruments/mongoid.rb +1 -1
- data/lib/scout_apm/instruments/moped.rb +2 -2
- data/lib/scout_apm/instruments/net_http.rb +1 -2
- data/lib/scout_apm/instruments/process/process_cpu.rb +5 -1
- data/lib/scout_apm/instruments/process/process_memory.rb +5 -1
- data/lib/scout_apm/instruments/sinatra.rb +14 -2
- data/lib/scout_apm/layaway.rb +33 -79
- data/lib/scout_apm/layaway_file.rb +2 -1
- data/lib/scout_apm/layer.rb +115 -0
- data/lib/scout_apm/layer_converter.rb +196 -0
- data/lib/scout_apm/metric_meta.rb +24 -4
- data/lib/scout_apm/metric_stats.rb +14 -4
- data/lib/scout_apm/request_manager.rb +26 -0
- data/lib/scout_apm/request_queue_time.rb +54 -0
- data/lib/scout_apm/serializers/payload_serializer.rb +8 -1
- data/lib/scout_apm/serializers/payload_serializer_to_json.rb +1 -0
- data/lib/scout_apm/slow_transaction.rb +3 -0
- data/lib/scout_apm/store.rb +122 -190
- data/lib/scout_apm/tracer.rb +54 -83
- data/lib/scout_apm/tracked_request.rb +168 -0
- data/lib/scout_apm/version.rb +1 -1
- data/lib/scout_apm.rb +18 -5
- 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, :
|
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
|
-
|
27
|
-
|
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
|
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
|
@@ -29,9 +29,21 @@ module ScoutApm
|
|
29
29
|
|
30
30
|
module SinatraInstruments
|
31
31
|
def dispatch_with_scout_instruments!
|
32
|
-
scout_controller_action = "
|
33
|
-
|
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
|
|
data/lib/scout_apm/layaway.rb
CHANGED
@@ -1,9 +1,5 @@
|
|
1
|
-
# Stores
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
70
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
85
|
-
|
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
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
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 =
|
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
|
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
|
61
|
+
end
|
62
|
+
end
|