scout_apm 1.2.4.pre → 1.2.4.pre1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fa64d725755deae00527598cd770bf40a773fcf8
4
- data.tar.gz: 17b868c65b2b6bd7749fb14dd7164a773248a26b
3
+ metadata.gz: f7457a6c1ecec53bd454c6e9ccc2d21fc21d6c61
4
+ data.tar.gz: 46579e9b5c9e9b948c9828d5552df54275d92d07
5
5
  SHA512:
6
- metadata.gz: 0ba12e4e11843ef25e3005d51c6e7e9f0d38a3db2a15f3c2e5bce014bc7ceaadf68e33f1b5443535cb938936232800755e5b20f3e21e3183d26598062fc9f018
7
- data.tar.gz: eaf8da676edae1147d935594e7ffdfa070bfcbd7fcf3c7bce18c835262a9e98121ae21e5fd916bd391325f4d85f8fcc4975b6f18949b7dac54e246b8d24a9f8d
6
+ metadata.gz: 915274645119dc9afabe6ce236774a19c41db1464f82f2d393541b29dfe8f27ce3d12bcb0087aca6bfdd5079420772341b7c1a7679b05062982d4ef7df78e462
7
+ data.tar.gz: b760b06a1cf2b6c9a29d9543af9784888016b324d328dd19ec6d36585d6f0066976018d34939d4966ed2ffbfc77fd9f964c9579dfa12e1a7fefe9397e8dec514
data/CHANGELOG.markdown CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  * Removing layaway file validation in main thread
4
4
  * Fixing :force so agent will start in tests
5
+ * Rate-limiting slow transactions to 10 per-reporting period
5
6
 
6
7
  # 1.2.3
7
8
 
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
1
  require "bundler/gem_tasks"
2
2
 
3
3
  task :default => :test
4
+
5
+ desc "Run Unit Tests"
4
6
  task :test do
5
7
  $: << File.expand_path(File.dirname(__FILE__) + "/test")
6
8
  Dir.glob('./test/**/*_test.rb').each { |file| require file }
data/lib/scout_apm.rb CHANGED
@@ -98,6 +98,7 @@ require 'scout_apm/context'
98
98
  require 'scout_apm/stackprof_tree_collapser'
99
99
  require 'scout_apm/slow_transaction'
100
100
  require 'scout_apm/slow_request_policy'
101
+ require 'scout_apm/slow_transaction_set'
101
102
  require 'scout_apm/capacity'
102
103
  require 'scout_apm/attribute_arranger'
103
104
 
@@ -65,7 +65,15 @@ module ScoutApm
65
65
  select { |meta,stats| meta.metric_name =~ /\AController/ }.
66
66
  inject(0) {|sum, (_, stat)| sum + stat.call_count }
67
67
 
68
- logger.info "[#{Time.parse(metadata[:agent_time]).strftime("%H:%M")}] Delivering #{metrics.length} Metrics for #{total_request_count} requests and #{slow_transactions.length} Slow Transaction Traces"
68
+ memory = metrics.
69
+ find {|meta,stats| meta.metric_name =~ /\AMemory/ }
70
+ process_log_str = if memory
71
+ "Recorded from #{memory.last.call_count} processes"
72
+ else
73
+ "Recorded across (unknown) processes"
74
+ end
75
+
76
+ logger.info "[#{Time.parse(metadata[:agent_time]).strftime("%H:%M")}] Delivering #{metrics.length} Metrics for #{total_request_count} requests and #{slow_transactions.length} Slow Transaction Traces, #{process_log_str}."
69
77
  logger.debug("Metrics: #{metrics.pretty_inspect}\nSlowTrans: #{slow_transactions.pretty_inspect}\nMetadata: #{metadata.inspect.pretty_inspect}")
70
78
  end
71
79
 
@@ -86,42 +86,42 @@ module ScoutApm
86
86
  end
87
87
  end
88
88
 
89
- # Take a TrackedRequest and turn it into either nil, or a SlowTransaction record
89
+ # Take a TrackedRequest and turn it into a slow transaction if needed
90
+ # return a 2 element array, [ Slow Transaction or Nil , Hash of metrics to store ]
90
91
  class LayerSlowTransactionConverter < LayerConverterBase
91
92
  def call
92
93
  policy = ScoutApm::Agent.instance.slow_request_policy.capture_type(root_layer.total_call_time)
93
94
 
94
- if policy == ScoutApm::SlowRequestPolicy::CAPTURE_NONE
95
- return nil
95
+ case policy
96
+ when ScoutApm::SlowRequestPolicy::CAPTURE_SUMMARY
97
+ return [nil, {}]
98
+ when ScoutApm::SlowRequestPolicy::CAPTURE_NONE
99
+ return [nil, {}]
96
100
  end
97
101
 
98
-
99
102
  scope = scope_layer
100
- return nil unless scope
103
+ return [nil, {}] unless scope
104
+
101
105
  uri = request.annotations[:uri] || ""
102
106
 
103
- metrics = case policy
104
- when ScoutApm::SlowRequestPolicy::CAPTURE_SUMMARY
105
- {}
106
- when ScoutApm::SlowRequestPolicy::CAPTURE_DETAIL
107
- create_metrics
108
- end
109
-
110
- stackprof = case policy
111
- when ScoutApm::SlowRequestPolicy::CAPTURE_SUMMARY
112
- []
113
- when ScoutApm::SlowRequestPolicy::CAPTURE_DETAIL
114
- # Disable stackprof output for now
115
- [] # request.stackprof
116
- end
117
-
118
- SlowTransaction.new(uri,
119
- scope.legacy_metric_name,
120
- root_layer.total_call_time,
121
- metrics,
122
- request.context,
123
- root_layer.stop_time,
124
- stackprof)
107
+ metrics = create_metrics
108
+ # Disable stackprof output for now
109
+ stackprof = [] # request.stackprof
110
+
111
+ meta = MetricMeta.new("SlowTransaction/#{scope_layer.legacy_metric_name}")
112
+ stat = MetricStats.new
113
+ stat.update!(1)
114
+
115
+ [
116
+ SlowTransaction.new(uri,
117
+ scope.legacy_metric_name,
118
+ root_layer.total_call_time,
119
+ metrics,
120
+ request.context,
121
+ root_layer.stop_time,
122
+ stackprof),
123
+ { meta => stat }
124
+ ]
125
125
 
126
126
  end
127
127
 
@@ -1,9 +1,8 @@
1
- # Long running class that provides a yes/no answer to see if a transaction counts as a "slow transaction"
1
+ # Long running class that determines if, and in how much detail a potentially
2
+ # slow transaction should be recorded in
2
3
  #
3
4
  # Rules:
4
5
  # - Runtime must be slower than a threshold
5
- # - Log detailed metrics only for the first X
6
- # - Stop logging anything after a maximum
7
6
 
8
7
  module ScoutApm
9
8
  class SlowRequestPolicy
@@ -16,68 +15,15 @@ module ScoutApm
16
15
  # It's not slow unless it's at least this slow
17
16
  SLOW_REQUEST_TIME_THRESHOLD = 2.0 # seconds
18
17
 
19
- # Stop recording detailed metrics after this count. Still record the fact
20
- # a slow request happened though
21
- MAX_DETAIL_PER_MINUTE = 10
22
-
23
- # Stop recording anything after this number of slow transactions in a
24
- # minute. Will also log a message once per minute that it is stopping
25
- # recording.
26
- MAX_PER_MINUTE = 500
27
-
28
- def initialize
29
- @minute_count = 0
30
- @detailed_count = 0 # How many detailed slow transactions have we captured this minute?
31
- @minute = Time.now.min
32
- @clipped_recording = false
33
- end
34
-
35
18
  def capture_type(time)
36
- reset_counters
37
-
38
19
  return CAPTURE_NONE unless slow_enough?(time)
39
- return CAPTURE_NONE if clip_recording?
40
-
41
- @minute_count += 1
42
-
43
- if @detailed_count < MAX_DETAIL_PER_MINUTE
44
- @detailed_count += 1
45
- return CAPTURE_DETAIL
46
- else
47
- return CAPTURE_SUMMARY
48
- end
49
-
20
+ return CAPTURE_DETAIL
50
21
  end
51
22
 
52
23
  private
53
24
 
54
- def reset_counters
55
- t = Time.now.min
56
- return if t == @minute
57
-
58
- @minute_count = 0
59
- @detailed_count = 0
60
- @minute = t
61
- @clipped_recording = false
62
- end
63
-
64
25
  def slow_enough?(time)
65
26
  time > SLOW_REQUEST_TIME_THRESHOLD
66
27
  end
67
-
68
- # Breaker for rapid-fire excessive slow requests.
69
- # If we trip this breaker continually, it will log once per minute that it is broken
70
- def clip_recording?
71
- if @minute_count > MAX_PER_MINUTE
72
- if !@clipped_recording
73
- ScoutApm::Agent.instance.logger.info("Skipping future slow requests this minute, reached limit of #{MAX_PER_MINUTE}")
74
- end
75
-
76
- @clipped_recording = true
77
- true
78
- else
79
- false
80
- end
81
- end
82
28
  end
83
29
  end
@@ -0,0 +1,67 @@
1
+ # In order to keep load down, only record a sample of Slow Transactions. In
2
+ # order to make that sampling as fair as possible, follow a basic algorithm:
3
+ #
4
+ # When adding a new SlowTransaction:
5
+ # * Just add it if there is an open spot
6
+ # * If there isn't an open spot, attempt to remove an over-represented
7
+ # endpoint instead ("attempt_to_evict"). Overrepresented is simply "has more
8
+ # than @fair number of SlowTransactions for that end point"
9
+ # * If there isn't an open spot, and nobody is valid to evict, drop the
10
+ # incoming SlowTransaction without adding.
11
+ #
12
+ # There is no way to remove SlowTransactions from this set, create a new object
13
+ # for each reporting period.
14
+ module ScoutApm
15
+ class SlowTransactionSet
16
+ include Enumerable
17
+
18
+ DEFAULT_TOTAL = 10
19
+ DEFAULT_FAIR = 1
20
+
21
+ attr_reader :total, :fair
22
+
23
+ def initialize(total=DEFAULT_TOTAL, fair=DEFAULT_FAIR)
24
+ @total = total
25
+ @fair = fair
26
+ @slow_transactions = []
27
+ end
28
+
29
+ def each
30
+ @slow_transactions.each { |s| yield s }
31
+ end
32
+
33
+ def <<(slow_transaction)
34
+ return if attempt_append(slow_transaction)
35
+ attempt_to_evict
36
+ attempt_append(slow_transaction)
37
+ end
38
+
39
+ def empty_slot?
40
+ @slow_transactions.length < total
41
+ end
42
+
43
+ def attempt_append(slow_transaction)
44
+ if empty_slot?
45
+ @slow_transactions.push(slow_transaction)
46
+ true
47
+ else
48
+ false
49
+ end
50
+ end
51
+
52
+ def attempt_to_evict
53
+ return if @slow_transactions.length == 0
54
+
55
+ overrepresented = @slow_transactions.
56
+ group_by { |st| st.metric_name }.
57
+ to_a.
58
+ sort_by { |(_, sts)| sts.length }.
59
+ last
60
+
61
+ if overrepresented[1].length > fair
62
+ fastest = overrepresented[1].sort_by { |st| st.total_call_time }.first
63
+ @slow_transactions.delete(fastest)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -11,7 +11,8 @@ module ScoutApm
11
11
  begin
12
12
  ScoutApm::Agent.instance.logger.debug("StackProf - Samples: #{raw_stackprof[:samples]}, GC: #{raw_stackprof[:gc_samples]}, missed: #{raw_stackprof[:missed_samples]}, Interval: #{raw_stackprof[:interval]}")
13
13
  rescue
14
- ScoutApm::Agent.instance.logger.debug("StackProf Raw - #{raw_stackprof.inspect}")
14
+ # Not a useful log message, currently stackprof is disabled
15
+ # ScoutApm::Agent.instance.logger.debug("StackProf Raw - #{raw_stackprof.inspect}")
15
16
  end
16
17
  end
17
18
  end
@@ -31,6 +31,7 @@ module ScoutApm
31
31
 
32
32
  # Save a new slow transaction
33
33
  def track_slow_transaction!(slow_transaction)
34
+ return unless slow_transaction
34
35
  @mutex.synchronize {
35
36
  reporting_periods[current_timestamp].merge_slow_transactions!(slow_transaction)
36
37
  }
@@ -91,7 +92,7 @@ module ScoutApm
91
92
  def initialize(timestamp)
92
93
  @timestamp = timestamp
93
94
 
94
- @slow_transactions = Array.new
95
+ @slow_transactions = SlowTransactionSet.new
95
96
  @aggregate_metrics = Hash.new
96
97
  end
97
98
 
@@ -103,9 +104,11 @@ module ScoutApm
103
104
  self
104
105
  end
105
106
 
106
- def merge_slow_transactions!(slow_transactions)
107
- @slow_transactions += Array(slow_transactions)
108
- trim_slow_transaction_metrics
107
+ def merge_slow_transactions!(new_transactions)
108
+ Array(new_transactions).each do |one_transaction|
109
+ @slow_transactions << one_transaction
110
+ end
111
+
109
112
  self
110
113
  end
111
114
 
@@ -117,7 +120,7 @@ module ScoutApm
117
120
  end
118
121
 
119
122
  def slow_transactions_payload
120
- @slow_transactions
123
+ @slow_transactions.to_a
121
124
  end
122
125
 
123
126
  private
@@ -139,7 +142,7 @@ module ScoutApm
139
142
 
140
143
  # We can't aggregate CPU, Memory, Capacity, or Controller, so pass through these metrics directly
141
144
  # TODO: Figure out a way to not have this duplicate what's in Samplers, and also on server's ingest
142
- PASSTHROUGH_METRICS = ["CPU", "Memory", "Instance", "Controller"]
145
+ PASSTHROUGH_METRICS = ["CPU", "Memory", "Instance", "Controller", "SlowTransaction"]
143
146
 
144
147
  # Absorbs a single new metric into the aggregates
145
148
  def absorb(metric)
@@ -130,8 +130,9 @@ module ScoutApm
130
130
  metrics = LayerMetricConverter.new(self).call
131
131
  ScoutApm::Agent.instance.store.track!(metrics)
132
132
 
133
- slow = LayerSlowTransactionConverter.new(self).call
133
+ slow, slow_metrics = LayerSlowTransactionConverter.new(self).call
134
134
  ScoutApm::Agent.instance.store.track_slow_transaction!(slow)
135
+ ScoutApm::Agent.instance.store.track!(slow_metrics)
135
136
 
136
137
  error_metrics = LayerErrorConverter.new(self).call
137
138
  ScoutApm::Agent.instance.store.track!(error_metrics)
@@ -1,4 +1,4 @@
1
1
  module ScoutApm
2
- VERSION = "1.2.4.pre"
2
+ VERSION = "1.2.4.pre1"
3
3
  end
4
4
 
@@ -13,7 +13,7 @@ class LayawayTest < Minitest::Test
13
13
  data = ScoutApm::Layaway.new
14
14
  t = ScoutApm::StoreReportingPeriodTimestamp.new
15
15
  data.add_reporting_period(t,ScoutApm::StoreReportingPeriod.new(t))
16
- assert_equal [TIMESTAMP,t], Marshal.load(File.read(DATA_FILE_PATH)).keys
16
+ assert_equal [TIMESTAMP,t].sort_by(&:timestamp), Marshal.load(File.read(DATA_FILE_PATH)).keys.sort_by(&:timestamp)
17
17
  end
18
18
 
19
19
  def test_merge_reporting_period
@@ -0,0 +1,93 @@
1
+ require 'test_helper'
2
+
3
+ require 'scout_apm/slow_transaction_set'
4
+ require 'scout_apm/slow_transaction'
5
+
6
+ class SlowTransactionSetTest < Minitest::Test
7
+ def test_adding_to_empty_set
8
+ set = ScoutApm::SlowTransactionSet.new(3, 1)
9
+ set << make_slow("Controller/Foo")
10
+ assert_equal 1, set.count
11
+ end
12
+
13
+ def test_adding_to_partially_full_set
14
+ set = ScoutApm::SlowTransactionSet.new(3, 1)
15
+ set << make_slow("Controller/Foo")
16
+ set << make_slow("Controller/Foo")
17
+ assert_equal 2, set.count
18
+ end
19
+
20
+ def test_overflow_of_one_type
21
+ max_size = 3
22
+ set = ScoutApm::SlowTransactionSet.new(max_size, 1)
23
+ set << make_slow("Controller/Foo")
24
+ set << make_slow("Controller/Foo")
25
+ set << make_slow("Controller/Foo")
26
+ set << make_slow("Controller/Foo")
27
+ set << make_slow("Controller/Foo")
28
+ set << make_slow("Controller/Foo")
29
+ assert_equal max_size, set.count
30
+ end
31
+
32
+ def test_eviction_of_overrepresented
33
+ max_size = 3
34
+ set = ScoutApm::SlowTransactionSet.new(max_size, 1)
35
+ set << make_slow("Controller/Foo")
36
+ set << make_slow("Controller/Foo")
37
+ set << make_slow("Controller/Foo")
38
+ set << make_slow("Controller/Foo")
39
+ set << make_slow("Controller/Foo")
40
+ set << make_slow("Controller/Bar")
41
+
42
+ # 3 total
43
+ assert_equal max_size, set.count
44
+ assert_equal 1, set.select{|sl| sl.metric_name == "Controller/Bar"}.length
45
+ assert_equal 2, set.select{|sl| sl.metric_name == "Controller/Foo"}.length
46
+ end
47
+
48
+ # Fill the set with /Foo records, then add a /Bar to evict. Check that the
49
+ # evicted one was the fastest of the Foos
50
+ def test_eviction_of_fastest
51
+ max_size = 3
52
+ set = ScoutApm::SlowTransactionSet.new(max_size, 1)
53
+
54
+ [1,2,3].shuffle.each do |seconds| # Shuffle to remove any assumptions on order
55
+ set << make_slow("Controller/Foo", seconds)
56
+ end
57
+ set << make_slow("Controller/Bar", 8)
58
+
59
+ # The foo taking 1 second should be evicted
60
+ assert_equal 2, set.select{|sl| sl.metric_name == "Controller/Foo"}.map{ |sl| sl.total_call_time}.min
61
+ end
62
+
63
+ def test_eviction_when_no_overrepresented
64
+ max_size = 4
65
+ fair = 2
66
+ set = ScoutApm::SlowTransactionSet.new(max_size, fair)
67
+
68
+ # Full, but each is at fair level
69
+ set << make_slow("Controller/Bar")
70
+ set << make_slow("Controller/Bar")
71
+ set << make_slow("Controller/Foo")
72
+ set << make_slow("Controller/Foo")
73
+
74
+ set << make_slow("Controller/Quux")
75
+ assert_equal max_size, set.count
76
+ assert_equal 0, set.select{|sl| sl.metric_name == "Controller/Quux" }.length
77
+ end
78
+
79
+ ##############
80
+ #### Helpers
81
+ ##############
82
+
83
+ def make_slow(metric, time=5)
84
+ ScoutApm::SlowTransaction.new(
85
+ "http://foo.app/#{metric}",
86
+ metric,
87
+ time,
88
+ {}, # metrics
89
+ {}, # context
90
+ Time.now, # end time
91
+ []) # stackprof
92
+ end
93
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_apm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.4.pre
4
+ version: 1.2.4.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Haynes
@@ -127,6 +127,7 @@ files:
127
127
  - lib/scout_apm/server_integrations/webrick.rb
128
128
  - lib/scout_apm/slow_request_policy.rb
129
129
  - lib/scout_apm/slow_transaction.rb
130
+ - lib/scout_apm/slow_transaction_set.rb
130
131
  - lib/scout_apm/stack_item.rb
131
132
  - lib/scout_apm/stackprof_tree_collapser.rb
132
133
  - lib/scout_apm/store.rb
@@ -150,6 +151,7 @@ files:
150
151
  - test/unit/instruments/active_record_instruments_test.rb
151
152
  - test/unit/layaway_test.rb
152
153
  - test/unit/serializers/payload_serializer_test.rb
154
+ - test/unit/slow_transaction_set_test.rb
153
155
  - test/unit/sql_sanitizer_test.rb
154
156
  homepage: https://github.com/scoutapp/scout_apm_ruby
155
157
  licenses: []
@@ -171,7 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
171
173
  version: 1.3.1
172
174
  requirements: []
173
175
  rubyforge_project: scout_apm
174
- rubygems_version: 2.4.8
176
+ rubygems_version: 2.2.2
175
177
  signing_key:
176
178
  specification_version: 4
177
179
  summary: Ruby application performance monitoring
@@ -184,4 +186,5 @@ test_files:
184
186
  - test/unit/instruments/active_record_instruments_test.rb
185
187
  - test/unit/layaway_test.rb
186
188
  - test/unit/serializers/payload_serializer_test.rb
189
+ - test/unit/slow_transaction_set_test.rb
187
190
  - test/unit/sql_sanitizer_test.rb