elastic-transport 8.0.0.pre1
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.
- checksums.yaml +7 -0
- data/.github/check_license_headers.rb +33 -0
- data/.github/license-header.txt +16 -0
- data/.github/workflows/license.yml +13 -0
- data/.github/workflows/tests.yml +45 -0
- data/.gitignore +19 -0
- data/CHANGELOG.md +224 -0
- data/Gemfile +38 -0
- data/LICENSE +202 -0
- data/README.md +552 -0
- data/Rakefile +87 -0
- data/elastic-transport.gemspec +74 -0
- data/lib/elastic/transport/client.rb +276 -0
- data/lib/elastic/transport/meta_header.rb +135 -0
- data/lib/elastic/transport/redacted.rb +73 -0
- data/lib/elastic/transport/transport/base.rb +450 -0
- data/lib/elastic/transport/transport/connections/collection.rb +126 -0
- data/lib/elastic/transport/transport/connections/connection.rb +160 -0
- data/lib/elastic/transport/transport/connections/selector.rb +91 -0
- data/lib/elastic/transport/transport/errors.rb +91 -0
- data/lib/elastic/transport/transport/http/curb.rb +120 -0
- data/lib/elastic/transport/transport/http/faraday.rb +95 -0
- data/lib/elastic/transport/transport/http/manticore.rb +179 -0
- data/lib/elastic/transport/transport/loggable.rb +83 -0
- data/lib/elastic/transport/transport/response.rb +36 -0
- data/lib/elastic/transport/transport/serializer/multi_json.rb +52 -0
- data/lib/elastic/transport/transport/sniffer.rb +101 -0
- data/lib/elastic/transport/version.rb +22 -0
- data/lib/elastic/transport.rb +37 -0
- data/lib/elastic-transport.rb +18 -0
- data/spec/elasticsearch/connections/collection_spec.rb +266 -0
- data/spec/elasticsearch/connections/selector_spec.rb +166 -0
- data/spec/elasticsearch/transport/base_spec.rb +264 -0
- data/spec/elasticsearch/transport/client_spec.rb +1651 -0
- data/spec/elasticsearch/transport/meta_header_spec.rb +274 -0
- data/spec/elasticsearch/transport/sniffer_spec.rb +275 -0
- data/spec/spec_helper.rb +90 -0
- data/test/integration/transport_test.rb +98 -0
- data/test/profile/client_benchmark_test.rb +132 -0
- data/test/test_helper.rb +83 -0
- data/test/unit/connection_test.rb +135 -0
- data/test/unit/response_test.rb +30 -0
- data/test/unit/serializer_test.rb +33 -0
- data/test/unit/transport_base_test.rb +664 -0
- data/test/unit/transport_curb_test.rb +135 -0
- data/test/unit/transport_faraday_test.rb +228 -0
- data/test/unit/transport_manticore_test.rb +251 -0
- metadata +412 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
# Licensed to Elasticsearch B.V. under one or more contributor
|
2
|
+
# license agreements. See the NOTICE file distributed with
|
3
|
+
# this work for additional information regarding copyright
|
4
|
+
# ownership. Elasticsearch B.V. licenses this file to you under
|
5
|
+
# the Apache License, Version 2.0 (the "License"); you may
|
6
|
+
# not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
12
|
+
# software distributed under the License is distributed on an
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
14
|
+
# KIND, either express or implied. See the License for the
|
15
|
+
# specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
|
18
|
+
module Elastic
|
19
|
+
module Transport
|
20
|
+
# Class for wrapping a hash that could have sensitive data.
|
21
|
+
# When printed, the sensitive values will be redacted.
|
22
|
+
#
|
23
|
+
# @since 6.1.1
|
24
|
+
class Redacted < ::Hash
|
25
|
+
def initialize(elements = nil)
|
26
|
+
super()
|
27
|
+
(elements || {}).each_pair{ |key, value| self[key] = value }
|
28
|
+
end
|
29
|
+
|
30
|
+
# The keys whose values will be redacted.
|
31
|
+
#
|
32
|
+
# @since 6.1.1
|
33
|
+
SENSITIVE_KEYS = [ :password,
|
34
|
+
:pwd ].freeze
|
35
|
+
|
36
|
+
# The replacement string used in place of the value for sensitive keys.
|
37
|
+
#
|
38
|
+
# @since 6.1.1
|
39
|
+
STRING_REPLACEMENT = '<REDACTED>'.freeze
|
40
|
+
|
41
|
+
# Get a string representation of the hash.
|
42
|
+
#
|
43
|
+
# @return [ String ] The string representation of the hash.
|
44
|
+
#
|
45
|
+
# @since 6.1.1
|
46
|
+
def inspect
|
47
|
+
redacted_string(:inspect)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Get a string representation of the hash.
|
51
|
+
#
|
52
|
+
# @return [ String ] The string representation of the hash.
|
53
|
+
#
|
54
|
+
# @since 6.1.1
|
55
|
+
def to_s
|
56
|
+
redacted_string(:to_s)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def redacted_string(method)
|
62
|
+
'{' + reduce([]) do |list, (k, v)|
|
63
|
+
list << "#{k.send(method)}=>#{redact(k, v, method)}"
|
64
|
+
end.join(', ') + '}'
|
65
|
+
end
|
66
|
+
|
67
|
+
def redact(k, v, method)
|
68
|
+
return STRING_REPLACEMENT if SENSITIVE_KEYS.include?(k.to_sym)
|
69
|
+
v.send(method)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,450 @@
|
|
1
|
+
# Licensed to Elasticsearch B.V. under one or more contributor
|
2
|
+
# license agreements. See the NOTICE file distributed with
|
3
|
+
# this work for additional information regarding copyright
|
4
|
+
# ownership. Elasticsearch B.V. licenses this file to you under
|
5
|
+
# the Apache License, Version 2.0 (the "License"); you may
|
6
|
+
# not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
12
|
+
# software distributed under the License is distributed on an
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
14
|
+
# KIND, either express or implied. See the License for the
|
15
|
+
# specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
|
18
|
+
module Elastic
|
19
|
+
module Transport
|
20
|
+
module Transport
|
21
|
+
# @abstract Module with common functionality for transport implementations.
|
22
|
+
#
|
23
|
+
module Base
|
24
|
+
include Loggable
|
25
|
+
|
26
|
+
DEFAULT_PORT = 9200
|
27
|
+
DEFAULT_PROTOCOL = 'http'
|
28
|
+
DEFAULT_RELOAD_AFTER = 10_000 # Requests
|
29
|
+
DEFAULT_RESURRECT_AFTER = 60 # Seconds
|
30
|
+
DEFAULT_MAX_RETRIES = 3 # Requests
|
31
|
+
DEFAULT_SERIALIZER_CLASS = Serializer::MultiJson
|
32
|
+
SANITIZED_PASSWORD = '*' * (rand(14)+1)
|
33
|
+
|
34
|
+
attr_reader :hosts, :options, :connections, :counter, :last_request_at, :protocol
|
35
|
+
attr_accessor :serializer, :sniffer, :logger, :tracer,
|
36
|
+
:reload_connections, :reload_after,
|
37
|
+
:resurrect_after
|
38
|
+
|
39
|
+
# Creates a new transport object
|
40
|
+
#
|
41
|
+
# @param arguments [Hash] Settings and options for the transport
|
42
|
+
# @param block [Proc] Lambda or Proc which can be evaluated in the context of the "session" object
|
43
|
+
#
|
44
|
+
# @option arguments [Array] :hosts An Array of normalized hosts information
|
45
|
+
# @option arguments [Array] :options A Hash with options (usually passed by {Client})
|
46
|
+
#
|
47
|
+
# @see Client#initialize
|
48
|
+
#
|
49
|
+
def initialize(arguments = {}, &block)
|
50
|
+
@state_mutex = Mutex.new
|
51
|
+
|
52
|
+
@hosts = arguments[:hosts] || []
|
53
|
+
@options = arguments[:options] || {}
|
54
|
+
@options[:http] ||= {}
|
55
|
+
@options[:retry_on_status] ||= []
|
56
|
+
|
57
|
+
@block = block
|
58
|
+
@compression = !!@options[:compression]
|
59
|
+
@connections = __build_connections
|
60
|
+
|
61
|
+
@serializer = options[:serializer] || ( options[:serializer_class] ? options[:serializer_class].new(self) : DEFAULT_SERIALIZER_CLASS.new(self) )
|
62
|
+
@protocol = options[:protocol] || DEFAULT_PROTOCOL
|
63
|
+
|
64
|
+
@logger = options[:logger]
|
65
|
+
@tracer = options[:tracer]
|
66
|
+
|
67
|
+
@sniffer = options[:sniffer_class] ? options[:sniffer_class].new(self) : Sniffer.new(self)
|
68
|
+
@counter = 0
|
69
|
+
@counter_mtx = Mutex.new
|
70
|
+
@last_request_at = Time.now
|
71
|
+
@reload_connections = options[:reload_connections]
|
72
|
+
@reload_after = options[:reload_connections].is_a?(Integer) ? options[:reload_connections] : DEFAULT_RELOAD_AFTER
|
73
|
+
@resurrect_after = options[:resurrect_after] || DEFAULT_RESURRECT_AFTER
|
74
|
+
@retry_on_status = Array(options[:retry_on_status]).map { |d| d.to_i }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns a connection from the connection pool by delegating to {Connections::Collection#get_connection}.
|
78
|
+
#
|
79
|
+
# Resurrects dead connection if the `resurrect_after` timeout has passed.
|
80
|
+
# Increments the counter and performs connection reloading if the `reload_connections` option is set.
|
81
|
+
#
|
82
|
+
# @return [Connections::Connection]
|
83
|
+
# @see Connections::Collection#get_connection
|
84
|
+
#
|
85
|
+
def get_connection(options={})
|
86
|
+
resurrect_dead_connections! if Time.now > @last_request_at + @resurrect_after
|
87
|
+
|
88
|
+
@counter_mtx.synchronize { @counter += 1 }
|
89
|
+
reload_connections! if reload_connections && counter % reload_after == 0
|
90
|
+
connections.get_connection(options)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Reloads and replaces the connection collection based on cluster information
|
94
|
+
#
|
95
|
+
# @see Sniffer#hosts
|
96
|
+
#
|
97
|
+
def reload_connections!
|
98
|
+
hosts = sniffer.hosts
|
99
|
+
__rebuild_connections :hosts => hosts, :options => options
|
100
|
+
self
|
101
|
+
rescue SnifferTimeoutError
|
102
|
+
log_error "[SnifferTimeoutError] Timeout when reloading connections."
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
# Tries to "resurrect" all eligible dead connections
|
107
|
+
#
|
108
|
+
# @see Connections::Connection#resurrect!
|
109
|
+
#
|
110
|
+
def resurrect_dead_connections!
|
111
|
+
connections.dead.each { |c| c.resurrect! }
|
112
|
+
end
|
113
|
+
|
114
|
+
# Rebuilds the connections collection in the transport.
|
115
|
+
#
|
116
|
+
# The methods *adds* new connections from the passed hosts to the collection,
|
117
|
+
# and *removes* all connections not contained in the passed hosts.
|
118
|
+
#
|
119
|
+
# @return [Connections::Collection]
|
120
|
+
# @api private
|
121
|
+
#
|
122
|
+
def __rebuild_connections(arguments={})
|
123
|
+
@state_mutex.synchronize do
|
124
|
+
@hosts = arguments[:hosts] || []
|
125
|
+
@options = arguments[:options] || {}
|
126
|
+
|
127
|
+
__close_connections
|
128
|
+
|
129
|
+
new_connections = __build_connections
|
130
|
+
stale_connections = @connections.all.select { |c| ! new_connections.include?(c) }
|
131
|
+
new_connections = new_connections.reject { |c| @connections.all.include?(c) }
|
132
|
+
|
133
|
+
@connections.remove(stale_connections)
|
134
|
+
@connections.add(new_connections)
|
135
|
+
@connections
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Builds and returns a collection of connections
|
140
|
+
#
|
141
|
+
# The adapters have to implement the {Base#__build_connection} method.
|
142
|
+
#
|
143
|
+
# @return [Connections::Collection]
|
144
|
+
# @api private
|
145
|
+
#
|
146
|
+
def __build_connections
|
147
|
+
Connections::Collection.new(
|
148
|
+
connections: __connections_from_host,
|
149
|
+
selector_class: options[:selector_class],
|
150
|
+
selector: options[:selector]
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Helper function: Maps over hosts, sets protocol, port and user/password and builds connections
|
155
|
+
#
|
156
|
+
# @return [Array<Connection>
|
157
|
+
# @api private
|
158
|
+
#
|
159
|
+
def __connections_from_host
|
160
|
+
hosts.map do |host|
|
161
|
+
host[:protocol] = host[:scheme] || options[:scheme] || options[:http][:scheme] || DEFAULT_PROTOCOL
|
162
|
+
host[:port] ||= options[:port] || options[:http][:port] || DEFAULT_PORT
|
163
|
+
if (options[:user] || options[:http][:user]) && !host[:user]
|
164
|
+
host[:user] ||= options[:user] || options[:http][:user]
|
165
|
+
host[:password] ||= options[:password] || options[:http][:password]
|
166
|
+
end
|
167
|
+
|
168
|
+
__build_connection(host, (options[:transport_options] || {}), @block)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# @abstract Build and return a connection.
|
173
|
+
# A transport implementation *must* implement this method.
|
174
|
+
# See {HTTP::Faraday#__build_connection} for an example.
|
175
|
+
#
|
176
|
+
# @return [Connections::Connection]
|
177
|
+
# @api private
|
178
|
+
#
|
179
|
+
def __build_connection(host, options={}, block=nil)
|
180
|
+
raise NoMethodError, "Implement this method in your class"
|
181
|
+
end
|
182
|
+
|
183
|
+
# Closes the connections collection
|
184
|
+
#
|
185
|
+
# @api private
|
186
|
+
#
|
187
|
+
def __close_connections
|
188
|
+
# A hook point for specific adapters when they need to close connections
|
189
|
+
end
|
190
|
+
|
191
|
+
# Log request and response information
|
192
|
+
#
|
193
|
+
# @api private
|
194
|
+
#
|
195
|
+
def __log_response(method, path, params, body, url, response, json, took, duration)
|
196
|
+
if logger
|
197
|
+
sanitized_url = url.to_s.gsub(/\/\/(.+):(.+)@/, '//' + '\1:' + SANITIZED_PASSWORD + '@')
|
198
|
+
log_info "#{method.to_s.upcase} #{sanitized_url} " +
|
199
|
+
"[status:#{response.status}, request:#{sprintf('%.3fs', duration)}, query:#{took}]"
|
200
|
+
log_debug "> #{__convert_to_json(body)}" if body
|
201
|
+
log_debug "< #{response.body}"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Trace the request in the `curl` format
|
206
|
+
#
|
207
|
+
# @api private
|
208
|
+
#
|
209
|
+
def __trace(method, path, params, headers, body, url, response, json, took, duration)
|
210
|
+
trace_url = "http://localhost:9200/#{path}?pretty" +
|
211
|
+
( params.empty? ? '' : "&#{::Faraday::Utils::ParamsHash[params].to_query}" )
|
212
|
+
trace_body = body ? " -d '#{__convert_to_json(body, :pretty => true)}'" : ''
|
213
|
+
trace_command = "curl -X #{method.to_s.upcase}"
|
214
|
+
trace_command += " -H '#{headers.collect { |k,v| "#{k}: #{v}" }.join(", ")}'" if headers && !headers.empty?
|
215
|
+
trace_command += " '#{trace_url}'#{trace_body}\n"
|
216
|
+
tracer.info trace_command
|
217
|
+
tracer.debug "# #{Time.now.iso8601} [#{response.status}] (#{format('%.3f', duration)}s)\n#"
|
218
|
+
tracer.debug json ? serializer.dump(json, :pretty => true).gsub(/^/, '# ').sub(/\}$/, "\n# }")+"\n" : "# #{response.body}\n"
|
219
|
+
end
|
220
|
+
|
221
|
+
# Raise error specific for the HTTP response status or a generic server error
|
222
|
+
#
|
223
|
+
# @api private
|
224
|
+
#
|
225
|
+
def __raise_transport_error(response)
|
226
|
+
error = ERRORS[response.status] || ServerError
|
227
|
+
raise error.new "[#{response.status}] #{response.body}"
|
228
|
+
end
|
229
|
+
|
230
|
+
# Converts any non-String object to JSON
|
231
|
+
#
|
232
|
+
# @api private
|
233
|
+
#
|
234
|
+
def __convert_to_json(o=nil, options={})
|
235
|
+
o = o.is_a?(String) ? o : serializer.dump(o, options)
|
236
|
+
end
|
237
|
+
|
238
|
+
# Returns a full URL based on information from host
|
239
|
+
#
|
240
|
+
# @param host [Hash] Host configuration passed in from {Client}
|
241
|
+
#
|
242
|
+
# @api private
|
243
|
+
def __full_url(host)
|
244
|
+
url = "#{host[:protocol]}://"
|
245
|
+
url += "#{CGI.escape(host[:user])}:#{CGI.escape(host[:password])}@" if host[:user]
|
246
|
+
url += host[:host]
|
247
|
+
url += ":#{host[:port]}" if host[:port]
|
248
|
+
url += host[:path] if host[:path]
|
249
|
+
url
|
250
|
+
end
|
251
|
+
|
252
|
+
# Performs a request to Elasticsearch, while handling logging, tracing, marking dead connections,
|
253
|
+
# retrying the request and reloading the connections.
|
254
|
+
#
|
255
|
+
# @abstract The transport implementation has to implement this method either in full,
|
256
|
+
# or by invoking this method with a block. See {HTTP::Faraday#perform_request} for an example.
|
257
|
+
#
|
258
|
+
# @param method [String] Request method
|
259
|
+
# @param path [String] The API endpoint
|
260
|
+
# @param params [Hash] Request parameters (will be serialized by {Connections::Connection#full_url})
|
261
|
+
# @param body [Hash] Request body (will be serialized by the {#serializer})
|
262
|
+
# @param headers [Hash] Request headers (will be serialized by the {#serializer})
|
263
|
+
# @param block [Proc] Code block to evaluate, passed from the implementation
|
264
|
+
#
|
265
|
+
# @return [Response]
|
266
|
+
# @raise [NoMethodError] If no block is passed
|
267
|
+
# @raise [ServerError] If request failed on server
|
268
|
+
# @raise [Error] If no connection is available
|
269
|
+
#
|
270
|
+
def perform_request(method, path, params = {}, body = nil, headers = nil, opts = {}, &block)
|
271
|
+
raise NoMethodError, 'Implement this method in your transport class' unless block_given?
|
272
|
+
|
273
|
+
start = Time.now
|
274
|
+
tries = 0
|
275
|
+
reload_on_failure = opts.fetch(:reload_on_failure, @options[:reload_on_failure])
|
276
|
+
|
277
|
+
max_retries = if opts.key?(:retry_on_failure)
|
278
|
+
opts[:retry_on_failure] === true ? DEFAULT_MAX_RETRIES : opts[:retry_on_failure]
|
279
|
+
elsif options.key?(:retry_on_failure)
|
280
|
+
options[:retry_on_failure] === true ? DEFAULT_MAX_RETRIES : options[:retry_on_failure]
|
281
|
+
end
|
282
|
+
|
283
|
+
params = params.clone
|
284
|
+
|
285
|
+
ignore = Array(params.delete(:ignore)).compact.map { |s| s.to_i }
|
286
|
+
|
287
|
+
begin
|
288
|
+
tries += 1
|
289
|
+
connection = get_connection or raise Error.new('Cannot get new connection from pool.')
|
290
|
+
|
291
|
+
if connection.connection.respond_to?(:params) && connection.connection.params.respond_to?(:to_hash)
|
292
|
+
params = connection.connection.params.merge(params.to_hash)
|
293
|
+
end
|
294
|
+
|
295
|
+
url = connection.full_url(path, params)
|
296
|
+
|
297
|
+
response = block.call(connection, url)
|
298
|
+
|
299
|
+
connection.healthy! if connection.failures > 0
|
300
|
+
|
301
|
+
# Raise an exception so we can catch it for `retry_on_status`
|
302
|
+
__raise_transport_error(response) if response.status.to_i >= 300 && @retry_on_status.include?(response.status.to_i)
|
303
|
+
|
304
|
+
rescue Elastic::Transport::Transport::ServerError => e
|
305
|
+
if response && @retry_on_status.include?(response.status)
|
306
|
+
log_warn "[#{e.class}] Attempt #{tries} to get response from #{url}"
|
307
|
+
if tries <= (max_retries || DEFAULT_MAX_RETRIES)
|
308
|
+
retry
|
309
|
+
else
|
310
|
+
log_fatal "[#{e.class}] Cannot get response from #{url} after #{tries} tries"
|
311
|
+
raise e
|
312
|
+
end
|
313
|
+
else
|
314
|
+
raise e
|
315
|
+
end
|
316
|
+
|
317
|
+
rescue *host_unreachable_exceptions => e
|
318
|
+
log_error "[#{e.class}] #{e.message} #{connection.host.inspect}"
|
319
|
+
|
320
|
+
connection.dead!
|
321
|
+
|
322
|
+
if reload_on_failure and tries < connections.all.size
|
323
|
+
log_warn "[#{e.class}] Reloading connections (attempt #{tries} of #{connections.all.size})"
|
324
|
+
reload_connections! and retry
|
325
|
+
end
|
326
|
+
|
327
|
+
if max_retries
|
328
|
+
log_warn "[#{e.class}] Attempt #{tries} connecting to #{connection.host.inspect}"
|
329
|
+
if tries <= max_retries
|
330
|
+
retry
|
331
|
+
else
|
332
|
+
log_fatal "[#{e.class}] Cannot connect to #{connection.host.inspect} after #{tries} tries"
|
333
|
+
raise e
|
334
|
+
end
|
335
|
+
else
|
336
|
+
raise e
|
337
|
+
end
|
338
|
+
|
339
|
+
rescue Exception => e
|
340
|
+
log_fatal "[#{e.class}] #{e.message} (#{connection.host.inspect if connection})"
|
341
|
+
raise e
|
342
|
+
|
343
|
+
end #/begin
|
344
|
+
|
345
|
+
duration = Time.now - start
|
346
|
+
|
347
|
+
if response.status.to_i >= 300
|
348
|
+
__log_response method, path, params, body, url, response, nil, 'N/A', duration
|
349
|
+
__trace method, path, params, connection.connection.headers, body, url, response, nil, 'N/A', duration if tracer
|
350
|
+
|
351
|
+
# Log the failure only when `ignore` doesn't match the response status
|
352
|
+
unless ignore.include?(response.status.to_i)
|
353
|
+
log_fatal "[#{response.status}] #{response.body}"
|
354
|
+
end
|
355
|
+
|
356
|
+
__raise_transport_error response unless ignore.include?(response.status.to_i)
|
357
|
+
end
|
358
|
+
|
359
|
+
json = serializer.load(response.body) if response.body && !response.body.empty? && response.headers && response.headers["content-type"] =~ /json/
|
360
|
+
took = (json['took'] ? sprintf('%.3fs', json['took']/1000.0) : 'n/a') rescue 'n/a'
|
361
|
+
|
362
|
+
unless ignore.include?(response.status.to_i)
|
363
|
+
__log_response method, path, params, body, url, response, json, took, duration
|
364
|
+
end
|
365
|
+
|
366
|
+
__trace method, path, params, connection.connection.headers, body, url, response, nil, 'N/A', duration if tracer
|
367
|
+
|
368
|
+
warnings(response.headers['warning']) if response.headers&.[]('warning')
|
369
|
+
|
370
|
+
Response.new response.status, json || response.body, response.headers
|
371
|
+
ensure
|
372
|
+
@last_request_at = Time.now
|
373
|
+
end
|
374
|
+
|
375
|
+
# @abstract Returns an Array of connection errors specific to the transport implementation.
|
376
|
+
# See {HTTP::Faraday#host_unreachable_exceptions} for an example.
|
377
|
+
#
|
378
|
+
# @return [Array]
|
379
|
+
#
|
380
|
+
def host_unreachable_exceptions
|
381
|
+
[Errno::ECONNREFUSED]
|
382
|
+
end
|
383
|
+
|
384
|
+
private
|
385
|
+
|
386
|
+
USER_AGENT_STR = 'User-Agent'.freeze
|
387
|
+
USER_AGENT_REGEX = /user\-?\_?agent/
|
388
|
+
CONTENT_TYPE_STR = 'Content-Type'.freeze
|
389
|
+
CONTENT_TYPE_REGEX = /content\-?\_?type/
|
390
|
+
DEFAULT_CONTENT_TYPE = 'application/json'.freeze
|
391
|
+
GZIP = 'gzip'.freeze
|
392
|
+
ACCEPT_ENCODING = 'Accept-Encoding'.freeze
|
393
|
+
GZIP_FIRST_TWO_BYTES = '1f8b'.freeze
|
394
|
+
HEX_STRING_DIRECTIVE = 'H*'.freeze
|
395
|
+
RUBY_ENCODING = '1.9'.respond_to?(:force_encoding)
|
396
|
+
|
397
|
+
def decompress_response(body)
|
398
|
+
return body unless use_compression?
|
399
|
+
return body unless gzipped?(body)
|
400
|
+
|
401
|
+
io = StringIO.new(body)
|
402
|
+
gzip_reader = if RUBY_ENCODING
|
403
|
+
Zlib::GzipReader.new(io, :encoding => 'ASCII-8BIT')
|
404
|
+
else
|
405
|
+
Zlib::GzipReader.new(io)
|
406
|
+
end
|
407
|
+
gzip_reader.read
|
408
|
+
end
|
409
|
+
|
410
|
+
def gzipped?(body)
|
411
|
+
body[0..1].unpack(HEX_STRING_DIRECTIVE)[0] == GZIP_FIRST_TWO_BYTES
|
412
|
+
end
|
413
|
+
|
414
|
+
def use_compression?
|
415
|
+
@compression
|
416
|
+
end
|
417
|
+
|
418
|
+
def apply_headers(client, options)
|
419
|
+
headers = options[:headers] || {}
|
420
|
+
headers[CONTENT_TYPE_STR] = find_value(headers, CONTENT_TYPE_REGEX) || DEFAULT_CONTENT_TYPE
|
421
|
+
headers[USER_AGENT_STR] = find_value(headers, USER_AGENT_REGEX) || user_agent_header(client)
|
422
|
+
client.headers[ACCEPT_ENCODING] = GZIP if use_compression?
|
423
|
+
client.headers.merge!(headers)
|
424
|
+
end
|
425
|
+
|
426
|
+
def find_value(hash, regex)
|
427
|
+
key_value = hash.find { |k,v| k.to_s.downcase =~ regex }
|
428
|
+
if key_value
|
429
|
+
hash.delete(key_value[0])
|
430
|
+
key_value[1]
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
def user_agent_header(client)
|
435
|
+
@user_agent ||= begin
|
436
|
+
meta = ["RUBY_VERSION: #{RUBY_VERSION}"]
|
437
|
+
if RbConfig::CONFIG && RbConfig::CONFIG['host_os']
|
438
|
+
meta << "#{RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase} #{RbConfig::CONFIG['target_cpu']}"
|
439
|
+
end
|
440
|
+
"elastic-transport-ruby/#{VERSION} (#{meta.join('; ')})"
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
def warnings(warning)
|
445
|
+
warn("warning: #{warning}")
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|