opensearch-transport 1.0.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 (44) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +3 -0
  3. data/.gitignore +17 -0
  4. data/Gemfile +47 -0
  5. data/LICENSE +202 -0
  6. data/README.md +551 -0
  7. data/Rakefile +89 -0
  8. data/lib/opensearch/transport/client.rb +354 -0
  9. data/lib/opensearch/transport/redacted.rb +84 -0
  10. data/lib/opensearch/transport/transport/base.rb +450 -0
  11. data/lib/opensearch/transport/transport/connections/collection.rb +136 -0
  12. data/lib/opensearch/transport/transport/connections/connection.rb +169 -0
  13. data/lib/opensearch/transport/transport/connections/selector.rb +101 -0
  14. data/lib/opensearch/transport/transport/errors.rb +100 -0
  15. data/lib/opensearch/transport/transport/http/curb.rb +140 -0
  16. data/lib/opensearch/transport/transport/http/faraday.rb +101 -0
  17. data/lib/opensearch/transport/transport/http/manticore.rb +188 -0
  18. data/lib/opensearch/transport/transport/loggable.rb +94 -0
  19. data/lib/opensearch/transport/transport/response.rb +46 -0
  20. data/lib/opensearch/transport/transport/serializer/multi_json.rb +62 -0
  21. data/lib/opensearch/transport/transport/sniffer.rb +111 -0
  22. data/lib/opensearch/transport/version.rb +31 -0
  23. data/lib/opensearch/transport.rb +46 -0
  24. data/lib/opensearch-transport.rb +27 -0
  25. data/opensearch-transport.gemspec +92 -0
  26. data/spec/opensearch/connections/collection_spec.rb +275 -0
  27. data/spec/opensearch/connections/selector_spec.rb +183 -0
  28. data/spec/opensearch/transport/base_spec.rb +313 -0
  29. data/spec/opensearch/transport/client_spec.rb +1818 -0
  30. data/spec/opensearch/transport/sniffer_spec.rb +284 -0
  31. data/spec/spec_helper.rb +99 -0
  32. data/test/integration/transport_test.rb +108 -0
  33. data/test/profile/client_benchmark_test.rb +141 -0
  34. data/test/test_helper.rb +97 -0
  35. data/test/unit/connection_test.rb +145 -0
  36. data/test/unit/response_test.rb +41 -0
  37. data/test/unit/serializer_test.rb +42 -0
  38. data/test/unit/transport_base_test.rb +673 -0
  39. data/test/unit/transport_curb_test.rb +143 -0
  40. data/test/unit/transport_faraday_test.rb +237 -0
  41. data/test/unit/transport_manticore_test.rb +191 -0
  42. data.tar.gz.sig +1 -0
  43. metadata +456 -0
  44. metadata.gz.sig +1 -0
@@ -0,0 +1,169 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # The OpenSearch Contributors require contributions made to
4
+ # this file be licensed under the Apache-2.0 license or a
5
+ # compatible open source license.
6
+ #
7
+ # Modifications Copyright OpenSearch Contributors. See
8
+ # GitHub history for details.
9
+ #
10
+ # Licensed to Elasticsearch B.V. under one or more contributor
11
+ # license agreements. See the NOTICE file distributed with
12
+ # this work for additional information regarding copyright
13
+ # ownership. Elasticsearch B.V. licenses this file to you under
14
+ # the Apache License, Version 2.0 (the "License"); you may
15
+ # not use this file except in compliance with the License.
16
+ # You may obtain a copy of the License at
17
+ #
18
+ # http://www.apache.org/licenses/LICENSE-2.0
19
+ #
20
+ # Unless required by applicable law or agreed to in writing,
21
+ # software distributed under the License is distributed on an
22
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
23
+ # KIND, either express or implied. See the License for the
24
+ # specific language governing permissions and limitations
25
+ # under the License.
26
+
27
+ module OpenSearch
28
+ module Transport
29
+ module Transport
30
+ module Connections
31
+ # Wraps the connection information and logic.
32
+ #
33
+ # The Connection instance wraps the host information (hostname, port, attributes, etc),
34
+ # as well as the "session" (a transport client object, such as a {HTTP::Faraday} instance).
35
+ #
36
+ # It provides methods to construct and properly encode the URLs and paths for passing them
37
+ # to the transport client object.
38
+ #
39
+ # It provides methods to handle connection livecycle (dead, alive, healthy).
40
+ #
41
+ class Connection
42
+ DEFAULT_RESURRECT_TIMEOUT = 60
43
+
44
+ attr_reader :host, :connection, :options, :failures, :dead_since
45
+
46
+ # @option arguments [Hash] :host Host information (example: `{host: 'localhost', port: 9200}`)
47
+ # @option arguments [Object] :connection The transport-specific physical connection or "session"
48
+ # @option arguments [Hash] :options Options (usually passed in from transport)
49
+ #
50
+ def initialize(arguments={})
51
+ @host = arguments[:host].is_a?(Hash) ? Redacted.new(arguments[:host]) : arguments[:host]
52
+ @connection = arguments[:connection]
53
+ @options = arguments[:options] || {}
54
+ @state_mutex = Mutex.new
55
+
56
+ @options[:resurrect_timeout] ||= DEFAULT_RESURRECT_TIMEOUT
57
+ @dead = false
58
+ @failures = 0
59
+ end
60
+
61
+ # Returns the complete endpoint URL with host, port, path and serialized parameters.
62
+ #
63
+ # @return [String]
64
+ #
65
+ def full_url(path, params = {})
66
+ url = "#{host[:protocol]}://"
67
+ url += "#{CGI.escape(host[:user])}:#{CGI.escape(host[:password])}@" if host[:user]
68
+ url += "#{host[:host]}:#{host[:port]}"
69
+ url += "#{host[:path]}" if host[:path]
70
+ full_path = full_path(path, params)
71
+ url += '/' unless full_path.match?(/^\//)
72
+ url += full_path
73
+ end
74
+
75
+ # Returns the complete endpoint path with serialized parameters.
76
+ #
77
+ # @return [String]
78
+ #
79
+ def full_path(path, params={})
80
+ path + (params.empty? ? '' : "?#{::Faraday::Utils::ParamsHash[params].to_query}")
81
+ end
82
+
83
+ # Returns true when this connection has been marked dead, false otherwise.
84
+ #
85
+ # @return [Boolean]
86
+ #
87
+ def dead?
88
+ @dead || false
89
+ end
90
+
91
+ # Marks this connection as dead, incrementing the `failures` counter and
92
+ # storing the current time as `dead_since`.
93
+ #
94
+ # @return [self]
95
+ #
96
+ def dead!
97
+ @state_mutex.synchronize do
98
+ @dead = true
99
+ @failures += 1
100
+ @dead_since = Time.now
101
+ end
102
+ self
103
+ end
104
+
105
+ # Marks this connection as alive, ie. it is eligible to be returned from the pool by the selector.
106
+ #
107
+ # @return [self]
108
+ #
109
+ def alive!
110
+ @state_mutex.synchronize do
111
+ @dead = false
112
+ end
113
+ self
114
+ end
115
+
116
+ # Marks this connection as healthy, ie. a request has been successfully performed with it.
117
+ #
118
+ # @return [self]
119
+ #
120
+ def healthy!
121
+ @state_mutex.synchronize do
122
+ @dead = false
123
+ @failures = 0
124
+ end
125
+ self
126
+ end
127
+
128
+ # Marks this connection as alive, if the required timeout has passed.
129
+ #
130
+ # @return [self,nil]
131
+ # @see DEFAULT_RESURRECT_TIMEOUT
132
+ # @see #resurrectable?
133
+ #
134
+ def resurrect!
135
+ alive! if resurrectable?
136
+ end
137
+
138
+ # Returns true if the connection is eligible to be resurrected as alive, false otherwise.
139
+ #
140
+ # @return [Boolean]
141
+ #
142
+ def resurrectable?
143
+ @state_mutex.synchronize {
144
+ Time.now > @dead_since + ( @options[:resurrect_timeout] * 2 ** (@failures-1) )
145
+ }
146
+ end
147
+
148
+ # Equality operator based on connection protocol, host, port and attributes
149
+ #
150
+ # @return [Boolean]
151
+ #
152
+ def ==(other)
153
+ self.host[:protocol] == other.host[:protocol] && \
154
+ self.host[:host] == other.host[:host] && \
155
+ self.host[:port].to_i == other.host[:port].to_i && \
156
+ self.host[:attributes] == other.host[:attributes]
157
+ end
158
+
159
+ # @return [String]
160
+ #
161
+ def to_s
162
+ "<#{self.class.name} host: #{host} (#{dead? ? 'dead since ' + dead_since.to_s : 'alive'})>"
163
+ end
164
+ end
165
+
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,101 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # The OpenSearch Contributors require contributions made to
4
+ # this file be licensed under the Apache-2.0 license or a
5
+ # compatible open source license.
6
+ #
7
+ # Modifications Copyright OpenSearch Contributors. See
8
+ # GitHub history for details.
9
+ #
10
+ # Licensed to Elasticsearch B.V. under one or more contributor
11
+ # license agreements. See the NOTICE file distributed with
12
+ # this work for additional information regarding copyright
13
+ # ownership. Elasticsearch B.V. licenses this file to you under
14
+ # the Apache License, Version 2.0 (the "License"); you may
15
+ # not use this file except in compliance with the License.
16
+ # You may obtain a copy of the License at
17
+ #
18
+ # http://www.apache.org/licenses/LICENSE-2.0
19
+ #
20
+ # Unless required by applicable law or agreed to in writing,
21
+ # software distributed under the License is distributed on an
22
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
23
+ # KIND, either express or implied. See the License for the
24
+ # specific language governing permissions and limitations
25
+ # under the License.
26
+
27
+ module OpenSearch
28
+ module Transport
29
+ module Transport
30
+ module Connections
31
+ module Selector
32
+
33
+ # @abstract Common functionality for connection selector implementations.
34
+ #
35
+ module Base
36
+ attr_reader :connections
37
+
38
+ # @option arguments [Connections::Collection] :connections Collection with connections.
39
+ #
40
+ def initialize(arguments={})
41
+ @connections = arguments[:connections]
42
+ end
43
+
44
+ # @abstract Selector strategies implement this method to
45
+ # select and return a connection from the pool.
46
+ #
47
+ # @return [Connection]
48
+ #
49
+ def select(options={})
50
+ raise NoMethodError, "Implement this method in the selector implementation."
51
+ end
52
+ end
53
+
54
+ # "Random connection" selector strategy.
55
+ #
56
+ class Random
57
+ include Base
58
+
59
+ # Returns a random connection from the collection.
60
+ #
61
+ # @return [Connections::Connection]
62
+ #
63
+ def select(options={})
64
+ connections.to_a.send( defined?(RUBY_VERSION) && RUBY_VERSION > '1.9' ? :sample : :choice)
65
+ end
66
+ end
67
+
68
+ # "Round-robin" selector strategy (default).
69
+ #
70
+ class RoundRobin
71
+ include Base
72
+
73
+ # @option arguments [Connections::Collection] :connections Collection with connections.
74
+ #
75
+ def initialize(arguments = {})
76
+ super
77
+ @mutex = Mutex.new
78
+ @current = nil
79
+ end
80
+
81
+ # Returns the next connection from the collection, rotating them in round-robin fashion.
82
+ #
83
+ # @return [Connections::Connection]
84
+ #
85
+ def select(options={})
86
+ @mutex.synchronize do
87
+ conns = connections
88
+ if @current && (@current < conns.size-1)
89
+ @current += 1
90
+ else
91
+ @current = 0
92
+ end
93
+ conns[@current]
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,100 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # The OpenSearch Contributors require contributions made to
4
+ # this file be licensed under the Apache-2.0 license or a
5
+ # compatible open source license.
6
+ #
7
+ # Modifications Copyright OpenSearch Contributors. See
8
+ # GitHub history for details.
9
+ #
10
+ # Licensed to Elasticsearch B.V. under one or more contributor
11
+ # license agreements. See the NOTICE file distributed with
12
+ # this work for additional information regarding copyright
13
+ # ownership. Elasticsearch B.V. licenses this file to you under
14
+ # the Apache License, Version 2.0 (the "License"); you may
15
+ # not use this file except in compliance with the License.
16
+ # You may obtain a copy of the License at
17
+ #
18
+ # http://www.apache.org/licenses/LICENSE-2.0
19
+ #
20
+ # Unless required by applicable law or agreed to in writing,
21
+ # software distributed under the License is distributed on an
22
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
23
+ # KIND, either express or implied. See the License for the
24
+ # specific language governing permissions and limitations
25
+ # under the License.
26
+
27
+ module OpenSearch
28
+ module Transport
29
+ module Transport
30
+
31
+ # Generic client error
32
+ #
33
+ class Error < StandardError; end
34
+
35
+ # Reloading connections timeout (1 sec by default)
36
+ #
37
+ class SnifferTimeoutError < Timeout::Error; end
38
+
39
+ # Elasticsearch server error (HTTP status 5xx)
40
+ #
41
+ class ServerError < Error; end
42
+
43
+ module Errors; end
44
+
45
+ HTTP_STATUSES = {
46
+ 300 => 'MultipleChoices',
47
+ 301 => 'MovedPermanently',
48
+ 302 => 'Found',
49
+ 303 => 'SeeOther',
50
+ 304 => 'NotModified',
51
+ 305 => 'UseProxy',
52
+ 307 => 'TemporaryRedirect',
53
+ 308 => 'PermanentRedirect',
54
+
55
+ 400 => 'BadRequest',
56
+ 401 => 'Unauthorized',
57
+ 402 => 'PaymentRequired',
58
+ 403 => 'Forbidden',
59
+ 404 => 'NotFound',
60
+ 405 => 'MethodNotAllowed',
61
+ 406 => 'NotAcceptable',
62
+ 407 => 'ProxyAuthenticationRequired',
63
+ 408 => 'RequestTimeout',
64
+ 409 => 'Conflict',
65
+ 410 => 'Gone',
66
+ 411 => 'LengthRequired',
67
+ 412 => 'PreconditionFailed',
68
+ 413 => 'RequestEntityTooLarge',
69
+ 414 => 'RequestURITooLong',
70
+ 415 => 'UnsupportedMediaType',
71
+ 416 => 'RequestedRangeNotSatisfiable',
72
+ 417 => 'ExpectationFailed',
73
+ 418 => 'ImATeapot',
74
+ 421 => 'TooManyConnectionsFromThisIP',
75
+ 426 => 'UpgradeRequired',
76
+ 429 => 'TooManyRequests',
77
+ 450 => 'BlockedByWindowsParentalControls',
78
+ 494 => 'RequestHeaderTooLarge',
79
+ 497 => 'HTTPToHTTPS',
80
+ 499 => 'ClientClosedRequest',
81
+
82
+ 500 => 'InternalServerError',
83
+ 501 => 'NotImplemented',
84
+ 502 => 'BadGateway',
85
+ 503 => 'ServiceUnavailable',
86
+ 504 => 'GatewayTimeout',
87
+ 505 => 'HTTPVersionNotSupported',
88
+ 506 => 'VariantAlsoNegotiates',
89
+ 510 => 'NotExtended'
90
+ }
91
+
92
+ ERRORS = HTTP_STATUSES.inject({}) do |sum, error|
93
+ status, name = error
94
+ sum[status] = Errors.const_set name, Class.new(ServerError)
95
+ sum
96
+ end
97
+
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,140 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # The OpenSearch Contributors require contributions made to
4
+ # this file be licensed under the Apache-2.0 license or a
5
+ # compatible open source license.
6
+ #
7
+ # Modifications Copyright OpenSearch Contributors. See
8
+ # GitHub history for details.
9
+ #
10
+ # Licensed to Elasticsearch B.V. under one or more contributor
11
+ # license agreements. See the NOTICE file distributed with
12
+ # this work for additional information regarding copyright
13
+ # ownership. Elasticsearch B.V. licenses this file to you under
14
+ # the Apache License, Version 2.0 (the "License"); you may
15
+ # not use this file except in compliance with the License.
16
+ # You may obtain a copy of the License at
17
+ #
18
+ # http://www.apache.org/licenses/LICENSE-2.0
19
+ #
20
+ # Unless required by applicable law or agreed to in writing,
21
+ # software distributed under the License is distributed on an
22
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
23
+ # KIND, either express or implied. See the License for the
24
+ # specific language governing permissions and limitations
25
+ # under the License.
26
+
27
+ module OpenSearch
28
+ module Transport
29
+ module Transport
30
+ module HTTP
31
+
32
+ # Alternative HTTP transport implementation, using the [_Curb_](https://rubygems.org/gems/curb) client.
33
+ #
34
+ # @see Transport::Base
35
+ #
36
+ class Curb
37
+ include Base
38
+
39
+ # Performs the request by invoking {Transport::Base#perform_request} with a block.
40
+ #
41
+ # @return [Response]
42
+ # @see Transport::Base#perform_request
43
+ #
44
+ def perform_request(method, path, params={}, body=nil, headers=nil, opts={})
45
+ super do |connection, url|
46
+ connection.connection.url = connection.full_url(path, params)
47
+
48
+ case method
49
+ when 'HEAD'
50
+ connection.connection.set :nobody, true
51
+ when 'GET', 'POST', 'PUT', 'DELETE'
52
+ connection.connection.set :nobody, false
53
+ connection.connection.put_data = __convert_to_json(body) if body
54
+ if headers
55
+ if connection.connection.headers
56
+ connection.connection.headers.merge!(headers)
57
+ else
58
+ connection.connection.headers = headers
59
+ end
60
+ end
61
+ else
62
+ raise ArgumentError, "Unsupported HTTP method: #{method}"
63
+ end
64
+
65
+ connection.connection.http(method.to_sym)
66
+
67
+ Response.new(
68
+ connection.connection.response_code,
69
+ decompress_response(connection.connection.body_str),
70
+ headers(connection)
71
+ )
72
+ end
73
+ end
74
+
75
+ def headers(connection)
76
+ headers_string = connection.connection.header_str
77
+ return nil if headers_string.nil?
78
+
79
+ response_headers = headers_string&.split(/\\r\\n|\r\n/).reject(&:empty?)
80
+ response_headers.shift # Removes HTTP status string
81
+ processed_header = response_headers.flat_map { |s| s.scan(/^(\S+): (.+)/) }
82
+ headers_hash = Hash[processed_header].transform_keys(&:downcase)
83
+ if headers_hash['content-type']&.match?(/application\/json/)
84
+ headers_hash['content-type'] = 'application/json'
85
+ end
86
+ headers_hash
87
+ end
88
+
89
+ # Builds and returns a connection
90
+ #
91
+ # @return [Connections::Connection]
92
+ #
93
+ def __build_connection(host, options={}, block=nil)
94
+ client = ::Curl::Easy.new
95
+ apply_headers(client, options)
96
+ client.url = __full_url(host)
97
+
98
+ if host[:user]
99
+ client.http_auth_types = host[:auth_type] || :basic
100
+ client.username = host[:user]
101
+ client.password = host[:password]
102
+ end
103
+
104
+ client.instance_eval(&block) if block
105
+
106
+ Connections::Connection.new :host => host, :connection => client
107
+ end
108
+
109
+ # Returns an array of implementation specific connection errors.
110
+ #
111
+ # @return [Array]
112
+ #
113
+ def host_unreachable_exceptions
114
+ [
115
+ ::Curl::Err::HostResolutionError,
116
+ ::Curl::Err::ConnectionFailedError,
117
+ ::Curl::Err::GotNothingError,
118
+ ::Curl::Err::RecvError,
119
+ ::Curl::Err::SendError,
120
+ ::Curl::Err::TimeoutError
121
+ ]
122
+ end
123
+
124
+ private
125
+
126
+ def user_agent_header(client)
127
+ @user_agent ||= begin
128
+ meta = ["RUBY_VERSION: #{RUBY_VERSION}"]
129
+ if RbConfig::CONFIG && RbConfig::CONFIG['host_os']
130
+ meta << "#{RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase} #{RbConfig::CONFIG['target_cpu']}"
131
+ end
132
+ meta << "Curb #{Curl::CURB_VERSION}"
133
+ "opensearch-ruby/#{VERSION} (#{meta.join('; ')})"
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,101 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # The OpenSearch Contributors require contributions made to
4
+ # this file be licensed under the Apache-2.0 license or a
5
+ # compatible open source license.
6
+ #
7
+ # Modifications Copyright OpenSearch Contributors. See
8
+ # GitHub history for details.
9
+ #
10
+ # Licensed to Elasticsearch B.V. under one or more contributor
11
+ # license agreements. See the NOTICE file distributed with
12
+ # this work for additional information regarding copyright
13
+ # ownership. Elasticsearch B.V. licenses this file to you under
14
+ # the Apache License, Version 2.0 (the "License"); you may
15
+ # not use this file except in compliance with the License.
16
+ # You may obtain a copy of the License at
17
+ #
18
+ # http://www.apache.org/licenses/LICENSE-2.0
19
+ #
20
+ # Unless required by applicable law or agreed to in writing,
21
+ # software distributed under the License is distributed on an
22
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
23
+ # KIND, either express or implied. See the License for the
24
+ # specific language governing permissions and limitations
25
+ # under the License.
26
+
27
+ module OpenSearch
28
+ module Transport
29
+ module Transport
30
+ module HTTP
31
+ # The default transport implementation, using the [_Faraday_](https://rubygems.org/gems/faraday)
32
+ # library for abstracting the HTTP client.
33
+ #
34
+ # @see Transport::Base
35
+ #
36
+ class Faraday
37
+ include Base
38
+
39
+ # Performs the request by invoking {Transport::Base#perform_request} with a block.
40
+ #
41
+ # @return [Response]
42
+ # @see Transport::Base#perform_request
43
+ #
44
+ def perform_request(method, path, params = {}, body = nil, headers = nil, opts = {})
45
+ super do |connection, url|
46
+ headers = if connection.connection.headers
47
+ if !headers.nil?
48
+ connection.connection.headers.merge(headers)
49
+ else
50
+ connection.connection.headers
51
+ end
52
+ else
53
+ headers
54
+ end
55
+
56
+ response = connection.connection.run_request(
57
+ method.downcase.to_sym,
58
+ url,
59
+ (body ? __convert_to_json(body) : nil),
60
+ headers
61
+ )
62
+
63
+ Response.new response.status, decompress_response(response.body), response.headers
64
+ end
65
+ end
66
+
67
+ # Builds and returns a connection
68
+ #
69
+ # @return [Connections::Connection]
70
+ #
71
+ def __build_connection(host, options={}, block=nil)
72
+ client = ::Faraday.new(__full_url(host), options, &block)
73
+ apply_headers(client, options)
74
+ Connections::Connection.new :host => host, :connection => client
75
+ end
76
+
77
+ # Returns an array of implementation specific connection errors.
78
+ #
79
+ # @return [Array]
80
+ #
81
+ def host_unreachable_exceptions
82
+ [::Faraday::ConnectionFailed, ::Faraday::TimeoutError]
83
+ end
84
+
85
+ private
86
+
87
+ def user_agent_header(client)
88
+ @user_agent ||= begin
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} #{RbConfig::CONFIG['target_cpu']}"
92
+ end
93
+ meta << "#{client.headers[USER_AGENT_STR]}"
94
+ "opensearch-ruby/#{VERSION} (#{meta.join('; ')})"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end