scout_apm 2.1.8 → 2.1.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.markdown +9 -0
  3. data/lib/scout_apm.rb +3 -0
  4. data/lib/scout_apm/agent.rb +9 -6
  5. data/lib/scout_apm/agent/reporting.rb +5 -3
  6. data/lib/scout_apm/background_job_integrations/delayed_job.rb +47 -1
  7. data/lib/scout_apm/background_worker.rb +4 -0
  8. data/lib/scout_apm/config.rb +2 -1
  9. data/lib/scout_apm/environment.rb +1 -1
  10. data/lib/scout_apm/histogram.rb +11 -2
  11. data/lib/scout_apm/instruments/mongoid.rb +14 -1
  12. data/lib/scout_apm/instruments/percentile_sampler.rb +36 -19
  13. data/lib/scout_apm/instruments/process/process_cpu.rb +3 -2
  14. data/lib/scout_apm/instruments/process/process_memory.rb +3 -3
  15. data/lib/scout_apm/layer_converters/converter_base.rb +193 -0
  16. data/lib/scout_apm/layer_converters/slow_job_converter.rb +14 -73
  17. data/lib/scout_apm/layer_converters/slow_request_converter.rb +13 -85
  18. data/lib/scout_apm/metric_set.rb +6 -0
  19. data/lib/scout_apm/reporter.rb +53 -15
  20. data/lib/scout_apm/request_histograms.rb +4 -0
  21. data/lib/scout_apm/scored_item_set.rb +7 -0
  22. data/lib/scout_apm/serializers/histograms_serializer_to_json.rb +21 -0
  23. data/lib/scout_apm/serializers/payload_serializer.rb +9 -3
  24. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +2 -1
  25. data/lib/scout_apm/serializers/slow_jobs_serializer_to_json.rb +1 -1
  26. data/lib/scout_apm/slow_job_record.rb +4 -1
  27. data/lib/scout_apm/slow_transaction.rb +18 -2
  28. data/lib/scout_apm/store.rb +42 -11
  29. data/lib/scout_apm/tracked_request.rb +1 -1
  30. data/lib/scout_apm/utils/gzip_helper.rb +24 -0
  31. data/lib/scout_apm/utils/numbers.rb +14 -0
  32. data/lib/scout_apm/version.rb +1 -1
  33. data/test/test_helper.rb +10 -0
  34. data/test/unit/config_test.rb +7 -9
  35. data/test/unit/histogram_test.rb +14 -0
  36. data/test/unit/instruments/percentile_sampler_test.rb +137 -0
  37. data/test/unit/serializers/payload_serializer_test.rb +3 -3
  38. data/test/unit/store_test.rb +51 -0
  39. data/test/unit/utils/numbers_test.rb +15 -0
  40. 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
- # Sometimes we start capturing a layer without knowing if we really
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
- meta_options = if subscope_layers.first && layer != subscope_layers.first # Don't scope under ourself.
94
- subscope_name = subscope_layers.first.legacy_metric_name
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
- scope = scope_layer
28
- return nil unless scope
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
- scope.legacy_metric_name,
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
- end
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
- # Sometimes we start capturing a layer without knowing if we really
91
- # want to make an entry for it. See ActiveRecord instrumentation for
92
- # an example. We start capturing before we know if a query is cached
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
@@ -49,5 +49,11 @@ module ScoutApm
49
49
  @combine_in_progress = false
50
50
  self
51
51
  end
52
+
53
+
54
+ def eql?(other)
55
+ metrics == other.metrics
56
+ end
57
+ alias :== :eql?
52
58
  end
53
59
  end
@@ -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
- # Some posts (typically ones under development) bypass the ingestion pipeline and go directly to the webserver. They use direct_host instead of host
23
- hosts = [:deploy_hook, :instant_trace].include?(type) ? config.value('direct_host') : config.value('host')
21
+ hosts = determine_hosts
24
22
 
25
- Array(hosts).each do |host|
26
- full_uri = uri(host)
27
- response = post(full_uri, payload, headers)
28
- unless response && response.is_a?(Net::HTTPSuccess)
29
- logger.warn "Error on checkin to #{full_uri.to_s}: #{response.inspect}"
30
- end
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=#{config.value('key')}&name=#{CGI.escape(Environment.instance.application_name)}")
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=#{config.value('key')}&name=#{CGI.escape(Environment.instance.application_name)}")
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=#{config.value('key')}&name=#{CGI.escape(config.value('name'))}")
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=#{config.value('key')}&name=#{CGI.escape(config.value('name'))}&instant_key=#{instant_key}")
44
- end.tap{|u| logger.debug("Posting to #{u.to_s}")}
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,proxy_uri.port,proxy_uri.user,proxy_uri.password).new(url.host, url.port)
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
@@ -39,6 +39,10 @@ module ScoutApm
39
39
  initialize_histograms_hash
40
40
  end
41
41
 
42
+ def raw(item)
43
+ @histograms[item]
44
+ end
45
+
42
46
  def initialize_histograms_hash
43
47
  @histograms = Hash.new { |h, k| h[k] = NumericHistogram.new(histogram_size) }
44
48
  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