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.
@@ -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
- Dir[File.expand_path('../validation/*.rb', __FILE__)].each do |filename|
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