scout_apm 3.0.0.pre1 → 3.0.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
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
+