right_agent 0.6.6 → 0.9.3

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