scout_apm 1.4.6 → 1.5.0.pre
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 +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)
|