scout_apm 3.0.0.pre1 → 3.0.0.pre2

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.markdown +12 -1
  3. data/lib/scout_apm.rb +3 -0
  4. data/lib/scout_apm/agent.rb +9 -6
  5. data/lib/scout_apm/agent/reporting.rb +5 -3
  6. data/lib/scout_apm/background_job_integrations/delayed_job.rb +47 -1
  7. data/lib/scout_apm/background_worker.rb +4 -0
  8. data/lib/scout_apm/config.rb +2 -1
  9. data/lib/scout_apm/environment.rb +1 -1
  10. data/lib/scout_apm/histogram.rb +11 -2
  11. data/lib/scout_apm/instruments/mongoid.rb +14 -1
  12. data/lib/scout_apm/instruments/percentile_sampler.rb +36 -19
  13. data/lib/scout_apm/instruments/process/process_cpu.rb +3 -2
  14. data/lib/scout_apm/instruments/process/process_memory.rb +3 -3
  15. data/lib/scout_apm/layer_converters/converter_base.rb +213 -0
  16. data/lib/scout_apm/layer_converters/slow_job_converter.rb +19 -93
  17. data/lib/scout_apm/layer_converters/slow_request_converter.rb +15 -100
  18. data/lib/scout_apm/metric_set.rb +6 -0
  19. data/lib/scout_apm/reporter.rb +53 -15
  20. data/lib/scout_apm/request_histograms.rb +4 -0
  21. data/lib/scout_apm/scored_item_set.rb +7 -0
  22. data/lib/scout_apm/serializers/histograms_serializer_to_json.rb +21 -0
  23. data/lib/scout_apm/serializers/payload_serializer.rb +9 -3
  24. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +2 -1
  25. data/lib/scout_apm/serializers/slow_jobs_serializer_to_json.rb +1 -1
  26. data/lib/scout_apm/slow_job_record.rb +4 -1
  27. data/lib/scout_apm/slow_transaction.rb +18 -2
  28. data/lib/scout_apm/store.rb +42 -11
  29. data/lib/scout_apm/tracked_request.rb +1 -1
  30. data/lib/scout_apm/utils/gzip_helper.rb +24 -0
  31. data/lib/scout_apm/utils/numbers.rb +14 -0
  32. data/lib/scout_apm/version.rb +2 -2
  33. data/test/test_helper.rb +10 -0
  34. data/test/unit/config_test.rb +7 -9
  35. data/test/unit/histogram_test.rb +14 -0
  36. data/test/unit/instruments/percentile_sampler_test.rb +137 -0
  37. data/test/unit/serializers/payload_serializer_test.rb +3 -3
  38. data/test/unit/store_test.rb +51 -0
  39. data/test/unit/utils/numbers_test.rb +15 -0
  40. metadata +10 -4
@@ -63,6 +63,12 @@ module ScoutApm
63
63
  end
64
64
  end
65
65
 
66
+ # Equal to another set only if exactly the same set of items is inside
67
+ def eql?(other)
68
+ items == other.items
69
+ end
70
+
71
+ alias :== :eql?
66
72
 
67
73
  private
68
74
 
@@ -75,5 +81,6 @@ module ScoutApm
75
81
  items[new_item.name] = [new_item.score, new_item.call]
76
82
  end
77
83
  end
84
+
78
85
  end
79
86
  end
@@ -0,0 +1,21 @@
1
+
2
+ module ScoutApm
3
+ module Serializers
4
+ class HistogramsSerializerToJson
5
+ attr_reader :histograms
6
+
7
+ def initialize(histograms)
8
+ @histograms = histograms
9
+ end
10
+
11
+ def as_json
12
+ histograms.map do |histo|
13
+ {
14
+ "name" => histo.name,
15
+ "histogram" => histo.histogram.as_json,
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,9 +2,9 @@
2
2
  module ScoutApm
3
3
  module Serializers
4
4
  class PayloadSerializer
5
- def self.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs)
5
+ def self.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms)
6
6
  if ScoutApm::Agent.instance.config.value("report_format") == 'json'
7
- ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs)
7
+ ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms)
8
8
  else
9
9
  metadata = metadata.dup
10
10
  metadata.default = nil
@@ -15,7 +15,13 @@ module ScoutApm
15
15
  :metrics => metrics,
16
16
  :slow_transactions => slow_transactions,
17
17
  :jobs => jobs,
18
- :slow_jobs => slow_jobs)
18
+ :slow_jobs => slow_jobs,
19
+
20
+ # as_json returns a ruby object. Since it's not a simple
21
+ # array, use this to maintain compatibility with json
22
+ # payloads. At this point, the marshal code branch is
23
+ # very rarely used anyway.
24
+ :histograms => HistogramsSerializerToJson.new(histograms).as_json)
19
25
  end
20
26
  end
21
27
 
@@ -2,7 +2,7 @@ module ScoutApm
2
2
  module Serializers
3
3
  module PayloadSerializerToJson
4
4
  class << self
5
- def serialize(metadata, metrics, slow_transactions, jobs, slow_jobs)
5
+ def serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms)
6
6
  metadata.merge!({:payload_version => 2})
7
7
 
8
8
  jsonify_hash({:metadata => metadata,
@@ -10,6 +10,7 @@ module ScoutApm
10
10
  :slow_transactions => rearrange_the_slow_transactions(slow_transactions),
11
11
  :jobs => JobsSerializerToJson.new(jobs).as_json,
12
12
  :slow_jobs => SlowJobsSerializerToJson.new(slow_jobs).as_json,
13
+ :histograms => HistogramsSerializerToJson.new(histograms).as_json,
13
14
  })
14
15
  end
15
16
 
@@ -25,6 +25,7 @@ module ScoutApm
25
25
  "metrics" => MetricsToJsonSerializer.new(job.metrics).as_json, # New style of metrics
26
26
  "allocation_metrics" => MetricsToJsonSerializer.new(job.allocation_metrics).as_json, # New style of metrics
27
27
  "context" => job.context.to_hash,
28
+ "truncated_metrics" => job.truncated_metrics,
28
29
 
29
30
  "score" => job.score,
30
31
  }
@@ -33,4 +34,3 @@ module ScoutApm
33
34
  end
34
35
  end
35
36
  end
36
-
@@ -21,8 +21,9 @@ module ScoutApm
21
21
  attr_reader :seconds_since_startup
22
22
  attr_reader :score
23
23
  attr_reader :git_sha
24
+ attr_reader :truncated_metrics
24
25
 
25
- def initialize(queue_name, job_name, time, total_time, exclusive_time, context, metrics, allocation_metrics, mem_delta, allocations, score)
26
+ def initialize(queue_name, job_name, time, total_time, exclusive_time, context, metrics, allocation_metrics, mem_delta, allocations, score, truncated_metrics)
26
27
  @queue_name = queue_name
27
28
  @job_name = job_name
28
29
  @time = time
@@ -37,6 +38,8 @@ module ScoutApm
37
38
  @hostname = ScoutApm::Environment.instance.hostname
38
39
  @git_sha = ScoutApm::Environment.instance.git_revision.sha
39
40
  @score = score
41
+ @truncated_metrics = truncated_metrics
42
+
40
43
  ScoutApm::Agent.instance.logger.debug { "Slow Job [#{metric_name}] - Call Time: #{total_call_time} Mem Delta: #{mem_delta}"}
41
44
  end
42
45
 
@@ -17,7 +17,9 @@ module ScoutApm
17
17
  attr_accessor :seconds_since_startup # hack - we need to reset these server side.
18
18
  attr_accessor :git_sha # hack - we need to reset these server side.
19
19
 
20
- def initialize(uri, metric_name, total_call_time, metrics, allocation_metrics, context, time, raw_stackprof, mem_delta, allocations, score)
20
+ attr_reader :truncated_metrics # True/False that says if we had to truncate the metrics of this trace
21
+
22
+ def initialize(uri, metric_name, total_call_time, metrics, allocation_metrics, context, time, raw_stackprof, mem_delta, allocations, score, truncated_metrics)
21
23
  @uri = uri
22
24
  @metric_name = metric_name
23
25
  @total_call_time = total_call_time
@@ -32,6 +34,8 @@ module ScoutApm
32
34
  @hostname = ScoutApm::Environment.instance.hostname
33
35
  @score = score
34
36
  @git_sha = ScoutApm::Environment.instance.git_revision.sha
37
+ @truncated_metrics = truncated_metrics
38
+
35
39
  ScoutApm::Agent.instance.logger.debug { "Slow Request [#{uri}] - Call Time: #{total_call_time} Mem Delta: #{mem_delta} Score: #{score}"}
36
40
  end
37
41
 
@@ -46,7 +50,19 @@ module ScoutApm
46
50
  end
47
51
 
48
52
  def as_json
49
- json_attributes = [:key, :time, :total_call_time, :uri, [:context, :context_hash], :score, :prof, :mem_delta, :allocations, :seconds_since_startup, :hostname, :git_sha]
53
+ json_attributes = [:key,
54
+ :time,
55
+ :total_call_time,
56
+ :uri,
57
+ [:context, :context_hash],
58
+ :score,
59
+ :prof,
60
+ :mem_delta,
61
+ :allocations,
62
+ :seconds_since_startup,
63
+ :hostname,
64
+ :git_sha,
65
+ :truncated_metrics]
50
66
  ScoutApm::AttributeArranger.call(self, json_attributes)
51
67
  end
52
68
 
@@ -26,7 +26,23 @@ module ScoutApm
26
26
  # Save newly collected metrics
27
27
  def track!(metrics, options={})
28
28
  @mutex.synchronize {
29
- current_period.absorb_metrics!(metrics)
29
+ period = if options[:timestamp]
30
+ @reporting_periods[options[:timestamp]]
31
+ else
32
+ current_period
33
+ end
34
+ period.absorb_metrics!(metrics)
35
+ }
36
+ end
37
+
38
+ def track_histograms!(histograms, options={})
39
+ @mutex.synchronize {
40
+ period = if options[:timestamp]
41
+ @reporting_periods[options[:timestamp]]
42
+ else
43
+ current_period
44
+ end
45
+ period.merge_histograms!(histograms)
30
46
  }
31
47
  end
32
48
 
@@ -67,15 +83,15 @@ module ScoutApm
67
83
  def write_to_layaway(layaway, force=false)
68
84
  ScoutApm::Agent.instance.logger.debug("Writing to layaway#{" (Forced)" if force}")
69
85
 
70
- @mutex.synchronize {
71
- reporting_periods.select { |time, rp| force || time.timestamp < current_timestamp.timestamp}.
86
+ reporting_periods.select { |time, rp| force || time.timestamp < current_timestamp.timestamp }.
87
+ each { |time, rp| collect_samplers(rp) }.
72
88
  each { |time, rp| write_reporting_period(layaway, time, rp) }
73
- }
74
89
  end
75
90
 
76
91
  def write_reporting_period(layaway, time, rp)
77
- collect_samplers(rp)
78
- layaway.write_reporting_period(rp)
92
+ @mutex.synchronize {
93
+ layaway.write_reporting_period(rp)
94
+ }
79
95
  rescue => e
80
96
  ScoutApm::Agent.instance.logger.warn("Failed writing data to layaway file: #{e.message} / #{e.backtrace}")
81
97
  ensure
@@ -91,12 +107,10 @@ module ScoutApm
91
107
  def collect_samplers(rp)
92
108
  @samplers.each do |sampler|
93
109
  begin
94
- metrics = sampler.metrics(rp.timestamp)
95
- rp.absorb_metrics!(metrics)
110
+ sampler.metrics(rp.timestamp, self)
96
111
  rescue => e
97
112
  ScoutApm::Agent.instance.logger.info "Error reading #{sampler.human_name} for period: #{rp}"
98
- ScoutApm::Agent.instance.logger.debug e.message
99
- ScoutApm::Agent.instance.logger.debug e.backtrace.join("\n")
113
+ ScoutApm::Agent.instance.logger.debug "#{e.message}\n\t#{e.backtrace.join("\n\t")}"
100
114
  end
101
115
  end
102
116
  end
@@ -159,6 +173,9 @@ module ScoutApm
159
173
  # A ScoredItemSet holding the "best" traces for the period
160
174
  attr_reader :job_traces
161
175
 
176
+ # An Array of HistogramsReport
177
+ attr_reader :histograms
178
+
162
179
  # A StoreReportingPeriodTimestamp representing the time that this
163
180
  # collection of metrics is for
164
181
  attr_reader :timestamp
@@ -171,6 +188,8 @@ module ScoutApm
171
188
  @request_traces = ScoredItemSet.new
172
189
  @job_traces = ScoredItemSet.new
173
190
 
191
+ @histograms = []
192
+
174
193
  @metric_set = MetricSet.new
175
194
  @jobs = Hash.new
176
195
  end
@@ -181,7 +200,8 @@ module ScoutApm
181
200
  merge_metrics!(other.metric_set).
182
201
  merge_slow_transactions!(other.slow_transactions_payload).
183
202
  merge_jobs!(other.jobs).
184
- merge_slow_jobs!(other.slow_jobs_payload)
203
+ merge_slow_jobs!(other.slow_jobs_payload).
204
+ merge_histograms!(other.histograms)
185
205
  self
186
206
  end
187
207
 
@@ -230,6 +250,17 @@ module ScoutApm
230
250
  self
231
251
  end
232
252
 
253
+ def merge_histograms!(new_histograms)
254
+ new_histograms = Array(new_histograms)
255
+ @histograms = (histograms + new_histograms).
256
+ group_by { |histo| histo.name }.
257
+ map { |(_, histos)|
258
+ histos.inject { |merged, histo| merged.combine!(histo) }
259
+ }
260
+
261
+ self
262
+ end
263
+
233
264
  #################################
234
265
  # Retrieve Metrics for reporting
235
266
  #################################
@@ -278,7 +278,7 @@ module ScoutApm
278
278
  # If there's an instant_key, it means we need to report this right away
279
279
  if instant?
280
280
  trace = slow_converter.call
281
- ScoutApm::InstantReporting.new(trace, instant_key).call()
281
+ ScoutApm::InstantReporting.new(trace, instant_key).call
282
282
  end
283
283
  end
284
284
 
@@ -0,0 +1,24 @@
1
+ module ScoutApm
2
+ module Utils
3
+ # A simple wrapper around Ruby's built-in gzip support.
4
+ class GzipHelper
5
+ DEFAULT_GZIP_LEVEL = 5
6
+
7
+ attr_reader :level
8
+
9
+ def initialize(level = DEFAULT_GZIP_LEVEL)
10
+ @level = level
11
+ end
12
+
13
+ def deflate(str)
14
+ strio = StringIO.new
15
+
16
+ gz = Zlib::GzipWriter.new(strio, level)
17
+ gz.write str
18
+ gz.close
19
+
20
+ strio.string
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ module ScoutApm
2
+ module Utils
3
+ class Numbers
4
+
5
+ # Round a float to a certain number of decimal places
6
+ def self.round(number, decimals)
7
+ factor = 10 ** decimals
8
+
9
+ (number * factor).round / factor.to_f
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -1,3 +1,3 @@
1
1
  module ScoutApm
2
- VERSION = "3.0.0.pre1"
3
- end
2
+ VERSION = "3.0.0.pre2"
3
+ end
data/test/test_helper.rb CHANGED
@@ -83,3 +83,13 @@ class Minitest::Test
83
83
  DATA_FILE_PATH = "#{DATA_FILE_DIR}/scout_apm.db"
84
84
  end
85
85
 
86
+
87
+ module CustomAsserts
88
+ def assert_false(thing)
89
+ assert !thing
90
+ end
91
+ end
92
+
93
+ class Minitest::Test
94
+ include CustomAsserts
95
+ end
@@ -7,24 +7,24 @@ class ConfigTest < Minitest::Test
7
7
  conf = ScoutApm::Config.without_file
8
8
 
9
9
  # nil for random keys
10
- assert_nil conf.value("log_file_path")
10
+ assert_nil conf.value('log_file_path')
11
11
 
12
12
  # but has values for defaulted keys
13
- assert conf.value("host")
13
+ assert conf.value('host')
14
14
 
15
15
  # and still reads from ENV
16
16
  ENV['SCOUT_CONFIG_TEST_KEY'] = 'testval'
17
- assert_equal 'testval', conf.value("config_test_key")
17
+ assert_equal 'testval', conf.value('config_test_key')
18
18
  end
19
19
 
20
20
  def test_loading_a_file
21
- set_rack_env("production")
21
+ set_rack_env('production')
22
22
 
23
- conf_file = File.expand_path("../../data/config_test_1.yml", __FILE__)
23
+ conf_file = File.expand_path('../../data/config_test_1.yml', __FILE__)
24
24
  conf = ScoutApm::Config.with_file(conf_file)
25
25
 
26
- assert_equal "debug", conf.value('log_level')
27
- assert_equal "APM Test Conf (Production)", conf.value('name')
26
+ assert_equal 'debug', conf.value('log_level')
27
+ assert_equal 'APM Test Conf (Production)', conf.value('name')
28
28
  end
29
29
 
30
30
  def test_loading_file_without_env_in_file
@@ -68,5 +68,3 @@ class ConfigTest < Minitest::Test
68
68
  assert_equal ["a"], coercion.coerce(["a"])
69
69
  end
70
70
  end
71
-
72
-
@@ -79,6 +79,20 @@ class HistogramTest < Minitest::Test
79
79
  assert combined.quantile(0) < combined.quantile(100)
80
80
  end
81
81
 
82
+ def test_combine_dedups_identicals
83
+ hist1 = ScoutApm::NumericHistogram.new(5)
84
+ hist2 = ScoutApm::NumericHistogram.new(5)
85
+ hist1.add(1)
86
+ hist1.add(2)
87
+ hist2.add(2)
88
+ hist2.add(3)
89
+
90
+ combined = hist1.combine!(hist2)
91
+ assert_equal 4, combined.total
92
+ assert_equal [[1, 1], [2, 2], [1, 3]],
93
+ combined.bins.map{|bin| [bin.count, bin.value.to_i] }
94
+ end
95
+
82
96
  def test_mean
83
97
  hist = ScoutApm::NumericHistogram.new(5)
84
98
  10.times {
@@ -0,0 +1,137 @@
1
+ require 'test_helper'
2
+
3
+ require 'scout_apm/instruments/percentile_sampler'
4
+
5
+ class PercentileSamplerTest < Minitest::Test
6
+ PercentileSampler = ScoutApm::Instruments::PercentileSampler
7
+ HistogramReport = ScoutApm::Instruments::HistogramReport
8
+
9
+ attr_reader :subject
10
+
11
+ def setup
12
+ @subject = PercentileSampler.new(logger, histograms)
13
+ end
14
+
15
+
16
+ def test_initialize_with_logger_and_histogram_set
17
+ assert_equal subject.logger, logger
18
+ assert_equal subject.histograms, histograms
19
+ end
20
+
21
+ def test_implements_instrument_interface
22
+ assert subject.respond_to?(:human_name)
23
+ end
24
+
25
+ def test_percentiles_returns_one_percentile_per_endpoint_at_time
26
+ histograms[time].add("foo", 10)
27
+ histograms[time].add("bar", 15)
28
+ histograms[time2].add("baz", 15)
29
+
30
+ assert_equal subject.percentiles(time).length, 2
31
+ assert_equal subject.percentiles(time2).length, 1
32
+ end
33
+
34
+ def test_percentiles_clears_time_from_hash
35
+ histograms[time].add("foo", 10)
36
+ histograms[time2].add("baz", 15)
37
+
38
+ subject.percentiles(time)
39
+
40
+ assert_false histograms.key?(time)
41
+ assert histograms.key?(time + 10)
42
+ end
43
+
44
+ def test_percentiles_returns_histogram_reports
45
+ histograms[time].add("foo", 10)
46
+
47
+ assert subject.percentiles(time).
48
+ all?{ |item| item.is_a?(HistogramReport) }
49
+ end
50
+
51
+ def test_percentiles_returns_correct_histogram_report
52
+ histograms[time].add("foo", 100)
53
+ histograms[time].add("foo", 200)
54
+ histograms[time].add("foo", 100)
55
+ histograms[time].add("foo", 300)
56
+
57
+ report = subject.percentiles(time).first
58
+ histogram = report.histogram
59
+
60
+ assert_equal "foo", report.name
61
+ assert_equal 4, histogram.total
62
+ assert_equal [[2, 100], [1, 200], [1, 300]],
63
+ histogram.bins.map{|bin| [bin.count, bin.value] }
64
+ end
65
+
66
+ def test_metrics_saves_histogram_to_store
67
+ store = mock
68
+ store.expects(:track_histograms!)
69
+ subject.metrics(ScoutApm::StoreReportingPeriodTimestamp.new(time), store)
70
+ end
71
+
72
+
73
+ ################################################################################
74
+ # HistogramReport Test
75
+ ################################################################################
76
+ def test_histogram_report_combine_refuses_to_combine_mismatched_name
77
+ assert_raises { HistogramReport.new("foo", histogram).combine!(HistogramReport.new("bar", histogram)) }
78
+ end
79
+
80
+ def test_histogram_report_merge_keeps_name
81
+ report1 = HistogramReport.new("foo", histogram)
82
+ report2 = HistogramReport.new("foo", histogram)
83
+ combined = report1.combine!(report2)
84
+
85
+ assert "foo", combined.name
86
+ end
87
+
88
+ def test_histogram_report_combine_merges_histograms
89
+ histogram1 = histogram
90
+ histogram2 = histogram
91
+ histogram1.add(1)
92
+ histogram1.add(2)
93
+ histogram2.add(2)
94
+ histogram2.add(3)
95
+
96
+ report1 = HistogramReport.new("foo", histogram1)
97
+ report2 = HistogramReport.new("foo", histogram2)
98
+ combined = report1.combine!(report2)
99
+
100
+ assert_equal 4, combined.histogram.total
101
+ assert_equal [[1, 1], [2, 2], [1, 3]],
102
+ combined.histogram.bins.map{|bin| [bin.count, bin.value.to_i] }
103
+ end
104
+
105
+ ################################################################################
106
+ # Test Helpers
107
+ ################################################################################
108
+ def logger
109
+ @logger ||= begin
110
+ @logger_io = StringIO.new
111
+ Logger.new(@logger_io)
112
+ end
113
+ end
114
+
115
+ def histograms
116
+ @histograms ||= begin
117
+ @request_histograms_by_time = Hash.new { |hash, key|
118
+ hash[key] = ScoutApm::RequestHistograms.new
119
+ }
120
+ end
121
+ end
122
+
123
+ def histogram
124
+ max_bins = 20
125
+ ScoutApm::NumericHistogram.new(max_bins)
126
+ end
127
+
128
+ # An arbitrary time
129
+ def time
130
+ @time ||= Time.now
131
+ end
132
+
133
+ def time2
134
+ time + 10
135
+ end
136
+ end
137
+