elastic-transport 8.0.0 → 8.4.0

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/license.yml +2 -2
  3. data/.github/workflows/otel.yml +48 -0
  4. data/.github/workflows/tests.yml +45 -5
  5. data/.gitignore +1 -1
  6. data/CHANGELOG.md +131 -8
  7. data/CONTRIBUTING.md +64 -0
  8. data/Gemfile +10 -9
  9. data/Gemfile-faraday1.gemfile +40 -0
  10. data/README.md +7 -528
  11. data/Rakefile +48 -1
  12. data/elastic-transport.gemspec +6 -9
  13. data/lib/elastic/transport/client.rb +66 -45
  14. data/lib/elastic/transport/meta_header.rb +21 -12
  15. data/lib/elastic/transport/opentelemetry.rb +166 -0
  16. data/lib/elastic/transport/transport/base.rb +74 -54
  17. data/lib/elastic/transport/transport/errors.rb +2 -4
  18. data/lib/elastic/transport/transport/http/curb.rb +30 -26
  19. data/lib/elastic/transport/transport/http/faraday.rb +30 -27
  20. data/lib/elastic/transport/transport/http/manticore.rb +10 -4
  21. data/lib/elastic/transport/transport/response.rb +3 -3
  22. data/lib/elastic/transport/transport/serializer/multi_json.rb +3 -3
  23. data/lib/elastic/transport/transport/sniffer.rb +3 -1
  24. data/lib/elastic/transport/version.rb +1 -1
  25. data/lib/elastic/transport.rb +1 -0
  26. data/spec/elastic/transport/base_spec.rb +26 -25
  27. data/spec/elastic/transport/client_spec.rb +91 -18
  28. data/spec/elastic/transport/http/manticore_spec.rb +20 -2
  29. data/spec/elastic/transport/meta_header_spec.rb +26 -11
  30. data/spec/elastic/transport/opentelemetry_spec.rb +325 -0
  31. data/spec/elastic/transport/sniffer_spec.rb +18 -0
  32. data/spec/spec_helper.rb +16 -1
  33. data/test/integration/jruby_test.rb +1 -1
  34. data/test/integration/transport_test.rb +86 -40
  35. data/test/test_helper.rb +9 -6
  36. data/test/unit/adapters_test.rb +104 -0
  37. data/test/unit/connection_test.rb +35 -37
  38. data/test/unit/transport_base_test.rb +7 -8
  39. data/test/unit/transport_curb_test.rb +2 -3
  40. data/test/unit/transport_manticore_test.rb +1 -1
  41. metadata +23 -76
@@ -24,12 +24,12 @@ module Elastic
24
24
  include Loggable
25
25
 
26
26
  DEFAULT_PORT = 9200
27
- DEFAULT_PROTOCOL = 'http'
27
+ DEFAULT_PROTOCOL = 'http'.freeze
28
28
  DEFAULT_RELOAD_AFTER = 10_000 # Requests
29
29
  DEFAULT_RESURRECT_AFTER = 60 # Seconds
30
30
  DEFAULT_MAX_RETRIES = 3 # Requests
31
31
  DEFAULT_SERIALIZER_CLASS = Serializer::MultiJson
32
- SANITIZED_PASSWORD = '*' * (rand(14)+1)
32
+ SANITIZED_PASSWORD = '*' * (rand(14) + 1)
33
33
 
34
34
  attr_reader :hosts, :options, :connections, :counter, :last_request_at, :protocol
35
35
  attr_accessor :serializer, :sniffer, :logger, :tracer,
@@ -59,7 +59,7 @@ module Elastic
59
59
  @compression = !!@options[:compression]
60
60
  @connections = __build_connections
61
61
 
62
- @serializer = options[:serializer] || ( options[:serializer_class] ? options[:serializer_class].new(self) : DEFAULT_SERIALIZER_CLASS.new(self) )
62
+ @serializer = options[:serializer] || ( options[:serializer_class] ? options[:serializer_class].new(self) : DEFAULT_SERIALIZER_CLASS.new(self))
63
63
  @protocol = options[:protocol] || DEFAULT_PROTOCOL
64
64
 
65
65
  @logger = options[:logger]
@@ -72,7 +72,7 @@ module Elastic
72
72
  @reload_connections = options[:reload_connections]
73
73
  @reload_after = options[:reload_connections].is_a?(Integer) ? options[:reload_connections] : DEFAULT_RELOAD_AFTER
74
74
  @resurrect_after = options[:resurrect_after] || DEFAULT_RESURRECT_AFTER
75
- @retry_on_status = Array(options[:retry_on_status]).map { |d| d.to_i }
75
+ @retry_on_status = Array(options[:retry_on_status]).map(&:to_i)
76
76
  end
77
77
 
78
78
  # Returns a connection from the connection pool by delegating to {Connections::Collection#get_connection}.
@@ -83,11 +83,11 @@ module Elastic
83
83
  # @return [Connections::Connection]
84
84
  # @see Connections::Collection#get_connection
85
85
  #
86
- def get_connection(options={})
86
+ def get_connection(options = {})
87
87
  resurrect_dead_connections! if Time.now > @last_request_at + @resurrect_after
88
88
 
89
89
  @counter_mtx.synchronize { @counter += 1 }
90
- reload_connections! if reload_connections && counter % reload_after == 0
90
+ reload_connections! if reload_connections && (counter % reload_after).zero?
91
91
  connections.get_connection(options)
92
92
  end
93
93
 
@@ -97,10 +97,10 @@ module Elastic
97
97
  #
98
98
  def reload_connections!
99
99
  hosts = sniffer.hosts
100
- __rebuild_connections :hosts => hosts, :options => options
100
+ __rebuild_connections(hosts: hosts, options: options)
101
101
  self
102
102
  rescue SnifferTimeoutError
103
- log_error "[SnifferTimeoutError] Timeout when reloading connections."
103
+ log_error('[SnifferTimeoutError] Timeout when reloading connections.')
104
104
  self
105
105
  end
106
106
 
@@ -109,7 +109,7 @@ module Elastic
109
109
  # @see Connections::Connection#resurrect!
110
110
  #
111
111
  def resurrect_dead_connections!
112
- connections.dead.each { |c| c.resurrect! }
112
+ connections.dead.each(&:resurrect!)
113
113
  end
114
114
 
115
115
  # Rebuilds the connections collection in the transport.
@@ -120,7 +120,7 @@ module Elastic
120
120
  # @return [Connections::Collection]
121
121
  # @api private
122
122
  #
123
- def __rebuild_connections(arguments={})
123
+ def __rebuild_connections(arguments = {})
124
124
  @state_mutex.synchronize do
125
125
  @hosts = arguments[:hosts] || []
126
126
  @options = arguments[:options] || {}
@@ -128,7 +128,7 @@ module Elastic
128
128
  __close_connections
129
129
 
130
130
  new_connections = __build_connections
131
- stale_connections = @connections.all.select { |c| ! new_connections.include?(c) }
131
+ stale_connections = @connections.all.reject { |c| new_connections.include?(c) }
132
132
  new_connections = new_connections.reject { |c| @connections.all.include?(c) }
133
133
 
134
134
  @connections.remove(stale_connections)
@@ -177,8 +177,8 @@ module Elastic
177
177
  # @return [Connections::Connection]
178
178
  # @api private
179
179
  #
180
- def __build_connection(host, options={}, block=nil)
181
- raise NoMethodError, "Implement this method in your class"
180
+ def __build_connection(host, options = {}, block = nil)
181
+ raise NoMethodError, 'Implement this method in your class'
182
182
  end
183
183
 
184
184
  # Closes the connections collection
@@ -209,14 +209,14 @@ module Elastic
209
209
  #
210
210
  def __trace(method, path, params, headers, body, url, response, json, took, duration)
211
211
  trace_url = "http://localhost:9200/#{path}?pretty" +
212
- ( params.empty? ? '' : "&#{::Faraday::Utils::ParamsHash[params].to_query}" )
212
+ (params.empty? ? '' : "&#{::Faraday::Utils::ParamsHash[params].to_query}")
213
213
  trace_body = body ? " -d '#{__convert_to_json(body, :pretty => true)}'" : ''
214
214
  trace_command = "curl -X #{method.to_s.upcase}"
215
- trace_command += " -H '#{headers.collect { |k,v| "#{k}: #{v}" }.join(", ")}'" if headers && !headers.empty?
215
+ trace_command += " -H '#{headers.collect { |k, v| "#{k}: #{v}" }.join(", ")}'" if headers && !headers.empty?
216
216
  trace_command += " '#{trace_url}'#{trace_body}\n"
217
217
  tracer.info trace_command
218
218
  tracer.debug "# #{Time.now.iso8601} [#{response.status}] (#{format('%.3f', duration)}s)\n#"
219
- tracer.debug json ? serializer.dump(json, :pretty => true).gsub(/^/, '# ').sub(/\}$/, "\n# }")+"\n" : "# #{response.body}\n"
219
+ tracer.debug json ? serializer.dump(json, pretty: true).gsub(/^/, '# ').sub(/\}$/, "\n# }")+"\n" : "# #{response.body}\n"
220
220
  end
221
221
 
222
222
  # Raise error specific for the HTTP response status or a generic server error
@@ -232,7 +232,7 @@ module Elastic
232
232
  #
233
233
  # @api private
234
234
  #
235
- def __convert_to_json(o=nil, options={})
235
+ def __convert_to_json(o = nil, options = {})
236
236
  o.is_a?(String) ? o : serializer.dump(o, options)
237
237
  end
238
238
 
@@ -276,40 +276,37 @@ module Elastic
276
276
  reload_on_failure = opts.fetch(:reload_on_failure, @options[:reload_on_failure])
277
277
  delay_on_retry = opts.fetch(:delay_on_retry, @options[:delay_on_retry])
278
278
 
279
- max_retries = if opts.key?(:retry_on_failure)
280
- opts[:retry_on_failure] === true ? DEFAULT_MAX_RETRIES : opts[:retry_on_failure]
281
- elsif options.key?(:retry_on_failure)
282
- options[:retry_on_failure] === true ? DEFAULT_MAX_RETRIES : options[:retry_on_failure]
283
- end
279
+ max_retries = max_retries(opts) || max_retries(options)
284
280
 
285
281
  params = params.clone
286
- ignore = Array(params.delete(:ignore)).compact.map { |s| s.to_i }
282
+ # Transforms ignore status codes to Integer
283
+ ignore = Array(params.delete(:ignore)).compact.map(&:to_i)
287
284
 
288
285
  begin
289
286
  sleep(delay_on_retry / 1000.0) if tries > 0
290
- tries += 1
287
+ tries += 1
291
288
  connection = get_connection or raise Error.new('Cannot get new connection from pool.')
292
289
 
293
- if connection.connection.respond_to?(:params) && connection.connection.params.respond_to?(:to_hash)
290
+ if connection.connection.respond_to?(:params) &&
291
+ connection.connection.params.respond_to?(:to_hash)
294
292
  params = connection.connection.params.merge(params.to_hash)
295
293
  end
296
294
 
297
- url = connection.full_url(path, params)
295
+ url = connection.full_url(path, params)
298
296
  response = block.call(connection, url)
299
- connection.healthy! if connection.failures > 0
297
+ connection.healthy! if connection.failures.positive?
300
298
 
301
299
  # 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)
300
+ __raise_transport_error(response) if response.status.to_i >= 300 &&
301
+ @retry_on_status.include?(response.status.to_i)
303
302
  rescue Elastic::Transport::Transport::ServerError => e
304
- if response && @retry_on_status.include?(response.status)
305
- log_warn "[#{e.class}] Attempt #{tries} to get response from #{url}"
306
- if tries <= (max_retries || DEFAULT_MAX_RETRIES)
307
- retry
308
- else
309
- log_fatal "[#{e.class}] Cannot get response from #{url} after #{tries} tries"
310
- raise e
311
- end
303
+ raise e unless response && @retry_on_status.include?(response.status)
304
+
305
+ log_warn "[#{e.class}] Attempt #{tries} to get response from #{url}"
306
+ if tries <= (max_retries || DEFAULT_MAX_RETRIES)
307
+ retry
312
308
  else
309
+ log_fatal "[#{e.class}] Cannot get response from #{url} after #{tries} tries"
313
310
  raise e
314
311
  end
315
312
  rescue *host_unreachable_exceptions => e
@@ -317,21 +314,21 @@ module Elastic
317
314
 
318
315
  connection.dead!
319
316
 
320
- if reload_on_failure and tries < connections.all.size
317
+ if reload_on_failure && tries < connections.all.size
321
318
  log_warn "[#{e.class}] Reloading connections (attempt #{tries} of #{connections.all.size})"
322
319
  reload_connections! and retry
323
320
  end
324
321
 
325
- if max_retries
326
- log_warn "[#{e.class}] Attempt #{tries} connecting to #{connection.host.inspect}"
327
- if tries <= max_retries
328
- retry
329
- else
330
- log_fatal "[#{e.class}] Cannot connect to #{connection.host.inspect} after #{tries} tries"
331
- raise e
332
- end
322
+ exception = Elastic::Transport::Transport::Error.new(e.message)
323
+
324
+ raise exception unless max_retries
325
+
326
+ log_warn "[#{e.class}] Attempt #{tries} connecting to #{connection.host.inspect}"
327
+ if tries <= max_retries
328
+ retry
333
329
  else
334
- raise e
330
+ log_fatal "[#{e.class}] Cannot connect to #{connection.host.inspect} after #{tries} tries"
331
+ raise exception
335
332
  end
336
333
  rescue Exception => e
337
334
  log_fatal "[#{e.class}] #{e.message} (#{connection.host.inspect if connection})"
@@ -349,8 +346,18 @@ module Elastic
349
346
  __raise_transport_error response unless ignore.include?(response.status.to_i)
350
347
  end
351
348
 
352
- json = serializer.load(response.body) if response.body && !response.body.empty? && response.headers && response.headers["content-type"] =~ /json/
353
- took = (json['took'] ? sprintf('%.3fs', json['took']/1000.0) : 'n/a') rescue 'n/a'
349
+ if response.body &&
350
+ !response.body.empty? &&
351
+ response.headers &&
352
+ response.headers["content-type"] =~ /json/
353
+
354
+ # Prevent Float value from automatically becoming BigDecimal when using Oj
355
+ load_options = {}
356
+ load_options[:mode] = :compat if ::MultiJson.adapter.to_s == "MultiJson::Adapters::Oj"
357
+
358
+ json = serializer.load(response.body, load_options)
359
+ end
360
+ took = (json['took'] ? sprintf('%.3fs', json['took'] / 1000.0) : 'n/a') rescue 'n/a'
354
361
  __log_response(method, path, params, body, url, response, json, took, duration) unless ignore.include?(response.status.to_i)
355
362
  __trace(method, path, params, connection_headers(connection), body, url, response, nil, 'N/A', duration) if tracer
356
363
  log_warn(response.headers['warning']) if response.headers&.[]('warning')
@@ -372,17 +379,21 @@ module Elastic
372
379
  private
373
380
 
374
381
  USER_AGENT_STR = 'User-Agent'.freeze
375
- USER_AGENT_REGEX = /user\-?\_?agent/
382
+ USER_AGENT_REGEX = /user-?_?agent/
376
383
  ACCEPT_ENCODING = 'Accept-Encoding'.freeze
377
384
  CONTENT_ENCODING = 'Content-Encoding'.freeze
378
385
  CONTENT_TYPE_STR = 'Content-Type'.freeze
379
- CONTENT_TYPE_REGEX = /content\-?\_?type/
386
+ CONTENT_TYPE_REGEX = /content-?_?type/
380
387
  DEFAULT_CONTENT_TYPE = 'application/json'.freeze
381
388
  GZIP = 'gzip'.freeze
382
389
  GZIP_FIRST_TWO_BYTES = '1f8b'.freeze
383
390
  HEX_STRING_DIRECTIVE = 'H*'.freeze
384
391
  RUBY_ENCODING = '1.9'.respond_to?(:force_encoding)
385
392
 
393
+ def max_retries(opts)
394
+ opts[:retry_on_failure] == true ? DEFAULT_MAX_RETRIES : opts[:retry_on_failure]
395
+ end
396
+
386
397
  def compress_request(body, headers)
387
398
  if body
388
399
  headers ||= {}
@@ -409,7 +420,7 @@ module Elastic
409
420
 
410
421
  io = StringIO.new(body)
411
422
  gzip_reader = if RUBY_ENCODING
412
- Zlib::GzipReader.new(io, :encoding => 'ASCII-8BIT')
423
+ Zlib::GzipReader.new(io, encoding: 'ASCII-8BIT')
413
424
  else
414
425
  Zlib::GzipReader.new(io)
415
426
  end
@@ -427,7 +438,7 @@ module Elastic
427
438
  end
428
439
 
429
440
  def apply_headers(client, options)
430
- headers = options[:headers] || {}
441
+ headers = options[:headers].clone || {}
431
442
  headers[CONTENT_TYPE_STR] = find_value(headers, CONTENT_TYPE_REGEX) || DEFAULT_CONTENT_TYPE
432
443
  headers[USER_AGENT_STR] = find_value(headers, USER_AGENT_REGEX) || user_agent_header(client)
433
444
  client.headers[ACCEPT_ENCODING] = GZIP if use_compression?
@@ -442,7 +453,7 @@ module Elastic
442
453
  end
443
454
  end
444
455
 
445
- def user_agent_header(client)
456
+ def user_agent_header(_client)
446
457
  @user_agent ||= begin
447
458
  meta = ["RUBY_VERSION: #{RUBY_VERSION}"]
448
459
  if RbConfig::CONFIG && RbConfig::CONFIG['host_os']
@@ -453,12 +464,21 @@ module Elastic
453
464
  end
454
465
 
455
466
  def connection_headers(connection)
456
- if defined?(Elastic::Transport::Transport::HTTP::Manticore) && self.class == Elastic::Transport::Transport::HTTP::Manticore
467
+ if defined?(Elastic::Transport::Transport::HTTP::Manticore) &&
468
+ instance_of?(Elastic::Transport::Transport::HTTP::Manticore)
457
469
  @request_options[:headers]
458
470
  else
459
471
  connection.connection.headers
460
472
  end
461
473
  end
474
+
475
+ def capture_otel_span_attributes(connection, url)
476
+ if defined?(::OpenTelemetry)
477
+ ::OpenTelemetry::Trace.current_span&.set_attribute('url.full', url)
478
+ ::OpenTelemetry::Trace.current_span&.set_attribute('server.address', connection.host[:host])
479
+ ::OpenTelemetry::Trace.current_span&.set_attribute('server.port', connection.host[:port].to_i)
480
+ end
481
+ end
462
482
  end
463
483
  end
464
484
  end
@@ -18,7 +18,6 @@
18
18
  module Elastic
19
19
  module Transport
20
20
  module Transport
21
-
22
21
  # Generic client error
23
22
  #
24
23
  class Error < StandardError; end
@@ -78,14 +77,13 @@ module Elastic
78
77
  505 => 'HTTPVersionNotSupported',
79
78
  506 => 'VariantAlsoNegotiates',
80
79
  510 => 'NotExtended'
81
- }
80
+ }.freeze
82
81
 
83
- ERRORS = HTTP_STATUSES.inject({}) do |sum, error|
82
+ ERRORS = HTTP_STATUSES.each_with_object({}) do |error, sum|
84
83
  status, name = error
85
84
  sum[status] = Errors.const_set name, Class.new(ServerError)
86
85
  sum
87
86
  end
88
-
89
87
  end
90
88
  end
91
89
  end
@@ -31,37 +31,41 @@ module Elastic
31
31
  # @see Transport::Base#perform_request
32
32
  #
33
33
  def perform_request(method, path, params={}, body=nil, headers=nil, opts={})
34
- super do |connection, _url|
34
+ super do |connection, url|
35
+ capture_otel_span_attributes(connection, url)
35
36
  connection.connection.url = connection.full_url(path, params)
36
37
  body = body ? __convert_to_json(body) : nil
37
38
  body, headers = compress_request(body, headers)
38
39
 
39
40
  case method
40
- when 'HEAD'
41
- connection.connection.set :nobody, true
42
- when 'GET', 'POST', 'PUT', 'DELETE'
43
- connection.connection.set :nobody, false
44
- connection.connection.put_data = body if body
41
+ when 'HEAD'
42
+ connection.connection.set :nobody, true
43
+ when 'GET', 'POST', 'PUT', 'DELETE'
44
+ connection.connection.set :nobody, false
45
+ connection.connection.put_data = body if body
45
46
 
46
- if headers
47
- if connection.connection.headers
48
- connection.connection.headers.merge!(headers)
49
- else
50
- connection.connection.headers = headers
51
- end
47
+ if headers
48
+ if connection.connection.headers
49
+ connection.connection.headers.merge!(headers)
50
+ else
51
+ connection.connection.headers = headers
52
52
  end
53
+ end
53
54
 
54
- else raise ArgumentError, "Unsupported HTTP method: #{method}"
55
+ else raise ArgumentError, "Unsupported HTTP method: #{method}"
55
56
  end
56
57
 
57
58
  connection.connection.http(method.to_sym)
59
+ header_string = connection.connection.header_str.to_s
58
60
 
59
- response_headers = {}
60
- response_headers['content-type'] = 'application/json' if connection.connection.header_str =~ /\/json/
61
+ _response_status, *response_headers = header_string.split(/[\r\n]+/).map(&:strip)
62
+ response_headers = Hash[response_headers.flat_map { |s| s.scan(/^(\S+): (.+)/) }].transform_keys(&:downcase)
61
63
 
62
- Response.new connection.connection.response_code,
63
- decompress_response(connection.connection.body_str),
64
- response_headers
64
+ Response.new(
65
+ connection.connection.response_code,
66
+ decompress_response(connection.connection.body_str),
67
+ response_headers
68
+ )
65
69
  end
66
70
  end
67
71
 
@@ -73,7 +77,7 @@ module Elastic
73
77
  client = ::Curl::Easy.new
74
78
 
75
79
  apply_headers(client, options)
76
- client.url = __full_url(host)
80
+ client.url = __full_url(host)
77
81
 
78
82
  if host[:user]
79
83
  client.http_auth_types = host[:auth_type] || :basic
@@ -105,13 +109,13 @@ module Elastic
105
109
 
106
110
  def user_agent_header(client)
107
111
  @user_agent ||= begin
108
- meta = ["RUBY_VERSION: #{RUBY_VERSION}"]
109
- if RbConfig::CONFIG && RbConfig::CONFIG['host_os']
110
- meta << "#{RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase} #{RbConfig::CONFIG['target_cpu']}"
111
- end
112
- meta << "Curb #{Curl::CURB_VERSION}"
113
- "elastic-transport-ruby/#{VERSION} (#{meta.join('; ')})"
114
- end
112
+ meta = ["RUBY_VERSION: #{RUBY_VERSION}"]
113
+ if RbConfig::CONFIG && RbConfig::CONFIG['host_os']
114
+ meta << "#{RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase} #{RbConfig::CONFIG['target_cpu']}"
115
+ end
116
+ meta << "Curb #{Curl::CURB_VERSION}"
117
+ "elastic-transport-ruby/#{VERSION} (#{meta.join('; ')})"
118
+ end
115
119
  end
116
120
  end
117
121
  end
@@ -34,26 +34,28 @@ module Elastic
34
34
  #
35
35
  def perform_request(method, path, params = {}, body = nil, headers = nil, opts = {})
36
36
  super do |connection, url|
37
- headers = if connection.connection.headers
38
- if !headers.nil?
39
- connection.connection.headers.merge(headers)
40
- else
41
- connection.connection.headers
42
- end
43
- else
44
- headers
45
- end
37
+ capture_otel_span_attributes(connection, url)
38
+ headers = parse_headers(headers, connection)
46
39
  body = body ? __convert_to_json(body) : nil
47
40
  body, headers = compress_request(body, headers)
48
41
 
49
- response = connection.connection.run_request(
50
- method.downcase.to_sym,
51
- url,
52
- body,
53
- headers
54
- )
42
+ response = connection.connection.run_request(method.downcase.to_sym, url, body, headers)
55
43
 
56
- Response.new response.status, decompress_response(response.body), response.headers
44
+ Response.new(response.status, decompress_response(response.body), response.headers)
45
+ end
46
+ end
47
+
48
+ # Merges headers already present in the connection and the ones passed in to perform_request
49
+ #
50
+ def parse_headers(headers, connection)
51
+ if connection.connection.headers
52
+ if !headers.nil?
53
+ connection.connection.headers.merge(headers)
54
+ else
55
+ connection.connection.headers
56
+ end
57
+ else
58
+ headers
57
59
  end
58
60
  end
59
61
 
@@ -73,10 +75,10 @@ module Elastic
73
75
  #
74
76
  def host_unreachable_exceptions
75
77
  [
76
- ::Faraday::ConnectionFailed,
77
- ::Faraday::TimeoutError,
78
- ::Faraday.const_defined?(:ServerError) ? ::Faraday::ServerError : nil,
79
- ::Faraday::SSLError
78
+ ::Faraday::ConnectionFailed,
79
+ ::Faraday::TimeoutError,
80
+ ::Faraday.const_defined?(:ServerError) ? ::Faraday::ServerError : nil,
81
+ ::Faraday::SSLError
80
82
  ].compact
81
83
  end
82
84
 
@@ -84,13 +86,14 @@ module Elastic
84
86
 
85
87
  def user_agent_header(client)
86
88
  @user_agent ||= begin
87
- meta = ["RUBY_VERSION: #{RUBY_VERSION}"]
88
- if RbConfig::CONFIG && RbConfig::CONFIG['host_os']
89
- meta << "#{RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase} #{RbConfig::CONFIG['target_cpu']}"
90
- end
91
- meta << "#{client.headers[USER_AGENT_STR]}"
92
- "elastic-transport-ruby/#{VERSION} (#{meta.join('; ')})"
93
- end
89
+ meta = ["RUBY_VERSION: #{RUBY_VERSION}"]
90
+ if RbConfig::CONFIG && RbConfig::CONFIG['host_os']
91
+ meta << "#{RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase} " \
92
+ "#{RbConfig::CONFIG['target_cpu']}"
93
+ end
94
+ meta << client.headers[USER_AGENT_STR]
95
+ "elastic-transport-ruby/#{VERSION} (#{meta.join('; ')})"
96
+ end
94
97
  end
95
98
  end
96
99
  end
@@ -89,12 +89,12 @@ module Elastic
89
89
  #
90
90
  def perform_request(method, path, params = {}, body = nil, headers = nil, opts = {})
91
91
  super do |connection, url|
92
+ capture_otel_span_attributes(connection, url)
92
93
  body = body ? __convert_to_json(body) : nil
93
- body, headers = compress_request(body, @request_options[:headers])
94
-
94
+ body, headers = compress_request(body, parse_headers(headers))
95
95
  params[:body] = body if body
96
96
  params[:headers] = headers if headers
97
- params = params.merge @request_options
97
+
98
98
  case method
99
99
  when 'GET'
100
100
  resp = connection.connection.get(url, params)
@@ -161,8 +161,14 @@ module Elastic
161
161
 
162
162
  private
163
163
 
164
+ def parse_headers(headers)
165
+ request_headers = @request_options.fetch(:headers, {})
166
+ headers = request_headers.merge(headers || {})
167
+ headers.empty? ? nil : headers
168
+ end
169
+
164
170
  def apply_headers(options)
165
- headers = options[:headers] || options.dig(:transport_options, :headers) || {}
171
+ headers = options[:headers].clone || options.dig(:transport_options, :headers).clone || {}
166
172
  headers[CONTENT_TYPE_STR] = find_value(headers, CONTENT_TYPE_REGEX) || DEFAULT_CONTENT_TYPE
167
173
  headers[USER_AGENT_STR] = find_value(headers, USER_AGENT_REGEX) || find_value(@request_options[:headers], USER_AGENT_REGEX) || user_agent_header
168
174
  headers[ACCEPT_ENCODING] = GZIP if use_compression?
@@ -19,16 +19,16 @@ module Elastic
19
19
  module Transport
20
20
  module Transport
21
21
  # Wraps the response from Elasticsearch.
22
- #
22
+ # It provides `body`, `status` and `headers` methods
23
23
  class Response
24
24
  attr_reader :status, :body, :headers
25
25
 
26
26
  # @param status [Integer] Response status code
27
27
  # @param body [String] Response body
28
28
  # @param headers [Hash] Response headers
29
- def initialize(status, body, headers={})
29
+ def initialize(status, body, headers = {})
30
30
  @status, @body, @headers = status, body, headers
31
- @body = body.force_encoding('UTF-8') if body.respond_to?(:force_encoding)
31
+ @body = body.force_encoding('UTF-8') if body.respond_to?(:force_encoding) && !body.frozen?
32
32
  end
33
33
  end
34
34
  end
@@ -24,7 +24,7 @@ module Elastic
24
24
  module Base
25
25
  # @param transport [Object] The instance of transport which uses this serializer
26
26
  #
27
- def initialize(transport=nil)
27
+ def initialize(transport = nil)
28
28
  @transport = transport
29
29
  end
30
30
  end
@@ -36,13 +36,13 @@ module Elastic
36
36
 
37
37
  # De-serialize a Hash from JSON string
38
38
  #
39
- def load(string, options={})
39
+ def load(string, options = {})
40
40
  ::MultiJson.load(string, options)
41
41
  end
42
42
 
43
43
  # Serialize a Hash to JSON string
44
44
  #
45
- def dump(object, options={})
45
+ def dump(object, options = {})
46
46
  ::MultiJson.dump(object, options)
47
47
  end
48
48
  end
@@ -76,7 +76,9 @@ module Elastic
76
76
  end
77
77
 
78
78
  def parse_publish_address(publish_address)
79
- # publish_address is in the format hostname/ip:port
79
+ # When publish_address is in the format 'inet[hostname/ip:port]'
80
+ return parse_address_port(publish_address[6..-2]) if publish_address =~ /^inet\[.*\]$/
81
+
80
82
  if publish_address =~ /\//
81
83
  parts = publish_address.partition('/')
82
84
  [ parts[0], parse_address_port(parts[2])[1] ]
@@ -17,6 +17,6 @@
17
17
 
18
18
  module Elastic
19
19
  module Transport
20
- VERSION = '8.0.0'.freeze
20
+ VERSION = '8.4.0'.freeze
21
21
  end
22
22
  end
@@ -34,5 +34,6 @@ require 'elastic/transport/transport/connections/collection'
34
34
  require 'elastic/transport/transport/http/faraday'
35
35
  require 'elastic/transport/client'
36
36
  require 'elastic/transport/redacted'
37
+ require 'elastic/transport/opentelemetry'
37
38
 
38
39
  require 'elastic/transport/version'