scout_apm 1.2.4.pre → 1.2.4.pre1

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