right_agent 0.5.1

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 (147) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +78 -0
  3. data/Rakefile +86 -0
  4. data/lib/right_agent.rb +66 -0
  5. data/lib/right_agent/actor.rb +163 -0
  6. data/lib/right_agent/actor_registry.rb +76 -0
  7. data/lib/right_agent/actors/agent_manager.rb +189 -0
  8. data/lib/right_agent/agent.rb +735 -0
  9. data/lib/right_agent/agent_config.rb +403 -0
  10. data/lib/right_agent/agent_identity.rb +209 -0
  11. data/lib/right_agent/agent_tags_manager.rb +213 -0
  12. data/lib/right_agent/audit_formatter.rb +107 -0
  13. data/lib/right_agent/broker_client.rb +683 -0
  14. data/lib/right_agent/command.rb +30 -0
  15. data/lib/right_agent/command/agent_manager_commands.rb +134 -0
  16. data/lib/right_agent/command/command_client.rb +136 -0
  17. data/lib/right_agent/command/command_constants.rb +42 -0
  18. data/lib/right_agent/command/command_io.rb +128 -0
  19. data/lib/right_agent/command/command_parser.rb +87 -0
  20. data/lib/right_agent/command/command_runner.rb +105 -0
  21. data/lib/right_agent/command/command_serializer.rb +63 -0
  22. data/lib/right_agent/console.rb +65 -0
  23. data/lib/right_agent/core_payload_types.rb +42 -0
  24. data/lib/right_agent/core_payload_types/cookbook.rb +61 -0
  25. data/lib/right_agent/core_payload_types/cookbook_position.rb +46 -0
  26. data/lib/right_agent/core_payload_types/cookbook_repository.rb +116 -0
  27. data/lib/right_agent/core_payload_types/cookbook_sequence.rb +70 -0
  28. data/lib/right_agent/core_payload_types/dev_repositories.rb +90 -0
  29. data/lib/right_agent/core_payload_types/event_categories.rb +38 -0
  30. data/lib/right_agent/core_payload_types/executable_bundle.rb +138 -0
  31. data/lib/right_agent/core_payload_types/login_policy.rb +72 -0
  32. data/lib/right_agent/core_payload_types/login_user.rb +62 -0
  33. data/lib/right_agent/core_payload_types/planned_volume.rb +94 -0
  34. data/lib/right_agent/core_payload_types/recipe_instantiation.rb +60 -0
  35. data/lib/right_agent/core_payload_types/repositories_bundle.rb +50 -0
  36. data/lib/right_agent/core_payload_types/right_script_attachment.rb +95 -0
  37. data/lib/right_agent/core_payload_types/right_script_instantiation.rb +73 -0
  38. data/lib/right_agent/core_payload_types/secure_document.rb +66 -0
  39. data/lib/right_agent/core_payload_types/secure_document_location.rb +63 -0
  40. data/lib/right_agent/core_payload_types/software_repository_instantiation.rb +61 -0
  41. data/lib/right_agent/daemonize.rb +35 -0
  42. data/lib/right_agent/dispatcher.rb +348 -0
  43. data/lib/right_agent/enrollment_result.rb +217 -0
  44. data/lib/right_agent/exceptions.rb +30 -0
  45. data/lib/right_agent/ha_broker_client.rb +1278 -0
  46. data/lib/right_agent/idempotent_request.rb +140 -0
  47. data/lib/right_agent/log.rb +418 -0
  48. data/lib/right_agent/monkey_patches.rb +29 -0
  49. data/lib/right_agent/monkey_patches/amqp_patch.rb +274 -0
  50. data/lib/right_agent/monkey_patches/ruby_patch.rb +49 -0
  51. data/lib/right_agent/monkey_patches/ruby_patch/array_patch.rb +29 -0
  52. data/lib/right_agent/monkey_patches/ruby_patch/darwin_patch.rb +24 -0
  53. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch.rb +24 -0
  54. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch/file_patch.rb +30 -0
  55. data/lib/right_agent/monkey_patches/ruby_patch/object_patch.rb +49 -0
  56. data/lib/right_agent/monkey_patches/ruby_patch/singleton_patch.rb +46 -0
  57. data/lib/right_agent/monkey_patches/ruby_patch/string_patch.rb +107 -0
  58. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch.rb +32 -0
  59. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/file_patch.rb +90 -0
  60. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/process_patch.rb +63 -0
  61. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/stdio_patch.rb +27 -0
  62. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/time_patch.rb +55 -0
  63. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/win32ole_patch.rb +34 -0
  64. data/lib/right_agent/multiplexer.rb +91 -0
  65. data/lib/right_agent/operation_result.rb +270 -0
  66. data/lib/right_agent/packets.rb +637 -0
  67. data/lib/right_agent/payload_formatter.rb +104 -0
  68. data/lib/right_agent/pid_file.rb +159 -0
  69. data/lib/right_agent/platform.rb +319 -0
  70. data/lib/right_agent/platform/darwin.rb +227 -0
  71. data/lib/right_agent/platform/linux.rb +268 -0
  72. data/lib/right_agent/platform/windows.rb +1204 -0
  73. data/lib/right_agent/scripts/agent_controller.rb +522 -0
  74. data/lib/right_agent/scripts/agent_deployer.rb +379 -0
  75. data/lib/right_agent/scripts/common_parser.rb +153 -0
  76. data/lib/right_agent/scripts/log_level_manager.rb +193 -0
  77. data/lib/right_agent/scripts/stats_manager.rb +256 -0
  78. data/lib/right_agent/scripts/usage.rb +58 -0
  79. data/lib/right_agent/secure_identity.rb +92 -0
  80. data/lib/right_agent/security.rb +32 -0
  81. data/lib/right_agent/security/cached_certificate_store_proxy.rb +63 -0
  82. data/lib/right_agent/security/certificate.rb +102 -0
  83. data/lib/right_agent/security/certificate_cache.rb +89 -0
  84. data/lib/right_agent/security/distinguished_name.rb +56 -0
  85. data/lib/right_agent/security/encrypted_document.rb +84 -0
  86. data/lib/right_agent/security/rsa_key_pair.rb +76 -0
  87. data/lib/right_agent/security/signature.rb +86 -0
  88. data/lib/right_agent/security/static_certificate_store.rb +69 -0
  89. data/lib/right_agent/sender.rb +937 -0
  90. data/lib/right_agent/serialize.rb +29 -0
  91. data/lib/right_agent/serialize/message_pack.rb +102 -0
  92. data/lib/right_agent/serialize/secure_serializer.rb +131 -0
  93. data/lib/right_agent/serialize/secure_serializer_initializer.rb +47 -0
  94. data/lib/right_agent/serialize/serializable.rb +135 -0
  95. data/lib/right_agent/serialize/serializer.rb +149 -0
  96. data/lib/right_agent/stats_helper.rb +731 -0
  97. data/lib/right_agent/subprocess.rb +38 -0
  98. data/lib/right_agent/tracer.rb +124 -0
  99. data/right_agent.gemspec +60 -0
  100. data/spec/actor_registry_spec.rb +81 -0
  101. data/spec/actor_spec.rb +99 -0
  102. data/spec/agent_config_spec.rb +226 -0
  103. data/spec/agent_identity_spec.rb +75 -0
  104. data/spec/agent_spec.rb +571 -0
  105. data/spec/broker_client_spec.rb +961 -0
  106. data/spec/command/agent_manager_commands_spec.rb +51 -0
  107. data/spec/command/command_io_spec.rb +93 -0
  108. data/spec/command/command_parser_spec.rb +79 -0
  109. data/spec/command/command_runner_spec.rb +72 -0
  110. data/spec/command/command_serializer_spec.rb +51 -0
  111. data/spec/core_payload_types/dev_repositories_spec.rb +64 -0
  112. data/spec/core_payload_types/executable_bundle_spec.rb +59 -0
  113. data/spec/core_payload_types/login_user_spec.rb +98 -0
  114. data/spec/core_payload_types/right_script_attachment_spec.rb +65 -0
  115. data/spec/core_payload_types/spec_helper.rb +23 -0
  116. data/spec/dispatcher_spec.rb +372 -0
  117. data/spec/enrollment_result_spec.rb +53 -0
  118. data/spec/ha_broker_client_spec.rb +1673 -0
  119. data/spec/idempotent_request_spec.rb +136 -0
  120. data/spec/log_spec.rb +177 -0
  121. data/spec/monkey_patches/amqp_patch_spec.rb +100 -0
  122. data/spec/monkey_patches/eventmachine_spec.rb +62 -0
  123. data/spec/monkey_patches/string_patch_spec.rb +99 -0
  124. data/spec/multiplexer_spec.rb +48 -0
  125. data/spec/operation_result_spec.rb +171 -0
  126. data/spec/packets_spec.rb +418 -0
  127. data/spec/platform/platform_spec.rb +60 -0
  128. data/spec/results_mock.rb +45 -0
  129. data/spec/secure_identity_spec.rb +50 -0
  130. data/spec/security/cached_certificate_store_proxy_spec.rb +56 -0
  131. data/spec/security/certificate_cache_spec.rb +71 -0
  132. data/spec/security/certificate_spec.rb +49 -0
  133. data/spec/security/distinguished_name_spec.rb +46 -0
  134. data/spec/security/encrypted_document_spec.rb +55 -0
  135. data/spec/security/rsa_key_pair_spec.rb +55 -0
  136. data/spec/security/signature_spec.rb +66 -0
  137. data/spec/security/static_certificate_store_spec.rb +52 -0
  138. data/spec/sender_spec.rb +887 -0
  139. data/spec/serialize/message_pack_spec.rb +131 -0
  140. data/spec/serialize/secure_serializer_spec.rb +102 -0
  141. data/spec/serialize/serializable_spec.rb +90 -0
  142. data/spec/serialize/serializer_spec.rb +174 -0
  143. data/spec/spec.opts +2 -0
  144. data/spec/spec_helper.rb +77 -0
  145. data/spec/stats_helper_spec.rb +681 -0
  146. data/spec/tracer_spec.rb +114 -0
  147. metadata +320 -0
@@ -0,0 +1,149 @@
1
+ #
2
+ # Copyright (c) 2009-2011 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require 'rubygems'
24
+ require 'date'
25
+ require 'json'
26
+
27
+ require File.normalize_path(File.join(File.dirname(__FILE__), 'message_pack'))
28
+
29
+ # Monkey patch common classes to support MessagePack serialization
30
+ # As with JSON, unserializing them is manual using existing methods such as parse
31
+ class Date
32
+ def to_msgpack(*a); to_s.to_msgpack(*a) end
33
+ end
34
+
35
+ class Time
36
+ def to_msgpack(*a); to_s.to_msgpack(*a) end
37
+ end
38
+
39
+ class DateTime
40
+ def to_msgpack(*a); to_s.to_msgpack(*a) end
41
+ end
42
+
43
+ module RightScale
44
+
45
+ # Cascade serializer supporting MessagePack and JSON serialization formats
46
+ # as well as secure serialization
47
+ class Serializer
48
+
49
+ class SerializationError < StandardError
50
+ attr_accessor :action, :packet
51
+ def initialize(action, packet, serializers, msg = nil)
52
+ @action, @packet = action, packet
53
+ msg = " (#{msg})" if msg && !msg.empty?
54
+ super("Could not #{action} packet using #{serializers.inspect}#{msg}")
55
+ end
56
+ end
57
+
58
+ # (Symbol) Preferred serialization format
59
+ attr_reader :format
60
+
61
+ # Initialize the serializer
62
+ # Do not cascade serializers if :secure is specified
63
+ #
64
+ # === Parameters
65
+ # preferred_format(Symbol|String):: Preferred serialization format: :msgpack, :json, or :secure
66
+ #
67
+ # === Raises
68
+ # ArgumentError:: If preferred format is not supported
69
+ def initialize(preferred_format = nil)
70
+ @format = (preferred_format ||= DEFAULT_FORMAT).to_sym
71
+ raise ArgumentError, "Serializer format #{@format.inspect} not one of #{FORMATS.inspect}" unless FORMATS.include?(@format)
72
+ @secure = (@format == :secure)
73
+ end
74
+
75
+ # Serialize object using preferred serializer
76
+ # Do not cascade
77
+ #
78
+ # === Parameters
79
+ # packet(Object):: Object to be serialized
80
+ # format(Symbol):: Override preferred format
81
+ #
82
+ # === Return
83
+ # (String):: Serialized object
84
+ def dump(packet, format = nil)
85
+ cascade_serializers(:dump, packet, [@secure ? SecureSerializer : SERIALIZERS[format || @format]])
86
+ end
87
+
88
+ # Unserialize object using cascaded serializers with order chosen by peaking at first byte
89
+ #
90
+ # === Parameters
91
+ # packet(String):: Data representing serialized object
92
+ #
93
+ # === Return
94
+ # (Object):: Unserialized object
95
+ def load(packet)
96
+ cascade_serializers(:load, packet, @secure ? [SecureSerializer] : order_serializers(packet))
97
+ end
98
+
99
+ private
100
+
101
+ # Supported serialization formats
102
+ SERIALIZERS = {:msgpack => MessagePack, :json => JSON}.freeze
103
+ MSGPACK_FIRST_SERIALIZERS = [MessagePack, JSON].freeze
104
+ JSON_FIRST_SERIALIZERS = MSGPACK_FIRST_SERIALIZERS.clone.reverse.freeze
105
+ FORMATS = (SERIALIZERS.keys + [:secure]).freeze
106
+ DEFAULT_FORMAT = :msgpack
107
+
108
+ # Apply serializers in order until one succeeds
109
+ #
110
+ # === Parameters
111
+ # action(Symbol):: Serialization action: :dump or :load
112
+ # packet(Object|String):: Object or serialized data on which action is to be performed
113
+ # serializers(Array):: Serializers to apply in order
114
+ #
115
+ # === Return
116
+ # (String|Object):: Result of serialization action
117
+ #
118
+ # === Raises
119
+ # SerializationError:: If none of the serializers can perform the requested action
120
+ def cascade_serializers(action, packet, serializers)
121
+ errors = []
122
+ serializers.map do |serializer|
123
+ obj = nil
124
+ begin
125
+ obj = serializer.__send__(action, packet)
126
+ rescue SecureSerializer::MissingCertificate, SecureSerializer::InvalidSignature => e
127
+ errors << Log.format("Failed to #{action} with #{serializer.name}", e)
128
+ rescue Exception => e
129
+ errors << Log.format("Failed to #{action} with #{serializer.name}", e, :trace)
130
+ end
131
+ return obj if obj
132
+ end
133
+ raise SerializationError.new(action, packet, serializers, errors.join("\n"))
134
+ end
135
+
136
+ # Determine likely serialization format and order serializers accordingly
137
+ #
138
+ # === Parameters
139
+ # packet(String):: Data representing serialized object
140
+ #
141
+ # === Return
142
+ # (Array):: Ordered serializers
143
+ def order_serializers(packet)
144
+ packet[0] > 127 ? MSGPACK_FIRST_SERIALIZERS : JSON_FIRST_SERIALIZERS
145
+ end
146
+
147
+ end # Serializer
148
+
149
+ end # RightScale
@@ -0,0 +1,731 @@
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", "version", stats["version"].to_i) if stats.has_key?("version")
519
+ str += brokers_str(stats["brokers"], name_width) if stats.has_key?("brokers")
520
+ stats.to_a.sort.each { |k, v| str += sub_stats_str(k[0..-7], v, name_width) if k.to_s =~ /stats$/ }
521
+ str
522
+ end
523
+
524
+ # Convert broker information to displayable format
525
+ #
526
+ # === Parameter
527
+ # brokers(Hash):: Broker stats with keys
528
+ # "brokers"(Array):: Stats for each broker in priority order as hash with keys
529
+ # "alias"(String):: Broker alias
530
+ # "identity"(String):: Broker identity
531
+ # "status"(Symbol):: Status of connection
532
+ # "disconnect last"(Hash|nil):: Last disconnect information with key "elapsed", or nil if none
533
+ # "disconnects"(Integer|nil):: Number of times lost connection, or nil if none
534
+ # "failure last"(Hash|nil):: Last connect failure information with key "elapsed", or nil if none
535
+ # "failures"(Integer|nil):: Number of failed attempts to connect to broker, or nil if none
536
+ # "retries"(Integer|nil):: Number of attempts to connect after failure, or nil if none
537
+ # "exceptions"(Hash|nil):: Exceptions raised per category, or nil if none
538
+ # "total"(Integer):: Total exceptions for this category
539
+ # "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
540
+ # "returns"(Hash|nil):: Message return activity stats with keys "total", "percent", "last", and "rate"
541
+ # with percentage breakdown per request type, or nil if none
542
+ # name_width(Integer):: Fixed width for left-justified name display
543
+ #
544
+ # === Return
545
+ # str(String):: Broker display with one line per broker plus exceptions
546
+ def brokers_str(brokers, name_width)
547
+ value_indent = " " * (name_width + SEPARATOR.size)
548
+ sub_name_width = MAX_SUB_STAT_NAME_WIDTH
549
+ sub_value_indent = " " * (name_width + sub_name_width + (SEPARATOR.size * 2))
550
+ str = sprintf("%-#{name_width}s#{SEPARATOR}", "brokers")
551
+ brokers["brokers"].each do |b|
552
+ disconnects = if b["disconnects"]
553
+ "#{b["disconnects"]} (#{elapsed(b["disconnect last"]["elapsed"])} ago)"
554
+ else
555
+ "none"
556
+ end
557
+ failures = if b["failures"]
558
+ retries = b["retries"]
559
+ retries = " w/ #{retries} #{retries != 1 ? 'retries' : 'retry'}" if retries
560
+ "#{b["failures"]} (#{elapsed(b["failure last"]["elapsed"])} ago#{retries})"
561
+ else
562
+ "none"
563
+ end
564
+ str += "#{b["alias"]}: #{b["identity"]} #{b["status"]}, disconnects: #{disconnects}, failures: #{failures}\n"
565
+ str += value_indent
566
+ end
567
+ str += sprintf("%-#{sub_name_width}s#{SEPARATOR}", "exceptions")
568
+ str += if brokers["exceptions"].nil? || brokers["exceptions"].empty?
569
+ "none\n"
570
+ else
571
+ exceptions_str(brokers["exceptions"], sub_value_indent) + "\n"
572
+ end
573
+ str += value_indent
574
+ str += sprintf("%-#{sub_name_width}s#{SEPARATOR}", "returns")
575
+ str += if brokers["returns"].nil? || brokers["returns"].empty?
576
+ "none\n"
577
+ else
578
+ wrap(activity_str(brokers["returns"]), MAX_SUB_STAT_VALUE_WIDTH, sub_value_indent, ", ") + "\n"
579
+ end
580
+ end
581
+
582
+ # Convert grouped set of statistics to displayable format
583
+ # Provide special formatting for stats named "exceptions"
584
+ # Break out percentages and total count for stats containing "percent" hash value
585
+ # sorted in descending percent order and followed by total count
586
+ # Convert to elapsed time for stats with name ending in "last"
587
+ # Add "/sec" to values with name ending in "rate"
588
+ # Add " sec" to values with name ending in "time"
589
+ # Add "%" to values with name ending in "percent" and drop "percent" from name
590
+ # Use elapsed time formatting for values with name ending in "age"
591
+ # Display any nil value, empty hash, or hash with a "total" value of 0 as "none"
592
+ # Display any floating point value or hash of values with at least two significant digits of precision
593
+ #
594
+ # === Parameters
595
+ # name(String):: Display name for the stat
596
+ # value(Object):: Value of this stat
597
+ # name_width(Integer):: Fixed width for left-justified name display
598
+ #
599
+ # === Return
600
+ # (String):: Single line display of stat
601
+ def sub_stats_str(name, value, name_width)
602
+ value_indent = " " * (name_width + SEPARATOR.size)
603
+ sub_name_width = MAX_SUB_STAT_NAME_WIDTH
604
+ sub_value_indent = " " * (name_width + sub_name_width + (SEPARATOR.size * 2))
605
+ sprintf("%-#{name_width}s#{SEPARATOR}", name) + value.to_a.sort.map do |attr|
606
+ k, v = attr
607
+ name = k =~ /percent$/ ? k[0..-9] : k
608
+ sprintf("%-#{sub_name_width}s#{SEPARATOR}", name) + if v.is_a?(Float) || v.is_a?(Integer)
609
+ str = k =~ /age$/ ? elapsed(v) : enough_precision(v)
610
+ str += "/sec" if k =~ /rate$/
611
+ str += " sec" if k =~ /time$/
612
+ str += "%" if k =~ /percent$/
613
+ str
614
+ elsif v.is_a?(Hash)
615
+ if v.empty? || v["total"] == 0
616
+ "none"
617
+ elsif v["total"]
618
+ wrap(activity_str(v), MAX_SUB_STAT_VALUE_WIDTH, sub_value_indent, ", ")
619
+ elsif k =~ /last$/
620
+ last_activity_str(v)
621
+ elsif k == "exceptions"
622
+ exceptions_str(v, sub_value_indent)
623
+ else
624
+ wrap(hash_str(v), MAX_SUB_STAT_VALUE_WIDTH, sub_value_indent, ", ")
625
+ end
626
+ else
627
+ "#{v || "none"}"
628
+ end + "\n"
629
+ end.join(value_indent)
630
+ end
631
+
632
+ # Convert activity information to displayable format
633
+ #
634
+ # === Parameters
635
+ # value(Hash|nil):: Information about activity, or nil if the total is 0
636
+ # "total"(Integer):: Total activity count
637
+ # "percent"(Hash):: Percentage for each type of activity if tracking type, otherwise omitted
638
+ # "last"(Hash):: Information about last activity
639
+ # "elapsed"(Integer):: Seconds since last activity started
640
+ # "type"(String):: Type of activity if tracking type, otherwise omitted
641
+ # "active"(Boolean):: Whether activity still active if tracking whether active, otherwise omitted
642
+ # "rate"(Float):: Recent average rate if measuring rate, otherwise omitted
643
+ # "duration"(Float):: Average duration of activity if tracking duration, otherwise omitted
644
+ #
645
+ # === Return
646
+ # str(String):: Activity stats in displayable format without any line separators
647
+ def activity_str(value)
648
+ str = ""
649
+ str += enough_precision(sort_value(value["percent"]).reverse).map { |k, v| "#{k}: #{v}%" }.join(", ") +
650
+ ", total: " if value["percent"]
651
+ str += "#{value['total']}"
652
+ str += ", last: #{last_activity_str(value['last'], single_item = true)}" if value["last"]
653
+ str += ", rate: #{enough_precision(value['rate'])}/sec" if value["rate"]
654
+ str += ", duration: #{enough_precision(value['duration'])} sec" if value["duration"]
655
+ str
656
+ end
657
+
658
+ # Convert last activity information to displayable format
659
+ #
660
+ # === Parameters
661
+ # last(Hash):: Information about last activity
662
+ # "elapsed"(Integer):: Seconds since last activity started
663
+ # "type"(String):: Type of activity if tracking type, otherwise omitted
664
+ # "active"(Boolean):: Whether activity still active if tracking whether active, otherwise omitted
665
+ # single_item:: Whether this is to appear as a single item in a comma-separated list
666
+ # in which case there should be no ':' in the formatted string
667
+ #
668
+ # === Return
669
+ # str(String):: Last activity in displayable format without any line separators
670
+ def last_activity_str(last, single_item = false)
671
+ str = "#{elapsed(last['elapsed'])} ago"
672
+ str += " and still active" if last["active"]
673
+ if last["type"]
674
+ if single_item
675
+ str = "#{last['type']} (#{str})"
676
+ else
677
+ str = "#{last['type']}: #{str}"
678
+ end
679
+ end
680
+ str
681
+ end
682
+
683
+ # Convert exception information to displayable format
684
+ #
685
+ # === Parameters
686
+ # exceptions(Hash):: Exceptions raised per category
687
+ # "total"(Integer):: Total exceptions for this category
688
+ # "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
689
+ # indent(String):: Indentation for each line
690
+ #
691
+ # === Return
692
+ # (String):: Exceptions in displayable format with line separators
693
+ def exceptions_str(exceptions, indent)
694
+ indent2 = indent + (" " * 4)
695
+ exceptions.to_a.sort.map do |k, v|
696
+ sprintf("%s total: %d, most recent:\n", k, v["total"]) + v["recent"].reverse.map do |e|
697
+ message = e["message"]
698
+ if message && message.size > (MAX_EXCEPTION_MESSAGE_WIDTH - 3)
699
+ message = e["message"][0, MAX_EXCEPTION_MESSAGE_WIDTH - 3] + "..."
700
+ end
701
+ indent + "(#{e["count"]}) #{time_at(e["when"])} #{e["type"]}: #{message}\n" + indent2 + "#{e["where"]}"
702
+ end.join("\n")
703
+ end.join("\n" + indent)
704
+ end
705
+
706
+ # Convert arbitrary nested hash to displayable format
707
+ # Sort hash by key, numerically if possible, otherwise as is
708
+ # Display any floating point values with one decimal place precision
709
+ # Display any empty values as "none"
710
+ #
711
+ # === Parameters
712
+ # hash(Hash):: Hash to be displayed
713
+ #
714
+ # === Return
715
+ # (String):: Single line hash display
716
+ def hash_str(hash)
717
+ str = ""
718
+ sort_key(hash).map do |k, v|
719
+ "#{k}: " + if v.is_a?(Float)
720
+ enough_precision(v)
721
+ elsif v.is_a?(Hash)
722
+ "[ " + hash_str(v) + " ]"
723
+ else
724
+ "#{v || "none"}"
725
+ end
726
+ end.join(", ")
727
+ end
728
+
729
+ end # StatsHelper
730
+
731
+ end # RightScale