scout_apm 1.4.6 → 1.5.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +9 -0
- data/lib/scout_apm/agent/reporting.rb +8 -6
- data/lib/scout_apm/agent.rb +10 -6
- data/lib/scout_apm/background_job_integrations/sidekiq.rb +23 -11
- data/lib/scout_apm/call_set.rb +61 -0
- data/lib/scout_apm/config.rb +2 -1
- data/lib/scout_apm/environment.rb +12 -7
- data/lib/scout_apm/histogram.rb +124 -0
- data/lib/scout_apm/instruments/.DS_Store +0 -0
- data/lib/scout_apm/instruments/action_controller_rails_2.rb +1 -0
- data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +1 -0
- data/lib/scout_apm/instruments/delayed_job.rb +1 -0
- data/lib/scout_apm/instruments/process/process_memory.rb +1 -1
- data/lib/scout_apm/instruments/sinatra.rb +1 -1
- data/lib/scout_apm/job_record.rb +76 -0
- data/lib/scout_apm/layaway.rb +4 -1
- data/lib/scout_apm/layaway_file.rb +4 -4
- data/lib/scout_apm/layer.rb +14 -4
- data/lib/scout_apm/layer_converters/converter_base.rb +30 -0
- data/lib/scout_apm/layer_converters/depth_first_walker.rb +36 -0
- data/lib/scout_apm/layer_converters/error_converter.rb +20 -0
- data/lib/scout_apm/layer_converters/job_converter.rb +84 -0
- data/lib/scout_apm/layer_converters/metric_converter.rb +45 -0
- data/lib/scout_apm/layer_converters/request_queue_time_converter.rb +60 -0
- data/lib/scout_apm/layer_converters/slow_job_converter.rb +88 -0
- data/lib/scout_apm/layer_converters/slow_request_converter.rb +111 -0
- data/lib/scout_apm/metric_meta.rb +9 -0
- data/lib/scout_apm/metric_set.rb +44 -0
- data/lib/scout_apm/reporter.rb +12 -5
- data/lib/scout_apm/serializers/jobs_serializer_to_json.rb +28 -0
- data/lib/scout_apm/serializers/metrics_to_json_serializer.rb +54 -0
- data/lib/scout_apm/serializers/payload_serializer.rb +5 -3
- data/lib/scout_apm/serializers/payload_serializer_to_json.rb +9 -4
- data/lib/scout_apm/serializers/slow_jobs_serializer_to_json.rb +29 -0
- data/lib/scout_apm/slow_item_set.rb +80 -0
- data/lib/scout_apm/slow_job_policy.rb +29 -0
- data/lib/scout_apm/slow_job_record.rb +33 -0
- data/lib/scout_apm/slow_transaction.rb +0 -22
- data/lib/scout_apm/stackprof_tree_collapser.rb +7 -8
- data/lib/scout_apm/store.rb +55 -35
- data/lib/scout_apm/tracked_request.rb +67 -10
- data/lib/scout_apm/utils/active_record_metric_name.rb +13 -0
- data/lib/scout_apm/utils/backtrace_parser.rb +31 -0
- data/lib/scout_apm/utils/fake_stack_prof.rb +1 -1
- data/lib/scout_apm/utils/sql_sanitizer.rb +6 -0
- data/lib/scout_apm/version.rb +1 -1
- data/lib/scout_apm.rb +25 -5
- data/test/unit/histogram_test.rb +93 -0
- data/test/unit/serializers/payload_serializer_test.rb +5 -5
- data/test/unit/{slow_transaction_set_test.rb → slow_item_set_test.rb} +8 -8
- data/test/unit/slow_job_policy_test.rb +55 -0
- metadata +30 -9
- data/lib/scout_apm/layer_converter.rb +0 -222
- data/lib/scout_apm/request_queue_time.rb +0 -57
- data/lib/scout_apm/slow_transaction_set.rb +0 -67
@@ -0,0 +1,36 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module LayerConverters
|
3
|
+
class DepthFirstWalker
|
4
|
+
attr_reader :root_layer
|
5
|
+
|
6
|
+
def initialize(root_layer)
|
7
|
+
@root_layer = root_layer
|
8
|
+
end
|
9
|
+
|
10
|
+
def before(&block)
|
11
|
+
@before_block = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def after(&block)
|
15
|
+
@after_block = block
|
16
|
+
end
|
17
|
+
|
18
|
+
def walk(layer=root_layer, &block)
|
19
|
+
# Need to run this for the root layer the first time through.
|
20
|
+
if layer == root_layer
|
21
|
+
@before_block.call(layer) if @before_block
|
22
|
+
yield layer
|
23
|
+
@after_block.call(layer) if @after_block
|
24
|
+
end
|
25
|
+
|
26
|
+
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
|
31
|
+
end
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module LayerConverters
|
3
|
+
class ErrorConverter < ConverterBase
|
4
|
+
def call
|
5
|
+
scope = scope_layer
|
6
|
+
|
7
|
+
# Should we mark a request as errored out if a middleware raises?
|
8
|
+
# How does that interact w/ a tool like Sentry or Honeybadger?
|
9
|
+
return {} unless scope
|
10
|
+
return {} unless request.error?
|
11
|
+
|
12
|
+
meta = MetricMeta.new("Errors/#{scope.legacy_metric_name}", {})
|
13
|
+
stat = MetricStats.new
|
14
|
+
stat.update!(1)
|
15
|
+
|
16
|
+
{ meta => stat }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# Queue/Critical (implicit count)
|
2
|
+
# Job/PasswordResetJob Scope=Queue/Critical (implicit count, & total time)
|
3
|
+
# JobMetric/Latency 10 Scope=Job/PasswordResetJob
|
4
|
+
# ActiveRecord/User/find Scope=Job/PasswordResetJob
|
5
|
+
# ActiveRecord/Message/find Scope=Job/PasswordResetJob
|
6
|
+
# HTTP/request Scope=Job/PasswordResetJob
|
7
|
+
# View/message/text Scope=Job/PasswordResetJob
|
8
|
+
# ActiveRecord/Config/find Scope=View/message/text
|
9
|
+
|
10
|
+
module ScoutApm
|
11
|
+
module LayerConverters
|
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
|
29
|
+
|
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
|
47
|
+
|
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
|
54
|
+
|
55
|
+
meta_options = {:scope => job_layer.legacy_metric_name}
|
56
|
+
|
57
|
+
walker.walk do |layer|
|
58
|
+
next if layer == job_layer
|
59
|
+
next if layer == queue_layer
|
60
|
+
|
61
|
+
# we don't need to use the full metric name for scoped metrics as we
|
62
|
+
# only display metrics aggregrated by type, just use "ActiveRecord"
|
63
|
+
# or similar.
|
64
|
+
metric_name = layer.type
|
65
|
+
|
66
|
+
meta = MetricMeta.new(metric_name, meta_options)
|
67
|
+
metric_hash[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
|
68
|
+
|
69
|
+
stat = metric_hash[meta]
|
70
|
+
stat.update!(layer.total_call_time, layer.total_exclusive_time)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Add the latency metric, which wasn't stored as a distinct layer
|
74
|
+
latency = request.annotations[:queue_latency] || 0
|
75
|
+
meta = MetricMeta.new("Latency", meta_options)
|
76
|
+
stat = MetricStats.new
|
77
|
+
stat.update!(latency)
|
78
|
+
metric_hash[meta] = stat
|
79
|
+
|
80
|
+
metric_hash
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Take a TrackedRequest and turn it into a hash of:
|
2
|
+
# MetricMeta => MetricStats
|
3
|
+
|
4
|
+
module ScoutApm
|
5
|
+
module LayerConverters
|
6
|
+
class MetricConverter < ConverterBase
|
7
|
+
def call
|
8
|
+
scope = scope_layer
|
9
|
+
|
10
|
+
# TODO: Track requests that never reach a Controller (for example, when
|
11
|
+
# Middleware decides to return rather than passing onward)
|
12
|
+
return {} unless scope
|
13
|
+
|
14
|
+
create_metrics
|
15
|
+
end
|
16
|
+
|
17
|
+
# Full metrics from this request. These get aggregated in Store for the
|
18
|
+
# overview metrics, or stored permanently in a SlowTransaction
|
19
|
+
# Some merging of metrics will happen here, so if a request calls the same
|
20
|
+
# ActiveRecord or View repeatedly, it'll get merged.
|
21
|
+
def create_metrics
|
22
|
+
metric_hash = Hash.new
|
23
|
+
|
24
|
+
walker.walk do |layer|
|
25
|
+
meta_options = if layer == scope_layer # We don't scope the controller under itself
|
26
|
+
{}
|
27
|
+
else
|
28
|
+
{:scope => scope_layer.legacy_metric_name}
|
29
|
+
end
|
30
|
+
|
31
|
+
# we don't need to use the full metric name for scoped metrics as we only display metrics aggregrated
|
32
|
+
# by type.
|
33
|
+
metric_name = meta_options.has_key?(:scope) ? layer.type : layer.legacy_metric_name
|
34
|
+
|
35
|
+
meta = MetricMeta.new(metric_name, meta_options)
|
36
|
+
metric_hash[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
|
37
|
+
|
38
|
+
stat = metric_hash[meta]
|
39
|
+
stat.update!(layer.total_call_time, layer.total_exclusive_time)
|
40
|
+
end
|
41
|
+
metric_hash
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module LayerConverters
|
3
|
+
class RequestQueueTimeConverter < ConverterBase
|
4
|
+
|
5
|
+
HEADERS = %w(X-Queue-Start X-Request-Start X-QUEUE-START X-REQUEST-START x-queue-start x-request-start)
|
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
|
11
|
+
end
|
12
|
+
|
13
|
+
def call
|
14
|
+
return {} unless headers
|
15
|
+
|
16
|
+
raw_start = locate_timestamp
|
17
|
+
return {} unless raw_start
|
18
|
+
|
19
|
+
parsed_start = parse(raw_start)
|
20
|
+
return {} unless parsed_start
|
21
|
+
|
22
|
+
request_start = root_layer.start_time
|
23
|
+
queue_time = (request_start - parsed_start).to_f
|
24
|
+
|
25
|
+
# If we end up with a negative value, just bail out and don't report anything
|
26
|
+
return {} if queue_time < 0
|
27
|
+
|
28
|
+
meta = MetricMeta.new("QueueTime/Request", {:scope => scope_layer.legacy_metric_name})
|
29
|
+
stat = MetricStats.new(true)
|
30
|
+
stat.update!(queue_time)
|
31
|
+
|
32
|
+
{ meta => stat }
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_reader :headers
|
38
|
+
|
39
|
+
# Looks through the possible headers with this data, and extracts the raw
|
40
|
+
# value of the header
|
41
|
+
# Returns nil if not found
|
42
|
+
def locate_timestamp
|
43
|
+
return nil unless headers
|
44
|
+
|
45
|
+
header = HEADERS.find { |candidate| headers[candidate] }
|
46
|
+
if header
|
47
|
+
data = headers[header]
|
48
|
+
data.to_s.gsub(/(t=|\.)/, '')
|
49
|
+
else
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns a timestamp in fractional seconds since epoch
|
55
|
+
def parse(time_string)
|
56
|
+
Time.at("#{time_string[0,10]}.#{time_string[10,13]}".to_f)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module LayerConverters
|
3
|
+
class SlowJobConverter < ConverterBase
|
4
|
+
def call
|
5
|
+
return unless request.job?
|
6
|
+
|
7
|
+
job_name = [queue_layer.name, job_layer.name]
|
8
|
+
|
9
|
+
slow_enough = ScoutApm::Agent.instance.slow_job_policy.slow?(job_name, root_layer.total_call_time)
|
10
|
+
return unless slow_enough
|
11
|
+
|
12
|
+
SlowJobRecord.new(
|
13
|
+
queue_layer.name,
|
14
|
+
job_layer.name,
|
15
|
+
root_layer.stop_time,
|
16
|
+
job_layer.total_call_time,
|
17
|
+
job_layer.total_exclusive_time,
|
18
|
+
request.context,
|
19
|
+
create_metrics)
|
20
|
+
end
|
21
|
+
|
22
|
+
def queue_layer
|
23
|
+
@queue_layer ||= find_first_layer_of_type("Queue")
|
24
|
+
end
|
25
|
+
|
26
|
+
def job_layer
|
27
|
+
@job_layer ||= find_first_layer_of_type("Job")
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_first_layer_of_type(layer_type)
|
31
|
+
walker.walk do |layer|
|
32
|
+
return layer if layer.type == layer_type
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_metrics
|
37
|
+
metric_hash = Hash.new
|
38
|
+
|
39
|
+
# Keep a list of subscopes, but only ever use the front one. The rest
|
40
|
+
# get pushed/popped in cases when we have many levels of subscopable
|
41
|
+
# layers. This lets us push/pop without otherwise keeping track very closely.
|
42
|
+
subscope_layers = []
|
43
|
+
|
44
|
+
walker.before do |layer|
|
45
|
+
if layer.subscopable?
|
46
|
+
subscope_layers.push(layer)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
walker.after do |layer|
|
51
|
+
if layer.subscopable?
|
52
|
+
subscope_layers.pop
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
walker.walk do |layer|
|
57
|
+
next if layer == queue_layer
|
58
|
+
|
59
|
+
meta_options = if subscope_layers.first && layer != subscope_layers.first # Don't scope under ourself.
|
60
|
+
subscope_name = subscope_layers.first.legacy_metric_name
|
61
|
+
{:scope => subscope_name}
|
62
|
+
elsif layer == job_layer # We don't scope the controller under itself
|
63
|
+
{}
|
64
|
+
else
|
65
|
+
{:scope => job_layer.legacy_metric_name}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Specific Metric
|
69
|
+
meta_options.merge!(:desc => layer.desc.to_s) if layer.desc
|
70
|
+
meta = MetricMeta.new(layer.legacy_metric_name, meta_options)
|
71
|
+
# this has moved - commenting out for now. will copy over bits from SlowRequestConverter
|
72
|
+
# meta.extra.merge!(:backtrace => ScoutApm::SlowTransaction.backtrace_parser(layer.backtrace)) if layer.backtrace
|
73
|
+
metric_hash[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
|
74
|
+
stat = metric_hash[meta]
|
75
|
+
stat.update!(layer.total_call_time, layer.total_exclusive_time)
|
76
|
+
|
77
|
+
# Merged Metric (no specifics, just sum up by type)
|
78
|
+
meta = MetricMeta.new("#{layer.type}/all")
|
79
|
+
metric_hash[meta] ||= MetricStats.new(false)
|
80
|
+
stat = metric_hash[meta]
|
81
|
+
stat.update!(layer.total_call_time, layer.total_exclusive_time)
|
82
|
+
end
|
83
|
+
|
84
|
+
metric_hash
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module LayerConverters
|
3
|
+
class SlowRequestConverter < ConverterBase
|
4
|
+
def call
|
5
|
+
scope = scope_layer
|
6
|
+
return [nil, {}] unless scope
|
7
|
+
|
8
|
+
policy = ScoutApm::Agent.instance.slow_request_policy.capture_type(root_layer.total_call_time)
|
9
|
+
if policy == ScoutApm::SlowRequestPolicy::CAPTURE_NONE
|
10
|
+
return [nil, {}]
|
11
|
+
end
|
12
|
+
|
13
|
+
# increment the slow transaction count if this is a slow transaction.
|
14
|
+
meta = MetricMeta.new("SlowTransaction/#{scope.legacy_metric_name}")
|
15
|
+
stat = MetricStats.new
|
16
|
+
stat.update!(1)
|
17
|
+
|
18
|
+
@backtraces = [] # An Array of MetricMetas that have a backtrace
|
19
|
+
|
20
|
+
uri = request.annotations[:uri] || ""
|
21
|
+
|
22
|
+
metrics = create_metrics
|
23
|
+
# Disable stackprof output for now
|
24
|
+
stackprof = [] # request.stackprof
|
25
|
+
|
26
|
+
[
|
27
|
+
SlowTransaction.new(uri,
|
28
|
+
scope.legacy_metric_name,
|
29
|
+
root_layer.total_call_time,
|
30
|
+
metrics,
|
31
|
+
request.context,
|
32
|
+
root_layer.stop_time,
|
33
|
+
stackprof),
|
34
|
+
{ meta => stat }
|
35
|
+
]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Iterates over the TrackedRequest's MetricMetas that have backtraces and attaches each to correct MetricMeta in the Metric Hash.
|
39
|
+
def attach_backtraces(metric_hash)
|
40
|
+
@backtraces.each do |meta_with_backtrace|
|
41
|
+
metric_hash.keys.find { |k| k == meta_with_backtrace }.backtrace = meta_with_backtrace.backtrace
|
42
|
+
end
|
43
|
+
metric_hash
|
44
|
+
end
|
45
|
+
|
46
|
+
# Full metrics from this request. These get aggregated in Store for the
|
47
|
+
# overview metrics, or stored permanently in a SlowTransaction
|
48
|
+
# Some merging of metrics will happen here, so if a request calls the same
|
49
|
+
# ActiveRecord or View repeatedly, it'll get merged.
|
50
|
+
def create_metrics
|
51
|
+
metric_hash = Hash.new
|
52
|
+
|
53
|
+
# Keep a list of subscopes, but only ever use the front one. The rest
|
54
|
+
# get pushed/popped in cases when we have many levels of subscopable
|
55
|
+
# layers. This lets us push/pop without otherwise keeping track very closely.
|
56
|
+
subscope_layers = []
|
57
|
+
|
58
|
+
walker.before do |layer|
|
59
|
+
if layer.subscopable?
|
60
|
+
subscope_layers.push(layer)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
walker.after do |layer|
|
65
|
+
if layer.subscopable?
|
66
|
+
subscope_layers.pop
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
walker.walk do |layer|
|
71
|
+
meta_options = if subscope_layers.first && layer != subscope_layers.first # Don't scope under ourself.
|
72
|
+
subscope_name = subscope_layers.first.legacy_metric_name
|
73
|
+
{:scope => subscope_name}
|
74
|
+
elsif layer == scope_layer # We don't scope the controller under itself
|
75
|
+
{}
|
76
|
+
else
|
77
|
+
{:scope => scope_layer.legacy_metric_name}
|
78
|
+
end
|
79
|
+
|
80
|
+
# Specific Metric
|
81
|
+
meta_options.merge!(:desc => layer.desc.to_s) if layer.desc
|
82
|
+
meta = MetricMeta.new(layer.legacy_metric_name, meta_options)
|
83
|
+
if layer.backtrace
|
84
|
+
bt = ScoutApm::Utils::BacktraceParser.new(layer.backtrace).call
|
85
|
+
if bt.any? # we could walk thru the call stack and not find in-app code
|
86
|
+
meta.backtrace = bt
|
87
|
+
# 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
|
88
|
+
# lost in the metric_hash if it is replaced by the new key.
|
89
|
+
@backtraces << meta
|
90
|
+
else
|
91
|
+
ScoutApm::Agent.instance.logger.debug { "Unable to capture an app-specific backtrace for #{meta.inspect}\n#{layer.backtrace}" }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
metric_hash[meta] ||= MetricStats.new( meta_options.has_key?(:scope) )
|
95
|
+
stat = metric_hash[meta]
|
96
|
+
stat.update!(layer.total_call_time, layer.total_exclusive_time)
|
97
|
+
|
98
|
+
# Merged Metric (no specifics, just sum up by type)
|
99
|
+
meta = MetricMeta.new("#{layer.type}/all")
|
100
|
+
metric_hash[meta] ||= MetricStats.new(false)
|
101
|
+
stat = metric_hash[meta]
|
102
|
+
stat.update!(layer.total_call_time, layer.total_exclusive_time)
|
103
|
+
end
|
104
|
+
|
105
|
+
metric_hash = attach_backtraces(metric_hash)
|
106
|
+
|
107
|
+
metric_hash
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -38,6 +38,15 @@ class MetricMeta
|
|
38
38
|
self.eql?(o)
|
39
39
|
end
|
40
40
|
|
41
|
+
# This should be abstracted to a true accessor ... earned it.
|
42
|
+
def backtrace=(bt)
|
43
|
+
extra[:backtrace] = bt
|
44
|
+
end
|
45
|
+
|
46
|
+
def backtrace
|
47
|
+
extra[:backtrace]
|
48
|
+
end
|
49
|
+
|
41
50
|
def hash
|
42
51
|
h = metric_name.downcase.hash
|
43
52
|
h ^= scope.downcase.hash unless scope.nil?
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
class MetricSet
|
3
|
+
# We can't aggregate CPU, Memory, Capacity, or Controller, so pass through these metrics directly
|
4
|
+
# TODO: Figure out a way to not have this duplicate what's in Samplers, and also on server's ingest
|
5
|
+
PASSTHROUGH_METRICS = ["CPU", "Memory", "Instance", "Controller", "SlowTransaction"]
|
6
|
+
|
7
|
+
attr_reader :metrics
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@metrics = Hash.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def absorb_all(metrics)
|
14
|
+
Array(metrics).each { |m| absorb(m) }
|
15
|
+
end
|
16
|
+
|
17
|
+
# Absorbs a single new metric into the aggregates
|
18
|
+
def absorb(metric)
|
19
|
+
meta, stat = metric
|
20
|
+
|
21
|
+
if PASSTHROUGH_METRICS.include?(meta.type) # Leave as-is, don't attempt to combine into an /all key
|
22
|
+
@metrics[meta] ||= MetricStats.new
|
23
|
+
@metrics[meta].combine!(stat)
|
24
|
+
|
25
|
+
elsif meta.type == "Errors" # Sadly special cased, we want both raw and aggregate values
|
26
|
+
@metrics[meta] ||= MetricStats.new
|
27
|
+
@metrics[meta].combine!(stat)
|
28
|
+
agg_meta = MetricMeta.new("Errors/Request", :scope => meta.scope)
|
29
|
+
@metrics[agg_meta] ||= MetricStats.new
|
30
|
+
@metrics[agg_meta].combine!(stat)
|
31
|
+
|
32
|
+
else # Combine down to a single /all key
|
33
|
+
agg_meta = MetricMeta.new("#{meta.type}/all", :scope => meta.scope)
|
34
|
+
@metrics[agg_meta] ||= MetricStats.new
|
35
|
+
@metrics[agg_meta].combine!(stat)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def combine!(other)
|
40
|
+
absorb_all(other.metrics)
|
41
|
+
self
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/scout_apm/reporter.rb
CHANGED
@@ -17,17 +17,24 @@ module ScoutApm
|
|
17
17
|
|
18
18
|
# TODO: Parse & return a real response object, not the HTTP Response object
|
19
19
|
def report(payload, headers = {})
|
20
|
-
|
20
|
+
Array(config.value('host')).each do |host|
|
21
|
+
|
22
|
+
full_uri = uri(host)
|
23
|
+
response = post(full_uri, payload, headers)
|
24
|
+
unless response && response.is_a?(Net::HTTPSuccess)
|
25
|
+
logger.warn "Error on checkin to #{full_uri.to_s}: #{response.inspect}"
|
26
|
+
end
|
27
|
+
end
|
21
28
|
end
|
22
29
|
|
23
|
-
def uri
|
30
|
+
def uri(host)
|
24
31
|
case type
|
25
32
|
when :checkin
|
26
|
-
URI.parse("#{
|
33
|
+
URI.parse("#{host}/apps/checkin.scout?key=#{config.value('key')}&name=#{CGI.escape(Environment.instance.application_name)}")
|
27
34
|
when :app_server_load
|
28
|
-
URI.parse("#{
|
35
|
+
URI.parse("#{host}/apps/app_server_load.scout?key=#{config.value('key')}&name=#{CGI.escape(Environment.instance.application_name)}")
|
29
36
|
when :deploy_hook
|
30
|
-
URI.parse("#{
|
37
|
+
URI.parse("#{host}/apps/deploy.scout?key=#{config.value('key')}&name=#{CGI.escape(config.value('name'))}")
|
31
38
|
end.tap{|u| logger.debug("Posting to #{u.to_s}")}
|
32
39
|
end
|
33
40
|
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module Serializers
|
3
|
+
class JobsSerializerToJson
|
4
|
+
attr_reader :jobs
|
5
|
+
|
6
|
+
# Jobs is a pre-deduped/combined set of job records.
|
7
|
+
def initialize(jobs)
|
8
|
+
@jobs = jobs
|
9
|
+
end
|
10
|
+
|
11
|
+
# An array of job records
|
12
|
+
def as_json
|
13
|
+
jobs.map do |job|
|
14
|
+
{
|
15
|
+
"queue" => job.queue_name,
|
16
|
+
"name" => job.job_name,
|
17
|
+
"count" => job.run_count,
|
18
|
+
"errors" => job.errors,
|
19
|
+
"total_time" => job.total_time.as_json,
|
20
|
+
"exclusive_time" => job.exclusive_time.as_json,
|
21
|
+
"metrics" => MetricsToJsonSerializer.new(job.metrics).as_json, # New style of metrics
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module Serializers
|
3
|
+
class MetricsToJsonSerializer
|
4
|
+
attr_reader :metrics
|
5
|
+
|
6
|
+
# A hash of meta => stat pairs
|
7
|
+
def initialize(metrics)
|
8
|
+
@metrics = metrics
|
9
|
+
end
|
10
|
+
|
11
|
+
def as_json
|
12
|
+
metrics.map{|meta, stat| metric_as_json(meta, stat) }
|
13
|
+
end
|
14
|
+
|
15
|
+
# Children metrics is a hash of meta=>stat pairs. Leave empty for no children.
|
16
|
+
# Supports only a single-level nesting, until we have redone metric
|
17
|
+
# classes, instead of Meta and Stats
|
18
|
+
def metric_as_json(meta, stat, child_metrics={})
|
19
|
+
|
20
|
+
{ "bucket" => meta.type,
|
21
|
+
"name" => meta.name, # No scope values needed here, since it's implied by the nesting.
|
22
|
+
|
23
|
+
"count" => stat.call_count,
|
24
|
+
"total_call_time" => stat.total_call_time,
|
25
|
+
"total_exclusive_time" => stat.total_exclusive_time,
|
26
|
+
"min_call_time" => stat.min_call_time,
|
27
|
+
"max_call_time" => stat.max_call_time,
|
28
|
+
|
29
|
+
# Pretty unsure how to synthesize histograms out of what we store now
|
30
|
+
"total_histogram" => [
|
31
|
+
[stat.total_exclusive_time / stat.call_count, stat.call_count],
|
32
|
+
],
|
33
|
+
"exclusive_histogram" => [
|
34
|
+
[stat.total_exclusive_time / stat.call_count, stat.call_count]
|
35
|
+
],
|
36
|
+
|
37
|
+
"metrics" => transform_child_metrics(child_metrics),
|
38
|
+
|
39
|
+
# Will later hold the exact SQL, or URL or whatever other detail
|
40
|
+
# about this query is necessary
|
41
|
+
"detail" => { :desc => meta.desc }.merge(meta.extra || {}),
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def transform_child_metrics(metrics)
|
46
|
+
metrics.map do |meta, stat|
|
47
|
+
metric_as_json(meta, stat)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
@@ -2,9 +2,9 @@
|
|
2
2
|
module ScoutApm
|
3
3
|
module Serializers
|
4
4
|
class PayloadSerializer
|
5
|
-
def self.serialize(metadata, metrics, slow_transactions)
|
5
|
+
def self.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs)
|
6
6
|
if ScoutApm::Agent.instance.config.value("report_format") == 'json'
|
7
|
-
ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions)
|
7
|
+
ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs)
|
8
8
|
else
|
9
9
|
metadata = metadata.dup
|
10
10
|
metadata.default = nil
|
@@ -13,7 +13,9 @@ module ScoutApm
|
|
13
13
|
metrics.default = nil
|
14
14
|
Marshal.dump(:metadata => metadata,
|
15
15
|
:metrics => metrics,
|
16
|
-
:slow_transactions => slow_transactions
|
16
|
+
:slow_transactions => slow_transactions,
|
17
|
+
:jobs => jobs,
|
18
|
+
:slow_jobs => slow_jobs)
|
17
19
|
end
|
18
20
|
end
|
19
21
|
|
@@ -2,13 +2,18 @@ module ScoutApm
|
|
2
2
|
module Serializers
|
3
3
|
module PayloadSerializerToJson
|
4
4
|
class << self
|
5
|
-
def serialize(metadata, metrics, slow_transactions)
|
6
|
-
rearranged_metrics = rearrange_the_metrics(metrics)
|
7
|
-
rearranged_slow_transactions = rearrange_the_slow_transactions(slow_transactions)
|
5
|
+
def serialize(metadata, metrics, slow_transactions, jobs, slow_jobs)
|
8
6
|
metadata.merge!({:payload_version => 2})
|
9
|
-
|
7
|
+
|
8
|
+
jsonify_hash({:metadata => metadata,
|
9
|
+
:metrics => rearrange_the_metrics(metrics),
|
10
|
+
:slow_transactions => rearrange_the_slow_transactions(slow_transactions),
|
11
|
+
:jobs => JobsSerializerToJson.new(jobs).as_json,
|
12
|
+
:slow_jobs => SlowJobsSerializerToJson.new(slow_jobs).as_json,
|
13
|
+
})
|
10
14
|
end
|
11
15
|
|
16
|
+
# Old style of metric serializing.
|
12
17
|
def rearrange_the_metrics(metrics)
|
13
18
|
metrics.to_a.map do |meta, stats|
|
14
19
|
stats.as_json.merge(:key => meta.as_json)
|