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 +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
|