scout_apm 2.1.8 → 2.1.9
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 +9 -0
- data/lib/scout_apm.rb +3 -0
- data/lib/scout_apm/agent.rb +9 -6
- data/lib/scout_apm/agent/reporting.rb +5 -3
- data/lib/scout_apm/background_job_integrations/delayed_job.rb +47 -1
- data/lib/scout_apm/background_worker.rb +4 -0
- data/lib/scout_apm/config.rb +2 -1
- data/lib/scout_apm/environment.rb +1 -1
- data/lib/scout_apm/histogram.rb +11 -2
- data/lib/scout_apm/instruments/mongoid.rb +14 -1
- data/lib/scout_apm/instruments/percentile_sampler.rb +36 -19
- data/lib/scout_apm/instruments/process/process_cpu.rb +3 -2
- data/lib/scout_apm/instruments/process/process_memory.rb +3 -3
- data/lib/scout_apm/layer_converters/converter_base.rb +193 -0
- data/lib/scout_apm/layer_converters/slow_job_converter.rb +14 -73
- data/lib/scout_apm/layer_converters/slow_request_converter.rb +13 -85
- data/lib/scout_apm/metric_set.rb +6 -0
- data/lib/scout_apm/reporter.rb +53 -15
- data/lib/scout_apm/request_histograms.rb +4 -0
- data/lib/scout_apm/scored_item_set.rb +7 -0
- data/lib/scout_apm/serializers/histograms_serializer_to_json.rb +21 -0
- data/lib/scout_apm/serializers/payload_serializer.rb +9 -3
- data/lib/scout_apm/serializers/payload_serializer_to_json.rb +2 -1
- data/lib/scout_apm/serializers/slow_jobs_serializer_to_json.rb +1 -1
- data/lib/scout_apm/slow_job_record.rb +4 -1
- data/lib/scout_apm/slow_transaction.rb +18 -2
- data/lib/scout_apm/store.rb +42 -11
- data/lib/scout_apm/tracked_request.rb +1 -1
- data/lib/scout_apm/utils/gzip_helper.rb +24 -0
- data/lib/scout_apm/utils/numbers.rb +14 -0
- data/lib/scout_apm/version.rb +1 -1
- data/test/test_helper.rb +10 -0
- data/test/unit/config_test.rb +7 -9
- data/test/unit/histogram_test.rb +14 -0
- data/test/unit/instruments/percentile_sampler_test.rb +137 -0
- data/test/unit/serializers/payload_serializer_test.rb +3 -3
- data/test/unit/store_test.rb +51 -0
- data/test/unit/utils/numbers_test.rb +15 -0
- metadata +10 -4
@@ -2,7 +2,6 @@ module ScoutApm
|
|
2
2
|
module LayerConverters
|
3
3
|
class SlowJobConverter < ConverterBase
|
4
4
|
def initialize(*)
|
5
|
-
@backtraces = []
|
6
5
|
super
|
7
6
|
|
8
7
|
# After call to super, so @request is populated
|
@@ -11,6 +10,8 @@ module ScoutApm
|
|
11
10
|
else
|
12
11
|
-1
|
13
12
|
end
|
13
|
+
|
14
|
+
setup_subscopable_callbacks
|
14
15
|
end
|
15
16
|
|
16
17
|
def name
|
@@ -32,6 +33,7 @@ module ScoutApm
|
|
32
33
|
mem_delta = ScoutApm::Instruments::Process::ProcessMemory.rss_to_mb(request.capture_mem_delta!)
|
33
34
|
|
34
35
|
timing_metrics, allocation_metrics = create_metrics
|
36
|
+
|
35
37
|
unless ScoutApm::Instruments::Allocations::ENABLED
|
36
38
|
allocation_metrics = {}
|
37
39
|
end
|
@@ -47,7 +49,9 @@ module ScoutApm
|
|
47
49
|
allocation_metrics,
|
48
50
|
mem_delta,
|
49
51
|
job_layer.total_allocations,
|
50
|
-
score
|
52
|
+
score,
|
53
|
+
limited?
|
54
|
+
)
|
51
55
|
end
|
52
56
|
|
53
57
|
def queue_layer
|
@@ -58,92 +62,29 @@ module ScoutApm
|
|
58
62
|
@job_layer ||= find_first_layer_of_type("Job")
|
59
63
|
end
|
60
64
|
|
65
|
+
def skip_layer?(layer)
|
66
|
+
super(layer) || layer == queue_layer
|
67
|
+
end
|
68
|
+
|
61
69
|
def create_metrics
|
62
70
|
metric_hash = Hash.new
|
63
71
|
allocation_metric_hash = Hash.new
|
64
72
|
|
65
|
-
# Keep a list of subscopes, but only ever use the front one. The rest
|
66
|
-
# get pushed/popped in cases when we have many levels of subscopable
|
67
|
-
# layers. This lets us push/pop without otherwise keeping track very closely.
|
68
|
-
subscope_layers = []
|
69
|
-
|
70
|
-
walker.before do |layer|
|
71
|
-
if layer.subscopable?
|
72
|
-
subscope_layers.push(layer)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
walker.after do |layer|
|
77
|
-
if layer.subscopable?
|
78
|
-
subscope_layers.pop
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
73
|
walker.walk do |layer|
|
83
|
-
|
84
|
-
# want to make an entry for it. See ActiveRecord instrumentation for
|
85
|
-
# an example. We start capturing before we know if a query is cached
|
86
|
-
# or not, and want to skip any cached queries.
|
87
|
-
next if layer.annotations[:ignorable]
|
74
|
+
next if skip_layer?(layer)
|
88
75
|
|
89
76
|
# The queue_layer is useful to capture for other reasons, but doesn't
|
90
77
|
# create a MetricMeta/Stat of its own
|
91
78
|
next if layer == queue_layer
|
92
79
|
|
93
|
-
|
94
|
-
|
95
|
-
{:scope => subscope_name}
|
96
|
-
elsif layer == job_layer # We don't scope the controller under itself
|
97
|
-
{}
|
98
|
-
else
|
99
|
-
{:scope => job_layer.legacy_metric_name}
|
100
|
-
end
|
101
|
-
|
102
|
-
# Specific Metric
|
103
|
-
meta_options.merge!(:desc => layer.desc.to_s) if layer.desc
|
104
|
-
meta = MetricMeta.new(layer.legacy_metric_name, meta_options)
|
105
|
-
meta.extra.merge!(layer.annotations)
|
106
|
-
|
107
|
-
if layer.backtrace
|
108
|
-
bt = ScoutApm::Utils::BacktraceParser.new(layer.backtrace).call
|
109
|
-
if bt.any? # we could walk thru the call stack and not find in-app code
|
110
|
-
meta.backtrace = bt
|
111
|
-
# Why not just call meta.backtrace and call it done? The walker could access a later later that generates the same MetricMeta but doesn't have a backtrace. This could be
|
112
|
-
# lost in the metric_hash if it is replaced by the new key.
|
113
|
-
@backtraces << meta
|
114
|
-
else
|
115
|
-
ScoutApm::Agent.instance.logger.debug { "Unable to capture an app-specific backtrace for #{meta.inspect}\n#{layer.backtrace}" }
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
metric_hash[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
|
120
|
-
allocation_metric_hash[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
|
121
|
-
stat = metric_hash[meta]
|
122
|
-
stat.update!(layer.total_call_time, layer.total_exclusive_time)
|
123
|
-
stat = allocation_metric_hash[meta]
|
124
|
-
stat.update!(layer.total_allocations, layer.total_exclusive_allocations)
|
125
|
-
|
126
|
-
# Merged Metric (no specifics, just sum up by type)
|
127
|
-
meta = MetricMeta.new("#{layer.type}/all")
|
128
|
-
metric_hash[meta] ||= MetricStats.new(false)
|
129
|
-
allocation_metric_hash[meta] ||= MetricStats.new(false)
|
130
|
-
stat = metric_hash[meta]
|
131
|
-
stat.update!(layer.total_call_time, layer.total_exclusive_time)
|
132
|
-
stat = allocation_metric_hash[meta]
|
133
|
-
stat.update!(layer.total_allocations, layer.total_exclusive_allocations)
|
80
|
+
store_specific_metric(layer, metric_hash, allocation_metric_hash)
|
81
|
+
store_aggregate_metric(layer, metric_hash, allocation_metric_hash)
|
134
82
|
end
|
135
83
|
|
136
84
|
metric_hash = attach_backtraces(metric_hash)
|
137
85
|
allocation_metric_hash = attach_backtraces(allocation_metric_hash)
|
138
86
|
|
139
|
-
[metric_hash,allocation_metric_hash]
|
140
|
-
end
|
141
|
-
|
142
|
-
def attach_backtraces(metric_hash)
|
143
|
-
@backtraces.each do |meta_with_backtrace|
|
144
|
-
metric_hash.keys.find { |k| k == meta_with_backtrace }.backtrace = meta_with_backtrace.backtrace
|
145
|
-
end
|
146
|
-
metric_hash
|
87
|
+
[metric_hash, allocation_metric_hash]
|
147
88
|
end
|
148
89
|
end
|
149
90
|
end
|
@@ -2,7 +2,6 @@ module ScoutApm
|
|
2
2
|
module LayerConverters
|
3
3
|
class SlowRequestConverter < ConverterBase
|
4
4
|
def initialize(*)
|
5
|
-
@backtraces = [] # An Array of MetricMetas that have a backtrace
|
6
5
|
super
|
7
6
|
|
8
7
|
# After call to super, so @request is populated
|
@@ -11,6 +10,8 @@ module ScoutApm
|
|
11
10
|
else
|
12
11
|
-1
|
13
12
|
end
|
13
|
+
|
14
|
+
setup_subscopable_callbacks
|
14
15
|
end
|
15
16
|
|
16
17
|
def name
|
@@ -24,8 +25,8 @@ module ScoutApm
|
|
24
25
|
# Unconditionally attempts to convert this into a SlowTransaction object.
|
25
26
|
# Can return nil if the request didn't have any scope_layer.
|
26
27
|
def call
|
27
|
-
|
28
|
-
return nil unless
|
28
|
+
return nil unless request.web?
|
29
|
+
return nil unless scope_layer
|
29
30
|
|
30
31
|
ScoutApm::Agent.instance.slow_request_policy.stored!(request)
|
31
32
|
|
@@ -35,12 +36,13 @@ module ScoutApm
|
|
35
36
|
uri = request.annotations[:uri] || ""
|
36
37
|
|
37
38
|
timing_metrics, allocation_metrics = create_metrics
|
39
|
+
|
38
40
|
unless ScoutApm::Instruments::Allocations::ENABLED
|
39
41
|
allocation_metrics = {}
|
40
42
|
end
|
41
43
|
|
42
44
|
SlowTransaction.new(uri,
|
43
|
-
|
45
|
+
scope_layer.legacy_metric_name,
|
44
46
|
root_layer.total_call_time,
|
45
47
|
timing_metrics,
|
46
48
|
allocation_metrics,
|
@@ -49,103 +51,29 @@ module ScoutApm
|
|
49
51
|
[], # stackprof, now unused.
|
50
52
|
mem_delta,
|
51
53
|
root_layer.total_allocations,
|
52
|
-
@points
|
53
|
-
|
54
|
-
|
55
|
-
# Iterates over the TrackedRequest's MetricMetas that have backtraces and attaches each to correct MetricMeta in the Metric Hash.
|
56
|
-
def attach_backtraces(metric_hash)
|
57
|
-
@backtraces.each do |meta_with_backtrace|
|
58
|
-
metric_hash.keys.find { |k| k == meta_with_backtrace }.backtrace = meta_with_backtrace.backtrace
|
59
|
-
end
|
60
|
-
metric_hash
|
54
|
+
@points,
|
55
|
+
limited?)
|
61
56
|
end
|
62
57
|
|
63
58
|
# Full metrics from this request. These get stored permanently in a SlowTransaction.
|
64
59
|
# Some merging of metrics will happen here, so if a request calls the same
|
65
60
|
# ActiveRecord or View repeatedly, it'll get merged.
|
66
|
-
#
|
61
|
+
#
|
67
62
|
# This returns a 2-element of Metric Hashes (the first element is timing metrics, the second element is allocation metrics)
|
68
63
|
def create_metrics
|
69
64
|
metric_hash = Hash.new
|
70
65
|
allocation_metric_hash = Hash.new
|
71
66
|
|
72
|
-
# Keep a list of subscopes, but only ever use the front one. The rest
|
73
|
-
# get pushed/popped in cases when we have many levels of subscopable
|
74
|
-
# layers. This lets us push/pop without otherwise keeping track very closely.
|
75
|
-
subscope_layers = []
|
76
|
-
|
77
|
-
walker.before do |layer|
|
78
|
-
if layer.subscopable?
|
79
|
-
subscope_layers.push(layer)
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
walker.after do |layer|
|
84
|
-
if layer.subscopable?
|
85
|
-
subscope_layers.pop
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
67
|
walker.walk do |layer|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
# or not, and want to skip any cached queries.
|
94
|
-
if layer.annotations[:ignorable]
|
95
|
-
next
|
96
|
-
end
|
97
|
-
|
98
|
-
meta_options = if subscope_layers.first && layer != subscope_layers.first # Don't scope under ourself.
|
99
|
-
subscope_name = subscope_layers.first.legacy_metric_name
|
100
|
-
{:scope => subscope_name}
|
101
|
-
elsif layer == scope_layer # We don't scope the controller under itself
|
102
|
-
{}
|
103
|
-
else
|
104
|
-
{:scope => scope_layer.legacy_metric_name}
|
105
|
-
end
|
106
|
-
|
107
|
-
# Specific Metric
|
108
|
-
meta_options.merge!(:desc => layer.desc.to_s) if layer.desc
|
109
|
-
meta = MetricMeta.new(layer.legacy_metric_name, meta_options)
|
110
|
-
meta.extra.merge!(layer.annotations)
|
111
|
-
if layer.backtrace
|
112
|
-
bt = ScoutApm::Utils::BacktraceParser.new(layer.backtrace).call
|
113
|
-
if bt.any? # we could walk thru the call stack and not find in-app code
|
114
|
-
meta.backtrace = bt
|
115
|
-
# Why not just call meta.backtrace and call it done? The walker
|
116
|
-
# could access a later later that generates the same MetricMeta
|
117
|
-
# but doesn't have a backtrace. This could be lost in the
|
118
|
-
# metric_hash if it is replaced by the new key.
|
119
|
-
@backtraces << meta
|
120
|
-
else
|
121
|
-
ScoutApm::Agent.instance.logger.debug { "Unable to capture an app-specific backtrace for #{meta.inspect}\n#{layer.backtrace}" }
|
122
|
-
end
|
123
|
-
end
|
124
|
-
metric_hash[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
|
125
|
-
allocation_metric_hash[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
|
126
|
-
# timing
|
127
|
-
stat = metric_hash[meta]
|
128
|
-
stat.update!(layer.total_call_time, layer.total_exclusive_time)
|
129
|
-
# allocations
|
130
|
-
stat = allocation_metric_hash[meta]
|
131
|
-
stat.update!(layer.total_allocations, layer.total_exclusive_allocations)
|
132
|
-
|
133
|
-
# Merged Metric (no specifics, just sum up by type)
|
134
|
-
meta = MetricMeta.new("#{layer.type}/all")
|
135
|
-
metric_hash[meta] ||= MetricStats.new(false)
|
136
|
-
allocation_metric_hash[meta] ||= MetricStats.new(false)
|
137
|
-
# timing
|
138
|
-
stat = metric_hash[meta]
|
139
|
-
stat.update!(layer.total_call_time, layer.total_exclusive_time)
|
140
|
-
# allocations
|
141
|
-
stat = allocation_metric_hash[meta]
|
142
|
-
stat.update!(layer.total_allocations, layer.total_exclusive_allocations)
|
68
|
+
next if skip_layer?(layer)
|
69
|
+
store_specific_metric(layer, metric_hash, allocation_metric_hash)
|
70
|
+
store_aggregate_metric(layer, metric_hash, allocation_metric_hash)
|
143
71
|
end
|
144
72
|
|
145
73
|
metric_hash = attach_backtraces(metric_hash)
|
146
74
|
allocation_metric_hash = attach_backtraces(allocation_metric_hash)
|
147
75
|
|
148
|
-
[metric_hash,allocation_metric_hash]
|
76
|
+
[metric_hash, allocation_metric_hash]
|
149
77
|
end
|
150
78
|
end
|
151
79
|
end
|
data/lib/scout_apm/metric_set.rb
CHANGED
data/lib/scout_apm/reporter.rb
CHANGED
@@ -17,31 +17,38 @@ module ScoutApm
|
|
17
17
|
@instant_key = instant_key
|
18
18
|
end
|
19
19
|
|
20
|
-
# TODO: Parse & return a real response object, not the HTTP Response object
|
21
20
|
def report(payload, headers = {})
|
22
|
-
|
23
|
-
hosts = [:deploy_hook, :instant_trace].include?(type) ? config.value('direct_host') : config.value('host')
|
21
|
+
hosts = determine_hosts
|
24
22
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
23
|
+
if config.value('compress_payload')
|
24
|
+
original_payload_size = payload.length
|
25
|
+
|
26
|
+
payload, compression_headers = compress_payload(payload)
|
27
|
+
headers.merge!(compression_headers)
|
28
|
+
|
29
|
+
compress_payload_size = payload.length
|
30
|
+
ScoutApm::Agent.instance.logger.debug("Compressed Payload: #{payload.inspect}")
|
31
|
+
ScoutApm::Agent.instance.logger.debug("Original Size: #{original_payload_size} Compressed Size: #{compress_payload_size}")
|
31
32
|
end
|
33
|
+
|
34
|
+
post_payload(hosts, payload, headers)
|
32
35
|
end
|
33
36
|
|
34
37
|
def uri(host)
|
38
|
+
encoded_app_name = CGI.escape(Environment.instance.application_name)
|
39
|
+
encoded_name = CGI.escape(config.value('name'))
|
40
|
+
key = config.value('key')
|
41
|
+
|
35
42
|
case type
|
36
43
|
when :checkin
|
37
|
-
URI.parse("#{host}/apps/checkin.scout?key=#{
|
44
|
+
URI.parse("#{host}/apps/checkin.scout?key=#{key}&name=#{encoded_app_name}")
|
38
45
|
when :app_server_load
|
39
|
-
URI.parse("#{host}/apps/app_server_load.scout?key=#{
|
46
|
+
URI.parse("#{host}/apps/app_server_load.scout?key=#{key}&name=#{encoded_app_name}")
|
40
47
|
when :deploy_hook
|
41
|
-
URI.parse("#{host}/apps/deploy.scout?key=#{
|
48
|
+
URI.parse("#{host}/apps/deploy.scout?key=#{key}&name=#{encoded_name}")
|
42
49
|
when :instant_trace
|
43
|
-
URI.parse("#{host}/apps/instant_trace.scout?key=#{
|
44
|
-
end.tap{|u| logger.debug("Posting to #{u
|
50
|
+
URI.parse("#{host}/apps/instant_trace.scout?key=#{key}&name=#{encoded_name}&instant_key=#{instant_key}")
|
51
|
+
end.tap { |u| logger.debug("Posting to #{u}") }
|
45
52
|
end
|
46
53
|
|
47
54
|
def can_report?
|
@@ -106,7 +113,10 @@ module ScoutApm
|
|
106
113
|
# Net::HTTP::Proxy returns a regular Net::HTTP class if the first argument (host) is nil.
|
107
114
|
def http(url)
|
108
115
|
proxy_uri = URI.parse(config.value('proxy').to_s)
|
109
|
-
http = Net::HTTP::Proxy(proxy_uri.host,
|
116
|
+
http = Net::HTTP::Proxy(proxy_uri.host,
|
117
|
+
proxy_uri.port,
|
118
|
+
proxy_uri.user,
|
119
|
+
proxy_uri.password).new(url.host, url.port)
|
110
120
|
if url.is_a?(URI::HTTPS)
|
111
121
|
http.use_ssl = true
|
112
122
|
http.ca_file = CA_FILE
|
@@ -114,5 +124,33 @@ module ScoutApm
|
|
114
124
|
end
|
115
125
|
http
|
116
126
|
end
|
127
|
+
|
128
|
+
def compress_payload(payload)
|
129
|
+
[
|
130
|
+
ScoutApm::Utils::GzipHelper.new.deflate(payload),
|
131
|
+
{ 'Content-Encoding' => 'gzip' }
|
132
|
+
]
|
133
|
+
end
|
134
|
+
|
135
|
+
# Some posts (typically ones under development) bypass the ingestion
|
136
|
+
# pipeline and go directly to the webserver. They use direct_host instead
|
137
|
+
# of host
|
138
|
+
def determine_hosts
|
139
|
+
if [:deploy_hook, :instant_trace].include?(type)
|
140
|
+
config.value('direct_host')
|
141
|
+
else
|
142
|
+
config.value('host')
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def post_payload(hosts, payload, headers)
|
147
|
+
Array(hosts).each do |host|
|
148
|
+
full_uri = uri(host)
|
149
|
+
response = post(full_uri, payload, headers)
|
150
|
+
unless response && response.is_a?(Net::HTTPSuccess)
|
151
|
+
logger.warn "Error on checkin to #{full_uri}: #{response.inspect}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
117
155
|
end
|
118
156
|
end
|
@@ -63,6 +63,12 @@ module ScoutApm
|
|
63
63
|
end
|
64
64
|
end
|
65
65
|
|
66
|
+
# Equal to another set only if exactly the same set of items is inside
|
67
|
+
def eql?(other)
|
68
|
+
items == other.items
|
69
|
+
end
|
70
|
+
|
71
|
+
alias :== :eql?
|
66
72
|
|
67
73
|
private
|
68
74
|
|
@@ -75,5 +81,6 @@ module ScoutApm
|
|
75
81
|
items[new_item.name] = [new_item.score, new_item.call]
|
76
82
|
end
|
77
83
|
end
|
84
|
+
|
78
85
|
end
|
79
86
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
module ScoutApm
|
3
|
+
module Serializers
|
4
|
+
class HistogramsSerializerToJson
|
5
|
+
attr_reader :histograms
|
6
|
+
|
7
|
+
def initialize(histograms)
|
8
|
+
@histograms = histograms
|
9
|
+
end
|
10
|
+
|
11
|
+
def as_json
|
12
|
+
histograms.map do |histo|
|
13
|
+
{
|
14
|
+
"name" => histo.name,
|
15
|
+
"histogram" => histo.histogram.as_json,
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|