scout_apm 3.0.0.pre1 → 3.0.0.pre2

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