scout_apm 3.0.0.pre11 → 3.0.0.pre12

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.markdown +13 -4
  3. data/Guardfile +1 -0
  4. data/lib/scout_apm.rb +6 -0
  5. data/lib/scout_apm/agent/reporting.rb +2 -1
  6. data/lib/scout_apm/attribute_arranger.rb +14 -1
  7. data/lib/scout_apm/config.rb +12 -0
  8. data/lib/scout_apm/db_query_metric_set.rb +80 -0
  9. data/lib/scout_apm/db_query_metric_stats.rb +102 -0
  10. data/lib/scout_apm/fake_store.rb +6 -0
  11. data/lib/scout_apm/instant/middleware.rb +6 -1
  12. data/lib/scout_apm/instruments/active_record.rb +111 -0
  13. data/lib/scout_apm/layer.rb +4 -0
  14. data/lib/scout_apm/layer_converters/allocation_metric_converter.rb +5 -6
  15. data/lib/scout_apm/layer_converters/converter_base.rb +7 -19
  16. data/lib/scout_apm/layer_converters/database_converter.rb +81 -0
  17. data/lib/scout_apm/layer_converters/depth_first_walker.rb +22 -10
  18. data/lib/scout_apm/layer_converters/error_converter.rb +5 -7
  19. data/lib/scout_apm/layer_converters/find_layer_by_type.rb +34 -0
  20. data/lib/scout_apm/layer_converters/histograms.rb +14 -0
  21. data/lib/scout_apm/layer_converters/job_converter.rb +35 -49
  22. data/lib/scout_apm/layer_converters/metric_converter.rb +16 -18
  23. data/lib/scout_apm/layer_converters/request_queue_time_converter.rb +10 -12
  24. data/lib/scout_apm/layer_converters/slow_job_converter.rb +33 -35
  25. data/lib/scout_apm/layer_converters/slow_request_converter.rb +22 -18
  26. data/lib/scout_apm/limited_layer.rb +4 -0
  27. data/lib/scout_apm/metric_meta.rb +0 -5
  28. data/lib/scout_apm/metric_stats.rb +8 -1
  29. data/lib/scout_apm/serializers/db_query_serializer_to_json.rb +15 -0
  30. data/lib/scout_apm/serializers/payload_serializer.rb +4 -3
  31. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +5 -2
  32. data/lib/scout_apm/slow_job_policy.rb +1 -10
  33. data/lib/scout_apm/slow_request_policy.rb +1 -10
  34. data/lib/scout_apm/store.rb +41 -22
  35. data/lib/scout_apm/tracked_request.rb +28 -40
  36. data/lib/scout_apm/utils/active_record_metric_name.rb +8 -4
  37. data/lib/scout_apm/version.rb +1 -1
  38. data/test/unit/db_query_metric_set_test.rb +56 -0
  39. data/test/unit/db_query_metric_stats_test.rb +113 -0
  40. data/test/unit/fake_store_test.rb +10 -0
  41. data/test/unit/layer_converters/depth_first_walker_test.rb +66 -0
  42. data/test/unit/layer_converters/metric_converter_test.rb +22 -0
  43. data/test/unit/layer_converters/stubs.rb +33 -0
  44. data/test/unit/serializers/payload_serializer_test.rb +3 -12
  45. data/test/unit/store_test.rb +4 -4
  46. data/test/unit/utils/active_record_metric_name_test.rb +8 -0
  47. metadata +20 -2
@@ -4,38 +4,36 @@ module ScoutApm
4
4
 
5
5
  HEADERS = %w(X-Queue-Start X-Request-Start X-QUEUE-START X-REQUEST-START x-queue-start x-request-start)
6
6
 
7
- # Headers is a hash of request headers. In Rails, request.headers would be appropriate
8
- def initialize(request)
9
- super(request)
10
- @headers = request.headers
7
+ def headers
8
+ request.headers
11
9
  end
12
10
 
13
- def call
14
- return {} unless headers
11
+ def record!
12
+ return unless request.web?
13
+
14
+ return unless headers
15
15
 
16
16
  raw_start = locate_timestamp
17
- return {} unless raw_start
17
+ return unless raw_start
18
18
 
19
19
  parsed_start = parse(raw_start)
20
- return {} unless parsed_start
20
+ return unless parsed_start
21
21
 
22
22
  request_start = root_layer.start_time
23
23
  queue_time = (request_start - parsed_start).to_f
24
24
 
25
25
  # If we end up with a negative value, just bail out and don't report anything
26
- return {} if queue_time < 0
26
+ return if queue_time < 0
27
27
 
28
28
  meta = MetricMeta.new("QueueTime/Request", {:scope => scope_layer.legacy_metric_name})
29
29
  stat = MetricStats.new(true)
30
30
  stat.update!(queue_time)
31
31
 
32
- { meta => stat }
32
+ @store.track!({ meta => stat })
33
33
  end
34
34
 
35
35
  private
36
36
 
37
- attr_reader :headers
38
-
39
37
  # Looks through the possible headers with this data, and extracts the raw
40
38
  # value of the header
41
39
  # Returns nil if not found
@@ -1,31 +1,33 @@
1
+ # Uses a different workflow than normal metrics. We ignore the shared walk of
2
+ # the layer tree, and instead wait until we're sure we even want to do any
3
+ # work. Only then do we go realize all the SlowJobRecord & metrics associated.
4
+ #
1
5
  module ScoutApm
2
6
  module LayerConverters
3
7
  class SlowJobConverter < ConverterBase
4
- def initialize(*)
5
- super
6
-
7
- # After call to super, so @request is populated
8
- @points = if request.job?
9
- ScoutApm::Agent.instance.slow_job_policy.score(request)
10
- else
11
- -1
12
- end
13
-
14
- setup_subscopable_callbacks
15
- end
8
+ ###################
9
+ # Converter API #
10
+ ###################
11
+ def record!
12
+ return nil unless request.job?
13
+ @points = ScoutApm::Agent.instance.slow_job_policy.score(request)
16
14
 
17
- def name
18
- request.unique_name
15
+ # Let the store know we're here, and if it wants our data, it will call
16
+ # back into #call
17
+ @store.track_slow_job!(self)
19
18
  end
20
19
 
21
- def score
22
- @points
23
- end
20
+ #####################
21
+ # ScoreItemSet API #
22
+ #####################
23
+ def name; request.unique_name; end
24
+ def score; @points; end
24
25
 
26
+ # Called by the set to force this to actually be created.
25
27
  def call
26
28
  return nil unless request.job?
27
- return nil unless queue_layer
28
- return nil unless job_layer
29
+ return nil unless layer_finder.queue
30
+ return nil unless layer_finder.job
29
31
 
30
32
  ScoutApm::Agent.instance.slow_job_policy.stored!(request)
31
33
 
@@ -54,26 +56,15 @@ module ScoutApm
54
56
  )
55
57
  end
56
58
 
57
- def queue_layer
58
- @queue_layer ||= find_first_layer_of_type("Queue")
59
- end
60
-
61
- def job_layer
62
- @job_layer ||= find_first_layer_of_type("Job")
63
- end
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
-
72
59
  def create_metrics
60
+ # Create a new walker, and wire up the subscope stuff
61
+ walker = LayerConverters::DepthFirstWalker.new(self.root_layer)
62
+ register_hooks(walker)
63
+
73
64
  metric_hash = Hash.new
74
65
  allocation_metric_hash = Hash.new
75
66
 
76
- walker.walk do |layer|
67
+ walker.on do |layer|
77
68
  next if skip_layer?(layer)
78
69
 
79
70
  debug_scoutprof(layer)
@@ -82,11 +73,18 @@ module ScoutApm
82
73
  store_aggregate_metric(layer, metric_hash, allocation_metric_hash)
83
74
  end
84
75
 
76
+ # And now run through the walk we just defined
77
+ walker.walk
78
+
85
79
  metric_hash = attach_backtraces(metric_hash)
86
80
  allocation_metric_hash = attach_backtraces(allocation_metric_hash)
87
81
 
88
82
  [metric_hash, allocation_metric_hash]
89
83
  end
84
+
85
+ def skip_layer?(layer); super(layer) || layer == queue_layer; end
86
+ def queue_layer; layer_finder.queue; end
87
+ def job_layer; layer_finder.job; end
90
88
  end
91
89
  end
92
90
  end
@@ -1,26 +1,23 @@
1
1
  module ScoutApm
2
2
  module LayerConverters
3
3
  class SlowRequestConverter < ConverterBase
4
- def initialize(*)
5
- super
6
-
7
- # After call to super, so @request is populated
8
- @points = if request.web?
9
- ScoutApm::Agent.instance.slow_request_policy.score(request)
10
- else
11
- -1
12
- end
4
+ ###################
5
+ # Converter API #
6
+ ###################
7
+ def record!
8
+ return nil unless request.web?
9
+ @points = ScoutApm::Agent.instance.slow_request_policy.score(request)
13
10
 
14
- setup_subscopable_callbacks
11
+ # Let the store know we're here, and if it wants our data, it will call
12
+ # back into #call
13
+ @store.track_slow_transaction!(self)
15
14
  end
16
15
 
17
- def name
18
- request.unique_name
19
- end
20
-
21
- def score
22
- @points
23
- end
16
+ #####################
17
+ # ScoreItemSet API #
18
+ #####################
19
+ def name; request.unique_name; end
20
+ def score; @points; end
24
21
 
25
22
  # Unconditionally attempts to convert this into a SlowTransaction object.
26
23
  # Can return nil if the request didn't have any scope_layer.
@@ -61,10 +58,14 @@ module ScoutApm
61
58
  #
62
59
  # This returns a 2-element of Metric Hashes (the first element is timing metrics, the second element is allocation metrics)
63
60
  def create_metrics
61
+ # Create a new walker, and wire up the subscope stuff
62
+ walker = LayerConverters::DepthFirstWalker.new(self.root_layer)
63
+ register_hooks(walker)
64
+
64
65
  metric_hash = Hash.new
65
66
  allocation_metric_hash = Hash.new
66
67
 
67
- walker.walk do |layer|
68
+ walker.on do |layer|
68
69
  next if skip_layer?(layer)
69
70
 
70
71
  debug_scoutprof(layer)
@@ -73,6 +74,9 @@ module ScoutApm
73
74
  store_aggregate_metric(layer, metric_hash, allocation_metric_hash)
74
75
  end
75
76
 
77
+ # And now run through the walk we just defined
78
+ walker.walk
79
+
76
80
  metric_hash = attach_backtraces(metric_hash)
77
81
  allocation_metric_hash = attach_backtraces(allocation_metric_hash)
78
82
 
@@ -66,6 +66,10 @@ module ScoutApm
66
66
  "<LimitedLayer type=#{type} count=#{count}>"
67
67
  end
68
68
 
69
+ def limited?
70
+ true
71
+ end
72
+
69
73
  ######################################################
70
74
  # Stub out some methods with static default values #
71
75
  ######################################################
@@ -33,11 +33,6 @@ class MetricMeta
33
33
  !!(metric_name =~ /\A(Controller|Job)\//)
34
34
  end
35
35
 
36
- # To avoid conflicts with different JSON libaries
37
- def to_json(*a)
38
- %Q[{"metric_id":#{metric_id || 'null'},"metric_name":#{metric_name.to_json},"scope":#{scope.to_json || 'null'}}]
39
- end
40
-
41
36
  def ==(o)
42
37
  self.eql?(o)
43
38
  end
@@ -58,7 +58,14 @@ class MetricStats
58
58
  end
59
59
 
60
60
  def as_json
61
- json_attributes = [:call_count, :total_call_time, :total_exclusive_time, :min_call_time, :max_call_time, :traces]
61
+ json_attributes = [
62
+ :call_count,
63
+ :max_call_time,
64
+ :min_call_time,
65
+ :total_call_time,
66
+ :total_exclusive_time,
67
+ :traces,
68
+ ]
62
69
  ScoutApm::AttributeArranger.call(self, json_attributes)
63
70
  end
64
71
  end
@@ -0,0 +1,15 @@
1
+ module ScoutApm
2
+ module Serializers
3
+ class DbQuerySerializerToJson
4
+ attr_reader :db_query_metrics
5
+
6
+ def initialize(db_query_metrics)
7
+ @db_query_metrics = db_query_metrics
8
+ end
9
+
10
+ def as_json
11
+ db_query_metrics.map{|metric| metric.as_json }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -2,9 +2,9 @@
2
2
  module ScoutApm
3
3
  module Serializers
4
4
  class PayloadSerializer
5
- def self.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms)
5
+ def self.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics)
6
6
  if ScoutApm::Agent.instance.config.value("report_format") == 'json'
7
- ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms)
7
+ ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics)
8
8
  else
9
9
  metadata = metadata.dup
10
10
  metadata.default = nil
@@ -21,7 +21,8 @@ module ScoutApm
21
21
  # array, use this to maintain compatibility with json
22
22
  # payloads. At this point, the marshal code branch is
23
23
  # very rarely used anyway.
24
- :histograms => HistogramsSerializerToJson.new(histograms).as_json)
24
+ :histograms => HistogramsSerializerToJson.new(histograms).as_json,
25
+ :db_query_metrics => db_query_metrics)
25
26
  end
26
27
  end
27
28
 
@@ -2,7 +2,7 @@ module ScoutApm
2
2
  module Serializers
3
3
  module PayloadSerializerToJson
4
4
  class << self
5
- def serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms)
5
+ def serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms, db_query_metrics)
6
6
  metadata.merge!({:payload_version => 2})
7
7
 
8
8
  jsonify_hash({:metadata => metadata,
@@ -11,7 +11,10 @@ module ScoutApm
11
11
  :jobs => JobsSerializerToJson.new(jobs).as_json,
12
12
  :slow_jobs => SlowJobsSerializerToJson.new(slow_jobs).as_json,
13
13
  :histograms => HistogramsSerializerToJson.new(histograms).as_json,
14
- })
14
+ :db_metrics => {
15
+ :query => DbQuerySerializerToJson.new(db_query_metrics).as_json,
16
+ },
17
+ })
15
18
  end
16
19
 
17
20
  # For the old style of metric serializing.
@@ -31,7 +31,7 @@ module ScoutApm
31
31
  end
32
32
 
33
33
  def stored!(request)
34
- last_seen[unique_name_for(request)] = Time.now
34
+ last_seen[request.unique_name] = Time.now
35
35
  end
36
36
 
37
37
  # Determine if this job trace should be fully analyzed by scoring it
@@ -61,15 +61,6 @@ module ScoutApm
61
61
 
62
62
  private
63
63
 
64
- def unique_name_for(request)
65
- scope_layer = LayerConverters::ConverterBase.new(request).scope_layer
66
- if scope_layer
67
- scope_layer.legacy_metric_name
68
- else
69
- :unknown
70
- end
71
- end
72
-
73
64
  # Time in seconds
74
65
  # Logarithm keeps huge times from swamping the other metrics.
75
66
  # 1+ is necessary to keep the log function in positive territory.
@@ -31,7 +31,7 @@ module ScoutApm
31
31
  end
32
32
 
33
33
  def stored!(request)
34
- last_seen[unique_name_for(request)] = Time.now
34
+ last_seen[request.unique_name] = Time.now
35
35
  end
36
36
 
37
37
  # Determine if this request trace should be fully analyzed by scoring it
@@ -61,15 +61,6 @@ module ScoutApm
61
61
 
62
62
  private
63
63
 
64
- def unique_name_for(request)
65
- scope_layer = LayerConverters::ConverterBase.new(request).scope_layer
66
- if scope_layer
67
- scope_layer.legacy_metric_name
68
- else
69
- :unknown
70
- end
71
- end
72
-
73
64
  # Time in seconds
74
65
  # Logarithm keeps huge times from swamping the other metrics.
75
66
  # 1+ is necessary to keep the log function in positive territory.
@@ -3,12 +3,6 @@
3
3
  # the layaway file for cross-process aggregation.
4
4
  module ScoutApm
5
5
  class Store
6
- # A hash of reporting periods. { StoreReportingPeriodTimestamp => StoreReportingPeriod }
7
- attr_reader :reporting_periods
8
-
9
- # Used to pull metrics into each reporting period, as that reporting period is finished.
10
- attr_reader :samplers
11
-
12
6
  def initialize
13
7
  @mutex = Mutex.new
14
8
  @reporting_periods = Hash.new { |h,k| h[k] = StoreReportingPeriod.new(k) }
@@ -20,32 +14,41 @@ module ScoutApm
20
14
  end
21
15
 
22
16
  def current_period
23
- reporting_periods[current_timestamp]
17
+ @reporting_periods[current_timestamp]
24
18
  end
19
+ private :current_period
20
+
21
+ def find_period(timestamp = nil)
22
+ if timestamp
23
+ @reporting_periods[timestamp]
24
+ else
25
+ current_period
26
+ end
27
+ end
28
+ private :find_period
25
29
 
26
30
  # Save newly collected metrics
27
31
  def track!(metrics, options={})
28
32
  @mutex.synchronize {
29
- period = if options[:timestamp]
30
- @reporting_periods[options[:timestamp]]
31
- else
32
- current_period
33
- end
33
+ period = find_period(options[:timestamp])
34
34
  period.absorb_metrics!(metrics)
35
35
  }
36
36
  end
37
37
 
38
38
  def track_histograms!(histograms, options={})
39
39
  @mutex.synchronize {
40
- period = if options[:timestamp]
41
- @reporting_periods[options[:timestamp]]
42
- else
43
- current_period
44
- end
40
+ period = find_period(options[:timestamp])
45
41
  period.merge_histograms!(histograms)
46
42
  }
47
43
  end
48
44
 
45
+ def track_db_query_metrics!(db_query_metric_set, options={})
46
+ @mutex.synchronize {
47
+ period = find_period(options[:timestamp])
48
+ period.merge_db_query_metrics!(db_query_metric_set)
49
+ }
50
+ end
51
+
49
52
  def track_one!(type, name, value, options={})
50
53
  meta = MetricMeta.new("#{type}/#{name}")
51
54
  stat = MetricStats.new(false)
@@ -83,7 +86,7 @@ module ScoutApm
83
86
  def write_to_layaway(layaway, force=false)
84
87
  ScoutApm::Agent.instance.logger.debug("Writing to layaway#{" (Forced)" if force}")
85
88
 
86
- reporting_periods.select { |time, rp| force || (time.timestamp < current_timestamp.timestamp) }.
89
+ @reporting_periods.select { |time, rp| force || (time.timestamp < current_timestamp.timestamp) }.
87
90
  each { |time, rp| collect_samplers(rp) }.
88
91
  each { |time, rp| write_reporting_period(layaway, time, rp) }
89
92
  end
@@ -95,10 +98,11 @@ module ScoutApm
95
98
  rescue => e
96
99
  ScoutApm::Agent.instance.logger.warn("Failed writing data to layaway file: #{e.message} / #{e.backtrace}")
97
100
  ensure
98
- ScoutApm::Agent.instance.logger.debug("Before delete, reporting periods length: #{reporting_periods.size}")
99
- deleted_items = reporting_periods.delete(time)
100
- ScoutApm::Agent.instance.logger.debug("After delete, reporting periods length: #{reporting_periods.size}. Did delete #{deleted_items}")
101
+ ScoutApm::Agent.instance.logger.debug("Before delete, reporting periods length: #{@reporting_periods.size}")
102
+ deleted_items = @reporting_periods.delete(time)
103
+ ScoutApm::Agent.instance.logger.debug("After delete, reporting periods length: #{@reporting_periods.size}. Did delete #{deleted_items}")
101
104
  end
105
+ private :write_reporting_period
102
106
 
103
107
  ######################################
104
108
  # Sampler support
@@ -116,6 +120,7 @@ module ScoutApm
116
120
  end
117
121
  end
118
122
  end
123
+ private :collect_samplers
119
124
  end
120
125
 
121
126
  # A timestamp, normalized to the beginning of a minute. Used as a hash key to
@@ -184,6 +189,8 @@ module ScoutApm
184
189
 
185
190
  attr_reader :metric_set
186
191
 
192
+ attr_reader :db_query_metric_set
193
+
187
194
  def initialize(timestamp)
188
195
  @timestamp = timestamp
189
196
 
@@ -193,6 +200,8 @@ module ScoutApm
193
200
  @histograms = []
194
201
 
195
202
  @metric_set = MetricSet.new
203
+ @db_query_metric_set = DbQueryMetricSet.new
204
+
196
205
  @jobs = Hash.new
197
206
  end
198
207
 
@@ -203,7 +212,8 @@ module ScoutApm
203
212
  merge_slow_transactions!(other.slow_transactions_payload).
204
213
  merge_jobs!(other.jobs).
205
214
  merge_slow_jobs!(other.slow_jobs_payload).
206
- merge_histograms!(other.histograms)
215
+ merge_histograms!(other.histograms).
216
+ merge_db_query_metrics!(other.db_query_metric_set)
207
217
  self
208
218
  end
209
219
 
@@ -224,6 +234,11 @@ module ScoutApm
224
234
  self
225
235
  end
226
236
 
237
+ def merge_db_query_metrics!(other_metric_set)
238
+ db_query_metric_set.combine!(other_metric_set)
239
+ self
240
+ end
241
+
227
242
  def merge_slow_transactions!(new_transactions)
228
243
  Array(new_transactions).each do |one_transaction|
229
244
  request_traces << one_transaction
@@ -282,6 +297,10 @@ module ScoutApm
282
297
  job_traces.to_a
283
298
  end
284
299
 
300
+ def db_query_metrics_payload
301
+ db_query_metric_set.metrics_to_report
302
+ end
303
+
285
304
  #################################
286
305
  # Debug Helpers
287
306
  #################################