right_support 1.1.2 → 1.2.0

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