scout_apm 3.0.0.pre1 → 3.0.0.pre2

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.markdown +12 -1
  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 +213 -0
  16. data/lib/scout_apm/layer_converters/slow_job_converter.rb +19 -93
  17. data/lib/scout_apm/layer_converters/slow_request_converter.rb +15 -100
  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 +2 -2
  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,108 +62,30 @@ module ScoutApm
58
62
  @job_layer ||= find_first_layer_of_type("Job")
59
63
  end
60
64
 
65
+
66
+ # The queue_layer is useful to capture for other reasons, but doesn't
67
+ # create a MetricMeta/Stat of its own
68
+ def skip_layer?(layer)
69
+ super(layer) || layer == queue_layer
70
+ end
71
+
61
72
  def create_metrics
62
73
  metric_hash = Hash.new
63
74
  allocation_metric_hash = Hash.new
64
75
 
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 = []
76
+ walker.walk do |layer|
77
+ next if skip_layer?(layer)
69
78
 
70
- walker.before do |layer|
71
- if layer.subscopable?
72
- subscope_layers.push(layer)
73
- end
74
- end
79
+ debug_scoutprof(layer)
75
80
 
76
- walker.after do |layer|
77
- if layer.subscopable?
78
- subscope_layers.pop
79
- end
81
+ store_specific_metric(layer, metric_hash, allocation_metric_hash)
82
+ store_aggregate_metric(layer, metric_hash, allocation_metric_hash)
80
83
  end
81
84
 
82
- 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]
88
-
89
- # The queue_layer is useful to capture for other reasons, but doesn't
90
- # create a MetricMeta/Stat of its own
91
- next if layer == queue_layer
92
-
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)
134
-
135
- stat.add_traces(layer.traces.as_json)
136
-
137
- # Debug logging for scoutprof traces
138
- if ScoutApm::Agent.instance.config.value('profile')
139
- if layer.type =~ %r{^(Controller|Queue|Job)$}.freeze
140
- ScoutApm::Agent.instance.logger.debug do
141
- traces_inspect = layer.traces.inspect
142
- "****** Slow Request #{layer.type} Traces (#{layer.name}, tet: #{layer.total_exclusive_time}, tct: #{layer.total_call_time}), total raw traces: #{layer.traces.cube.total_count}, total clean traces: #{layer.traces.total_count}, skipped gc: #{layer.traces.skipped_in_gc}, skipped handler: #{layer.traces.skipped_in_handler}, skipped registered #{layer.traces.skipped_in_job_registered}, skipped not_running #{layer.traces.skipped_in_not_running}:\n#{traces_inspect}"
143
- end
144
- end
145
- else
146
- if layer.type =~ %r{^(Controller|Queue|Job)$}.freeze
147
- ScoutApm::Agent.instance.logger.debug "****** Slow Request #{layer.type} Traces: Scoutprof is not enabled"
148
- end
149
- end
150
- end # walker.walk
151
-
152
85
  metric_hash = attach_backtraces(metric_hash)
153
86
  allocation_metric_hash = attach_backtraces(allocation_metric_hash)
154
87
 
155
- [metric_hash,allocation_metric_hash]
156
- end
157
-
158
- def attach_backtraces(metric_hash)
159
- @backtraces.each do |meta_with_backtrace|
160
- metric_hash.keys.find { |k| k == meta_with_backtrace }.backtrace = meta_with_backtrace.backtrace
161
- end
162
- metric_hash
88
+ [metric_hash, allocation_metric_hash]
163
89
  end
164
90
  end
165
91
  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,119 +51,32 @@ 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 = []
67
+ walker.walk do |layer|
68
+ next if skip_layer?(layer)
76
69
 
77
- walker.before do |layer|
78
- if layer.subscopable?
79
- subscope_layers.push(layer)
80
- end
81
- end
70
+ debug_scoutprof(layer)
82
71
 
83
- walker.after do |layer|
84
- if layer.subscopable?
85
- subscope_layers.pop
86
- end
87
- end
88
-
89
- 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
- stat.add_traces(layer.traces.as_json)
130
-
131
- # Debug logging for scoutprof traces
132
- if ScoutApm::Agent.instance.config.value('profile')
133
- if layer.type =~ %r{^(Controller|Queue|Job)$}.freeze
134
- ScoutApm::Agent.instance.logger.debug do
135
- traces_inspect = layer.traces.inspect
136
- "****** Slow Request #{layer.type} Traces (#{layer.name}, tet: #{layer.total_exclusive_time}, tct: #{layer.total_call_time}), total raw traces: #{layer.traces.cube.total_count}, total clean traces: #{layer.traces.total_count}, skipped gc: #{layer.traces.skipped_in_gc}, skipped handler: #{layer.traces.skipped_in_handler}, skipped registered #{layer.traces.skipped_in_job_registered}, skipped not_running #{layer.traces.skipped_in_not_running}:\n#{traces_inspect}"
137
- end
138
- end
139
- else
140
- if layer.type =~ %r{^(Controller|Queue|Job)$}.freeze
141
- ScoutApm::Agent.instance.logger.debug "****** Slow Request #{layer.type} Traces: Scoutprof is not enabled"
142
- end
143
- end
144
-
145
- # allocations
146
- stat = allocation_metric_hash[meta]
147
- stat.update!(layer.total_allocations, layer.total_exclusive_allocations)
148
-
149
- # Merged Metric (no specifics, just sum up by type)
150
- meta = MetricMeta.new("#{layer.type}/all")
151
- metric_hash[meta] ||= MetricStats.new(false)
152
- allocation_metric_hash[meta] ||= MetricStats.new(false)
153
- # timing
154
- stat = metric_hash[meta]
155
- stat.update!(layer.total_call_time, layer.total_exclusive_time)
156
- # allocations
157
- stat = allocation_metric_hash[meta]
158
- stat.update!(layer.total_allocations, layer.total_exclusive_allocations)
72
+ store_specific_metric(layer, metric_hash, allocation_metric_hash)
73
+ store_aggregate_metric(layer, metric_hash, allocation_metric_hash)
159
74
  end
160
75
 
161
76
  metric_hash = attach_backtraces(metric_hash)
162
77
  allocation_metric_hash = attach_backtraces(allocation_metric_hash)
163
78
 
164
- [metric_hash,allocation_metric_hash]
79
+ [metric_hash, allocation_metric_hash]
165
80
  end
166
81
  end
167
82
  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