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