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
@@ -80,6 +80,10 @@ module ScoutApm
80
80
  @trace_index = ScoutApm::Instruments::Stacks.current_trace_index
81
81
  end
82
82
 
83
+ def limited?
84
+ false
85
+ end
86
+
83
87
  def add_child(child)
84
88
  @children ||= LayerChildrenSet.new
85
89
  @children << child
@@ -1,16 +1,15 @@
1
1
  module ScoutApm
2
2
  module LayerConverters
3
3
  class AllocationMetricConverter < ConverterBase
4
- def call
5
- scope = scope_layer
6
- return {} unless scope
7
- return {} unless ScoutApm::Instruments::Allocations::ENABLED
4
+ def record!
5
+ return unless scope_layer
6
+ return unless ScoutApm::Instruments::Allocations::ENABLED
8
7
 
9
- meta = MetricMeta.new("ObjectAllocations", {:scope => scope.legacy_metric_name})
8
+ meta = MetricMeta.new("ObjectAllocations", {:scope => scope_layer.legacy_metric_name})
10
9
  stat = MetricStats.new
11
10
  stat.update!(root_layer.total_allocations)
12
11
 
13
- { meta => stat }
12
+ @store.track!({ meta => stat })
14
13
  end
15
14
  end
16
15
  end
@@ -2,34 +2,22 @@ module ScoutApm
2
2
  module LayerConverters
3
3
  class ConverterBase
4
4
 
5
- attr_reader :walker
6
5
  attr_reader :request
7
6
  attr_reader :root_layer
7
+ attr_reader :layer_finder
8
8
 
9
- def initialize(request)
9
+ def initialize(request, layer_finder, store=nil)
10
10
  @request = request
11
+ @layer_finder = layer_finder
12
+ @store = store
13
+
11
14
  @root_layer = request.root_layer
12
15
  @backtraces = []
13
- @walker = DepthFirstWalker.new(root_layer)
14
-
15
16
  @limited = false
16
17
  end
17
18
 
18
- # Scope is determined by the first Controller we hit. Most of the time
19
- # there will only be 1 anyway. But if you have a controller that calls
20
- # another controller method, we may pick that up:
21
- # def update
22
- # show
23
- # render :update
24
- # end
25
19
  def scope_layer
26
- @scope_layer ||= find_first_layer_of_type("Controller") || find_first_layer_of_type("Job")
27
- end
28
-
29
- def find_first_layer_of_type(layer_type)
30
- walker.walk do |layer|
31
- return layer if layer.type == layer_type
32
- end
20
+ layer_finder.scope
33
21
  end
34
22
 
35
23
  ################################################################################
@@ -39,7 +27,7 @@ module ScoutApm
39
27
  # Keep a list of subscopes, but only ever use the front one. The rest
40
28
  # get pushed/popped in cases when we have many levels of subscopable
41
29
  # layers. This lets us push/pop without otherwise keeping track very closely.
42
- def setup_subscopable_callbacks
30
+ def register_hooks(walker)
43
31
  @subscope_layers = []
44
32
 
45
33
  walker.before do |layer|
@@ -0,0 +1,81 @@
1
+ module ScoutApm
2
+ module LayerConverters
3
+ class DatabaseConverter < ConverterBase
4
+ def initialize(*)
5
+ super
6
+ @db_query_metric_set = DbQueryMetricSet.new
7
+ end
8
+
9
+ def register_hooks(walker)
10
+ super
11
+
12
+ return unless scope_layer
13
+
14
+ walker.on do |layer|
15
+ next if skip_layer?(layer)
16
+
17
+ stat = DbQueryMetricStats.new(
18
+ model_name(layer),
19
+ operation_name(layer),
20
+ scope_layer.legacy_metric_name, # controller_scope
21
+ 1, # count, this is a single query, so 1
22
+ layer.total_call_time,
23
+ records_returned(layer)
24
+ )
25
+ @db_query_metric_set << stat
26
+ end
27
+ end
28
+
29
+ def record!
30
+ # Everything in the metric set here is from a single transaction, which
31
+ # we want to keep track of. (One web call did a User#find 10 times, but
32
+ # only due to 1 http request)
33
+ @db_query_metric_set.increment_transaction_count!
34
+ @store.track_db_query_metrics!(@db_query_metric_set)
35
+ end
36
+
37
+ def skip_layer?(layer)
38
+ layer.type != 'ActiveRecord' ||
39
+ layer.limited? ||
40
+ super
41
+ end
42
+
43
+ private
44
+
45
+
46
+ # If we can't name the model, default to:
47
+ DEFAULT_MODEL = "SQL"
48
+
49
+ # If we can't name the operation, default to:
50
+ DEFAULT_OPERATION = "other"
51
+
52
+ def model_name(layer)
53
+ if layer.name.respond_to?(:model)
54
+ layer.name.model || DEFAULT_MODEL
55
+ else
56
+ DEFAULT_MODEL
57
+ end
58
+ rescue
59
+ DEFAULT_MODEL
60
+ end
61
+
62
+ def operation_name(layer)
63
+ if layer.name.respond_to?(:normalized_operation)
64
+ layer.name.normalized_operation || DEFAULT_OPERATION
65
+ else
66
+ DEFAULT_OPERATION
67
+ end
68
+ rescue
69
+ DEFAULT_OPERATION
70
+ end
71
+
72
+ def records_returned(layer)
73
+ if layer.annotations
74
+ layer.annotations.fetch(:record_count, 0)
75
+ else
76
+ 0
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -5,30 +5,42 @@ module ScoutApm
5
5
 
6
6
  def initialize(root_layer)
7
7
  @root_layer = root_layer
8
+
9
+ @on_blocks = []
10
+ @before_blocks = []
11
+ @after_blocks = []
8
12
  end
9
13
 
10
14
  def before(&block)
11
- @before_block = block
15
+ @before_blocks << block
12
16
  end
13
17
 
14
18
  def after(&block)
15
- @after_block = block
19
+ @after_blocks << block
20
+ end
21
+
22
+ def on(&block)
23
+ @on_blocks << block
16
24
  end
17
25
 
18
- def walk(layer=root_layer, &block)
26
+ def walk(layer=root_layer)
19
27
  # Need to run this for the root layer the first time through.
20
28
  if layer == root_layer
21
- @before_block.call(layer) if @before_block
22
- yield layer
23
- @after_block.call(layer) if @after_block
29
+ @before_blocks.each{|b| b.call(layer) }
30
+ @on_blocks.each{|b| b.call(layer) }
24
31
  end
25
32
 
26
33
  layer.children.each do |child|
27
- @before_block.call(child) if @before_block
28
- yield child
29
- walk(child, &block)
30
- @after_block.call(child) if @after_block
34
+ @before_blocks.each{|b| b.call(child) }
35
+ @on_blocks.each{|b| b.call(child) }
36
+ walk(child)
37
+ @after_blocks.each{|b| b.call(child) }
31
38
  end
39
+
40
+ if layer == root_layer
41
+ @after_blocks.each{|b| b.call(layer) }
42
+ end
43
+
32
44
  nil
33
45
  end
34
46
  end
@@ -1,19 +1,17 @@
1
1
  module ScoutApm
2
2
  module LayerConverters
3
3
  class ErrorConverter < ConverterBase
4
- def call
5
- scope = scope_layer
6
-
4
+ def record!
7
5
  # Should we mark a request as errored out if a middleware raises?
8
6
  # How does that interact w/ a tool like Sentry or Honeybadger?
9
- return {} unless scope
10
- return {} unless request.error?
7
+ return unless scope_layer
8
+ return unless request.error?
11
9
 
12
- meta = MetricMeta.new("Errors/#{scope.legacy_metric_name}", {})
10
+ meta = MetricMeta.new("Errors/#{scope_layer.legacy_metric_name}", {})
13
11
  stat = MetricStats.new
14
12
  stat.update!(1)
15
13
 
16
- { meta => stat }
14
+ @store.track!({ meta => stat })
17
15
  end
18
16
  end
19
17
  end
@@ -0,0 +1,34 @@
1
+ # Scope is determined by the first Controller we hit. Most of the time
2
+ # there will only be 1 anyway. But if you have a controller that calls
3
+ # another controller method, we may pick that up:
4
+ # def update
5
+ # show
6
+ # render :update
7
+ # end
8
+ module ScoutApm
9
+ module LayerConverters
10
+ class FindLayerByType
11
+ def initialize(request)
12
+ @request = request
13
+ end
14
+
15
+ def scope
16
+ @scope ||= call(["Controller", "Job"])
17
+ end
18
+
19
+ def job
20
+ @job ||= call(["Job"])
21
+ end
22
+
23
+ def queue
24
+ @queue ||= call(["Queue"])
25
+ end
26
+
27
+ def call(layer_types)
28
+ walker = DepthFirstWalker.new(@request.root_layer)
29
+ walker.on {|l| return l if layer_types.include?(l.type) }
30
+ walker.walk
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,14 @@
1
+ module ScoutApm
2
+ module LayerConverters
3
+ class Histograms < ConverterBase
4
+ # Updates immediate and long-term histograms for both job and web requests
5
+ def record!
6
+ if request.unique_name != :unknown
7
+ ScoutApm::Agent.instance.request_histograms.add(request.unique_name, root_layer.total_call_time)
8
+ ScoutApm::Agent.instance.request_histograms_by_time[@store.current_timestamp].
9
+ add(request.unique_name, root_layer.total_call_time)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -10,53 +10,19 @@
10
10
  module ScoutApm
11
11
  module LayerConverters
12
12
  class JobConverter < ConverterBase
13
- def call
14
- return unless request.job?
15
-
16
- JobRecord.new(
17
- queue_layer.name,
18
- job_layer.name,
19
- job_layer.total_call_time,
20
- job_layer.total_exclusive_time,
21
- errors,
22
- create_metrics
23
- )
24
- end
25
-
26
- def queue_layer
27
- @queue_layer ||= find_first_layer_of_type("Queue")
28
- end
13
+ attr_reader :meta_options
29
14
 
30
- def job_layer
31
- @job_layer ||= find_first_layer_of_type("Job")
32
- end
33
-
34
- def errors
35
- if request.error?
36
- 1
37
- else
38
- 0
39
- end
40
- end
41
-
42
- def find_first_layer_of_type(layer_type)
43
- walker.walk do |layer|
44
- return layer if layer.type == layer_type
45
- end
46
- end
15
+ def register_hooks(walker)
16
+ return unless request.job?
47
17
 
48
- # Full metrics from this request. These get aggregated in Store for the
49
- # overview metrics, or stored permanently in a SlowTransaction
50
- # Some merging of metrics will happen here, so if a request calls the same
51
- # ActiveRecord or View repeatedly, it'll get merged.
52
- def create_metrics
53
- metric_hash = Hash.new
18
+ super
54
19
 
55
- meta_options = {:scope => job_layer.legacy_metric_name}
20
+ @metrics = Hash.new
21
+ @meta_options = {:scope => layer_finder.job.legacy_metric_name}
56
22
 
57
- walker.walk do |layer|
58
- next if layer == job_layer
59
- next if layer == queue_layer
23
+ walker.on do |layer|
24
+ next if layer == layer_finder.job
25
+ next if layer == layer_finder.queue
60
26
  next if skip_layer?(layer)
61
27
 
62
28
  # we don't need to use the full metric name for scoped metrics as we
@@ -65,20 +31,40 @@ module ScoutApm
65
31
  metric_name = layer.type
66
32
 
67
33
  meta = MetricMeta.new(metric_name, meta_options)
68
- metric_hash[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
34
+ @metrics[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
69
35
 
70
- stat = metric_hash[meta]
36
+ stat = @metrics[meta]
71
37
  stat.update!(layer.total_call_time, layer.total_exclusive_time)
72
38
  end
73
39
 
74
- # Add the latency metric, which wasn't stored as a distinct layer
40
+ end
41
+
42
+ def record!
43
+ return unless request.job?
44
+
45
+ errors = request.error? ? 1 : 0
46
+ add_latency_metric!
47
+
48
+ record = JobRecord.new(
49
+ layer_finder.queue.name,
50
+ layer_finder.job.name,
51
+ layer_finder.job.total_call_time,
52
+ layer_finder.job.total_exclusive_time,
53
+ errors,
54
+ @metrics
55
+ )
56
+
57
+ @store.track_job!(record)
58
+ end
59
+
60
+ # This isn't stored as a specific layer, so grabbing it doesn't use the
61
+ # walker callbacks
62
+ def add_latency_metric!
75
63
  latency = request.annotations[:queue_latency] || 0
76
64
  meta = MetricMeta.new("Latency", meta_options)
77
65
  stat = MetricStats.new
78
66
  stat.update!(latency)
79
- metric_hash[meta] = stat
80
-
81
- metric_hash
67
+ @metrics[meta] = stat
82
68
  end
83
69
  end
84
70
  end
@@ -1,26 +1,21 @@
1
1
  # Take a TrackedRequest and turn it into a hash of:
2
2
  # MetricMeta => MetricStats
3
+
4
+ # Full metrics from this request. These get aggregated in Store for the
5
+ # overview metrics, or stored permanently in a SlowTransaction
6
+ # Some merging of metrics will happen here, so if a request calls the same
7
+ # ActiveRecord or View repeatedly, it'll get merged.
3
8
  module ScoutApm
4
9
  module LayerConverters
5
10
  class MetricConverter < ConverterBase
6
- def call
7
- scope = scope_layer
8
-
9
- # TODO: Track requests that never reach a Controller (for example, when
10
- # Middleware decides to return rather than passing onward)
11
- return {} unless scope
11
+ def register_hooks(walker)
12
+ super
12
13
 
13
- create_metrics
14
- end
14
+ @metrics = {}
15
15
 
16
- # Full metrics from this request. These get aggregated in Store for the
17
- # overview metrics, or stored permanently in a SlowTransaction
18
- # Some merging of metrics will happen here, so if a request calls the same
19
- # ActiveRecord or View repeatedly, it'll get merged.
20
- def create_metrics
21
- metric_hash = Hash.new
16
+ return unless scope_layer
22
17
 
23
- walker.walk do |layer|
18
+ walker.on do |layer|
24
19
  next if skip_layer?(layer)
25
20
 
26
21
  meta_options = if layer == scope_layer # We don't scope the controller under itself
@@ -34,12 +29,15 @@ module ScoutApm
34
29
  metric_name = meta_options.has_key?(:scope) ? layer.type : layer.legacy_metric_name
35
30
 
36
31
  meta = MetricMeta.new(metric_name, meta_options)
37
- metric_hash[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
32
+ @metrics[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
38
33
 
39
- stat = metric_hash[meta]
34
+ stat = @metrics[meta]
40
35
  stat.update!(layer.total_call_time, layer.total_exclusive_time)
41
36
  end
42
- metric_hash
37
+ end
38
+
39
+ def record!
40
+ @store.track!(@metrics)
43
41
  end
44
42
  end
45
43
  end