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.
- data/lib/right_agent/agent.rb +26 -25
- data/lib/right_agent/agent_config.rb +28 -2
- data/lib/right_agent/command/command_constants.rb +2 -2
- data/lib/right_agent/core_payload_types/executable_bundle.rb +3 -21
- data/lib/right_agent/core_payload_types/login_user.rb +19 -4
- data/lib/right_agent/core_payload_types/recipe_instantiation.rb +7 -1
- data/lib/right_agent/core_payload_types/right_script_instantiation.rb +7 -1
- data/lib/right_agent/dispatcher.rb +6 -19
- data/lib/right_agent/idempotent_request.rb +72 -17
- data/lib/right_agent/monkey_patches/ruby_patch.rb +0 -1
- data/lib/right_agent/monkey_patches.rb +0 -1
- data/lib/right_agent/operation_result.rb +27 -4
- data/lib/right_agent/packets.rb +47 -23
- data/lib/right_agent/platform/darwin.rb +33 -2
- data/lib/right_agent/platform/linux.rb +98 -2
- data/lib/right_agent/platform/windows.rb +41 -6
- data/lib/right_agent/platform.rb +11 -2
- data/lib/right_agent/scripts/agent_controller.rb +2 -1
- data/lib/right_agent/scripts/agent_deployer.rb +2 -2
- data/lib/right_agent/scripts/stats_manager.rb +7 -3
- data/lib/right_agent/sender.rb +45 -28
- data/lib/right_agent.rb +2 -5
- data/right_agent.gemspec +5 -3
- data/spec/agent_config_spec.rb +1 -1
- data/spec/agent_spec.rb +26 -20
- data/spec/core_payload_types/login_user_spec.rb +7 -3
- data/spec/idempotent_request_spec.rb +218 -48
- data/spec/operation_result_spec.rb +19 -0
- data/spec/packets_spec.rb +42 -1
- data/spec/platform/darwin.rb +11 -0
- data/spec/platform/linux.rb +23 -0
- data/spec/platform/linux_volume_manager_spec.rb +43 -43
- data/spec/platform/platform_spec.rb +35 -32
- data/spec/platform/windows.rb +11 -0
- data/spec/sender_spec.rb +21 -25
- metadata +47 -40
- data/lib/right_agent/broker_client.rb +0 -686
- data/lib/right_agent/ha_broker_client.rb +0 -1327
- data/lib/right_agent/monkey_patches/amqp_patch.rb +0 -274
- data/lib/right_agent/monkey_patches/ruby_patch/string_patch.rb +0 -107
- data/lib/right_agent/stats_helper.rb +0 -745
- data/spec/broker_client_spec.rb +0 -962
- data/spec/ha_broker_client_spec.rb +0 -1695
- data/spec/monkey_patches/amqp_patch_spec.rb +0 -100
- data/spec/monkey_patches/string_patch_spec.rb +0 -99
- 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
|