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.
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)