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 +4 -4
- data/.github/workflows/otel.yml +48 -0
- data/.github/workflows/tests.yml +7 -7
- data/CHANGELOG.md +3 -3
- data/Gemfile +3 -0
- data/lib/elastic/transport/client.rb +28 -2
- data/lib/elastic/transport/meta_header.rb +2 -0
- data/lib/elastic/transport/opentelemetry.rb +157 -0
- data/lib/elastic/transport/transport/base.rb +8 -0
- data/lib/elastic/transport/transport/http/curb.rb +2 -1
- data/lib/elastic/transport/transport/http/faraday.rb +1 -0
- data/lib/elastic/transport/transport/http/manticore.rb +1 -0
- data/lib/elastic/transport/version.rb +1 -1
- data/lib/elastic/transport.rb +1 -0
- data/spec/elastic/transport/opentelemetry_spec.rb +306 -0
- data/spec/spec_helper.rb +12 -0
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e52d8e6c8f71d5d4c41362c059e32edb1604d8f63ed7cb13688ea75eaf4d5509
|
4
|
+
data.tar.gz: 4107c6f56a20d397102eb724734ee661d820af2c8b96dae63e72154b40e661f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/.github/workflows/tests.yml
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
name:
|
1
|
+
name: main tests
|
2
2
|
on:
|
3
3
|
push:
|
4
4
|
branches:
|
5
|
-
-
|
5
|
+
- main
|
6
6
|
pull_request:
|
7
7
|
branches:
|
8
|
-
-
|
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', '
|
19
|
-
es_version: ['8.
|
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', '
|
57
|
-
es_version: ['8.
|
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.
|
1
|
+
## 8.3.0
|
2
2
|
|
3
|
-
Tested versions of Ruby: (MRI) 3.0, 3.1, 3.2,
|
3
|
+
Tested versions of Ruby: (MRI) 3.0, 3.1, 3.2, JRuby 9.3 and JRuby 9.4
|
4
4
|
|
5
|
-
|
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
@@ -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
|
-
|
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
|
@@ -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,
|
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
|
data/lib/elastic/transport.rb
CHANGED
@@ -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.
|
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:
|
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.
|
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
|