right_support 1.1.2 → 1.2.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/README.rdoc +38 -14
- data/lib/right_support.rb +1 -1
- data/lib/right_support/crypto/signed_hash.rb +11 -2
- data/lib/right_support/db.rb +1 -3
- data/lib/right_support/db/cassandra_model.rb +299 -18
- data/lib/right_support/log.rb +4 -0
- data/lib/right_support/log/exception_logger.rb +86 -0
- data/lib/right_support/log/filter_logger.rb +75 -0
- data/lib/right_support/log/mixin.rb +104 -0
- data/lib/right_support/log/multiplexer.rb +93 -0
- data/lib/right_support/log/null_logger.rb +76 -0
- data/lib/right_support/net.rb +5 -3
- data/lib/right_support/net/balancing/health_check.rb +40 -23
- data/lib/right_support/net/request_balancer.rb +19 -32
- data/lib/right_support/rack.rb +2 -3
- data/lib/right_support/rack/custom_logger.rb +29 -8
- data/lib/right_support/rack/request_logger.rb +113 -0
- data/lib/right_support/ruby.rb +4 -3
- data/lib/right_support/ruby/easy_singleton.rb +24 -0
- data/lib/right_support/ruby/object_extensions.rb +18 -2
- data/lib/right_support/ruby/string_extensions.rb +120 -0
- data/lib/right_support/stats.rb +34 -0
- data/lib/right_support/stats/activity.rb +206 -0
- data/lib/right_support/stats/exceptions.rb +96 -0
- data/lib/right_support/stats/helpers.rb +438 -0
- data/lib/right_support/validation.rb +2 -4
- data/right_support.gemspec +3 -5
- metadata +18 -20
@@ -0,0 +1,438 @@
|
|
1
|
+
# Copyright (c) 2009-2012 RightScale Inc
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
# a copy of this software and associated documentation files (the
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
# the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
module RightSupport
|
23
|
+
|
24
|
+
# Helper functions that are useful when gathering or displaying statistics
|
25
|
+
module Stats
|
26
|
+
|
27
|
+
# Maximum characters in stat name
|
28
|
+
MAX_STAT_NAME_WIDTH = 11
|
29
|
+
|
30
|
+
# Maximum characters in sub-stat name
|
31
|
+
MAX_SUB_STAT_NAME_WIDTH = 17
|
32
|
+
|
33
|
+
# Maximum characters in sub-stat value line
|
34
|
+
MAX_SUB_STAT_VALUE_WIDTH = 80
|
35
|
+
|
36
|
+
# Maximum characters displayed for exception message
|
37
|
+
MAX_EXCEPTION_MESSAGE_WIDTH = 60
|
38
|
+
|
39
|
+
# Separator between stat name and stat value
|
40
|
+
SEPARATOR = " : "
|
41
|
+
|
42
|
+
# Time constants
|
43
|
+
MINUTE = 60
|
44
|
+
HOUR = 60 * MINUTE
|
45
|
+
DAY = 24 * HOUR
|
46
|
+
|
47
|
+
# Convert 0 value to nil
|
48
|
+
# This is in support of displaying "none" rather than 0
|
49
|
+
#
|
50
|
+
# === Parameters
|
51
|
+
# value(Integer|Float):: Value to be converted
|
52
|
+
#
|
53
|
+
# === Returns
|
54
|
+
# (Integer|Float|nil):: nil if value is 0, otherwise the original value
|
55
|
+
def self.nil_if_zero(value)
|
56
|
+
value == 0 ? nil : value
|
57
|
+
end
|
58
|
+
|
59
|
+
# Convert values hash into percentages
|
60
|
+
#
|
61
|
+
# === Parameters
|
62
|
+
# values(Hash):: Values to be converted whose sum is the total for calculating percentages
|
63
|
+
#
|
64
|
+
# === Return
|
65
|
+
# (Hash):: Converted values with keys "total" and "percent" with latter being a hash with values as percentages
|
66
|
+
def self.percentage(values)
|
67
|
+
total = 0
|
68
|
+
values.each_value { |v| total += v }
|
69
|
+
percent = {}
|
70
|
+
values.each { |k, v| percent[k] = (v / total.to_f) * 100.0 } if total > 0
|
71
|
+
{"percent" => percent, "total" => total}
|
72
|
+
end
|
73
|
+
|
74
|
+
# Convert elapsed time in seconds to displayable format
|
75
|
+
#
|
76
|
+
# === Parameters
|
77
|
+
# time(Integer|Float):: Elapsed time
|
78
|
+
#
|
79
|
+
# === Return
|
80
|
+
# (String):: Display string
|
81
|
+
def self.elapsed(time)
|
82
|
+
time = time.to_i
|
83
|
+
if time <= MINUTE
|
84
|
+
"#{time} sec"
|
85
|
+
elsif time <= HOUR
|
86
|
+
minutes = time / MINUTE
|
87
|
+
seconds = time - (minutes * MINUTE)
|
88
|
+
"#{minutes} min #{seconds} sec"
|
89
|
+
elsif time <= DAY
|
90
|
+
hours = time / HOUR
|
91
|
+
minutes = (time - (hours * HOUR)) / MINUTE
|
92
|
+
"#{hours} hr #{minutes} min"
|
93
|
+
else
|
94
|
+
days = time / DAY
|
95
|
+
hours = (time - (days * DAY)) / HOUR
|
96
|
+
minutes = (time - (days * DAY) - (hours * HOUR)) / MINUTE
|
97
|
+
"#{days} day#{days == 1 ? '' : 's'} #{hours} hr #{minutes} min"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Determine enough precision for floating point value(s) so that all have
|
102
|
+
# at least two significant digits and then convert each value to a decimal digit
|
103
|
+
# string of that precision after applying rounding
|
104
|
+
# When precision is wide ranging, limit precision of the larger numbers
|
105
|
+
#
|
106
|
+
# === Parameters
|
107
|
+
# value(Float|Array|Hash):: Value(s) to be converted
|
108
|
+
#
|
109
|
+
# === Return
|
110
|
+
# (String|Array|Hash):: Value(s) converted to decimal digit string
|
111
|
+
def self.enough_precision(value)
|
112
|
+
scale = [1.0, 10.0, 100.0, 1000.0, 10000.0, 100000.0]
|
113
|
+
enough = lambda { |v| (v >= 10.0 ? 0 :
|
114
|
+
(v >= 1.0 ? 1 :
|
115
|
+
(v >= 0.1 ? 2 :
|
116
|
+
(v >= 0.01 ? 3 :
|
117
|
+
(v > 0.001 ? 4 :
|
118
|
+
(v > 0.0 ? 5 : 0)))))) }
|
119
|
+
digit_str = lambda { |p, v| sprintf("%.#{p}f", (v * scale[p]).round / scale[p])}
|
120
|
+
|
121
|
+
if value.is_a?(Float)
|
122
|
+
digit_str.call(enough.call(value), value)
|
123
|
+
elsif value.is_a?(Array)
|
124
|
+
min, max = value.map { |_, v| enough.call(v) }.minmax
|
125
|
+
precision = (max - min) > 1 ? min + 1 : max
|
126
|
+
value.map { |k, v| [k, digit_str.call([precision, enough.call(v)].max, v)] }
|
127
|
+
elsif value.is_a?(Hash)
|
128
|
+
min, max = value.to_a.map { |_, v| enough.call(v) }.minmax
|
129
|
+
precision = (max - min) > 1 ? min + 1 : max
|
130
|
+
value.to_a.inject({}) { |s, v| s[v[0]] = digit_str.call([precision, enough.call(v[1])].max, v[1]); s }
|
131
|
+
else
|
132
|
+
value.to_s
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Wrap string by breaking it into lines at the specified separator
|
137
|
+
#
|
138
|
+
# === Parameters
|
139
|
+
# string(String):: String to be wrapped
|
140
|
+
# max_length(Integer):: Maximum length of a line excluding indentation
|
141
|
+
# indent(String):: Indentation for each line
|
142
|
+
# separator(String):: Separator at which to make line breaks
|
143
|
+
#
|
144
|
+
# === Return
|
145
|
+
# (String):: Multi-line string
|
146
|
+
def self.wrap(string, max_length, indent, separator)
|
147
|
+
all = []
|
148
|
+
line = ""
|
149
|
+
for l in string.split(separator)
|
150
|
+
if (line + l).length >= max_length
|
151
|
+
all.push(line)
|
152
|
+
line = ""
|
153
|
+
end
|
154
|
+
line += line == "" ? l : separator + l
|
155
|
+
end
|
156
|
+
all.push(line).join(separator + "\n" + indent)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Format UTC time value
|
160
|
+
#
|
161
|
+
# === Parameters
|
162
|
+
# time(Integer):: Time in seconds in Unix-epoch to be formatted
|
163
|
+
#
|
164
|
+
# (String):: Formatted time string
|
165
|
+
def self.time_at(time)
|
166
|
+
Time.at(time).strftime("%a %b %d %H:%M:%S")
|
167
|
+
end
|
168
|
+
|
169
|
+
# Sort hash elements by key in ascending order into array of key/value pairs
|
170
|
+
# Sort keys numerically if possible, otherwise as is
|
171
|
+
#
|
172
|
+
# === Parameters
|
173
|
+
# hash(Hash):: Data to be sorted
|
174
|
+
#
|
175
|
+
# === Return
|
176
|
+
# (Array):: Key/value pairs from hash in key sorted order
|
177
|
+
def self.sort_key(hash)
|
178
|
+
hash.to_a.map { |k, v| [k =~ /^\d+$/ ? k.to_i : k, v] }.sort
|
179
|
+
end
|
180
|
+
|
181
|
+
# Sort hash elements by value in ascending order into array of key/value pairs
|
182
|
+
#
|
183
|
+
# === Parameters
|
184
|
+
# hash(Hash):: Data to be sorted
|
185
|
+
#
|
186
|
+
# === Return
|
187
|
+
# (Array):: Key/value pairs from hash in value sorted order
|
188
|
+
def self.sort_value(hash)
|
189
|
+
hash.to_a.sort { |a, b| a[1] <=> b[1] }
|
190
|
+
end
|
191
|
+
|
192
|
+
# Converts server statistics to a displayable format
|
193
|
+
#
|
194
|
+
# === Parameters
|
195
|
+
# stats(Hash):: Statistics with generic keys "name", "identity", "hostname", "service uptime",
|
196
|
+
# "machine uptime", "stat time", "last reset time", "version", and "broker" with the
|
197
|
+
# latter two and "machine uptime" being optional; any other keys ending with "stats"
|
198
|
+
# have an associated hash value that is displayed in sorted key order
|
199
|
+
#
|
200
|
+
# === Return
|
201
|
+
# (String):: Display string
|
202
|
+
def self.stats_str(stats)
|
203
|
+
name_width = MAX_STAT_NAME_WIDTH
|
204
|
+
str = stats["name"] ? sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "name", stats["name"]) : ""
|
205
|
+
str += sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "identity", stats["identity"]) +
|
206
|
+
sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "hostname", stats["hostname"]) +
|
207
|
+
sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "stat time", time_at(stats["stat time"])) +
|
208
|
+
sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "last reset", time_at(stats["last reset time"])) +
|
209
|
+
sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "service up", elapsed(stats["service uptime"]))
|
210
|
+
str += sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "machine up", elapsed(stats["machine uptime"])) if stats.has_key?("machine uptime")
|
211
|
+
str += sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "memory KB", stats["memory"]) if stats.has_key?("memory")
|
212
|
+
str += sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "version", stats["version"].to_i) if stats.has_key?("version")
|
213
|
+
str += brokers_str(stats["brokers"], name_width) if stats.has_key?("brokers")
|
214
|
+
stats.to_a.sort.each { |k, v| str += sub_stats_str(k[0..-7], v, name_width) if k.to_s =~ /stats$/ }
|
215
|
+
str
|
216
|
+
end
|
217
|
+
|
218
|
+
# Convert broker information to displayable format
|
219
|
+
#
|
220
|
+
# === Parameter
|
221
|
+
# brokers(Hash):: Broker stats with keys
|
222
|
+
# "brokers"(Array):: Stats for each broker in priority order as hash with keys
|
223
|
+
# "alias"(String):: Broker alias
|
224
|
+
# "identity"(String):: Broker identity
|
225
|
+
# "status"(Symbol):: Status of connection
|
226
|
+
# "disconnect last"(Hash|nil):: Last disconnect information with key "elapsed", or nil if none
|
227
|
+
# "disconnects"(Integer|nil):: Number of times lost connection, or nil if none
|
228
|
+
# "failure last"(Hash|nil):: Last connect failure information with key "elapsed", or nil if none
|
229
|
+
# "failures"(Integer|nil):: Number of failed attempts to connect to broker, or nil if none
|
230
|
+
# "retries"(Integer|nil):: Number of attempts to connect after failure, or nil if none
|
231
|
+
# "exceptions"(Hash|nil):: Exceptions raised per category, or nil if none
|
232
|
+
# "total"(Integer):: Total exceptions for this category
|
233
|
+
# "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
|
234
|
+
# "heartbeat"(Integer|nil):: Number of seconds between AMQP heartbeats, or nil if heartbeat disabled
|
235
|
+
# "returns"(Hash|nil):: Message return activity stats with keys "total", "percent", "last", and "rate"
|
236
|
+
# with percentage breakdown per request type, or nil if none
|
237
|
+
# name_width(Integer):: Fixed width for left-justified name display
|
238
|
+
#
|
239
|
+
# === Return
|
240
|
+
# str(String):: Broker display with one line per broker plus exceptions
|
241
|
+
def self.brokers_str(brokers, name_width)
|
242
|
+
value_indent = " " * (name_width + SEPARATOR.size)
|
243
|
+
sub_name_width = MAX_SUB_STAT_NAME_WIDTH
|
244
|
+
sub_value_indent = " " * (name_width + sub_name_width + (SEPARATOR.size * 2))
|
245
|
+
str = sprintf("%-#{name_width}s#{SEPARATOR}", "brokers")
|
246
|
+
brokers["brokers"].each do |b|
|
247
|
+
disconnects = if b["disconnects"]
|
248
|
+
"#{b["disconnects"]} (#{elapsed(b["disconnect last"]["elapsed"])} ago)"
|
249
|
+
else
|
250
|
+
"none"
|
251
|
+
end
|
252
|
+
failures = if b["failures"]
|
253
|
+
retries = b["retries"]
|
254
|
+
retries = " w/ #{retries} #{retries != 1 ? 'retries' : 'retry'}" if retries
|
255
|
+
"#{b["failures"]} (#{elapsed(b["failure last"]["elapsed"])} ago#{retries})"
|
256
|
+
else
|
257
|
+
"none"
|
258
|
+
end
|
259
|
+
str += "#{b["alias"]}: #{b["identity"]} #{b["status"]}, disconnects: #{disconnects}, failures: #{failures}\n"
|
260
|
+
str += value_indent
|
261
|
+
end
|
262
|
+
str += sprintf("%-#{sub_name_width}s#{SEPARATOR}", "exceptions")
|
263
|
+
str += if brokers["exceptions"].nil? || brokers["exceptions"].empty?
|
264
|
+
"none\n"
|
265
|
+
else
|
266
|
+
exceptions_str(brokers["exceptions"], sub_value_indent) + "\n"
|
267
|
+
end
|
268
|
+
str += value_indent
|
269
|
+
str += sprintf("%-#{sub_name_width}s#{SEPARATOR}", "heartbeat")
|
270
|
+
str += if [nil, 0].include?(brokers["heartbeat"])
|
271
|
+
"none\n"
|
272
|
+
else
|
273
|
+
"#{brokers["heartbeat"]} sec\n"
|
274
|
+
end
|
275
|
+
str += value_indent
|
276
|
+
str += sprintf("%-#{sub_name_width}s#{SEPARATOR}", "returns")
|
277
|
+
str += if brokers["returns"].nil? || brokers["returns"].empty?
|
278
|
+
"none\n"
|
279
|
+
else
|
280
|
+
wrap(activity_str(brokers["returns"]), MAX_SUB_STAT_VALUE_WIDTH, sub_value_indent, ", ") + "\n"
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Convert grouped set of statistics to displayable format
|
285
|
+
# Provide special formatting for stats named "exceptions"
|
286
|
+
# Break out percentages and total count for stats containing "percent" hash value
|
287
|
+
# sorted in descending percent order and followed by total count
|
288
|
+
# Convert to elapsed time for stats with name ending in "last"
|
289
|
+
# Add "/sec" to values with name ending in "rate"
|
290
|
+
# Add " sec" to values with name ending in "time"
|
291
|
+
# Add "%" to values with name ending in "percent" and drop "percent" from name
|
292
|
+
# Use elapsed time formatting for values with name ending in "age"
|
293
|
+
# Display any nil value, empty hash, or hash with a "total" value of 0 as "none"
|
294
|
+
# Display any floating point value or hash of values with at least two significant digits of precision
|
295
|
+
#
|
296
|
+
# === Parameters
|
297
|
+
# name(String):: Display name for the stat
|
298
|
+
# value(Object):: Value of this stat
|
299
|
+
# name_width(Integer):: Fixed width for left-justified name display
|
300
|
+
#
|
301
|
+
# === Return
|
302
|
+
# (String):: Single line display of stat
|
303
|
+
def self.sub_stats_str(name, value, name_width)
|
304
|
+
value_indent = " " * (name_width + SEPARATOR.size)
|
305
|
+
sub_name_width = MAX_SUB_STAT_NAME_WIDTH
|
306
|
+
sub_value_indent = " " * (name_width + sub_name_width + (SEPARATOR.size * 2))
|
307
|
+
sprintf("%-#{name_width}s#{SEPARATOR}", name) + value.to_a.sort.map do |attr|
|
308
|
+
k, v = attr
|
309
|
+
name = k =~ /percent$/ ? k[0..-9] : k
|
310
|
+
sprintf("%-#{sub_name_width}s#{SEPARATOR}", name) + if v.is_a?(Float) || v.is_a?(Integer)
|
311
|
+
str = k =~ /age$/ ? elapsed(v) : enough_precision(v)
|
312
|
+
str += "/sec" if k =~ /rate$/
|
313
|
+
str += " sec" if k =~ /time$/
|
314
|
+
str += "%" if k =~ /percent$/
|
315
|
+
str
|
316
|
+
elsif v.is_a?(Hash)
|
317
|
+
if v.empty? || v["total"] == 0
|
318
|
+
"none"
|
319
|
+
elsif v["total"]
|
320
|
+
wrap(activity_str(v), MAX_SUB_STAT_VALUE_WIDTH, sub_value_indent, ", ")
|
321
|
+
elsif k =~ /last$/
|
322
|
+
last_activity_str(v)
|
323
|
+
elsif k == "exceptions"
|
324
|
+
exceptions_str(v, sub_value_indent)
|
325
|
+
else
|
326
|
+
wrap(hash_str(v), MAX_SUB_STAT_VALUE_WIDTH, sub_value_indent, ", ")
|
327
|
+
end
|
328
|
+
else
|
329
|
+
"#{v || "none"}"
|
330
|
+
end + "\n"
|
331
|
+
end.join(value_indent)
|
332
|
+
end
|
333
|
+
|
334
|
+
# Convert activity information to displayable format
|
335
|
+
#
|
336
|
+
# === Parameters
|
337
|
+
# value(Hash|nil):: Information about activity, or nil if the total is 0
|
338
|
+
# "total"(Integer):: Total activity count
|
339
|
+
# "percent"(Hash):: Percentage for each type of activity if tracking type, otherwise omitted
|
340
|
+
# "last"(Hash):: Information about last activity
|
341
|
+
# "elapsed"(Integer):: Seconds since last activity started
|
342
|
+
# "type"(String):: Type of activity if tracking type, otherwise omitted
|
343
|
+
# "active"(Boolean):: Whether activity still active if tracking whether active, otherwise omitted
|
344
|
+
# "rate"(Float):: Recent average rate if measuring rate, otherwise omitted
|
345
|
+
# "duration"(Float):: Average duration of activity if tracking duration, otherwise omitted
|
346
|
+
#
|
347
|
+
# === Return
|
348
|
+
# str(String):: Activity stats in displayable format without any line separators
|
349
|
+
def self.activity_str(value)
|
350
|
+
str = ""
|
351
|
+
str += enough_precision(sort_value(value["percent"]).reverse).map { |k, v| "#{k}: #{v}%" }.join(", ") +
|
352
|
+
", total: " if value["percent"]
|
353
|
+
str += "#{value['total']}"
|
354
|
+
str += ", last: #{last_activity_str(value['last'], single_item = true)}" if value["last"]
|
355
|
+
str += ", rate: #{enough_precision(value['rate'])}/sec" if value["rate"]
|
356
|
+
str += ", duration: #{enough_precision(value['duration'])} sec" if value["duration"]
|
357
|
+
value.each do |name, data|
|
358
|
+
unless ["total", "percent", "last", "rate", "duration"].include?(name)
|
359
|
+
str += ", #{name}: #{data.is_a?(String) ? data : data.inspect}"
|
360
|
+
end
|
361
|
+
end
|
362
|
+
str
|
363
|
+
end
|
364
|
+
|
365
|
+
# Convert last activity information to displayable format
|
366
|
+
#
|
367
|
+
# === Parameters
|
368
|
+
# last(Hash):: Information about last activity
|
369
|
+
# "elapsed"(Integer):: Seconds since last activity started
|
370
|
+
# "type"(String):: Type of activity if tracking type, otherwise omitted
|
371
|
+
# "active"(Boolean):: Whether activity still active if tracking whether active, otherwise omitted
|
372
|
+
# single_item:: Whether this is to appear as a single item in a comma-separated list
|
373
|
+
# in which case there should be no ':' in the formatted string
|
374
|
+
#
|
375
|
+
# === Return
|
376
|
+
# str(String):: Last activity in displayable format without any line separators
|
377
|
+
def self.last_activity_str(last, single_item = false)
|
378
|
+
str = "#{elapsed(last['elapsed'])} ago"
|
379
|
+
str += " and still active" if last["active"]
|
380
|
+
if last["type"]
|
381
|
+
if single_item
|
382
|
+
str = "#{last['type']} (#{str})"
|
383
|
+
else
|
384
|
+
str = "#{last['type']}: #{str}"
|
385
|
+
end
|
386
|
+
end
|
387
|
+
str
|
388
|
+
end
|
389
|
+
|
390
|
+
# Convert exception information to displayable format
|
391
|
+
#
|
392
|
+
# === Parameters
|
393
|
+
# exceptions(Hash):: Exceptions raised per category
|
394
|
+
# "total"(Integer):: Total exceptions for this category
|
395
|
+
# "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
|
396
|
+
# indent(String):: Indentation for each line
|
397
|
+
#
|
398
|
+
# === Return
|
399
|
+
# (String):: Exceptions in displayable format with line separators
|
400
|
+
def self.exceptions_str(exceptions, indent)
|
401
|
+
indent2 = indent + (" " * 4)
|
402
|
+
exceptions.to_a.sort.map do |k, v|
|
403
|
+
sprintf("%s total: %d, most recent:\n", k, v["total"]) + v["recent"].reverse.map do |e|
|
404
|
+
message = e["message"]
|
405
|
+
if message && message.size > (MAX_EXCEPTION_MESSAGE_WIDTH - 3)
|
406
|
+
message = e["message"][0, MAX_EXCEPTION_MESSAGE_WIDTH - 3] + "..."
|
407
|
+
end
|
408
|
+
indent + "(#{e["count"]}) #{time_at(e["when"])} #{e["type"]}: #{message}\n" + indent2 + "#{e["where"]}"
|
409
|
+
end.join("\n")
|
410
|
+
end.join("\n" + indent)
|
411
|
+
end
|
412
|
+
|
413
|
+
# Convert arbitrary nested hash to displayable format
|
414
|
+
# Sort hash by key, numerically if possible, otherwise as is
|
415
|
+
# Display any floating point values with one decimal place precision
|
416
|
+
# Display any empty values as "none"
|
417
|
+
#
|
418
|
+
# === Parameters
|
419
|
+
# hash(Hash):: Hash to be displayed
|
420
|
+
#
|
421
|
+
# === Return
|
422
|
+
# (String):: Single line hash display
|
423
|
+
def self.hash_str(hash)
|
424
|
+
str = ""
|
425
|
+
sort_key(hash).map do |k, v|
|
426
|
+
"#{k}: " + if v.is_a?(Float)
|
427
|
+
enough_precision(v)
|
428
|
+
elsif v.is_a?(Hash)
|
429
|
+
"[ " + hash_str(v) + " ]"
|
430
|
+
else
|
431
|
+
"#{v || "none"}"
|
432
|
+
end
|
433
|
+
end.join(", ")
|
434
|
+
end
|
435
|
+
|
436
|
+
end # Stats
|
437
|
+
|
438
|
+
end # RightSupport
|
@@ -34,10 +34,8 @@ module RightSupport
|
|
34
34
|
|
35
35
|
end
|
36
36
|
end
|
37
|
-
|
38
|
-
|
39
|
-
require filename
|
40
|
-
end
|
37
|
+
require 'right_support/validation/openssl'
|
38
|
+
require 'right_support/validation/ssh'
|
41
39
|
|
42
40
|
RightSupport::Validation.constants.each do |const|
|
43
41
|
const = RightSupport::Validation.const_get(const) #string to constant
|