right_support 2.7.0 → 2.8.0
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.
- data/Gemfile +1 -1
- data/Gemfile.lock +23 -21
- data/Rakefile +6 -1
- data/VERSION +1 -1
- data/lib/right_support/net/request_balancer.rb +2 -2
- data/lib/right_support/stats/activity.rb +103 -44
- data/lib/right_support/stats/exceptions.rb +54 -17
- data/lib/right_support/stats/helpers.rb +108 -125
- data/right_support.gemspec +8 -12
- data/spec/stats/activity_spec.rb +234 -122
- data/spec/stats/exceptions_spec.rb +172 -66
- data/spec/stats/helpers_spec.rb +57 -0
- metadata +25 -25
data/Gemfile
CHANGED
@@ -23,7 +23,7 @@ group :development do
|
|
23
23
|
gem "ruby-debug", ">= 0.10", :platforms => :ruby_18
|
24
24
|
gem "ruby-debug19", ">= 0.11.6", :platforms => :ruby_19
|
25
25
|
gem "rdoc", ">= 2.4.2"
|
26
|
-
gem "flexmock", "~> 0
|
26
|
+
gem "flexmock", "~> 1.0"
|
27
27
|
gem "syntax", "~> 1.0.0" #rspec will syntax-highlight code snippets if this gem is available
|
28
28
|
gem "nokogiri", "~> 1.5"
|
29
29
|
end
|
data/Gemfile.lock
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
GIT
|
2
2
|
remote: git@github.com:rightscale/right_develop.git
|
3
|
-
revision:
|
3
|
+
revision: dedba69a68e8b56cc88db7d6dc518d42747b0586
|
4
4
|
branch: master
|
5
5
|
specs:
|
6
|
-
right_develop (1.
|
6
|
+
right_develop (1.2.2)
|
7
7
|
actionpack (>= 2.3.0, < 4.0)
|
8
8
|
builder (~> 3.0)
|
9
9
|
cucumber (~> 1.0)
|
10
10
|
rake (>= 0.8.7, < 0.10)
|
11
11
|
right_support (~> 2.0)
|
12
12
|
rspec (>= 1.3, < 3.0)
|
13
|
-
trollop (
|
13
|
+
trollop (>= 1.0, < 3.0)
|
14
14
|
|
15
15
|
GEM
|
16
16
|
remote: http://rubygems.org/
|
@@ -21,16 +21,17 @@ GEM
|
|
21
21
|
activesupport (2.3.18)
|
22
22
|
addressable (2.2.8)
|
23
23
|
archive-tar-minitar (0.5.2)
|
24
|
-
builder (3.2.
|
24
|
+
builder (3.2.2)
|
25
25
|
columnize (0.3.6)
|
26
|
-
cucumber (1.
|
26
|
+
cucumber (1.3.3)
|
27
27
|
builder (>= 2.1.2)
|
28
28
|
diff-lcs (>= 1.1.3)
|
29
|
-
gherkin (~> 2.
|
30
|
-
multi_json (~> 1.
|
31
|
-
|
32
|
-
|
33
|
-
|
29
|
+
gherkin (~> 2.12.0)
|
30
|
+
multi_json (~> 1.7.5)
|
31
|
+
multi_test (~> 0.0.1)
|
32
|
+
diff-lcs (1.2.4)
|
33
|
+
flexmock (1.3.2)
|
34
|
+
gherkin (2.12.0)
|
34
35
|
multi_json (~> 1.3)
|
35
36
|
git (1.2.5)
|
36
37
|
jeweler (1.8.4)
|
@@ -46,7 +47,8 @@ GEM
|
|
46
47
|
macaddr (1.6.1)
|
47
48
|
systemu (~> 2.5.0)
|
48
49
|
mime-types (1.22)
|
49
|
-
multi_json (1.7.
|
50
|
+
multi_json (1.7.7)
|
51
|
+
multi_test (0.0.1)
|
50
52
|
net-ssh (2.6.6)
|
51
53
|
nokogiri (1.5.9)
|
52
54
|
rack (1.1.6)
|
@@ -56,15 +58,15 @@ GEM
|
|
56
58
|
json (~> 1.4)
|
57
59
|
rest-client (1.6.7)
|
58
60
|
mime-types (>= 1.16)
|
59
|
-
right_support (2.
|
60
|
-
rspec (2.
|
61
|
-
rspec-core (~> 2.
|
62
|
-
rspec-expectations (~> 2.
|
63
|
-
rspec-mocks (~> 2.
|
64
|
-
rspec-core (2.
|
65
|
-
rspec-expectations (2.
|
61
|
+
right_support (2.7.0)
|
62
|
+
rspec (2.14.0)
|
63
|
+
rspec-core (~> 2.14.0)
|
64
|
+
rspec-expectations (~> 2.14.0)
|
65
|
+
rspec-mocks (~> 2.14.0)
|
66
|
+
rspec-core (2.14.0)
|
67
|
+
rspec-expectations (2.14.0)
|
66
68
|
diff-lcs (>= 1.1.3, < 2.0)
|
67
|
-
rspec-mocks (2.
|
69
|
+
rspec-mocks (2.14.1)
|
68
70
|
ruby-debug (0.10.4)
|
69
71
|
columnize (>= 0.1)
|
70
72
|
ruby-debug-base (~> 0.10.4.0)
|
@@ -83,7 +85,7 @@ GEM
|
|
83
85
|
simple_uuid (0.3.0)
|
84
86
|
syntax (1.0.0)
|
85
87
|
systemu (2.5.2)
|
86
|
-
trollop (
|
88
|
+
trollop (2.0)
|
87
89
|
uuid (2.3.7)
|
88
90
|
macaddr (~> 1.0)
|
89
91
|
uuidtools (2.1.3)
|
@@ -94,7 +96,7 @@ PLATFORMS
|
|
94
96
|
|
95
97
|
DEPENDENCIES
|
96
98
|
addressable (~> 2.2.7)
|
97
|
-
flexmock (~> 0
|
99
|
+
flexmock (~> 1.0)
|
98
100
|
jeweler (~> 1.8.3)
|
99
101
|
net-ssh (~> 2.0)
|
100
102
|
nokogiri (~> 1.5)
|
data/Rakefile
CHANGED
@@ -45,7 +45,7 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
|
|
45
45
|
end
|
46
46
|
|
47
47
|
require 'jeweler'
|
48
|
-
Jeweler::Tasks.new do |gem|
|
48
|
+
tasks = Jeweler::Tasks.new do |gem|
|
49
49
|
# gem is a Gem::Specification; see http://docs.rubygems.org/read/chapter/20 for more options
|
50
50
|
gem.name = "right_support"
|
51
51
|
gem.homepage = "https://github.com/rightscale/right_support"
|
@@ -55,6 +55,11 @@ Jeweler::Tasks.new do |gem|
|
|
55
55
|
gem.email = "support@rightscale.com"
|
56
56
|
gem.authors = ['Tony Spataro', 'Sergey Sergyenko', 'Ryan Williamson', 'Lee Kirchhoff', 'Alexey Karpik', 'Scott Messier']
|
57
57
|
end
|
58
|
+
|
59
|
+
# Never auto-commit during operations that change the repository. Allows developers to decide on their own commit comment
|
60
|
+
# and/or aggregate version bumps into other fixes.
|
61
|
+
tasks.jeweler.commit = false
|
62
|
+
|
58
63
|
Jeweler::RubygemsDotOrgTasks.new
|
59
64
|
|
60
65
|
CLEAN.include('pkg')
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.8.0
|
@@ -233,7 +233,7 @@ module RightSupport::Net
|
|
233
233
|
loop do
|
234
234
|
if n > 0
|
235
235
|
do_retry = @options[:retry] || DEFAULT_RETRY_PROC
|
236
|
-
do_retry = do_retry.call(@ips || @endpoints, n) if do_retry.respond_to?(:call)
|
236
|
+
do_retry = do_retry.call((@ips.nil? || @ips.empty?) ? @endpoints : @ips, n) if do_retry.respond_to?(:call)
|
237
237
|
break if (do_retry.is_a?(Integer) && n >= do_retry) || [nil, false].include?(do_retry)
|
238
238
|
end
|
239
239
|
|
@@ -370,7 +370,7 @@ module RightSupport::Net
|
|
370
370
|
@resolved_hostnames = RightSupport::Net::DNS.resolve_with_hostnames(@endpoints)
|
371
371
|
resolved_endpoints = []
|
372
372
|
@resolved_hostnames.each_value{ |v| resolved_endpoints.concat(v) }
|
373
|
-
logger.info("RequestBalancer: resolved #{@endpoints.inspect} to #{resolved_endpoints.inspect}")
|
373
|
+
logger.info("RequestBalancer: resolved #{@endpoints.inspect} to #{resolved_endpoints.inspect}") if resolved_endpoints != @ips
|
374
374
|
@ips = resolved_endpoints
|
375
375
|
@policy.set_endpoints(@ips)
|
376
376
|
@resolved_at = Time.now.to_i
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2009-
|
1
|
+
# Copyright (c) 2009-2013 RightScale Inc
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining
|
4
4
|
# a copy of this software and associated documentation files (the
|
@@ -44,8 +44,7 @@ module RightSupport::Stats
|
|
44
44
|
|
45
45
|
# Initialize activity data
|
46
46
|
#
|
47
|
-
#
|
48
|
-
# measure_rate(Boolean):: Whether to measure activity rate
|
47
|
+
# @param measure_rate [Boolean] Whether to measure activity rate
|
49
48
|
def initialize(measure_rate = true)
|
50
49
|
@measure_rate = measure_rate
|
51
50
|
reset
|
@@ -53,8 +52,7 @@ module RightSupport::Stats
|
|
53
52
|
|
54
53
|
# Reset statistics
|
55
54
|
#
|
56
|
-
#
|
57
|
-
# true:: Always return true
|
55
|
+
# @return [TrueClass] Always return true
|
58
56
|
def reset
|
59
57
|
@interval = 0.0
|
60
58
|
@last_start_time = Time.now
|
@@ -67,17 +65,15 @@ module RightSupport::Stats
|
|
67
65
|
end
|
68
66
|
|
69
67
|
# Mark the start of an activity and update counts and average rate
|
70
|
-
# with weighting toward
|
68
|
+
# with weighting toward past activity
|
71
69
|
# Ignore the update if its type contains "stats"
|
72
70
|
#
|
73
|
-
#
|
74
|
-
# type(String|Symbol):: Type of activity, with anything that is not a symbol, true, or false
|
71
|
+
# @param type [String, Symbol] Type of activity, with anything that is not a symbol, true, or false
|
75
72
|
# automatically converted to a String and truncated to MAX_TYPE_SIZE characters,
|
76
73
|
# defaults to nil
|
77
|
-
# id
|
74
|
+
# @param id [String] Unique identifier associated with this activity
|
78
75
|
#
|
79
|
-
#
|
80
|
-
# now(Time):: Update time
|
76
|
+
# @return [Time] Update time
|
81
77
|
def update(type = nil, id = nil)
|
82
78
|
now = Time.now
|
83
79
|
if type.nil? || !(type =~ /stats/)
|
@@ -99,12 +95,10 @@ module RightSupport::Stats
|
|
99
95
|
|
100
96
|
# Mark the finish of an activity and update the average duration
|
101
97
|
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
# id(String):: Unique identifier associated with this activity
|
98
|
+
# @param start_time [Time] Time when activity started, defaults to last time update was called
|
99
|
+
# @param id [String] Unique identifier associated with this activity
|
105
100
|
#
|
106
|
-
#
|
107
|
-
# duration(Float):: Activity duration in seconds
|
101
|
+
# @return [Float] Activity duration in seconds
|
108
102
|
def finish(start_time = nil, id = nil)
|
109
103
|
now = Time.now
|
110
104
|
start_time ||= @last_start_time
|
@@ -116,8 +110,7 @@ module RightSupport::Stats
|
|
116
110
|
|
117
111
|
# Convert average interval to average rate
|
118
112
|
#
|
119
|
-
#
|
120
|
-
# (Float|nil):: Recent average rate, or nil if total is 0
|
113
|
+
# @return [Float, NilClass] Recent average rate, or nil if total is 0
|
121
114
|
def avg_rate
|
122
115
|
if @total > 0
|
123
116
|
if @interval == 0.0 then 0.0 else 1.0 / @interval end
|
@@ -127,19 +120,18 @@ module RightSupport::Stats
|
|
127
120
|
|
128
121
|
# Get average duration of activity
|
129
122
|
#
|
130
|
-
#
|
131
|
-
#
|
123
|
+
# @return [Float, NilClass] Average duration in seconds of activity weighted
|
124
|
+
# toward past activity, or nil if total is 0
|
132
125
|
def avg_duration
|
133
126
|
@avg_duration if @total > 0
|
134
127
|
end
|
135
128
|
|
136
129
|
# Get stats about last activity
|
137
130
|
#
|
138
|
-
#
|
139
|
-
#
|
140
|
-
# "
|
141
|
-
# "
|
142
|
-
# "active"(Boolean):: Whether activity still active
|
131
|
+
# @return [Hash, NilClass] Information about last activity, or nil if the total is 0
|
132
|
+
# "elapsed" [Integer] Seconds since last activity started
|
133
|
+
# "type" [String] Type of activity if specified, otherwise omitted
|
134
|
+
# "active" [Boolean] Whether activity still active
|
143
135
|
def last
|
144
136
|
if @total > 0
|
145
137
|
result = {"elapsed" => (Time.now - @last_start_time).to_i}
|
@@ -151,10 +143,9 @@ module RightSupport::Stats
|
|
151
143
|
|
152
144
|
# Convert count per type into percentage by type
|
153
145
|
#
|
154
|
-
#
|
155
|
-
#
|
156
|
-
# "
|
157
|
-
# "percent"(Hash):: Percentage for each type of activity if tracking type, otherwise omitted
|
146
|
+
# @return [Hash, NilClass] Converted counts, or nil if total is 0
|
147
|
+
# "total" [Integer] Total activity count
|
148
|
+
# "percent" [Hash] Percentage for each type of activity if tracking type, otherwise omitted
|
158
149
|
def percentage
|
159
150
|
if @total > 0
|
160
151
|
percent = {}
|
@@ -165,15 +156,14 @@ module RightSupport::Stats
|
|
165
156
|
|
166
157
|
# Get stat summary including all aspects of activity that were measured except duration
|
167
158
|
#
|
168
|
-
#
|
169
|
-
#
|
170
|
-
# "
|
171
|
-
# "
|
172
|
-
#
|
173
|
-
# "
|
174
|
-
# "
|
175
|
-
#
|
176
|
-
# "rate"(Float):: Recent average rate if measuring rate, otherwise omitted
|
159
|
+
# @return [Hash, NilClass] Information about activity, or nil if the total is 0
|
160
|
+
# "total" [Integer] Total activity count
|
161
|
+
# "percent" [Hash] Percentage for each type of activity if tracking type, otherwise omitted
|
162
|
+
# "last" [Hash] Information about last activity
|
163
|
+
# "elapsed" [Integer] Seconds since last activity started
|
164
|
+
# "type" [String] Type of activity if tracking type, otherwise omitted
|
165
|
+
# "active" [Boolean] Whether activity still active if tracking whether active, otherwise omitted
|
166
|
+
# "rate" [Float] Recent average rate if measuring rate, otherwise omitted
|
177
167
|
def all
|
178
168
|
if @total > 0
|
179
169
|
result = if @count_per_type.empty?
|
@@ -189,18 +179,87 @@ module RightSupport::Stats
|
|
189
179
|
|
190
180
|
protected
|
191
181
|
|
192
|
-
# Calculate smoothed average with weighting toward
|
182
|
+
# Calculate smoothed average with weighting toward past activity
|
193
183
|
#
|
194
|
-
#
|
195
|
-
#
|
196
|
-
# value(Float|Integer):: New value
|
184
|
+
# @param current [Float, Integer] Current average value
|
185
|
+
# @param value [Float, Integer] New value
|
197
186
|
#
|
198
|
-
#
|
199
|
-
# (Float):: New average
|
187
|
+
# @return [Float] New average
|
200
188
|
def average(current, value)
|
201
189
|
((current * (RECENT_SIZE - 1)) + value) / RECENT_SIZE.to_f
|
202
190
|
end
|
203
191
|
|
192
|
+
public
|
193
|
+
|
194
|
+
# Aggregate the stats from multiple 'all' calls
|
195
|
+
#
|
196
|
+
# @param stats [Array] Hashes that are to be aggregated
|
197
|
+
#
|
198
|
+
# @return [Hash, NilClass] Summed information about activity, or nil if the total is 0
|
199
|
+
# "total" [Integer] Total activity count
|
200
|
+
# "percent" [Hash] Percentage for each type of activity if tracking type, otherwise omitted
|
201
|
+
# "last" [Hash] Information about last activity
|
202
|
+
# "elapsed" [Integer] Seconds since last activity started
|
203
|
+
# "type" [String] Type of activity if tracking type, otherwise omitted
|
204
|
+
# "active" [Boolean] Whether activity still active if tracking whether active, otherwise omitted
|
205
|
+
# "rate" [Float] Recent average rate if measuring rate, otherwise omitted
|
206
|
+
def self.all(stats)
|
207
|
+
if (total = stats.inject(0) { |t, s| t += s["total"] if s && s["total"]; t }) > 0
|
208
|
+
all = percentage(stats, total)
|
209
|
+
all["last"] = last(stats.map { |s| s["last"] if s })
|
210
|
+
rate = avg_rate(stats.map { |s| {"rate" => s["rate"], "total" => s["total"]} if s }, total)
|
211
|
+
all["rate"] = rate if rate
|
212
|
+
all
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Aggregate multiple percentage stats
|
217
|
+
#
|
218
|
+
# @param stats [Array] List of stats containing "percent" hash and "total" value
|
219
|
+
# @param total [Integer] Overall total
|
220
|
+
#
|
221
|
+
# @return [Hash] Converted counts
|
222
|
+
# "total" [Integer] Total activity count
|
223
|
+
# "percent" [Hash] Percentage for each type of activity if tracking type, omitted if no data
|
224
|
+
def self.percentage(stats, total)
|
225
|
+
count_per_type = {}
|
226
|
+
stats.each do |s|
|
227
|
+
if s
|
228
|
+
t = s["total"]
|
229
|
+
s["percent"].each do |k, v|
|
230
|
+
count_per_type[k] = (count_per_type[k] || 0) + ((v / 100.0) * t).round
|
231
|
+
end if s["percent"]
|
232
|
+
end
|
233
|
+
end
|
234
|
+
if count_per_type.empty?
|
235
|
+
{"total" => total}
|
236
|
+
else
|
237
|
+
percent = {}
|
238
|
+
count_per_type.each { |k, v| percent[k] = (v / total.to_f) * 100.0 }
|
239
|
+
{"percent" => percent, "total" => total}
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Compute average rate from multiple average rates
|
244
|
+
#
|
245
|
+
# @param stats [Array] List of stats containing hash of "rate" and "total"
|
246
|
+
# @param total [Integer] Overall total
|
247
|
+
#
|
248
|
+
# @return [Fixnum, NilClass] Average rate or nil if no average rate data
|
249
|
+
def self.avg_rate(stats, total)
|
250
|
+
sum = stats.inject(nil) { |sum, stat| sum = (sum || 0.0) + (stat["rate"] * stat["total"]) if stat["rate"]; sum }
|
251
|
+
sum / total if sum
|
252
|
+
end
|
253
|
+
|
254
|
+
# Determine last activity from multiple last activity stats
|
255
|
+
#
|
256
|
+
# @param stats [Array] Multiple last activity stats
|
257
|
+
#
|
258
|
+
# @return [Hash, NilClass] Last activity, or nil if no last activity data
|
259
|
+
def self.last(stats)
|
260
|
+
stats.inject(nil) { |l, s| l = s if s && (l.nil? || (l["elapsed"] > s["elapsed"] && (s["type"] || s["active"]))); l }
|
261
|
+
end
|
262
|
+
|
204
263
|
end # Activity
|
205
264
|
|
206
265
|
end # RightScale::Stats
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2009-
|
1
|
+
# Copyright (c) 2009-2013 RightScale Inc
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining
|
4
4
|
# a copy of this software and associated documentation files (the
|
@@ -29,20 +29,19 @@ module RightSupport::Stats
|
|
29
29
|
# Maximum number of recent exceptions to track per category
|
30
30
|
MAX_RECENT_EXCEPTIONS = 10
|
31
31
|
|
32
|
-
#
|
33
|
-
# "total"
|
34
|
-
# "recent"
|
32
|
+
# [Hash] Exceptions raised per category with keys
|
33
|
+
# "total" [Integer] Total exceptions for this category
|
34
|
+
# "recent" [Array] Most recent as a hash of "count", "type", "message", "when", and "where"
|
35
35
|
attr_reader :stats
|
36
36
|
alias :all :stats
|
37
37
|
|
38
38
|
# Initialize exception data
|
39
39
|
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
# server(Server):: Server where exception occurred
|
40
|
+
# @param server [Object] Server where exceptions are originating, must be defined for callbacks
|
41
|
+
# @param callback [Proc] Block with following parameters to be activated when an exception occurs
|
42
|
+
# exception [Exception] Exception
|
43
|
+
# message [Packet] Message being processed
|
44
|
+
# server [Server] Server where exception occurred
|
46
45
|
def initialize(server = nil, callback = nil)
|
47
46
|
@server = server
|
48
47
|
@callback = callback
|
@@ -51,8 +50,7 @@ module RightSupport::Stats
|
|
51
50
|
|
52
51
|
# Reset statistics
|
53
52
|
#
|
54
|
-
#
|
55
|
-
# true:: Always return true
|
53
|
+
# @return [TrueClass] Always return true
|
56
54
|
def reset
|
57
55
|
@stats = nil
|
58
56
|
true
|
@@ -62,12 +60,10 @@ module RightSupport::Stats
|
|
62
60
|
# Catch any exceptions since this function may be called from within an EM block
|
63
61
|
# and an exception here would then derail EM
|
64
62
|
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
# exception(Exception):: Exception
|
63
|
+
# @param category [String] Exception category
|
64
|
+
# @param exception [Exception] Exception
|
68
65
|
#
|
69
|
-
#
|
70
|
-
# true:: Always return true
|
66
|
+
# @return [TrueClass] Always return true
|
71
67
|
def track(category, exception, message = nil)
|
72
68
|
begin
|
73
69
|
@callback.call(exception, message, @server) if @server && @callback && message
|
@@ -91,6 +87,47 @@ module RightSupport::Stats
|
|
91
87
|
true
|
92
88
|
end
|
93
89
|
|
90
|
+
# Aggregate the stats from multiple 'all' calls
|
91
|
+
#
|
92
|
+
# @param stats [Array] Hashes that are to be aggregated
|
93
|
+
#
|
94
|
+
# @return [Hash, NilClass] Exceptions raised per category with keys
|
95
|
+
# "total" [Integer] Total exceptions for this category
|
96
|
+
# "recent" [Array] Most recent as a hash of "count", "type", "message", "when", and "where"
|
97
|
+
def self.all(stats)
|
98
|
+
all = nil
|
99
|
+
stats.each do |s|
|
100
|
+
if s
|
101
|
+
all ||= {}
|
102
|
+
s.each do |category, exceptions|
|
103
|
+
if (all_exceptions = all[category])
|
104
|
+
all_exceptions["total"] += exceptions["total"]
|
105
|
+
all_recent = all_exceptions["recent"]
|
106
|
+
exceptions["recent"].each do |e|
|
107
|
+
i = 0
|
108
|
+
last = nil
|
109
|
+
all_recent.each do |all_e|
|
110
|
+
break if e["when"] < all_e["when"] && (last.nil? || e["when"] >= last["when"])
|
111
|
+
last = all_e
|
112
|
+
i += 1
|
113
|
+
end
|
114
|
+
if last && last["type"] == e["type"] && last["message"] == e["message"] && last["where"] == e["where"]
|
115
|
+
last["count"] += e["count"]
|
116
|
+
last["when"] = e["when"]
|
117
|
+
else
|
118
|
+
all_recent.insert(i, e)
|
119
|
+
all_recent.shift if all_recent.size > MAX_RECENT_EXCEPTIONS
|
120
|
+
end
|
121
|
+
end
|
122
|
+
else
|
123
|
+
all[category] = exceptions
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
all
|
129
|
+
end
|
130
|
+
|
94
131
|
end # Exceptions
|
95
132
|
|
96
133
|
end # RightSupport::Stats
|