right_agent 0.6.6 → 0.9.3

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.
Files changed (46) hide show
  1. data/lib/right_agent/agent.rb +26 -25
  2. data/lib/right_agent/agent_config.rb +28 -2
  3. data/lib/right_agent/command/command_constants.rb +2 -2
  4. data/lib/right_agent/core_payload_types/executable_bundle.rb +3 -21
  5. data/lib/right_agent/core_payload_types/login_user.rb +19 -4
  6. data/lib/right_agent/core_payload_types/recipe_instantiation.rb +7 -1
  7. data/lib/right_agent/core_payload_types/right_script_instantiation.rb +7 -1
  8. data/lib/right_agent/dispatcher.rb +6 -19
  9. data/lib/right_agent/idempotent_request.rb +72 -17
  10. data/lib/right_agent/monkey_patches/ruby_patch.rb +0 -1
  11. data/lib/right_agent/monkey_patches.rb +0 -1
  12. data/lib/right_agent/operation_result.rb +27 -4
  13. data/lib/right_agent/packets.rb +47 -23
  14. data/lib/right_agent/platform/darwin.rb +33 -2
  15. data/lib/right_agent/platform/linux.rb +98 -2
  16. data/lib/right_agent/platform/windows.rb +41 -6
  17. data/lib/right_agent/platform.rb +11 -2
  18. data/lib/right_agent/scripts/agent_controller.rb +2 -1
  19. data/lib/right_agent/scripts/agent_deployer.rb +2 -2
  20. data/lib/right_agent/scripts/stats_manager.rb +7 -3
  21. data/lib/right_agent/sender.rb +45 -28
  22. data/lib/right_agent.rb +2 -5
  23. data/right_agent.gemspec +5 -3
  24. data/spec/agent_config_spec.rb +1 -1
  25. data/spec/agent_spec.rb +26 -20
  26. data/spec/core_payload_types/login_user_spec.rb +7 -3
  27. data/spec/idempotent_request_spec.rb +218 -48
  28. data/spec/operation_result_spec.rb +19 -0
  29. data/spec/packets_spec.rb +42 -1
  30. data/spec/platform/darwin.rb +11 -0
  31. data/spec/platform/linux.rb +23 -0
  32. data/spec/platform/linux_volume_manager_spec.rb +43 -43
  33. data/spec/platform/platform_spec.rb +35 -32
  34. data/spec/platform/windows.rb +11 -0
  35. data/spec/sender_spec.rb +21 -25
  36. metadata +47 -40
  37. data/lib/right_agent/broker_client.rb +0 -686
  38. data/lib/right_agent/ha_broker_client.rb +0 -1327
  39. data/lib/right_agent/monkey_patches/amqp_patch.rb +0 -274
  40. data/lib/right_agent/monkey_patches/ruby_patch/string_patch.rb +0 -107
  41. data/lib/right_agent/stats_helper.rb +0 -745
  42. data/spec/broker_client_spec.rb +0 -962
  43. data/spec/ha_broker_client_spec.rb +0 -1695
  44. data/spec/monkey_patches/amqp_patch_spec.rb +0 -100
  45. data/spec/monkey_patches/string_patch_spec.rb +0 -99
  46. data/spec/stats_helper_spec.rb +0 -686
@@ -1,745 +0,0 @@
1
- # Copyright (c) 2009-2011 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 RightScale
23
-
24
- # Mixin for collecting and displaying operational statistics for servers
25
- module StatsHelper
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
- # Track activity statistics
48
- class ActivityStats
49
-
50
- # Number of samples included when calculating average recent activity
51
- # with the smoothing formula A = ((A * (RECENT_SIZE - 1)) + V) / RECENT_SIZE,
52
- # where A is the current recent average and V is the new activity value
53
- # As a rough guide, it takes approximately 2 * RECENT_SIZE activity values
54
- # at value V for average A to reach 90% of the original difference between A and V
55
- # For example, for A = 0, V = 1, RECENT_SIZE = 3 the progression for A is
56
- # 0, 0.3, 0.5, 0.7, 0.8, 0.86, 0.91, 0.94, 0.96, 0.97, 0.98, 0.99, ...
57
- RECENT_SIZE = 3
58
-
59
- # Maximum string length for activity type
60
- MAX_TYPE_SIZE = 60
61
-
62
- # (Integer) Total activity count
63
- attr_reader :total
64
-
65
- # (Hash) Count of activity per type
66
- attr_reader :count_per_type
67
-
68
- # Initialize activity data
69
- #
70
- # === Parameters
71
- # measure_rate(Boolean):: Whether to measure activity rate
72
- def initialize(measure_rate = true)
73
- @measure_rate = measure_rate
74
- reset
75
- end
76
-
77
- # Reset statistics
78
- #
79
- # === Return
80
- # true:: Always return true
81
- def reset
82
- @interval = 0.0
83
- @last_start_time = Time.now
84
- @avg_duration = nil
85
- @total = 0
86
- @count_per_type = {}
87
- @last_type = nil
88
- @last_id = nil
89
- true
90
- end
91
-
92
- # Mark the start of an activity and update counts and average rate
93
- # with weighting toward recent activity
94
- # Ignore the update if its type contains "stats"
95
- #
96
- # === Parameters
97
- # type(String|Symbol):: Type of activity, with anything that is not a symbol, true, or false
98
- # automatically converted to a String and truncated to MAX_TYPE_SIZE characters,
99
- # defaults to nil
100
- # id(String):: Unique identifier associated with this activity
101
- #
102
- # === Return
103
- # now(Time):: Update time
104
- def update(type = nil, id = nil)
105
- now = Time.now
106
- if type.nil? || !(type =~ /stats/)
107
- @interval = average(@interval, now - @last_start_time) if @measure_rate
108
- @last_start_time = now
109
- @total += 1
110
- unless type.nil?
111
- unless [Symbol, TrueClass, FalseClass].include?(type.class)
112
- type = type.inspect unless type.is_a?(String)
113
- type = type[0, MAX_TYPE_SIZE - 3] + "..." if type.size > (MAX_TYPE_SIZE - 3)
114
- end
115
- @count_per_type[type] = (@count_per_type[type] || 0) + 1
116
- end
117
- @last_type = type
118
- @last_id = id
119
- end
120
- now
121
- end
122
-
123
- # Mark the finish of an activity and update the average duration
124
- #
125
- # === Parameters
126
- # start_time(Time):: Time when activity started, defaults to last time update was called
127
- # id(String):: Unique identifier associated with this activity
128
- #
129
- # === Return
130
- # duration(Float):: Activity duration in seconds
131
- def finish(start_time = nil, id = nil)
132
- now = Time.now
133
- start_time ||= @last_start_time
134
- duration = now - start_time
135
- @avg_duration = average(@avg_duration || 0.0, duration)
136
- @last_id = 0 if id && id == @last_id
137
- duration
138
- end
139
-
140
- # Convert average interval to average rate
141
- #
142
- # === Return
143
- # (Float|nil):: Recent average rate, or nil if total is 0
144
- def avg_rate
145
- if @total > 0
146
- if @interval == 0.0 then 0.0 else 1.0 / @interval end
147
- end
148
- end
149
-
150
-
151
- # Get average duration of activity
152
- #
153
- # === Return
154
- # (Float|nil) Average duration in seconds of activity weighted toward recent activity, or nil if total is 0
155
- def avg_duration
156
- @avg_duration if @total > 0
157
- end
158
-
159
- # Get stats about last activity
160
- #
161
- # === Return
162
- # (Hash|nil):: Information about last activity, or nil if the total is 0
163
- # "elapsed"(Integer):: Seconds since last activity started
164
- # "type"(String):: Type of activity if specified, otherwise omitted
165
- # "active"(Boolean):: Whether activity still active
166
- def last
167
- if @total > 0
168
- result = {"elapsed" => (Time.now - @last_start_time).to_i}
169
- result["type"] = @last_type if @last_type
170
- result["active"] = @last_id != 0 if !@last_id.nil?
171
- result
172
- end
173
- end
174
-
175
- # Convert count per type into percentage by type
176
- #
177
- # === Return
178
- # (Hash|nil):: Converted counts, or nil if total is 0
179
- # "total"(Integer):: Total activity count
180
- # "percent"(Hash):: Percentage for each type of activity if tracking type, otherwise omitted
181
- def percentage
182
- if @total > 0
183
- percent = {}
184
- @count_per_type.each { |k, v| percent[k] = (v / @total.to_f) * 100.0 }
185
- {"percent" => percent, "total" => @total}
186
- end
187
- end
188
-
189
- # Get stat summary including all aspects of activity that were measured except duration
190
- #
191
- # === Return
192
- # (Hash|nil):: Information about activity, or nil if the total is 0
193
- # "total"(Integer):: Total activity count
194
- # "percent"(Hash):: Percentage for each type of activity if tracking type, otherwise omitted
195
- # "last"(Hash):: Information about last activity
196
- # "elapsed"(Integer):: Seconds since last activity started
197
- # "type"(String):: Type of activity if tracking type, otherwise omitted
198
- # "active"(Boolean):: Whether activity still active if tracking whether active, otherwise omitted
199
- # "rate"(Float):: Recent average rate if measuring rate, otherwise omitted
200
- def all
201
- if @total > 0
202
- result = if @count_per_type.empty?
203
- {"total" => @total}
204
- else
205
- percentage
206
- end
207
- result.merge!("last" => last)
208
- result.merge!("rate" => avg_rate) if @measure_rate
209
- result
210
- end
211
- end
212
-
213
- protected
214
-
215
- # Calculate smoothed average with weighting toward recent activity
216
- #
217
- # === Parameters
218
- # current(Float|Integer):: Current average value
219
- # value(Float|Integer):: New value
220
- #
221
- # === Return
222
- # (Float):: New average
223
- def average(current, value)
224
- ((current * (RECENT_SIZE - 1)) + value) / RECENT_SIZE.to_f
225
- end
226
-
227
- end # ActivityStats
228
-
229
- # Track exception statistics
230
- class ExceptionStats
231
-
232
- # Maximum number of recent exceptions to track per category
233
- MAX_RECENT_EXCEPTIONS = 10
234
-
235
- # (Hash) Exceptions raised per category with keys
236
- # "total"(Integer):: Total exceptions for this category
237
- # "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
238
- attr_reader :stats
239
- alias :all :stats
240
-
241
- # Initialize exception data
242
- #
243
- # === Parameters
244
- # server(Object):: Server where exceptions are originating, must be defined for callbacks
245
- # callback(Proc):: Block with following parameters to be activated when an exception occurs
246
- # exception(Exception):: Exception
247
- # message(Packet):: Message being processed
248
- # server(Server):: Server where exception occurred
249
- def initialize(server = nil, callback = nil)
250
- @server = server
251
- @callback = callback
252
- reset
253
- end
254
-
255
- # Reset statistics
256
- #
257
- # === Return
258
- # true:: Always return true
259
- def reset
260
- @stats = nil
261
- true
262
- end
263
-
264
- # Track exception statistics and optionally make callback to report exception
265
- # Catch any exceptions since this function may be called from within an EM block
266
- # and an exception here would then derail EM
267
- #
268
- # === Parameters
269
- # category(String):: Exception category
270
- # exception(Exception):: Exception
271
- #
272
- # === Return
273
- # true:: Always return true
274
- def track(category, exception, message = nil)
275
- begin
276
- @callback.call(exception, message, @server) if @server && @callback && message
277
- @stats ||= {}
278
- exceptions = (@stats[category] ||= {"total" => 0, "recent" => []})
279
- exceptions["total"] += 1
280
- recent = exceptions["recent"]
281
- last = recent.last
282
- if last && last["type"] == exception.class.name && last["message"] == exception.message && last["where"] == exception.backtrace.first
283
- last["count"] += 1
284
- last["when"] = Time.now.to_i
285
- else
286
- backtrace = exception.backtrace.first if exception.backtrace
287
- recent.shift if recent.size >= MAX_RECENT_EXCEPTIONS
288
- recent.push({"count" => 1, "when" => Time.now.to_i, "type" => exception.class.name,
289
- "message" => exception.message, "where" => backtrace})
290
- end
291
- rescue Exception => e
292
- Log.error("Failed to track exception '#{exception}' due to: #{e}\n" + e.backtrace.join("\n")) rescue nil
293
- end
294
- true
295
- end
296
-
297
- end # ExceptionStats
298
-
299
- # Utility functions that are useful on there own
300
- class Utilities
301
-
302
- # Convert values hash into percentages
303
- #
304
- # === Parameters
305
- # values(Hash):: Values to be converted whose sum is the total for calculating percentages
306
- #
307
- # === Return
308
- # (Hash):: Converted values with keys "total" and "percent" with latter being a hash with values as percentages
309
- def self.percentage(values)
310
- total = 0
311
- values.each_value { |v| total += v }
312
- percent = {}
313
- values.each { |k, v| percent[k] = (v / total.to_f) * 100.0 } if total > 0
314
- {"percent" => percent, "total" => total}
315
- end
316
-
317
- # Convert elapsed time in seconds to displayable format
318
- #
319
- # === Parameters
320
- # time(Integer|Float):: Elapsed time
321
- #
322
- # === Return
323
- # (String):: Display string
324
- def self.elapsed(time)
325
- time = time.to_i
326
- if time <= MINUTE
327
- "#{time} sec"
328
- elsif time <= HOUR
329
- minutes = time / MINUTE
330
- seconds = time - (minutes * MINUTE)
331
- "#{minutes} min #{seconds} sec"
332
- elsif time <= DAY
333
- hours = time / HOUR
334
- minutes = (time - (hours * HOUR)) / MINUTE
335
- "#{hours} hr #{minutes} min"
336
- else
337
- days = time / DAY
338
- hours = (time - (days * DAY)) / HOUR
339
- minutes = (time - (days * DAY) - (hours * HOUR)) / MINUTE
340
- "#{days} day#{days == 1 ? '' : 's'} #{hours} hr #{minutes} min"
341
- end
342
- end
343
-
344
- # Determine enough precision for floating point value(s) so that all have
345
- # at least two significant digits and then convert each value to a decimal digit
346
- # string of that precision after applying rounding
347
- # When precision is wide ranging, limit precision of the larger numbers
348
- #
349
- # === Parameters
350
- # value(Float|Array|Hash):: Value(s) to be converted
351
- #
352
- # === Return
353
- # (String|Array|Hash):: Value(s) converted to decimal digit string
354
- def self.enough_precision(value)
355
- scale = [1.0, 10.0, 100.0, 1000.0, 10000.0, 100000.0]
356
- enough = lambda { |v| (v >= 10.0 ? 0 :
357
- (v >= 1.0 ? 1 :
358
- (v >= 0.1 ? 2 :
359
- (v >= 0.01 ? 3 :
360
- (v > 0.001 ? 4 :
361
- (v > 0.0 ? 5 : 0)))))) }
362
- digit_str = lambda { |p, v| sprintf("%.#{p}f", (v * scale[p]).round / scale[p])}
363
-
364
- if value.is_a?(Float)
365
- digit_str.call(enough.call(value), value)
366
- elsif value.is_a?(Array)
367
- min, max = value.map { |_, v| enough.call(v) }.minmax
368
- precision = (max - min) > 1 ? min + 1 : max
369
- value.map { |k, v| [k, digit_str.call([precision, enough.call(v)].max, v)] }
370
- elsif value.is_a?(Hash)
371
- min, max = value.to_a.map { |_, v| enough.call(v) }.minmax
372
- precision = (max - min) > 1 ? min + 1 : max
373
- value.to_a.inject({}) { |s, v| s[v[0]] = digit_str.call([precision, enough.call(v[1])].max, v[1]); s }
374
- else
375
- value.to_s
376
- end
377
- end
378
-
379
- # Wrap string by breaking it into lines at the specified separator
380
- #
381
- # === Parameters
382
- # string(String):: String to be wrapped
383
- # max_length(Integer):: Maximum length of a line excluding indentation
384
- # indent(String):: Indentation for each line
385
- # separator(String):: Separator at which to make line breaks
386
- #
387
- # === Return
388
- # (String):: Multi-line string
389
- def self.wrap(string, max_length, indent, separator)
390
- all = []
391
- line = ""
392
- for l in string.split(separator)
393
- if (line + l).length >= max_length
394
- all.push(line)
395
- line = ""
396
- end
397
- line += line == "" ? l : separator + l
398
- end
399
- all.push(line).join(separator + "\n" + indent)
400
- end
401
-
402
- end
403
-
404
- # Convert 0 value to nil
405
- # This is in support of displaying "none" rather than 0
406
- #
407
- # === Parameters
408
- # value(Integer|Float):: Value to be converted
409
- #
410
- # === Returns
411
- # (Integer|Float|nil):: nil if value is 0, otherwise the original value
412
- def nil_if_zero(value)
413
- value == 0 ? nil : value
414
- end
415
-
416
- # Convert values hash into percentages
417
- #
418
- # === Parameters
419
- # values(Hash):: Values to be converted whose sum is the total for calculating percentages
420
- #
421
- # === Return
422
- # (Hash):: Converted values with keys "total" and "percent" with latter being a hash with values as percentages
423
- def percentage(values)
424
- Utilities.percentage(values)
425
- end
426
-
427
- # Determine enough precision for floating point value(s) so that all have
428
- # at least two significant digits and then convert each value to a decimal digit
429
- # string of that precision after applying rounding
430
- # When precision is wide ranging, limit precision of the larger numbers
431
- #
432
- # === Parameters
433
- # value(Float|Array|Hash):: Value(s) to be converted
434
- #
435
- # === Return
436
- # (String|Array|Hash):: Value(s) converted to decimal digit string
437
- def enough_precision(value)
438
- Utilities.enough_precision(value)
439
- end
440
-
441
- # Wrap string by breaking it into lines at the specified separator
442
- #
443
- # === Parameters
444
- # string(String):: String to be wrapped
445
- # max_length(Integer):: Maximum length of a line excluding indentation
446
- # indent(String):: Indentation for each line
447
- # separator(String):: Separator at which to make line breaks
448
- #
449
- # === Return
450
- # (String):: Multi-line string
451
- def wrap(string, max_length, indent, separator)
452
- Utilities.wrap(string, max_length, indent, separator)
453
- end
454
-
455
- # Convert elapsed time in seconds to displayable format
456
- #
457
- # === Parameters
458
- # time(Integer|Float):: Elapsed time
459
- #
460
- # === Return
461
- # (String):: Display string
462
- def elapsed(time)
463
- Utilities.elapsed(time)
464
- end
465
-
466
- # Format UTC time value
467
- #
468
- # === Parameters
469
- # time(Integer):: Time in seconds in Unix-epoch to be formatted
470
- #
471
- # (String):: Formatted time string
472
- def time_at(time)
473
- Time.at(time).strftime("%a %b %d %H:%M:%S")
474
- end
475
-
476
- # Sort hash elements by key in ascending order into array of key/value pairs
477
- # Sort keys numerically if possible, otherwise as is
478
- #
479
- # === Parameters
480
- # hash(Hash):: Data to be sorted
481
- #
482
- # === Return
483
- # (Array):: Key/value pairs from hash in key sorted order
484
- def sort_key(hash)
485
- hash.to_a.map { |k, v| [k =~ /^\d+$/ ? k.to_i : k, v] }.sort
486
- end
487
-
488
- # Sort hash elements by value in ascending order into array of key/value pairs
489
- #
490
- # === Parameters
491
- # hash(Hash):: Data to be sorted
492
- #
493
- # === Return
494
- # (Array):: Key/value pairs from hash in value sorted order
495
- def sort_value(hash)
496
- hash.to_a.sort { |a, b| a[1] <=> b[1] }
497
- end
498
-
499
- # Converts server statistics to a displayable format
500
- #
501
- # === Parameters
502
- # stats(Hash):: Statistics with generic keys "name", "identity", "hostname", "service uptime",
503
- # "machine uptime", "stat time", "last reset time", "version", and "broker" with the
504
- # latter two and "machine uptime" being optional; any other keys ending with "stats"
505
- # have an associated hash value that is displayed in sorted key order
506
- #
507
- # === Return
508
- # (String):: Display string
509
- def stats_str(stats)
510
- name_width = MAX_STAT_NAME_WIDTH
511
- str = stats["name"] ? sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "name", stats["name"]) : ""
512
- str += sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "identity", stats["identity"]) +
513
- sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "hostname", stats["hostname"]) +
514
- sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "stat time", time_at(stats["stat time"])) +
515
- sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "last reset", time_at(stats["last reset time"])) +
516
- sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "service up", elapsed(stats["service uptime"]))
517
- str += sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "machine up", elapsed(stats["machine uptime"])) if stats.has_key?("machine uptime")
518
- str += sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "memory KB", stats["memory"]) if stats.has_key?("memory")
519
- str += sprintf("%-#{name_width}s#{SEPARATOR}%s\n", "version", stats["version"].to_i) if stats.has_key?("version")
520
- str += brokers_str(stats["brokers"], name_width) if stats.has_key?("brokers")
521
- stats.to_a.sort.each { |k, v| str += sub_stats_str(k[0..-7], v, name_width) if k.to_s =~ /stats$/ }
522
- str
523
- end
524
-
525
- # Convert broker information to displayable format
526
- #
527
- # === Parameter
528
- # brokers(Hash):: Broker stats with keys
529
- # "brokers"(Array):: Stats for each broker in priority order as hash with keys
530
- # "alias"(String):: Broker alias
531
- # "identity"(String):: Broker identity
532
- # "status"(Symbol):: Status of connection
533
- # "disconnect last"(Hash|nil):: Last disconnect information with key "elapsed", or nil if none
534
- # "disconnects"(Integer|nil):: Number of times lost connection, or nil if none
535
- # "failure last"(Hash|nil):: Last connect failure information with key "elapsed", or nil if none
536
- # "failures"(Integer|nil):: Number of failed attempts to connect to broker, or nil if none
537
- # "retries"(Integer|nil):: Number of attempts to connect after failure, or nil if none
538
- # "exceptions"(Hash|nil):: Exceptions raised per category, or nil if none
539
- # "total"(Integer):: Total exceptions for this category
540
- # "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
541
- # "heartbeat"(Integer|nil):: Number of seconds between AMQP heartbeats, or nil if heartbeat disabled
542
- # "returns"(Hash|nil):: Message return activity stats with keys "total", "percent", "last", and "rate"
543
- # with percentage breakdown per request type, or nil if none
544
- # name_width(Integer):: Fixed width for left-justified name display
545
- #
546
- # === Return
547
- # str(String):: Broker display with one line per broker plus exceptions
548
- def brokers_str(brokers, name_width)
549
- value_indent = " " * (name_width + SEPARATOR.size)
550
- sub_name_width = MAX_SUB_STAT_NAME_WIDTH
551
- sub_value_indent = " " * (name_width + sub_name_width + (SEPARATOR.size * 2))
552
- str = sprintf("%-#{name_width}s#{SEPARATOR}", "brokers")
553
- brokers["brokers"].each do |b|
554
- disconnects = if b["disconnects"]
555
- "#{b["disconnects"]} (#{elapsed(b["disconnect last"]["elapsed"])} ago)"
556
- else
557
- "none"
558
- end
559
- failures = if b["failures"]
560
- retries = b["retries"]
561
- retries = " w/ #{retries} #{retries != 1 ? 'retries' : 'retry'}" if retries
562
- "#{b["failures"]} (#{elapsed(b["failure last"]["elapsed"])} ago#{retries})"
563
- else
564
- "none"
565
- end
566
- str += "#{b["alias"]}: #{b["identity"]} #{b["status"]}, disconnects: #{disconnects}, failures: #{failures}\n"
567
- str += value_indent
568
- end
569
- str += sprintf("%-#{sub_name_width}s#{SEPARATOR}", "exceptions")
570
- str += if brokers["exceptions"].nil? || brokers["exceptions"].empty?
571
- "none\n"
572
- else
573
- exceptions_str(brokers["exceptions"], sub_value_indent) + "\n"
574
- end
575
- str += value_indent
576
- str += sprintf("%-#{sub_name_width}s#{SEPARATOR}", "heartbeat")
577
- str += if [nil, 0].include?(brokers["heartbeat"])
578
- "none\n"
579
- else
580
- "#{brokers["heartbeat"]} sec\n"
581
- end
582
- str += value_indent
583
- str += sprintf("%-#{sub_name_width}s#{SEPARATOR}", "returns")
584
- str += if brokers["returns"].nil? || brokers["returns"].empty?
585
- "none\n"
586
- else
587
- wrap(activity_str(brokers["returns"]), MAX_SUB_STAT_VALUE_WIDTH, sub_value_indent, ", ") + "\n"
588
- end
589
- end
590
-
591
- # Convert grouped set of statistics to displayable format
592
- # Provide special formatting for stats named "exceptions"
593
- # Break out percentages and total count for stats containing "percent" hash value
594
- # sorted in descending percent order and followed by total count
595
- # Convert to elapsed time for stats with name ending in "last"
596
- # Add "/sec" to values with name ending in "rate"
597
- # Add " sec" to values with name ending in "time"
598
- # Add "%" to values with name ending in "percent" and drop "percent" from name
599
- # Use elapsed time formatting for values with name ending in "age"
600
- # Display any nil value, empty hash, or hash with a "total" value of 0 as "none"
601
- # Display any floating point value or hash of values with at least two significant digits of precision
602
- #
603
- # === Parameters
604
- # name(String):: Display name for the stat
605
- # value(Object):: Value of this stat
606
- # name_width(Integer):: Fixed width for left-justified name display
607
- #
608
- # === Return
609
- # (String):: Single line display of stat
610
- def sub_stats_str(name, value, name_width)
611
- value_indent = " " * (name_width + SEPARATOR.size)
612
- sub_name_width = MAX_SUB_STAT_NAME_WIDTH
613
- sub_value_indent = " " * (name_width + sub_name_width + (SEPARATOR.size * 2))
614
- sprintf("%-#{name_width}s#{SEPARATOR}", name) + value.to_a.sort.map do |attr|
615
- k, v = attr
616
- name = k =~ /percent$/ ? k[0..-9] : k
617
- sprintf("%-#{sub_name_width}s#{SEPARATOR}", name) + if v.is_a?(Float) || v.is_a?(Integer)
618
- str = k =~ /age$/ ? elapsed(v) : enough_precision(v)
619
- str += "/sec" if k =~ /rate$/
620
- str += " sec" if k =~ /time$/
621
- str += "%" if k =~ /percent$/
622
- str
623
- elsif v.is_a?(Hash)
624
- if v.empty? || v["total"] == 0
625
- "none"
626
- elsif v["total"]
627
- wrap(activity_str(v), MAX_SUB_STAT_VALUE_WIDTH, sub_value_indent, ", ")
628
- elsif k =~ /last$/
629
- last_activity_str(v)
630
- elsif k == "exceptions"
631
- exceptions_str(v, sub_value_indent)
632
- else
633
- wrap(hash_str(v), MAX_SUB_STAT_VALUE_WIDTH, sub_value_indent, ", ")
634
- end
635
- else
636
- "#{v || "none"}"
637
- end + "\n"
638
- end.join(value_indent)
639
- end
640
-
641
- # Convert activity information to displayable format
642
- #
643
- # === Parameters
644
- # value(Hash|nil):: Information about activity, or nil if the total is 0
645
- # "total"(Integer):: Total activity count
646
- # "percent"(Hash):: Percentage for each type of activity if tracking type, otherwise omitted
647
- # "last"(Hash):: Information about last activity
648
- # "elapsed"(Integer):: Seconds since last activity started
649
- # "type"(String):: Type of activity if tracking type, otherwise omitted
650
- # "active"(Boolean):: Whether activity still active if tracking whether active, otherwise omitted
651
- # "rate"(Float):: Recent average rate if measuring rate, otherwise omitted
652
- # "duration"(Float):: Average duration of activity if tracking duration, otherwise omitted
653
- #
654
- # === Return
655
- # str(String):: Activity stats in displayable format without any line separators
656
- def activity_str(value)
657
- str = ""
658
- str += enough_precision(sort_value(value["percent"]).reverse).map { |k, v| "#{k}: #{v}%" }.join(", ") +
659
- ", total: " if value["percent"]
660
- str += "#{value['total']}"
661
- str += ", last: #{last_activity_str(value['last'], single_item = true)}" if value["last"]
662
- str += ", rate: #{enough_precision(value['rate'])}/sec" if value["rate"]
663
- str += ", duration: #{enough_precision(value['duration'])} sec" if value["duration"]
664
- value.each do |name, data|
665
- unless ["total", "percent", "last", "rate", "duration"].include?(name)
666
- str += ", #{name}: #{data.is_a?(String) ? data : data.inspect}"
667
- end
668
- end
669
- str
670
- end
671
-
672
- # Convert last activity information to displayable format
673
- #
674
- # === Parameters
675
- # last(Hash):: Information about last activity
676
- # "elapsed"(Integer):: Seconds since last activity started
677
- # "type"(String):: Type of activity if tracking type, otherwise omitted
678
- # "active"(Boolean):: Whether activity still active if tracking whether active, otherwise omitted
679
- # single_item:: Whether this is to appear as a single item in a comma-separated list
680
- # in which case there should be no ':' in the formatted string
681
- #
682
- # === Return
683
- # str(String):: Last activity in displayable format without any line separators
684
- def last_activity_str(last, single_item = false)
685
- str = "#{elapsed(last['elapsed'])} ago"
686
- str += " and still active" if last["active"]
687
- if last["type"]
688
- if single_item
689
- str = "#{last['type']} (#{str})"
690
- else
691
- str = "#{last['type']}: #{str}"
692
- end
693
- end
694
- str
695
- end
696
-
697
- # Convert exception information to displayable format
698
- #
699
- # === Parameters
700
- # exceptions(Hash):: Exceptions raised per category
701
- # "total"(Integer):: Total exceptions for this category
702
- # "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
703
- # indent(String):: Indentation for each line
704
- #
705
- # === Return
706
- # (String):: Exceptions in displayable format with line separators
707
- def exceptions_str(exceptions, indent)
708
- indent2 = indent + (" " * 4)
709
- exceptions.to_a.sort.map do |k, v|
710
- sprintf("%s total: %d, most recent:\n", k, v["total"]) + v["recent"].reverse.map do |e|
711
- message = e["message"]
712
- if message && message.size > (MAX_EXCEPTION_MESSAGE_WIDTH - 3)
713
- message = e["message"][0, MAX_EXCEPTION_MESSAGE_WIDTH - 3] + "..."
714
- end
715
- indent + "(#{e["count"]}) #{time_at(e["when"])} #{e["type"]}: #{message}\n" + indent2 + "#{e["where"]}"
716
- end.join("\n")
717
- end.join("\n" + indent)
718
- end
719
-
720
- # Convert arbitrary nested hash to displayable format
721
- # Sort hash by key, numerically if possible, otherwise as is
722
- # Display any floating point values with one decimal place precision
723
- # Display any empty values as "none"
724
- #
725
- # === Parameters
726
- # hash(Hash):: Hash to be displayed
727
- #
728
- # === Return
729
- # (String):: Single line hash display
730
- def hash_str(hash)
731
- str = ""
732
- sort_key(hash).map do |k, v|
733
- "#{k}: " + if v.is_a?(Float)
734
- enough_precision(v)
735
- elsif v.is_a?(Hash)
736
- "[ " + hash_str(v) + " ]"
737
- else
738
- "#{v || "none"}"
739
- end
740
- end.join(", ")
741
- end
742
-
743
- end # StatsHelper
744
-
745
- end # RightScale