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.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +13 -4
- data/Guardfile +1 -0
- data/lib/scout_apm.rb +6 -0
- data/lib/scout_apm/agent/reporting.rb +2 -1
- data/lib/scout_apm/attribute_arranger.rb +14 -1
- data/lib/scout_apm/config.rb +12 -0
- data/lib/scout_apm/db_query_metric_set.rb +80 -0
- data/lib/scout_apm/db_query_metric_stats.rb +102 -0
- data/lib/scout_apm/fake_store.rb +6 -0
- data/lib/scout_apm/instant/middleware.rb +6 -1
- data/lib/scout_apm/instruments/active_record.rb +111 -0
- data/lib/scout_apm/layer.rb +4 -0
- data/lib/scout_apm/layer_converters/allocation_metric_converter.rb +5 -6
- data/lib/scout_apm/layer_converters/converter_base.rb +7 -19
- data/lib/scout_apm/layer_converters/database_converter.rb +81 -0
- data/lib/scout_apm/layer_converters/depth_first_walker.rb +22 -10
- data/lib/scout_apm/layer_converters/error_converter.rb +5 -7
- data/lib/scout_apm/layer_converters/find_layer_by_type.rb +34 -0
- data/lib/scout_apm/layer_converters/histograms.rb +14 -0
- data/lib/scout_apm/layer_converters/job_converter.rb +35 -49
- data/lib/scout_apm/layer_converters/metric_converter.rb +16 -18
- data/lib/scout_apm/layer_converters/request_queue_time_converter.rb +10 -12
- data/lib/scout_apm/layer_converters/slow_job_converter.rb +33 -35
- data/lib/scout_apm/layer_converters/slow_request_converter.rb +22 -18
- data/lib/scout_apm/limited_layer.rb +4 -0
- data/lib/scout_apm/metric_meta.rb +0 -5
- data/lib/scout_apm/metric_stats.rb +8 -1
- data/lib/scout_apm/serializers/db_query_serializer_to_json.rb +15 -0
- data/lib/scout_apm/serializers/payload_serializer.rb +4 -3
- data/lib/scout_apm/serializers/payload_serializer_to_json.rb +5 -2
- data/lib/scout_apm/slow_job_policy.rb +1 -10
- data/lib/scout_apm/slow_request_policy.rb +1 -10
- data/lib/scout_apm/store.rb +41 -22
- data/lib/scout_apm/tracked_request.rb +28 -40
- data/lib/scout_apm/utils/active_record_metric_name.rb +8 -4
- data/lib/scout_apm/version.rb +1 -1
- data/test/unit/db_query_metric_set_test.rb +56 -0
- data/test/unit/db_query_metric_stats_test.rb +113 -0
- data/test/unit/fake_store_test.rb +10 -0
- data/test/unit/layer_converters/depth_first_walker_test.rb +66 -0
- data/test/unit/layer_converters/metric_converter_test.rb +22 -0
- data/test/unit/layer_converters/stubs.rb +33 -0
- data/test/unit/serializers/payload_serializer_test.rb +3 -12
- data/test/unit/store_test.rb +4 -4
- data/test/unit/utils/active_record_metric_name_test.rb +8 -0
- metadata +20 -2
data/lib/scout_apm/layer.rb
CHANGED
@@ -1,16 +1,15 @@
|
|
1
1
|
module ScoutApm
|
2
2
|
module LayerConverters
|
3
3
|
class AllocationMetricConverter < ConverterBase
|
4
|
-
def
|
5
|
-
|
6
|
-
return
|
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 =>
|
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
|
-
|
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
|
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
|
-
@
|
15
|
+
@before_blocks << block
|
12
16
|
end
|
13
17
|
|
14
18
|
def after(&block)
|
15
|
-
@
|
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
|
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
|
-
@
|
22
|
-
|
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
|
-
@
|
28
|
-
|
29
|
-
walk(child
|
30
|
-
@
|
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
|
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
|
10
|
-
return
|
7
|
+
return unless scope_layer
|
8
|
+
return unless request.error?
|
11
9
|
|
12
|
-
meta = MetricMeta.new("Errors/#{
|
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
|
-
|
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
|
31
|
-
|
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
|
-
|
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
|
-
|
20
|
+
@metrics = Hash.new
|
21
|
+
@meta_options = {:scope => layer_finder.job.legacy_metric_name}
|
56
22
|
|
57
|
-
walker.
|
58
|
-
next if layer ==
|
59
|
-
next if 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
|
-
|
34
|
+
@metrics[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
|
69
35
|
|
70
|
-
stat =
|
36
|
+
stat = @metrics[meta]
|
71
37
|
stat.update!(layer.total_call_time, layer.total_exclusive_time)
|
72
38
|
end
|
73
39
|
|
74
|
-
|
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
|
-
|
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
|
7
|
-
|
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
|
-
|
14
|
-
end
|
14
|
+
@metrics = {}
|
15
15
|
|
16
|
-
|
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.
|
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
|
-
|
32
|
+
@metrics[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
|
38
33
|
|
39
|
-
stat =
|
34
|
+
stat = @metrics[meta]
|
40
35
|
stat.update!(layer.total_call_time, layer.total_exclusive_time)
|
41
36
|
end
|
42
|
-
|
37
|
+
end
|
38
|
+
|
39
|
+
def record!
|
40
|
+
@store.track!(@metrics)
|
43
41
|
end
|
44
42
|
end
|
45
43
|
end
|