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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.markdown +9 -0
  3. data/lib/scout_apm/agent/reporting.rb +8 -6
  4. data/lib/scout_apm/agent.rb +10 -6
  5. data/lib/scout_apm/background_job_integrations/sidekiq.rb +23 -11
  6. data/lib/scout_apm/call_set.rb +61 -0
  7. data/lib/scout_apm/config.rb +2 -1
  8. data/lib/scout_apm/environment.rb +12 -7
  9. data/lib/scout_apm/histogram.rb +124 -0
  10. data/lib/scout_apm/instruments/.DS_Store +0 -0
  11. data/lib/scout_apm/instruments/action_controller_rails_2.rb +1 -0
  12. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +1 -0
  13. data/lib/scout_apm/instruments/delayed_job.rb +1 -0
  14. data/lib/scout_apm/instruments/process/process_memory.rb +1 -1
  15. data/lib/scout_apm/instruments/sinatra.rb +1 -1
  16. data/lib/scout_apm/job_record.rb +76 -0
  17. data/lib/scout_apm/layaway.rb +4 -1
  18. data/lib/scout_apm/layaway_file.rb +4 -4
  19. data/lib/scout_apm/layer.rb +14 -4
  20. data/lib/scout_apm/layer_converters/converter_base.rb +30 -0
  21. data/lib/scout_apm/layer_converters/depth_first_walker.rb +36 -0
  22. data/lib/scout_apm/layer_converters/error_converter.rb +20 -0
  23. data/lib/scout_apm/layer_converters/job_converter.rb +84 -0
  24. data/lib/scout_apm/layer_converters/metric_converter.rb +45 -0
  25. data/lib/scout_apm/layer_converters/request_queue_time_converter.rb +60 -0
  26. data/lib/scout_apm/layer_converters/slow_job_converter.rb +88 -0
  27. data/lib/scout_apm/layer_converters/slow_request_converter.rb +111 -0
  28. data/lib/scout_apm/metric_meta.rb +9 -0
  29. data/lib/scout_apm/metric_set.rb +44 -0
  30. data/lib/scout_apm/reporter.rb +12 -5
  31. data/lib/scout_apm/serializers/jobs_serializer_to_json.rb +28 -0
  32. data/lib/scout_apm/serializers/metrics_to_json_serializer.rb +54 -0
  33. data/lib/scout_apm/serializers/payload_serializer.rb +5 -3
  34. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +9 -4
  35. data/lib/scout_apm/serializers/slow_jobs_serializer_to_json.rb +29 -0
  36. data/lib/scout_apm/slow_item_set.rb +80 -0
  37. data/lib/scout_apm/slow_job_policy.rb +29 -0
  38. data/lib/scout_apm/slow_job_record.rb +33 -0
  39. data/lib/scout_apm/slow_transaction.rb +0 -22
  40. data/lib/scout_apm/stackprof_tree_collapser.rb +7 -8
  41. data/lib/scout_apm/store.rb +55 -35
  42. data/lib/scout_apm/tracked_request.rb +67 -10
  43. data/lib/scout_apm/utils/active_record_metric_name.rb +13 -0
  44. data/lib/scout_apm/utils/backtrace_parser.rb +31 -0
  45. data/lib/scout_apm/utils/fake_stack_prof.rb +1 -1
  46. data/lib/scout_apm/utils/sql_sanitizer.rb +6 -0
  47. data/lib/scout_apm/version.rb +1 -1
  48. data/lib/scout_apm.rb +25 -5
  49. data/test/unit/histogram_test.rb +93 -0
  50. data/test/unit/serializers/payload_serializer_test.rb +5 -5
  51. data/test/unit/{slow_transaction_set_test.rb → slow_item_set_test.rb} +8 -8
  52. data/test/unit/slow_job_policy_test.rb +55 -0
  53. metadata +30 -9
  54. data/lib/scout_apm/layer_converter.rb +0 -222
  55. data/lib/scout_apm/request_queue_time.rb +0 -57
  56. 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
@@ -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
- post(uri, payload, headers)
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("#{config.value('host')}/apps/checkin.scout?key=#{config.value('key')}&name=#{CGI.escape(Environment.instance.application_name)}")
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("#{config.value('host')}/apps/app_server_load.scout?key=#{config.value('key')}&name=#{CGI.escape(Environment.instance.application_name)}")
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("#{config.value('host')}/apps/deploy.scout?key=#{config.value('key')}&name=#{CGI.escape(config.value('name'))}")
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
- jsonify_hash({:metadata => metadata, :metrics => rearranged_metrics, :slow_transactions => rearranged_slow_transactions})
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)