elastic-transport 8.2.5 → 8.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f216469707e9c57ce1ef8c9285504300763bc3603b95bb25d434c4d4cb584fec
4
- data.tar.gz: 6ae9924b648db98d0a5d7d2f6770f0d3ff18a766b9f93545d11a5239dc3ae8c6
3
+ metadata.gz: e52d8e6c8f71d5d4c41362c059e32edb1604d8f63ed7cb13688ea75eaf4d5509
4
+ data.tar.gz: 4107c6f56a20d397102eb724734ee661d820af2c8b96dae63e72154b40e661f3
5
5
  SHA512:
6
- metadata.gz: d5e73feba635548e4109cf1c95c62cc1a12a04f46817e2edcc180cd54f0147ca1e200e5a7eb4278fa1a851fcde61ead7e9407b78adcdb81bec85569112733269
7
- data.tar.gz: 666785c03ab0fc8d4f21636bc0856669c838ea546c983d8d2d9e41d20ea04e274ae3991261ddcdd101d64aef6db288c6da5c76fa5ec0e1002a61660b0e3f6dc9
6
+ metadata.gz: 7c69236ddfa00fb917b7c0440539f0cfec3f16d8464fd0cee0293eb8b88a11240bfff4c45b2da83aecb1e3427859473cfe4862189160b15aa376690a3a0b0e16
7
+ data.tar.gz: 286a89fb6a350273c60d6c85c8082eec0cf51860624244755e81bc375d385863fe21f7c4e50dffd7c0935c875fd72645fa053f3edec7a8fb65a9cad06bdea726
@@ -0,0 +1,48 @@
1
+ name: opentelemetry
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ pull_request:
7
+ branches:
8
+ - main
9
+ jobs:
10
+ test-otel:
11
+ name: 'Test Open Telemetry'
12
+ env:
13
+ TEST_ES_SERVER: http://localhost:9250
14
+ PORT: 9250
15
+ TEST_WITH_OTEL: true
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ ruby: ['3.2', 'jruby-9.4']
20
+ es_version: ['8.9-SNAPSHOT']
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v2
24
+ - name: Increase system limits
25
+ run: |
26
+ sudo swapoff -a
27
+ sudo sysctl -w vm.swappiness=1
28
+ sudo sysctl -w fs.file-max=262144
29
+ sudo sysctl -w vm.max_map_count=262144
30
+ - uses: elastic/elastic-github-actions/elasticsearch@master
31
+ with:
32
+ stack-version: ${{ matrix.es_version }}
33
+ security-enabled: false
34
+ - uses: ruby/setup-ruby@v1
35
+ with:
36
+ ruby-version: ${{ matrix.ruby }}
37
+ - name: Build and test with Rake
38
+ run: |
39
+ sudo apt-get update
40
+ sudo apt-get install libcurl4-openssl-dev
41
+ ruby -v
42
+ bundle install
43
+ - name: unit tests
44
+ run: bundle exec rake test:unit
45
+ - name: specs
46
+ run: bundle exec rake test:spec
47
+ - name: integration tests
48
+ run: bundle exec rake test:integration
@@ -1,11 +1,11 @@
1
- name: 8.2 tests
1
+ name: main tests
2
2
  on:
3
3
  push:
4
4
  branches:
5
- - 8.2
5
+ - main
6
6
  pull_request:
7
7
  branches:
8
- - 8.2
8
+ - main
9
9
  jobs:
10
10
  test:
11
11
  name: 'Main tests'
@@ -15,8 +15,8 @@ jobs:
15
15
  strategy:
16
16
  fail-fast: false
17
17
  matrix:
18
- ruby: [ '3.0', '3.1', '3.2', '3.3', 'jruby-9.3', 'jruby-9.4' ]
19
- es_version: ['8.11-SNAPSHOT', '8.12-SNAPSHOT']
18
+ ruby: [ '3.0', '3.1', '3.2', 'jruby-9.3', 'jruby-9.4' ]
19
+ es_version: ['8.9-SNAPSHOT', '8.10-SNAPSHOT', '8.11.0-SNAPSHOT']
20
20
  runs-on: ubuntu-latest
21
21
  steps:
22
22
  - uses: actions/checkout@v3
@@ -53,8 +53,8 @@ jobs:
53
53
  strategy:
54
54
  fail-fast: false
55
55
  matrix:
56
- ruby: [ '3.0', '3.1', '3.2', '3.3', 'jruby-9.3' ]
57
- es_version: ['8.12-SNAPSHOT']
56
+ ruby: [ '3.0', '3.1', '3.2', 'jruby-9.3' ]
57
+ es_version: ['8.9-SNAPSHOT']
58
58
  runs-on: ubuntu-latest
59
59
  steps:
60
60
  - uses: actions/checkout@v3
data/CHANGELOG.md CHANGED
@@ -1,8 +1,8 @@
1
- ## 8.2.5
1
+ ## 8.3.0
2
2
 
3
- Tested versions of Ruby: (MRI) 3.0, 3.1, 3.2, 3.3, JRuby 9.3 and JRuby 9.4
3
+ Tested versions of Ruby: (MRI) 3.0, 3.1, 3.2, JRuby 9.3 and JRuby 9.4
4
4
 
5
- - Removes unneccessary `require 'base64'` found thanks to warning in Ruby 3.3. So this removes the warning too if you were using Ruby 3.3.
5
+ This release adds native support for OpenTelemetry. Documentation will be added to [the official Transport documentation](https://www.elastic.co/guide/en/elasticsearch/client/ruby-api/current/transport.html). Pull Request: [#54](https://github.com/elastic/elastic-transport-ruby/pull/54).
6
6
 
7
7
  ## 8.2.4
8
8
 
data/Gemfile CHANGED
@@ -32,4 +32,7 @@ group :development, :test do
32
32
  else
33
33
  gem 'pry-byebug'
34
34
  end
35
+ if RUBY_VERSION >= '3.0'
36
+ gem 'opentelemetry-sdk', require: false
37
+ end
35
38
  end
@@ -15,6 +15,7 @@
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
17
 
18
+ require 'base64'
18
19
  require 'elastic/transport/meta_header'
19
20
 
20
21
  module Elastic
@@ -163,14 +164,39 @@ module Elastic
163
164
  @transport_class.new(hosts: @hosts, options: @arguments)
164
165
  end
165
166
  end
167
+
168
+ if defined?(::OpenTelemetry) && ENV[OpenTelemetry::ENV_VARIABLE_ENABLED] != 'false'
169
+ @otel = OpenTelemetry.new(@arguments)
170
+ end
166
171
  end
167
172
 
168
173
  # Performs a request through delegation to {#transport}.
169
174
  #
170
- def perform_request(method, path, params = {}, body = nil, headers = nil)
175
+ def perform_request(method, path, params = {}, body = nil, headers = nil, opts = {})
171
176
  method = @send_get_body_as if 'GET' == method && body
172
177
  validate_ca_fingerprints if @ca_fingerprint
173
- transport.perform_request(method, path, params, body, headers)
178
+ if @otel
179
+ # If no endpoint is specified in the opts, use the HTTP method name
180
+ span_name = opts[:endpoint] || method
181
+ @otel.tracer.in_span(span_name) do |span|
182
+ span['http.request.method'] = method
183
+ span['db.system'] = 'elasticsearch'
184
+ opts[:defined_params]&.each do |k, v|
185
+ if v.respond_to?(:join)
186
+ span["db.elasticsearch.path_parts.#{k}"] = v.join(',')
187
+ else
188
+ span["db.elasticsearch.path_parts.#{k}"] = v
189
+ end
190
+ end
191
+ if body_as_json = @otel.process_body(body, opts[:endpoint])
192
+ span['db.statement'] = body_as_json
193
+ end
194
+ span['db.operation'] = opts[:endpoint] if opts[:endpoint]
195
+ transport.perform_request(method, path, params || {}, body, headers)
196
+ end
197
+ else
198
+ transport.perform_request(method, path, params || {}, body, headers)
199
+ end
174
200
  end
175
201
 
176
202
  private
@@ -15,6 +15,8 @@
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
17
 
18
+ require 'base64'
19
+
18
20
  module Elastic
19
21
  module Transport
20
22
  # Methods for the Elastic meta header used by Cloud.
@@ -0,0 +1,157 @@
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
+ # Wrapper object for Open Telemetry objects, associated config and functionality.
21
+ #
22
+ # @api private
23
+ class OpenTelemetry
24
+ OTEL_TRACER_NAME = 'elasticsearch-api'
25
+ # Valid values for the enabled config are 'true' and 'false'. Default is 'true'.
26
+ ENV_VARIABLE_ENABLED = 'OTEL_RUBY_INSTRUMENTATION_ELASTICSEARCH_ENABLED'
27
+ # Describes how to handle search queries in the request body when assigned to
28
+ # a span attribute.
29
+ # Valid values are 'raw', 'omit', 'sanitize'. Default is 'omit'.
30
+ ENV_VARIABLE_BODY_STRATEGY = 'OTEL_INSTRUMENTATION_ELASTICSEARCH_CAPTURE_SEARCH_QUERY'
31
+ DEFAULT_BODY_STRATEGY = 'omit'
32
+ # A string list of keys whose values are redacted. This is only relevant if the body strategy is
33
+ # 'sanitize'. For example, a config 'sensitive-key,other-key' will redact the values at
34
+ # 'sensitive-key' and 'other-key' in addition to the default keys.
35
+ ENV_VARIABLE_BODY_SANITIZE_KEYS = 'OTEL_RUBY_INSTRUMENTATION_ELASTICSEARCH_SEARCH_QUERY_SANITIZE_KEYS'
36
+
37
+ # A list of the Elasticsearch endpoints that qualify as "search" endpoints. The search query in
38
+ # the request body may be captured for these endpoints, depending on the body capture strategy.
39
+ SEARCH_ENDPOINTS = Set[
40
+ "search",
41
+ "async_search.submit",
42
+ "msearch",
43
+ "eql.search",
44
+ "terms_enum",
45
+ "search_template",
46
+ "msearch_template",
47
+ "render_search_template",
48
+ ]
49
+
50
+ # Initialize the Open Telemetry wrapper object. Takes the options originally passed to
51
+ # Client#initialize.
52
+ def initialize(opts)
53
+ @tracer = (opts[:opentelemetry_tracer_provider] || ::OpenTelemetry.tracer_provider).tracer(
54
+ OTEL_TRACER_NAME, Elastic::Transport::VERSION
55
+ )
56
+ @body_strategy = ENV[ENV_VARIABLE_BODY_STRATEGY] || DEFAULT_BODY_STRATEGY
57
+ @sanitize_keys = ENV[ENV_VARIABLE_BODY_SANITIZE_KEYS]&.split(',')&.collect! do |pattern|
58
+ Regexp.new(pattern.gsub('*', '.*'))
59
+ end
60
+ end
61
+ attr_accessor :tracer
62
+
63
+ # Process the request body. Applies the body strategy, which can be one of the following:
64
+ # 'omit': return nil
65
+ # 'sanitize': redact values at the default list of keys + any additional keys provided in
66
+ # the OTEL_RUBY_INSTRUMENTATION_ELASTICSEARCH_SEARCH_QUERY_SANITIZE_KEYS env variable.
67
+ # 'raw': return the original body, unchanged
68
+ def process_body(body, endpoint)
69
+ unless @body_strategy == 'omit' || !SEARCH_ENDPOINTS.include?(endpoint)
70
+ if @body_strategy == 'sanitize'
71
+ Sanitizer.sanitize(body, @sanitize_keys).to_json
72
+ elsif @body_strategy == 'raw'
73
+ body&.is_a?(String) ? body : body.to_json
74
+ end
75
+ end
76
+ end
77
+
78
+ # Replaces values in a hash with 'REDACTED', given a set of keys to match on.
79
+ class Sanitizer
80
+ class << self
81
+ FILTERED = 'REDACTED'
82
+ DEFAULT_KEY_PATTERNS =
83
+ %w[password passwd pwd secret *key *token* *session* *credit* *card* *auth* set-cookie].map! do |p|
84
+ Regexp.new(p.gsub('*', '.*'))
85
+ end
86
+
87
+ def sanitize(body, key_patterns = [])
88
+ patterns = DEFAULT_KEY_PATTERNS
89
+ patterns += key_patterns if key_patterns
90
+ sanitize!(DeepDup.dup(body), patterns)
91
+ end
92
+
93
+ private
94
+
95
+ def sanitize!(obj, key_patterns)
96
+ return obj unless obj.is_a?(Hash)
97
+
98
+ obj.each_pair do |k, v|
99
+ if filter_key?(key_patterns, k)
100
+ obj[k] = FILTERED
101
+ elsif v.is_a?(Hash)
102
+ sanitize!(v, key_patterns)
103
+ else
104
+ next
105
+ end
106
+ end
107
+ end
108
+
109
+ def filter_key?(key_patterns, key)
110
+ key_patterns.any? { |regex| regex.match(key) }
111
+ end
112
+ end
113
+ end
114
+
115
+ # Makes a deep copy of an Array or Hash
116
+ # NB: Not guaranteed to work well with complex objects, only simple Hash,
117
+ # Array, String, Number, etc.
118
+ class DeepDup
119
+ def initialize(obj)
120
+ @obj = obj
121
+ end
122
+
123
+ def dup
124
+ deep_dup(@obj)
125
+ end
126
+
127
+ def self.dup(obj)
128
+ new(obj).dup
129
+ end
130
+
131
+ private
132
+
133
+ def deep_dup(obj)
134
+ case obj
135
+ when Hash then hash(obj)
136
+ when Array then array(obj)
137
+ else obj.dup
138
+ end
139
+ end
140
+
141
+ def array(arr)
142
+ arr.map { |obj| deep_dup(obj) }
143
+ end
144
+
145
+ def hash(hsh)
146
+ result = hsh.dup
147
+
148
+ hsh.each_pair do |key, value|
149
+ result[key] = deep_dup(value)
150
+ end
151
+
152
+ result
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -471,6 +471,14 @@ module Elastic
471
471
  connection.connection.headers
472
472
  end
473
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
474
482
  end
475
483
  end
476
484
  end
@@ -31,7 +31,8 @@ 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)
@@ -34,6 +34,7 @@ module Elastic
34
34
  #
35
35
  def perform_request(method, path, params = {}, body = nil, headers = nil, opts = {})
36
36
  super do |connection, url|
37
+ capture_otel_span_attributes(connection, url)
37
38
  headers = parse_headers(headers, connection)
38
39
  body = body ? __convert_to_json(body) : nil
39
40
  body, headers = compress_request(body, headers)
@@ -89,6 +89,7 @@ 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
94
  body, headers = compress_request(body, parse_headers(headers))
94
95
  params[:body] = body if body
@@ -17,6 +17,6 @@
17
17
 
18
18
  module Elastic
19
19
  module Transport
20
- VERSION = '8.2.5'.freeze
20
+ VERSION = '8.3.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'
@@ -0,0 +1,306 @@
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
+ require 'spec_helper'
19
+
20
+ if defined?(::OpenTelemetry)
21
+ describe Elastic::Transport::OpenTelemetry do
22
+ let(:exporter) { EXPORTER }
23
+ before { exporter.reset }
24
+ after { exporter.reset }
25
+ let(:span) { exporter.finished_spans[0] }
26
+
27
+ let(:client) do
28
+ Elastic::Transport::Client.new(hosts: ELASTICSEARCH_HOSTS).tap do |_client|
29
+ allow(_client).to receive(:__build_connections)
30
+ end
31
+ end
32
+
33
+ let(:otel) { described_class.new }
34
+
35
+ context 'when the client is created with a tracer provider' do
36
+ let(:tracer_provider) do
37
+ double('tracer_provider').tap do |tp|
38
+ expect(tp).to receive(:tracer).with(
39
+ Elastic::Transport::OpenTelemetry::OTEL_TRACER_NAME, Elastic::Transport::VERSION
40
+ )
41
+ end
42
+ end
43
+
44
+ it 'uses the tracer provider to get a tracer' do
45
+ Elastic::Transport::Client.new(opentelemetry_tracer_provider: tracer_provider)
46
+ end
47
+ end
48
+
49
+ context 'when path parameters' do
50
+ before do
51
+ client.perform_request('DELETE', '/users', nil, nil, nil)
52
+ rescue
53
+ end
54
+ after do
55
+ client.perform_request('DELETE', '/users', nil, nil, nil)
56
+ rescue
57
+ end
58
+
59
+ it 'creates a span with path parameters' do
60
+ client.perform_request(
61
+ 'POST', '/users/_create/abc', nil, { name: 'otel-test' }, nil,
62
+ defined_params: {'index' => 'users', 'id' => 'abc'}, endpoint: 'create'
63
+ )
64
+
65
+ span = exporter.finished_spans.find { |s| s.name == 'create' }
66
+ expect(span.name).to eql('create')
67
+ expect(span.attributes['db.system']).to eql('elasticsearch')
68
+ expect(span.attributes['db.elasticsearch.path_parts.index']).to eql('users')
69
+ expect(span.attributes['db.elasticsearch.path_parts.id']).to eq('abc')
70
+ expect(span.attributes['db.operation']).to eq('create')
71
+ expect(span.attributes['db.statement']).to be_nil
72
+ expect(span.attributes['http.request.method']).to eq('POST')
73
+ expect(span.attributes['server.address']).to eq('localhost')
74
+ expect(span.attributes['server.port']).to eq(TEST_PORT.to_i)
75
+ end
76
+
77
+ context 'with list a path parameter' do
78
+ it 'creates a span with path parameters' do
79
+ client.perform_request(
80
+ 'GET', '_cluster/state/foo,bar', {}, nil, {},
81
+ { defined_params: { metric: ['foo', 'bar']}, endpoint: 'cluster.state' }
82
+ )
83
+
84
+ span = exporter.finished_spans.find { |s| s.name == 'cluster.state' }
85
+ expect(span.name).to eql('cluster.state')
86
+ expect(span.attributes['db.system']).to eql('elasticsearch')
87
+ expect(span.attributes['db.elasticsearch.path_parts.metric']).to eql('foo,bar')
88
+ expect(span.attributes['db.operation']).to eq('cluster.state')
89
+ expect(span.attributes['db.statement']).to be_nil
90
+ expect(span.attributes['http.request.method']).to eq('GET')
91
+ expect(span.attributes['server.address']).to eq('localhost')
92
+ expect(span.attributes['server.port']).to eq(TEST_PORT.to_i)
93
+ end
94
+ end
95
+ end
96
+
97
+ context 'when a request is instrumented' do
98
+ let(:body) do
99
+ { query: { match: { password: { query: 'secret'} } } }
100
+ end
101
+
102
+ it 'creates a span and omits db.statement' do
103
+ client.perform_request('GET', '/_search', nil, body, nil, endpoint: 'search')
104
+
105
+ expect(span.name).to eql('search')
106
+ expect(span.attributes['db.system']).to eql('elasticsearch')
107
+ expect(span.attributes['db.operation']).to eq('search')
108
+ expect(span.attributes['db.statement']).to be_nil
109
+ expect(span.attributes['http.request.method']).to eq('GET')
110
+ expect(span.attributes['server.address']).to eq('localhost')
111
+ expect(span.attributes['server.port']).to eq(TEST_PORT.to_i)
112
+ end
113
+
114
+ context 'when body is sanitized' do
115
+ context 'no custom keys' do
116
+ let(:sanitized_body) do
117
+ { query: { match: { password: 'REDACTED' } } }
118
+ end
119
+
120
+ around(:example) do |ex|
121
+ body_strategy = ENV[described_class::ENV_VARIABLE_BODY_STRATEGY]
122
+ ENV[described_class::ENV_VARIABLE_BODY_STRATEGY] = 'sanitize'
123
+ ex.run
124
+ ENV[described_class::ENV_VARIABLE_BODY_STRATEGY] = body_strategy
125
+ end
126
+
127
+ it 'sanitizes the body' do
128
+ client.perform_request('GET', '/_search', nil, body, nil, endpoint: 'search')
129
+
130
+ expect(span.attributes['db.statement']).to eq(sanitized_body.to_json)
131
+ end
132
+ end
133
+
134
+ context 'with custom keys' do
135
+ let(:body) do
136
+ { query: { match: { sensitive: { query: 'secret'} } } }
137
+ end
138
+
139
+ let(:sanitized_body) do
140
+ { query: { match: { sensitive: 'REDACTED' } } }
141
+ end
142
+
143
+ around(:example) do |ex|
144
+ body_strategy = ENV[described_class::ENV_VARIABLE_BODY_STRATEGY]
145
+ ENV[described_class::ENV_VARIABLE_BODY_STRATEGY] = 'sanitize'
146
+
147
+ keys = ENV[described_class::ENV_VARIABLE_BODY_SANITIZE_KEYS]
148
+ ENV[described_class::ENV_VARIABLE_BODY_SANITIZE_KEYS] = 'sensitive'
149
+
150
+ ex.run
151
+
152
+ ENV[described_class::ENV_VARIABLE_BODY_STRATEGY] = body_strategy
153
+ ENV[described_class::ENV_VARIABLE_BODY_SANITIZE_KEYS] = keys
154
+ end
155
+
156
+ it 'sanitizes the body' do
157
+ client.perform_request('GET', '/_search', nil, body, nil, endpoint: 'search')
158
+
159
+ expect(span.attributes['db.statement']).to eq(sanitized_body.to_json)
160
+ end
161
+ end
162
+ end
163
+
164
+ context 'when body strategy is set to raw' do
165
+ let(:body) do
166
+ { query: { match: { sensitive: { query: 'secret'} } } }
167
+ end
168
+
169
+ around(:example) do |ex|
170
+ body_strategy = ENV[described_class::ENV_VARIABLE_BODY_STRATEGY]
171
+ ENV[described_class::ENV_VARIABLE_BODY_STRATEGY] = 'raw'
172
+ ex.run
173
+ ENV[described_class::ENV_VARIABLE_BODY_STRATEGY] = body_strategy
174
+ end
175
+
176
+ context 'when the body is a string' do
177
+ it 'includes the raw body' do
178
+ client.perform_request('GET', '/_search', nil, body.to_json, nil, endpoint: 'search')
179
+ expect(span.attributes['db.statement']).to eq(body.to_json)
180
+ end
181
+ end
182
+
183
+ context' when the body is a hash' do
184
+ it 'includes the raw body' do
185
+ client.perform_request('GET', '/_search', nil, body, nil, endpoint: 'search')
186
+ expect(span.attributes['db.statement']).to eq(body.to_json)
187
+ end
188
+ end
189
+ end
190
+
191
+ context 'when body strategy is set to omit' do
192
+ let(:body) do
193
+ { query: { match: { sensitive: { query: 'secret'} } } }
194
+ end
195
+
196
+ around(:example) do |ex|
197
+ body_strategy = ENV[described_class::ENV_VARIABLE_BODY_STRATEGY]
198
+ ENV[described_class::ENV_VARIABLE_BODY_STRATEGY] = 'omit'
199
+ ex.run
200
+ ENV[described_class::ENV_VARIABLE_BODY_STRATEGY] = body_strategy
201
+ end
202
+
203
+ it 'does not include anything' do
204
+ client.perform_request('GET', '/_search', nil, body, nil, endpoint: 'search')
205
+ expect(span.attributes['db.statement']).to be_nil
206
+ end
207
+ end
208
+
209
+ context 'a non-search endpoint' do
210
+ let(:body) do
211
+ { query: { match: { something: "test" } } }
212
+ end
213
+
214
+ it 'does not capture db.statement' do
215
+ client.perform_request(
216
+ 'POST', '_all/_delete_by_query', nil, body, nil, endpoint: 'delete_by_query'
217
+ )
218
+
219
+ expect(span.attributes['db.statement']).to be_nil
220
+ end
221
+ end
222
+
223
+ context 'when no endpoint or defined params are provided' do
224
+ it 'creates a span with default values' do
225
+ client.perform_request(
226
+ 'GET', '_cluster/state/foo,bar', {}, nil, {}
227
+ )
228
+
229
+ span = exporter.finished_spans.find { |s| s.name == 'GET' }
230
+ expect(span.name).to eql('GET')
231
+ expect(span.attributes['db.system']).to eql('elasticsearch')
232
+ expect(span.attributes['db.elasticsearch.path_parts']).to be_nil
233
+ expect(span.attributes['db.operation']).to be_nil
234
+ expect(span.attributes['db.statement']).to be_nil
235
+ expect(span.attributes['http.request.method']).to eq('GET')
236
+ expect(span.attributes['server.address']).to eq('localhost')
237
+ expect(span.attributes['server.port']).to eq(TEST_PORT.to_i)
238
+ end
239
+ end
240
+ end
241
+
242
+ context 'when the ENV variable OTEL_RUBY_INSTRUMENTATION_ELASTICSEARCH_ENABLED is set' do
243
+ context 'to true' do
244
+ around do |ex|
245
+ original_setting = ENV[described_class::ENV_VARIABLE_ENABLED]
246
+ ENV[described_class::ENV_VARIABLE_ENABLED] = 'true'
247
+ ex.run
248
+ ENV[described_class::ENV_VARIABLE_ENABLED] = original_setting
249
+ end
250
+
251
+ it 'instruments' do
252
+ client.perform_request('GET', '/_search', nil, nil, nil, endpoint: 'search')
253
+ expect(span.name).to eq('search')
254
+ end
255
+ end
256
+
257
+ context 'to false' do
258
+ around do |ex|
259
+ original_setting = ENV[described_class::ENV_VARIABLE_ENABLED]
260
+ ENV[described_class::ENV_VARIABLE_ENABLED] = 'false'
261
+ ex.run
262
+ ENV[described_class::ENV_VARIABLE_ENABLED] = original_setting
263
+ end
264
+
265
+ it 'does not instrument' do
266
+ client.perform_request('GET', '/_search', nil, nil, nil, endpoint: 'search')
267
+ expect(span).to be_nil
268
+ end
269
+ end
270
+ end
271
+
272
+ describe Elastic::Transport::OpenTelemetry::Sanitizer do
273
+ let(:key_patterns) { nil }
274
+
275
+ context '#sanitize' do
276
+ let(:body) do
277
+ { query: { match: { password: "test" } } }
278
+ end
279
+
280
+ let(:expected_body) do
281
+ { query: { match: { password: "REDACTED" } } }
282
+ end
283
+
284
+ it 'redacts sensitive values' do
285
+ expect(described_class.sanitize(body, key_patterns)).to eq(expected_body)
286
+ end
287
+
288
+ context 'with specified key patterns' do
289
+ let(:key_patterns) { [/something/] }
290
+
291
+ let(:body) do
292
+ { query: { match: { something: "test" } } }
293
+ end
294
+
295
+ let(:expected_body) do
296
+ { query: { match: { something: "REDACTED" } } }
297
+ end
298
+
299
+ it 'redacts sensitive values' do
300
+ expect(described_class.sanitize(body, key_patterns)).to eq(expected_body)
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
data/spec/spec_helper.rb CHANGED
@@ -91,3 +91,15 @@ RSpec.configure do |config|
91
91
  config.formatter = 'documentation'
92
92
  config.color = true
93
93
  end
94
+
95
+ if ENV['TEST_WITH_OTEL'] == 'true'
96
+ require 'opentelemetry-sdk'
97
+ EXPORTER = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new
98
+ span_processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(EXPORTER)
99
+
100
+ OpenTelemetry::SDK.configure do |c|
101
+ c.error_handler = ->(exception:, message:) { raise(exception || message) }
102
+ c.logger = Logger.new($stderr, level: ENV.fetch('OTEL_LOG_LEVEL', 'fatal').to_sym)
103
+ c.add_span_processor span_processor
104
+ end
105
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elastic-transport
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.2.5
4
+ version: 8.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elastic Client Library Maintainers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-24 00:00:00.000000000 Z
11
+ date: 2023-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -263,6 +263,7 @@ files:
263
263
  - ".github/check_license_headers.rb"
264
264
  - ".github/license-header.txt"
265
265
  - ".github/workflows/license.yml"
266
+ - ".github/workflows/otel.yml"
266
267
  - ".github/workflows/tests.yml"
267
268
  - ".gitignore"
268
269
  - CHANGELOG.md
@@ -277,6 +278,7 @@ files:
277
278
  - lib/elastic/transport.rb
278
279
  - lib/elastic/transport/client.rb
279
280
  - lib/elastic/transport/meta_header.rb
281
+ - lib/elastic/transport/opentelemetry.rb
280
282
  - lib/elastic/transport/redacted.rb
281
283
  - lib/elastic/transport/transport/base.rb
282
284
  - lib/elastic/transport/transport/connections/collection.rb
@@ -299,6 +301,7 @@ files:
299
301
  - spec/elastic/transport/http/faraday_spec.rb
300
302
  - spec/elastic/transport/http/manticore_spec.rb
301
303
  - spec/elastic/transport/meta_header_spec.rb
304
+ - spec/elastic/transport/opentelemetry_spec.rb
302
305
  - spec/elastic/transport/sniffer_spec.rb
303
306
  - spec/spec_helper.rb
304
307
  - test/integration/jruby_test.rb
@@ -337,7 +340,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
337
340
  - !ruby/object:Gem::Version
338
341
  version: '0'
339
342
  requirements: []
340
- rubygems_version: 3.5.3
343
+ rubygems_version: 3.4.19
341
344
  signing_key:
342
345
  specification_version: 4
343
346
  summary: Low level Ruby client for Elastic services.
@@ -350,6 +353,7 @@ test_files:
350
353
  - spec/elastic/transport/http/faraday_spec.rb
351
354
  - spec/elastic/transport/http/manticore_spec.rb
352
355
  - spec/elastic/transport/meta_header_spec.rb
356
+ - spec/elastic/transport/opentelemetry_spec.rb
353
357
  - spec/elastic/transport/sniffer_spec.rb
354
358
  - spec/spec_helper.rb
355
359
  - test/integration/jruby_test.rb