elastic-transport 8.2.5 → 8.3.0

Sign up to get free protection for your applications and to get access to all the features.
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