right_agent 2.0.8-x86-mingw32 → 2.1.0-x86-mingw32

Sign up to get free protection for your applications and to get access to all the features.
@@ -27,14 +27,13 @@ require 'json'
27
27
  require 'openssl'
28
28
  require 'right_amqp'
29
29
 
30
- require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'monkey_patches'))
31
- require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'protocol_version_mixin'))
32
30
  require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'payload_formatter'))
33
31
  require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'packets'))
34
32
  require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'enrollment_result'))
35
33
  require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'console'))
36
34
  require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'daemonize'))
37
35
  require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'exceptions'))
36
+ require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'http_exceptions'))
38
37
  require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'multiplexer'))
39
38
  require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'tracer'))
40
39
  require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'audit_formatter'))
@@ -162,7 +162,7 @@ class AgentManager
162
162
  options = RightScale::SerializationHelper.symbolize_keys(options)
163
163
  res = success_result
164
164
  begin
165
- if error = @agent.connect(options[:host], options[:port], options[:id], options[:priority], options[:force])
165
+ if (error = @agent.connect(options[:host], options[:port], options[:id], options[:priority], options[:force]))
166
166
  res = error_result(error)
167
167
  end
168
168
  rescue Exception => e
@@ -185,7 +185,7 @@ class AgentManager
185
185
  options = RightScale::SerializationHelper.symbolize_keys(options)
186
186
  res = success_result
187
187
  begin
188
- if error = @agent.disconnect(options[:host], options[:port], options[:remove])
188
+ if (error = @agent.disconnect(options[:host], options[:port], options[:remove]))
189
189
  res = error_result(error)
190
190
  end
191
191
  rescue Exception => e
@@ -206,7 +206,7 @@ class AgentManager
206
206
  options = RightScale::SerializationHelper.symbolize_keys(options)
207
207
  res = success_result
208
208
  begin
209
- if error = @agent.connect_failed(options[:brokers])
209
+ if (error = @agent.connect_failed(options[:brokers]))
210
210
  res = error_result(error)
211
211
  end
212
212
  rescue Exception => e
@@ -138,6 +138,8 @@ module RightScale
138
138
  # exceeding MAX_QUEUED_REQUESTS or by repeated failures to access RightNet when online (no argument)
139
139
  # :services(Symbol):: List of services provided by this agent; defaults to all methods exposed by actors
140
140
  # :secure(Boolean):: true indicates to use security features of RabbitMQ to restrict agents to themselves
141
+ # :fiber_pool_size(Integer):: Size of fiber pool
142
+ # :fiber_pool(FiberPool):: Fiber pool configured for use with EventMachine when making HTTP requests
141
143
  # :mode(Symbol):: RightNet communication mode: :http or :amqp; defaults to :amqp
142
144
  # :api_url(String):: Domain name for HTTP access to RightApi server
143
145
  # :account_id(Integer):: Identifier for account owning this agent
@@ -218,7 +220,7 @@ module RightScale
218
220
  # Need to give EM (on Windows) a chance to respond to the AMQP handshake
219
221
  # before doing anything interesting to prevent AMQP handshake from
220
222
  # timing-out; delay post-connected activity a second
221
- EM.add_timer(1) { start_service }
223
+ EM_S.add_timer(1) { start_service }
222
224
  elsif status == :failed
223
225
  terminate("failed to connect to any brokers during startup")
224
226
  elsif status == :timeout
@@ -575,6 +577,7 @@ module RightScale
575
577
  end
576
578
 
577
579
  @options[:async_response] = true unless @options.has_key?(:async_response)
580
+ @options[:non_blocking] = true if @options[:fiber_pool_size].to_i > 0
578
581
 
579
582
  @identity = @options[:identity]
580
583
  parsed_identity = AgentIdentity.parse(@identity)
@@ -613,7 +616,7 @@ module RightScale
613
616
  @history.update("run")
614
617
  start_console if @options[:console] && !@options[:daemonize]
615
618
  EM.next_tick { @options[:ready_callback].call } if @options[:ready_callback]
616
- EM.defer { @client.listen(nil) { |e| handle_event(e) } } if @mode == :http
619
+ @client.listen(nil) { |e| handle_event(e) } if @mode == :http
617
620
 
618
621
  # Need to keep reconnect interval at least :connect_timeout in size,
619
622
  # otherwise connection_status callback will not timeout prior to next
@@ -640,15 +643,22 @@ module RightScale
640
643
  if ["Push", "Request"].include?(event[:type])
641
644
  # Use next_tick to ensure that on main reactor thread
642
645
  # so that any data access is thread safe
643
- EM.next_tick do
646
+ EM_S.next_tick do
644
647
  begin
645
648
  if (result = @dispatcher.dispatch(event_to_packet(event))) && event[:type] == "Request"
646
649
  @client.notify(result_to_event(result), [result.to])
647
650
  end
651
+ rescue Dispatcher::DuplicateRequest
648
652
  rescue Exception => e
649
653
  Log.error("Failed sending response for <#{event[:uuid]}>", e, :trace)
650
654
  end
651
655
  end
656
+ elsif event[:type] == "Result"
657
+ if (data = event[:data]) && (result = data[:result]) && result.respond_to?(:non_delivery?) && result.non_delivery?
658
+ Log.info("Non-delivery of event <#{data[:request_uuid]}>: #{result.content}")
659
+ else
660
+ Log.error("Unexpected Result event from #{event[:from]}: #{event.inspect}")
661
+ end
652
662
  else
653
663
  Log.error("Unrecognized event type #{event[:type]} from #{event[:from]}")
654
664
  end
@@ -975,7 +985,7 @@ module RightScale
975
985
  def setup_status_checks(interval)
976
986
  @check_status_count = 0
977
987
  @check_status_brokers = @client.all if @mode != :http
978
- @check_status_timer = EM::PeriodicTimer.new(interval) { check_status }
988
+ @check_status_timer = EM_S::PeriodicTimer.new(interval) { check_status }
979
989
  true
980
990
  end
981
991
 
@@ -23,6 +23,16 @@
23
23
 
24
24
  CLIENTS_BASE_DIR = File.join(File.dirname(__FILE__), 'clients')
25
25
 
26
+ unless defined?(Fiber)
27
+ # To avoid load errors when using pre-1.9 ruby
28
+ class Fiber
29
+ def self.current; nil end
30
+ def self.yield; [] end
31
+ end
32
+ end
33
+
34
+ require File.normalize_path(File.join(CLIENTS_BASE_DIR, 'non_blocking_client'))
35
+ require File.normalize_path(File.join(CLIENTS_BASE_DIR, 'blocking_client'))
26
36
  require File.normalize_path(File.join(CLIENTS_BASE_DIR, 'balanced_http_client'))
27
37
  require File.normalize_path(File.join(CLIENTS_BASE_DIR, 'base_retry_client'))
28
38
  require File.normalize_path(File.join(CLIENTS_BASE_DIR, 'auth_client'))
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2013 RightScale Inc
2
+ # Copyright (c) 2013-2014 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -21,12 +21,15 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
- require 'restclient'
25
-
26
24
  module RightScale
27
25
 
28
- # HTTP REST client for request balanced access to RightScale servers
29
- # It is intended for use by instance agents and by infrastructure servers
26
+ # HTTP REST client for request-balanced access to RightScale servers
27
+ # Requests can be made using the EventMachine asynchronous HTTP interface
28
+ # in an efficient i/o non-blocking fashion using fibers or they can be made
29
+ # using the RestClient interface; either way they are synchronous to the client
30
+ # For the non-blocking i/o approach this class must be used from a spawned fiber
31
+ # rather than the root fiber
32
+ # This class is intended for use by instance agents and by infrastructure servers
30
33
  # and therefore supports both session cookie and global session-based authentication
31
34
  class BalancedHttpClient
32
35
 
@@ -41,12 +44,15 @@ module RightScale
41
44
  # Default time for HTTP connection to open
42
45
  DEFAULT_OPEN_TIMEOUT = 2
43
46
 
44
- # Default time to wait for health check response
47
+ # Time to wait for health check response
45
48
  HEALTH_CHECK_TIMEOUT = 5
46
49
 
47
50
  # Default time to wait for response from request
48
51
  DEFAULT_REQUEST_TIMEOUT = 30
49
52
 
53
+ # Maximum time between uses of an HTTP connection
54
+ CONNECTION_REUSE_TIMEOUT = 5
55
+
50
56
  # Default health check path
51
57
  DEFAULT_HEALTH_CHECK_PATH = "/health-check"
52
58
 
@@ -66,38 +72,41 @@ module RightScale
66
72
  # defaults to DEFAULT_HEALTH_CHECK_PATH
67
73
  # @option options [Array] :filter_params symbols or strings for names of request parameters
68
74
  # whose values are to be hidden when logging; can be augmented on individual requests
75
+ # @option options [Boolean] :non_blocking i/o is to be used for HTTP requests by applying
76
+ # EM::HttpRequest and fibers instead of RestClient; requests remain synchronous
69
77
  def initialize(urls, options = {})
70
78
  @urls = split(urls)
71
79
  @api_version = options[:api_version]
72
80
  @server_name = options[:server_name]
73
81
  @filter_params = (options[:filter_params] || []).map { |p| p.to_s }
74
82
 
75
- # Create health check proc for use by request balancer
76
- # Strip user and password from host name since health-check does not require authorization
77
- @health_check_proc = Proc.new do |host|
78
- uri = URI.parse(host)
79
- uri.user = uri.password = nil
80
- uri.path = uri.path + (options[:health_check_path] || DEFAULT_HEALTH_CHECK_PATH)
81
- check_options = {
82
- :open_timeout => DEFAULT_OPEN_TIMEOUT,
83
- :timeout => HEALTH_CHECK_TIMEOUT }
84
- check_options[:headers] = {"X-API-Version" => @api_version} if @api_version
85
- RightSupport::Net::HTTPClient.new.get(uri.to_s, check_options)
86
- end
87
-
88
- # Initialize use of proxy if defined
89
- if (proxy_var = PROXY_ENVIRONMENT_VARIABLES.detect { |v| ENV.has_key?(v) })
90
- proxy = ENV[proxy_var].match(/^[[:alpha:]]+:\/\//) ? URI.parse(ENV[proxy_var]) : URI.parse("http://" + ENV[proxy_var])
91
- RestClient.proxy = proxy.to_s if proxy
92
- end
83
+ # Create appropriate underlying HTTP client
84
+ @http_client = options[:non_blocking] ? NonBlockingClient.new(options) : BlockingClient.new(options)
93
85
 
94
- # Initialize request balancer
95
- balancer_options = {
96
- :policy => RightSupport::Net::LB::HealthCheck,
97
- :health_check => @health_check_proc }
86
+ # Initialize health check and its use in request balancer
87
+ balancer_options = {:policy => RightSupport::Net::LB::HealthCheck, :health_check => @http_client.health_check_proc }
98
88
  @balancer = RightSupport::Net::RequestBalancer.new(@urls, balancer_options)
99
89
  end
100
90
 
91
+ # Check health of server
92
+ #
93
+ # @param [String] host name of server
94
+ #
95
+ # @return [Object] health check result from server
96
+ #
97
+ # @raise [NotResponding] server is not responding
98
+ def check_health(host = nil)
99
+ begin
100
+ @http_client.health_check_proc.call(host || @urls.first)
101
+ rescue StandardError => e
102
+ if e.respond_to?(:http_code) && RETRY_STATUS_CODES.include?(e.http_code)
103
+ raise NotResponding.new("#{@server_name || host} not responding", e)
104
+ else
105
+ raise
106
+ end
107
+ end
108
+ end
109
+
101
110
  def get(*args)
102
111
  request(:get, *args)
103
112
  end
@@ -110,25 +119,16 @@ module RightScale
110
119
  request(:put, *args)
111
120
  end
112
121
 
113
- def delete(*args)
114
- request(:delete, *args)
122
+ def poll(*args)
123
+ request(:poll, *args)
115
124
  end
116
125
 
117
- def check_health(host = nil)
118
- begin
119
- @health_check_proc.call(host || @urls.first)
120
- rescue StandardError => e
121
- if e.respond_to?(:http_code) && RETRY_STATUS_CODES.include?(e.http_code)
122
- raise NotResponding.new("#{@server_name || host} not responding", e)
123
- else
124
- raise
125
- end
126
- end
126
+ def delete(*args)
127
+ request(:delete, *args)
127
128
  end
128
129
 
129
- protected
130
-
131
- # Make request via request balancer
130
+ # Make HTTP request
131
+ # If polling, continue to poll until receive data, timeout, or hit error
132
132
  # Encode request parameters and response using JSON
133
133
  # Apply configured authorization scheme
134
134
  # Log request/response with filtered parameters included for failure or debug mode
@@ -144,98 +144,110 @@ module RightScale
144
144
  # parameters whose values are to be hidden when logging in addition to the ones
145
145
  # provided during object initialization
146
146
  # @option options [Hash] :headers to be added to request
147
+ # @option options [Numeric] :poll_timeout maximum wait for individual poll; defaults to :request_timeout
147
148
  # @option options [Symbol] :log_level to use when logging information about the request other than errors
148
149
  #
149
150
  # @return [Object] result returned by receiver of request
150
151
  #
151
152
  # @raise [NotResponding] server not responding, recommend retry
153
+ # @raise [HttpException] HTTP failure with associated status code
152
154
  def request(verb, path, params = {}, options = {})
153
- result = nil
154
- host_picked = nil
155
155
  started_at = Time.now
156
156
  filter = @filter_params + (options[:filter_params] || []).map { |p| p.to_s }
157
+ log_level = options[:log_level] || Log.level
157
158
  request_uuid = options[:request_uuid] || RightSupport::Data::UUID.generate
158
- log_level = options[:log_level] || :info
159
+ connect_options, request_options = @http_client.options(verb, path, params, request_headers(request_uuid, options), options)
159
160
 
160
- Log.send(log_level, "Requesting #{verb.to_s.upcase} <#{request_uuid}> " + log_text(path, params, filter))
161
+ Log.send(log_level, "Requesting #{verb.to_s.upcase} <#{request_uuid}> " + log_text(path, params, filter, log_level))
161
162
 
162
- begin
163
- request_options = {
164
- :open_timeout => options[:open_timeout] || DEFAULT_OPEN_TIMEOUT,
165
- :timeout => options[:request_timeout] || DEFAULT_REQUEST_TIMEOUT,
166
- :headers => {
167
- "X-Request-Lineage-Uuid" => request_uuid,
168
- :accept => "application/json" } }
169
- request_options[:headers]["X-API-Version"] = @api_version if @api_version
170
- request_options[:headers].merge!(options[:headers]) if options[:headers]
171
- request_options[:headers]["X-DEBUG"] = true if Log.level == :debug
172
-
173
- if [:get, :delete].include?(verb)
174
- request_options[:query] = params if params.is_a?(Hash) && params.any?
175
- else
176
- request_options[:payload] = JSON.dump(params)
177
- request_options[:headers][:content_type] = "application/json"
178
- end
163
+ used = {}
164
+ result, code, body, headers = if verb != :poll
165
+ rest_request(verb, path, connect_options, request_options, used)
166
+ else
167
+ poll_request(path, connect_options, request_options, options[:request_timeout], started_at, used)
168
+ end
179
169
 
180
- response = @balancer.request do |host|
181
- uri = URI.parse(host)
182
- uri.user = uri.password = nil
183
- host_picked = uri.to_s
184
- RightSupport::Net::HTTPClient.new.send(verb, host + path, request_options)
185
- end
186
- rescue RightSupport::Net::NoResult => e
187
- handle_no_result(e, host_picked) do |e2|
188
- report_failure(host_picked, path, params, filter, request_uuid, started_at, e2)
189
- end
190
- rescue Exception => e
191
- report_failure(host_picked, path, params, filter, request_uuid, started_at, e)
192
- raise
170
+ log_success(result, code, body, headers, used[:host], path, request_uuid, started_at, log_level)
171
+ result
172
+ rescue RightSupport::Net::NoResult => e
173
+ handle_no_result(e, used[:host]) do |e2|
174
+ log_failure(used[:host], path, params, filter, request_uuid, started_at, log_level, e2)
193
175
  end
176
+ rescue RestClient::Exception => e
177
+ e2 = HttpExceptions.convert(e)
178
+ log_failure(used[:host], path, params, filter, request_uuid, started_at, log_level, e2)
179
+ raise e2
180
+ rescue Exception => e
181
+ log_failure(used[:host], path, params, filter, request_uuid, started_at, log_level, e)
182
+ raise
183
+ end
184
+
185
+ protected
194
186
 
195
- response(response, host_picked, path, request_uuid, started_at, log_level)
187
+ # Construct headers for request
188
+ #
189
+ # @param [String] request_uuid uniquely identifying request
190
+ # @param [Hash] options per #request
191
+ #
192
+ # @return [Hash] headers for request
193
+ def request_headers(request_uuid, options)
194
+ headers = {"X-Request-Lineage-Uuid" => request_uuid, :accept => "application/json"}
195
+ headers["X-API-Version"] = @api_version if @api_version
196
+ headers.merge!(options[:headers]) if options[:headers]
197
+ headers["X-DEBUG"] = true if Log.level == :debug
198
+ headers
196
199
  end
197
200
 
198
- # Process HTTP response by extracting result and logging request completion
199
- # Extract result from location header for 201 response
200
- # JSON-decode body of other 2xx responses except for 204
201
+ # Make REST request
201
202
  #
202
- # @param [RestClient::Response, NilClass] response received
203
- # @param [String] host server URL where request was completed
203
+ # @param [Symbol] verb for HTTP REST request
204
204
  # @param [String] path in URI for desired resource
205
- # @param [String] request_uuid uniquely identifying request
206
- # @param [Time] started_at time for request
207
- # @param [Symbol] log_level to use when logging information about the request
208
- # other than errors
205
+ # @param [Hash] connect_options for HTTP connection
206
+ # @param [Hash] request_options for HTTP request
207
+ # @param [Hash] used container for returning :host used for request;
208
+ # needed so that can return it even when the request fails with an exception
209
209
  #
210
- # @return [Object] JSON-decoded response body
211
- def response(response, host, path, request_uuid, started_at, log_level)
212
- result = nil
213
- code = "nil"
214
- length = "-"
215
-
216
- if response
217
- code = response.code
218
- body = response.body
219
- headers = response.headers
220
- if (200..207).include?(code)
221
- if code == 201
222
- result = headers[:location]
223
- elsif code == 204 || body.nil? || (body.respond_to?(:empty?) && body.empty?)
224
- result = nil
225
- else
226
- result = JSON.load(body)
227
- result = nil if result.respond_to?(:empty?) && result.empty?
228
- end
229
- end
230
- length = headers[:content_length] || body.size
210
+ # @return [Array] result to be returned followed by response code, body, and headers
211
+ #
212
+ # @raise [NotResponding] server not responding, recommend retry
213
+ # @raise [HttpException] HTTP failure with associated status code
214
+ def rest_request(verb, path, connect_options, request_options, used)
215
+ result, code, body, headers = @balancer.request do |host|
216
+ uri = URI.parse(host)
217
+ uri.user = uri.password = nil
218
+ used[:host] = uri.to_s
219
+ @http_client.request(verb, path, host, connect_options, request_options)
231
220
  end
221
+ [result, code, body, headers]
222
+ end
232
223
 
233
- duration = "%.0fms" % ((Time.now - started_at) * 1000)
234
- completed = "Completed <#{request_uuid}> in #{duration} | #{code} [#{host}#{path}] | #{length} bytes"
235
- completed << " | #{result.inspect}" if Log.level == :debug
236
- Log.send(log_level, completed)
237
-
238
- result
224
+ # Make long-polling request
225
+ #
226
+ # @param [String] path in URI for desired resource
227
+ # @param [Hash] connect_options for HTTP connection
228
+ # @param [Hash] request_options for HTTP request
229
+ # @param [Integer] request_timeout for a non-nil result
230
+ # @param [Time] started_at time for request
231
+ # @param [Hash] used container for returning :host used for request;
232
+ # needed so that can return it even when the request fails with an exception
233
+ #
234
+ # @return [Array] result to be returned followed by response code, body, and headers
235
+ #
236
+ # @raise [NotResponding] server not responding, recommend retry
237
+ # @raise [HttpException] HTTP failure with associated status code
238
+ def poll_request(path, connect_options, request_options, request_timeout, started_at, used)
239
+ result = code = body = headers = nil
240
+ if (connection = @http_client.connections[path]).nil? || Time.now >= connection[:expires_at]
241
+ # Use normal :get request using request balancer for first poll
242
+ result, code, body, headers = rest_request(:get, path, connect_options, request_options.dup, used)
243
+ return [result, code, body, headers] if (Time.now - started_at) >= request_timeout
244
+ end
245
+ if result.nil? && (connection = @http_client.connections[path]) && Time.now < connection[:expires_at]
246
+ # Continue to poll using same connection until get result, timeout, or hit error
247
+ used[:host] = connection[:host]
248
+ result, code, body, headers = @http_client.poll(connection, request_options, started_at + request_timeout)
249
+ end
250
+ [result, code, body, headers]
239
251
  end
240
252
 
241
253
  # Handle no result from balancer
@@ -260,7 +272,7 @@ module RightScale
260
272
  if no_result.details.empty?
261
273
  yield(no_result)
262
274
  raise NotResponding.new("#{server_name} not responding", no_result)
263
- elsif (e.respond_to?(:http_code) && RETRY_STATUS_CODES.include?(e.http_code))
275
+ elsif e.respond_to?(:http_code) && RETRY_STATUS_CODES.include?(e.http_code)
264
276
  yield(e)
265
277
  if e.http_code == 504 && (e.http_body && !e.http_body.empty?)
266
278
  raise NotResponding.new(e.http_body, e)
@@ -274,7 +286,30 @@ module RightScale
274
286
  true
275
287
  end
276
288
 
277
- # Report request failure to logs
289
+ # Log successful request completion
290
+ #
291
+ # @param [Object] result to be returned to client
292
+ # @param [Integer, NilClass] code for response status
293
+ # @param [Object] body of response
294
+ # @param [Hash] headers for response
295
+ # @param [String] host server URL where request was completed
296
+ # @param [String] path in URI for desired resource
297
+ # @param [String] request_uuid uniquely identifying request
298
+ # @param [Time] started_at time for request
299
+ # @param [Symbol] log_level to use when logging information about the request
300
+ # other than errors
301
+ #
302
+ # @return [TrueClass] always true
303
+ def log_success(result, code, body, headers, host, path, request_uuid, started_at, log_level)
304
+ length = (headers && headers[:content_length]) || (body && body.size) || "-"
305
+ duration = "%.0fms" % ((Time.now - started_at) * 1000)
306
+ completed = "Completed <#{request_uuid}> in #{duration} | #{code || "nil"} [#{host}#{path}] | #{length} bytes"
307
+ completed << " | #{result.inspect}" if log_level == :debug
308
+ Log.send(log_level, completed)
309
+ true
310
+ end
311
+
312
+ # Log request failure
278
313
  # Also report it as audit entry if an instance is targeted
279
314
  #
280
315
  # @param [String] host server URL where request was attempted if known
@@ -283,13 +318,14 @@ module RightScale
283
318
  # @param [Array] filter list of parameters whose value is to be hidden
284
319
  # @param [String] request_uuid uniquely identifying request
285
320
  # @param [Time] started_at time for request
321
+ # @param [Symbol] log_level to use when logging information about the request
286
322
  # @param [Exception, String] exception or message that should be logged
287
323
  #
288
324
  # @return [TrueClass] Always return true
289
- def report_failure(host, path, params, filter, request_uuid, started_at, exception)
290
- status = exception.respond_to?(:http_code) ? exception.http_code : "nil"
325
+ def log_failure(host, path, params, filter, request_uuid, started_at, log_level, exception)
326
+ code = exception.respond_to?(:http_code) ? exception.http_code : "nil"
291
327
  duration = "%.0fms" % ((Time.now - started_at) * 1000)
292
- Log.error("Failed <#{request_uuid}> in #{duration} | #{status} " + log_text(path, params, filter, host, exception))
328
+ Log.error("Failed <#{request_uuid}> in #{duration} | #{code} " + log_text(path, params, filter, log_level, host, exception))
293
329
  true
294
330
  end
295
331
 
@@ -298,12 +334,13 @@ module RightScale
298
334
  # @param [String] path in URI for desired resource
299
335
  # @param [Hash] params for HTTP request
300
336
  # @param [Array, NilClass] filter augmentation to base filter list
337
+ # @param [Symbol] log_level to use when logging information about the request
301
338
  # @param [String] host server URL where request was attempted if known
302
339
  # @param [Exception, String, NilClass] exception or failure message that should be logged
303
340
  #
304
341
  # @return [String] Log text
305
- def log_text(path, params, filter, host = nil, exception = nil)
306
- filtered_params = (exception || Log.level == :debug) ? filter(params, filter).inspect : nil
342
+ def log_text(path, params, filter, log_level, host = nil, exception = nil)
343
+ filtered_params = (exception || log_level == :debug) ? filter(params, filter).inspect : nil
307
344
  text = filtered_params ? "#{path} #{filtered_params}" : path
308
345
  text = "[#{host}#{text}]" if host
309
346
  text << " | #{self.class.exception_text(exception)}" if exception
@@ -338,6 +375,57 @@ module RightScale
338
375
 
339
376
  public
340
377
 
378
+ # Format query parameters for inclusion in URI
379
+ # It can only handle parameters that can be converted to a string or arrays of same,
380
+ # not hashes or arrays/hashes that recursively contain arrays and/or hashes
381
+ #
382
+ # @param params [Hash] Parameters that are converted to <key>=<escaped_value> format
383
+ # and any value that is an array has each of its values formatted as <key>[]=<escaped_value>
384
+ #
385
+ # @return [String] Formatted parameter string with parameters separated by '&'
386
+ def self.format(params)
387
+ p = []
388
+ params.each do |k, v|
389
+ if v.is_a?(Array)
390
+ v.each { |v2| p << "#{k.to_s}[]=#{CGI.escape(v2.to_s)}" }
391
+ else
392
+ p << "#{k.to_s}=#{CGI.escape(v.to_s)}"
393
+ end
394
+ end
395
+ p.join("&")
396
+ end
397
+
398
+ # Process HTTP response to produce result for client
399
+ # Extract result from location header for 201 response
400
+ # JSON-decode body of other 2xx responses except for 204
401
+ # Raise exception if request failed
402
+ #
403
+ # @param [Integer] code for response status
404
+ # @param [Object] body of response
405
+ # @param [Hash] headers for response
406
+ # @param [Boolean] decode JSON-encoded body on success
407
+ #
408
+ # @return [Object] JSON-decoded response body
409
+ #
410
+ # @raise [HttpException] HTTP failure with associated status code
411
+ def self.response(code, body, headers, decode)
412
+ if (200..207).include?(code)
413
+ if code == 201
414
+ result = headers[:location]
415
+ elsif code == 204 || body.nil? || (body.respond_to?(:empty?) && body.empty?)
416
+ result = nil
417
+ elsif decode
418
+ result = JSON.load(body)
419
+ result = nil if result.respond_to?(:empty?) && result.empty?
420
+ else
421
+ result = body
422
+ end
423
+ else
424
+ raise HttpExceptions.create(code, body, headers)
425
+ end
426
+ result
427
+ end
428
+
341
429
  # Extract text of exception for logging
342
430
  # For RestClient exceptions extract useful info from http_body attribute
343
431
  #
@@ -348,7 +436,7 @@ module RightScale
348
436
  case exception
349
437
  when String
350
438
  exception
351
- when RestClient::Exception
439
+ when HttpException, RestClient::Exception
352
440
  if exception.http_body.nil? || exception.http_body.empty? || exception.http_body =~ /^<html>| html /
353
441
  exception.message
354
442
  else