scout_apm 3.0.0.pre11 → 3.0.0.pre12

Sign up to get free protection for your applications and to get access to all the features.
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
  #################################