logstash-output-elasticsearch 10.8.4-java → 10.8.6-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/lib/logstash/outputs/elasticsearch/http_client.rb +49 -14
- data/lib/logstash/plugin_mixins/elasticsearch/common.rb +10 -2
- data/logstash-output-elasticsearch.gemspec +1 -1
- data/spec/integration/outputs/ilm_spec.rb +16 -16
- data/spec/unit/http_client_builder_spec.rb +9 -9
- data/spec/unit/outputs/elasticsearch/http_client/pool_spec.rb +3 -3
- data/spec/unit/outputs/elasticsearch/http_client_spec.rb +57 -38
- data/spec/unit/outputs/elasticsearch_proxy_spec.rb +3 -3
- data/spec/unit/outputs/elasticsearch_spec.rb +105 -14
- data/spec/unit/outputs/error_whitelist_spec.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 69557d21ffe4079cabafcf86949f41d85cb6781f8898cebdc54b354117333b6b
|
4
|
+
data.tar.gz: a65b40a961335837f9ccff55472c0aeef033c5248cdcc579ffa98c6560fa377c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e8be38c81c89f8dca5dad83c79106180967cb5ed6806ed4a0ce97db1296a15bd8a462da80ef4a663807648164ac410d3d57fc46b2412ef497b1f9d0a4d7b57c6
|
7
|
+
data.tar.gz: 1843e98054e65374fe4b72c5938b0d808fecca799294783893d75c955977ec0d34020cd688ec7a16e4e22bf8c6c2b9e53343e9bfd4dd0cbccee10e601d0b2e0f
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
## 10.8.6
|
2
|
+
- Fixed an issue where a single over-size event being rejected by Elasticsearch would cause the entire entire batch to be retried indefinitely. The oversize event will still be retried on its own and logging has been improved to include payload sizes in this situation [#972](https://github.com/logstash-plugins/logstash-output-elasticsearch/pull/972)
|
3
|
+
- Fixed an issue with `http_compression => true` where a well-compressed payload could fit under our outbound 20MB limit but expand beyond Elasticsearch's 100MB limit, causing bulk failures. Bulk grouping is now determined entirely by the decompressed payload size [#823](https://github.com/logstash-plugins/logstash-output-elasticsearch/issues/823)
|
4
|
+
- Improved debug-level logging about bulk requests.
|
5
|
+
|
6
|
+
## 10.8.5
|
7
|
+
- Feat: assert returned item count from _bulk [#997](https://github.com/logstash-plugins/logstash-output-elasticsearch/pull/997)
|
8
|
+
|
1
9
|
## 10.8.4
|
2
10
|
- Fixed an issue where a retried request would drop "update" parameters [#800](https://github.com/logstash-plugins/logstash-output-elasticsearch/pull/800)
|
3
11
|
|
@@ -109,27 +109,50 @@ module LogStash; module Outputs; class ElasticSearch;
|
|
109
109
|
body_stream = StringIO.new
|
110
110
|
if http_compression
|
111
111
|
body_stream.set_encoding "BINARY"
|
112
|
-
stream_writer =
|
113
|
-
else
|
112
|
+
stream_writer = gzip_writer(body_stream)
|
113
|
+
else
|
114
114
|
stream_writer = body_stream
|
115
115
|
end
|
116
116
|
bulk_responses = []
|
117
|
-
|
117
|
+
batch_actions = []
|
118
|
+
bulk_actions.each_with_index do |action, index|
|
118
119
|
as_json = action.is_a?(Array) ?
|
119
120
|
action.map {|line| LogStash::Json.dump(line)}.join("\n") :
|
120
121
|
LogStash::Json.dump(action)
|
121
122
|
as_json << "\n"
|
122
|
-
if (
|
123
|
-
|
123
|
+
if (stream_writer.pos + as_json.bytesize) > TARGET_BULK_BYTES && stream_writer.pos > 0
|
124
|
+
stream_writer.flush # ensure writer has sync'd buffers before reporting sizes
|
125
|
+
logger.debug("Sending partial bulk request for batch with one or more actions remaining.",
|
126
|
+
:action_count => batch_actions.size,
|
127
|
+
:payload_size => stream_writer.pos,
|
128
|
+
:content_length => body_stream.size,
|
129
|
+
:batch_offset => (index + 1 - batch_actions.size))
|
130
|
+
bulk_responses << bulk_send(body_stream, batch_actions)
|
131
|
+
body_stream.truncate(0) && body_stream.seek(0)
|
132
|
+
stream_writer = gzip_writer(body_stream) if http_compression
|
133
|
+
batch_actions.clear
|
124
134
|
end
|
125
135
|
stream_writer.write(as_json)
|
136
|
+
batch_actions << action
|
126
137
|
end
|
127
138
|
stream_writer.close if http_compression
|
128
|
-
|
139
|
+
logger.debug("Sending final bulk request for batch.",
|
140
|
+
:action_count => batch_actions.size,
|
141
|
+
:payload_size => stream_writer.pos,
|
142
|
+
:content_length => body_stream.size,
|
143
|
+
:batch_offset => (actions.size - batch_actions.size))
|
144
|
+
bulk_responses << bulk_send(body_stream, batch_actions) if body_stream.size > 0
|
129
145
|
body_stream.close if !http_compression
|
130
146
|
join_bulk_responses(bulk_responses)
|
131
147
|
end
|
132
148
|
|
149
|
+
def gzip_writer(io)
|
150
|
+
fail(ArgumentError, "Cannot create gzip writer on IO with unread bytes") unless io.eof?
|
151
|
+
fail(ArgumentError, "Cannot create gzip writer on non-empty IO") unless io.pos == 0
|
152
|
+
|
153
|
+
Zlib::GzipWriter.new(io, Zlib::DEFAULT_COMPRESSION, Zlib::DEFAULT_STRATEGY)
|
154
|
+
end
|
155
|
+
|
133
156
|
def join_bulk_responses(bulk_responses)
|
134
157
|
{
|
135
158
|
"errors" => bulk_responses.any? {|r| r["errors"] == true},
|
@@ -137,25 +160,37 @@ module LogStash; module Outputs; class ElasticSearch;
|
|
137
160
|
}
|
138
161
|
end
|
139
162
|
|
140
|
-
def bulk_send(body_stream)
|
163
|
+
def bulk_send(body_stream, batch_actions)
|
141
164
|
params = http_compression ? {:headers => {"Content-Encoding" => "gzip"}} : {}
|
142
|
-
# Discard the URL
|
143
165
|
response = @pool.post(@bulk_path, params, body_stream.string)
|
144
|
-
if !body_stream.closed?
|
145
|
-
body_stream.truncate(0)
|
146
|
-
body_stream.seek(0)
|
147
|
-
end
|
148
166
|
|
149
167
|
@bulk_response_metrics.increment(response.code.to_s)
|
150
168
|
|
151
|
-
|
169
|
+
case response.code
|
170
|
+
when 200 # OK
|
171
|
+
LogStash::Json.load(response.body)
|
172
|
+
when 413 # Payload Too Large
|
173
|
+
logger.warn("Bulk request rejected: `413 Payload Too Large`", :action_count => batch_actions.size, :content_length => body_stream.size)
|
174
|
+
emulate_batch_error_response(batch_actions, response.code, 'payload_too_large')
|
175
|
+
else
|
152
176
|
url = ::LogStash::Util::SafeURI.new(response.final_url)
|
153
177
|
raise ::LogStash::Outputs::ElasticSearch::HttpClient::Pool::BadResponseCodeError.new(
|
154
178
|
response.code, url, body_stream.to_s, response.body
|
155
179
|
)
|
156
180
|
end
|
181
|
+
end
|
157
182
|
|
158
|
-
|
183
|
+
def emulate_batch_error_response(actions, http_code, reason)
|
184
|
+
{
|
185
|
+
"errors" => true,
|
186
|
+
"items" => actions.map do |action|
|
187
|
+
action = action.first if action.is_a?(Array)
|
188
|
+
request_action, request_parameters = action.first
|
189
|
+
{
|
190
|
+
request_action => {"status" => http_code, "error" => { "type" => reason }}
|
191
|
+
}
|
192
|
+
end
|
193
|
+
}
|
159
194
|
end
|
160
195
|
|
161
196
|
def get(path)
|
@@ -204,8 +204,16 @@ module LogStash; module PluginMixins; module ElasticSearch
|
|
204
204
|
return
|
205
205
|
end
|
206
206
|
|
207
|
+
responses = bulk_response["items"]
|
208
|
+
if responses.size != actions.size # can not map action -> response reliably
|
209
|
+
# an ES bug (on 7.10.2, 7.11.1) where a _bulk request to index X documents would return Y (> X) items
|
210
|
+
msg = "Sent #{actions.size} documents but Elasticsearch returned #{responses.size} responses"
|
211
|
+
@logger.warn(msg, actions: actions, responses: responses)
|
212
|
+
fail("#{msg} (likely a bug with _bulk endpoint)")
|
213
|
+
end
|
214
|
+
|
207
215
|
actions_to_retry = []
|
208
|
-
|
216
|
+
responses.each_with_index do |response,idx|
|
209
217
|
action_type, action_props = response.first
|
210
218
|
|
211
219
|
status = action_props["status"]
|
@@ -291,7 +299,7 @@ module LogStash; module PluginMixins; module ElasticSearch
|
|
291
299
|
retry unless @stopping.true?
|
292
300
|
rescue ::LogStash::Outputs::ElasticSearch::HttpClient::Pool::BadResponseCodeError => e
|
293
301
|
@bulk_request_metrics.increment(:failures)
|
294
|
-
log_hash = {:code => e.response_code, :url => e.url.sanitized.to_s}
|
302
|
+
log_hash = {:code => e.response_code, :url => e.url.sanitized.to_s, :content_length => e.request_body.bytesize}
|
295
303
|
log_hash[:body] = e.response_body if @logger.debug? # Generally this is too verbose
|
296
304
|
message = "Encountered a retryable error. Will Retry with exponential backoff "
|
297
305
|
|
@@ -5,7 +5,7 @@ shared_examples_for 'an ILM enabled Logstash' do
|
|
5
5
|
context 'with a policy with a maximum number of documents' do
|
6
6
|
let (:policy) { small_max_doc_policy }
|
7
7
|
let (:ilm_policy_name) { "logstash-policy-custom"}
|
8
|
-
let (:settings) { super.merge("ilm_policy" => ilm_policy_name)}
|
8
|
+
let (:settings) { super().merge("ilm_policy" => ilm_policy_name)}
|
9
9
|
|
10
10
|
it 'should rollover when the policy max docs is reached' do
|
11
11
|
put_policy(@es, ilm_policy_name, policy)
|
@@ -54,7 +54,7 @@ shared_examples_for 'an ILM enabled Logstash' do
|
|
54
54
|
context 'with a policy where the maximum number of documents is not reached' do
|
55
55
|
let (:policy) { large_max_doc_policy }
|
56
56
|
let (:ilm_policy_name) { "logstash-policy-custom-policy"}
|
57
|
-
let (:settings) { super.merge("ilm_policy" => ilm_policy_name)}
|
57
|
+
let (:settings) { super().merge("ilm_policy" => ilm_policy_name)}
|
58
58
|
|
59
59
|
it 'should ingest into a single index when max docs is not reached' do
|
60
60
|
put_policy(@es,ilm_policy_name, policy)
|
@@ -119,7 +119,7 @@ shared_examples_for 'an ILM disabled Logstash' do
|
|
119
119
|
context 'with an existing policy that will roll over' do
|
120
120
|
let (:policy) { small_max_doc_policy }
|
121
121
|
let (:ilm_policy_name) { "logstash-policy-3_docs"}
|
122
|
-
let (:settings) { super.merge("ilm_policy" => ilm_policy_name)}
|
122
|
+
let (:settings) { super().merge("ilm_policy" => ilm_policy_name)}
|
123
123
|
|
124
124
|
it 'should not roll over indices' do
|
125
125
|
subject.register
|
@@ -155,7 +155,7 @@ shared_examples_for 'an ILM disabled Logstash' do
|
|
155
155
|
|
156
156
|
context 'with a custom template name' do
|
157
157
|
let (:template_name) { "logstash_custom_template_name" }
|
158
|
-
let (:settings) { super.merge('template_name' => template_name)}
|
158
|
+
let (:settings) { super().merge('template_name' => template_name)}
|
159
159
|
|
160
160
|
it 'should not write the ILM settings into the template' do
|
161
161
|
subject.register
|
@@ -195,7 +195,7 @@ shared_examples_for 'an Elasticsearch instance that does not support index lifec
|
|
195
195
|
subject { LogStash::Outputs::ElasticSearch.new(settings) }
|
196
196
|
|
197
197
|
context 'when ilm is enabled in Logstash' do
|
198
|
-
let (:settings) { super.merge!({ 'ilm_enabled' => true }) }
|
198
|
+
let (:settings) { super().merge!({ 'ilm_enabled' => true }) }
|
199
199
|
|
200
200
|
it 'should raise a configuration error' do
|
201
201
|
expect do
|
@@ -210,13 +210,13 @@ shared_examples_for 'an Elasticsearch instance that does not support index lifec
|
|
210
210
|
end
|
211
211
|
|
212
212
|
context 'when ilm is disabled in Logstash' do
|
213
|
-
let (:settings) { super.merge!({ 'ilm_enabled' => false }) }
|
213
|
+
let (:settings) { super().merge!({ 'ilm_enabled' => false }) }
|
214
214
|
|
215
215
|
it_behaves_like 'an ILM disabled Logstash'
|
216
216
|
end
|
217
217
|
|
218
218
|
context 'when ilm is set to auto in Logstash' do
|
219
|
-
let (:settings) { super.merge!({ 'ilm_enabled' => 'auto' }) }
|
219
|
+
let (:settings) { super().merge!({ 'ilm_enabled' => 'auto' }) }
|
220
220
|
|
221
221
|
it_behaves_like 'an ILM disabled Logstash'
|
222
222
|
end
|
@@ -286,7 +286,7 @@ if ESHelper.es_version_satisfies?(">= 6.6")
|
|
286
286
|
|
287
287
|
context 'when using the default policy' do
|
288
288
|
context 'with a custom pattern' do
|
289
|
-
let (:settings) { super.merge("ilm_pattern" => "000001")}
|
289
|
+
let (:settings) { super().merge("ilm_pattern" => "000001")}
|
290
290
|
it 'should create a rollover alias' do
|
291
291
|
expect(@es.indices.exists_alias(name: "logstash")).to be_falsey
|
292
292
|
subject.register
|
@@ -346,7 +346,7 @@ if ESHelper.es_version_satisfies?(">= 6.6")
|
|
346
346
|
|
347
347
|
context 'when not using the default policy' do
|
348
348
|
let (:ilm_policy_name) {"logstash-policy-small"}
|
349
|
-
let (:settings) { super.merge("ilm_policy" => ilm_policy_name)}
|
349
|
+
let (:settings) { super().merge("ilm_policy" => ilm_policy_name)}
|
350
350
|
let (:policy) { small_max_doc_policy }
|
351
351
|
|
352
352
|
before do
|
@@ -363,7 +363,7 @@ if ESHelper.es_version_satisfies?(">= 6.6")
|
|
363
363
|
|
364
364
|
context 'when using a time based policy' do
|
365
365
|
let (:ilm_policy_name) {"logstash-policy-time"}
|
366
|
-
let (:settings) { super.merge("ilm_policy" => ilm_policy_name)}
|
366
|
+
let (:settings) { super().merge("ilm_policy" => ilm_policy_name)}
|
367
367
|
let (:policy) { max_age_policy("1d") }
|
368
368
|
|
369
369
|
before do
|
@@ -409,7 +409,7 @@ if ESHelper.es_version_satisfies?(">= 6.6")
|
|
409
409
|
let (:template) { "spec/fixtures/template-with-policy-es6x.json" }
|
410
410
|
end
|
411
411
|
|
412
|
-
let (:settings) { super.merge("template" => template,
|
412
|
+
let (:settings) { super().merge("template" => template,
|
413
413
|
"index" => "overwrite-4")}
|
414
414
|
|
415
415
|
it 'should not overwrite the index patterns' do
|
@@ -426,7 +426,7 @@ if ESHelper.es_version_satisfies?(">= 6.6")
|
|
426
426
|
let (:ilm_rollover_alias) { "logstash_the_cat_in_the_hat" }
|
427
427
|
let (:index) { ilm_rollover_alias }
|
428
428
|
let(:expected_index) { index }
|
429
|
-
let (:settings) { super.merge("ilm_policy" => ilm_policy_name,
|
429
|
+
let (:settings) { super().merge("ilm_policy" => ilm_policy_name,
|
430
430
|
"template" => template,
|
431
431
|
"ilm_rollover_alias" => ilm_rollover_alias)}
|
432
432
|
|
@@ -480,7 +480,7 @@ if ESHelper.es_version_satisfies?(">= 6.6")
|
|
480
480
|
|
481
481
|
context 'with a different template_name' do
|
482
482
|
let (:template_name) { "logstash_custom_template_name" }
|
483
|
-
let (:settings) { super.merge('template_name' => template_name)}
|
483
|
+
let (:settings) { super().merge('template_name' => template_name)}
|
484
484
|
|
485
485
|
it_behaves_like 'an ILM enabled Logstash'
|
486
486
|
|
@@ -514,7 +514,7 @@ if ESHelper.es_version_satisfies?(">= 6.6")
|
|
514
514
|
end
|
515
515
|
|
516
516
|
context 'when ilm_enabled is the default' do
|
517
|
-
let (:settings) { super.tap{|x|x.delete('ilm_enabled')}}
|
517
|
+
let (:settings) { super().tap{|x|x.delete('ilm_enabled')}}
|
518
518
|
|
519
519
|
if ESHelper.es_version_satisfies?(">=7.0")
|
520
520
|
context 'when Elasticsearch is version 7 or above' do
|
@@ -530,13 +530,13 @@ if ESHelper.es_version_satisfies?(">= 6.6")
|
|
530
530
|
end
|
531
531
|
|
532
532
|
context 'with ilm disabled' do
|
533
|
-
let (:settings) { super.merge('ilm_enabled' => false )}
|
533
|
+
let (:settings) { super().merge('ilm_enabled' => false )}
|
534
534
|
|
535
535
|
it_behaves_like 'an ILM disabled Logstash'
|
536
536
|
end
|
537
537
|
|
538
538
|
context 'with ilm disabled using a string' do
|
539
|
-
let (:settings) { super.merge('ilm_enabled' => 'false' )}
|
539
|
+
let (:settings) { super().merge('ilm_enabled' => 'false' )}
|
540
540
|
|
541
541
|
it_behaves_like 'an ILM disabled Logstash'
|
542
542
|
end
|
@@ -40,10 +40,10 @@ describe LogStash::Outputs::ElasticSearch::HttpClientBuilder do
|
|
40
40
|
|
41
41
|
context "when setting bulk_path" do
|
42
42
|
let(:bulk_path) { "/meh" }
|
43
|
-
let(:options) { super.merge("bulk_path" => bulk_path) }
|
43
|
+
let(:options) { super().merge("bulk_path" => bulk_path) }
|
44
44
|
|
45
45
|
context "when using path" do
|
46
|
-
let(:options) { super.merge("path" => "/path") }
|
46
|
+
let(:options) { super().merge("path" => "/path") }
|
47
47
|
it "ignores the path setting" do
|
48
48
|
expect(described_class).to receive(:create_http_client) do |options|
|
49
49
|
expect(options[:bulk_path]).to eq(bulk_path)
|
@@ -66,7 +66,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClientBuilder do
|
|
66
66
|
|
67
67
|
context "when using path" do
|
68
68
|
let(:path) { "/meh" }
|
69
|
-
let(:options) { super.merge("path" => path) }
|
69
|
+
let(:options) { super().merge("path" => path) }
|
70
70
|
it "sets bulk_path to path+_bulk" do
|
71
71
|
expect(described_class).to receive(:create_http_client) do |options|
|
72
72
|
expect(options[:bulk_path]).to eq("#{path}/_bulk")
|
@@ -88,10 +88,10 @@ describe LogStash::Outputs::ElasticSearch::HttpClientBuilder do
|
|
88
88
|
describe "healthcheck_path" do
|
89
89
|
context "when setting healthcheck_path" do
|
90
90
|
let(:healthcheck_path) { "/meh" }
|
91
|
-
let(:options) { super.merge("healthcheck_path" => healthcheck_path) }
|
91
|
+
let(:options) { super().merge("healthcheck_path" => healthcheck_path) }
|
92
92
|
|
93
93
|
context "when using path" do
|
94
|
-
let(:options) { super.merge("path" => "/path") }
|
94
|
+
let(:options) { super().merge("path" => "/path") }
|
95
95
|
it "ignores the path setting" do
|
96
96
|
expect(described_class).to receive(:create_http_client) do |options|
|
97
97
|
expect(options[:healthcheck_path]).to eq(healthcheck_path)
|
@@ -114,7 +114,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClientBuilder do
|
|
114
114
|
|
115
115
|
context "when using path" do
|
116
116
|
let(:path) { "/meh" }
|
117
|
-
let(:options) { super.merge("path" => path) }
|
117
|
+
let(:options) { super().merge("path" => path) }
|
118
118
|
it "sets healthcheck_path to path" do
|
119
119
|
expect(described_class).to receive(:create_http_client) do |options|
|
120
120
|
expect(options[:healthcheck_path]).to eq(path)
|
@@ -136,10 +136,10 @@ describe LogStash::Outputs::ElasticSearch::HttpClientBuilder do
|
|
136
136
|
describe "sniffing_path" do
|
137
137
|
context "when setting sniffing_path" do
|
138
138
|
let(:sniffing_path) { "/meh" }
|
139
|
-
let(:options) { super.merge("sniffing_path" => sniffing_path) }
|
139
|
+
let(:options) { super().merge("sniffing_path" => sniffing_path) }
|
140
140
|
|
141
141
|
context "when using path" do
|
142
|
-
let(:options) { super.merge("path" => "/path") }
|
142
|
+
let(:options) { super().merge("path" => "/path") }
|
143
143
|
it "ignores the path setting" do
|
144
144
|
expect(described_class).to receive(:create_http_client) do |options|
|
145
145
|
expect(options[:sniffing_path]).to eq(sniffing_path)
|
@@ -162,7 +162,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClientBuilder do
|
|
162
162
|
|
163
163
|
context "when using path" do
|
164
164
|
let(:path) { "/meh" }
|
165
|
-
let(:options) { super.merge("path" => path) }
|
165
|
+
let(:options) { super().merge("path" => path) }
|
166
166
|
it "sets sniffing_path to path+_nodes/http" do
|
167
167
|
expect(described_class).to receive(:create_http_client) do |options|
|
168
168
|
expect(options[:sniffing_path]).to eq("#{path}/_nodes/http")
|
@@ -68,7 +68,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
|
|
68
68
|
|
69
69
|
context "and setting healthcheck_path" do
|
70
70
|
let(:healthcheck_path) { "/my/health" }
|
71
|
-
let(:options) { super.merge(:healthcheck_path => healthcheck_path) }
|
71
|
+
let(:options) { super().merge(:healthcheck_path => healthcheck_path) }
|
72
72
|
it "performs the healthcheck to the healthcheck_path" do
|
73
73
|
expect(adapter).to receive(:perform_request) do |url, method, req_path, _, _|
|
74
74
|
expect(method).to eq(:head)
|
@@ -130,7 +130,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
|
|
130
130
|
end
|
131
131
|
|
132
132
|
context "when enabled" do
|
133
|
-
let(:options) { super.merge(:sniffing => true)}
|
133
|
+
let(:options) { super().merge(:sniffing => true)}
|
134
134
|
|
135
135
|
it "should start the sniffer" do
|
136
136
|
expect(subject.sniffer_alive?).to eql(true)
|
@@ -247,7 +247,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
|
|
247
247
|
end
|
248
248
|
|
249
249
|
let(:options) do
|
250
|
-
super.merge(:license_checker => license_checker)
|
250
|
+
super().merge(:license_checker => license_checker)
|
251
251
|
end
|
252
252
|
|
253
253
|
context 'when LicenseChecker#acceptable_license? returns false' do
|
@@ -48,7 +48,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
|
|
48
48
|
describe "ssl" do
|
49
49
|
context "when SSL is true" do
|
50
50
|
let(:ssl) { true }
|
51
|
-
let(:base_options) { super.merge(:hosts => [http_hostname_port]) }
|
51
|
+
let(:base_options) { super().merge(:hosts => [http_hostname_port]) }
|
52
52
|
|
53
53
|
it "should refuse to handle an http url" do
|
54
54
|
expect {
|
@@ -59,7 +59,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
|
|
59
59
|
|
60
60
|
context "when SSL is false" do
|
61
61
|
let(:ssl) { false }
|
62
|
-
let(:base_options) { super.merge(:hosts => [https_hostname_port]) }
|
62
|
+
let(:base_options) { super().merge(:hosts => [https_hostname_port]) }
|
63
63
|
|
64
64
|
it "should refuse to handle an https url" do
|
65
65
|
expect {
|
@@ -69,7 +69,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
|
|
69
69
|
end
|
70
70
|
|
71
71
|
describe "ssl is nil" do
|
72
|
-
let(:base_options) { super.merge(:hosts => [https_hostname_port]) }
|
72
|
+
let(:base_options) { super().merge(:hosts => [https_hostname_port]) }
|
73
73
|
it "should handle an ssl url correctly when SSL is nil" do
|
74
74
|
subject
|
75
75
|
expect(subject.host_to_url(https_hostname_port).to_s).to eq(https_hostname_port.to_s + "/")
|
@@ -79,14 +79,14 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
|
|
79
79
|
|
80
80
|
describe "path" do
|
81
81
|
let(:url) { http_hostname_port_path }
|
82
|
-
let(:base_options) { super.merge(:hosts => [url]) }
|
82
|
+
let(:base_options) { super().merge(:hosts => [url]) }
|
83
83
|
|
84
84
|
it "should allow paths in a url" do
|
85
85
|
expect(subject.host_to_url(url)).to eq(url)
|
86
86
|
end
|
87
87
|
|
88
88
|
context "with the path option set" do
|
89
|
-
let(:base_options) { super.merge(:client_settings => {:path => "/otherpath"}) }
|
89
|
+
let(:base_options) { super().merge(:client_settings => {:path => "/otherpath"}) }
|
90
90
|
|
91
91
|
it "should not allow paths in two places" do
|
92
92
|
expect {
|
@@ -97,7 +97,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
|
|
97
97
|
|
98
98
|
context "with a path missing a leading /" do
|
99
99
|
let(:url) { http_hostname_port }
|
100
|
-
let(:base_options) { super.merge(:client_settings => {:path => "otherpath"}) }
|
100
|
+
let(:base_options) { super().merge(:client_settings => {:path => "otherpath"}) }
|
101
101
|
|
102
102
|
|
103
103
|
it "should automatically insert a / in front of path overlays" do
|
@@ -204,7 +204,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
|
|
204
204
|
end
|
205
205
|
|
206
206
|
describe "#bulk" do
|
207
|
-
subject { described_class.new(base_options) }
|
207
|
+
subject(:http_client) { described_class.new(base_options) }
|
208
208
|
|
209
209
|
require "json"
|
210
210
|
let(:message) { "hey" }
|
@@ -212,42 +212,61 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
|
|
212
212
|
["index", {:_id=>nil, :_index=>"logstash"}, {"message"=> message}],
|
213
213
|
]}
|
214
214
|
|
215
|
-
|
216
|
-
|
217
|
-
let(:message) { "a" * (target_bulk_bytes + 1) }
|
215
|
+
[true,false].each do |http_compression_enabled|
|
216
|
+
context "with `http_compression => #{http_compression_enabled}`" do
|
218
217
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
218
|
+
let(:base_options) { super().merge(:client_settings => {:http_compression => http_compression_enabled}) }
|
219
|
+
|
220
|
+
before(:each) do
|
221
|
+
if http_compression_enabled
|
222
|
+
expect(http_client).to receive(:gzip_writer).at_least(:once).and_call_original
|
223
|
+
else
|
224
|
+
expect(http_client).to_not receive(:gzip_writer)
|
225
|
+
end
|
223
226
|
end
|
224
|
-
s = subject.send(:bulk, actions)
|
225
|
-
end
|
226
|
-
end
|
227
227
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
228
|
+
context "if a message is over TARGET_BULK_BYTES" do
|
229
|
+
let(:target_bulk_bytes) { LogStash::Outputs::ElasticSearch::TARGET_BULK_BYTES }
|
230
|
+
let(:message) { "a" * (target_bulk_bytes + 1) }
|
231
|
+
|
232
|
+
it "should be handled properly" do
|
233
|
+
allow(subject).to receive(:join_bulk_responses)
|
234
|
+
expect(subject).to receive(:bulk_send).once do |data|
|
235
|
+
if !http_compression_enabled
|
236
|
+
expect(data.size).to be > target_bulk_bytes
|
237
|
+
else
|
238
|
+
expect(Zlib::gunzip(data.string).size).to be > target_bulk_bytes
|
239
|
+
end
|
240
|
+
end
|
241
|
+
s = subject.send(:bulk, actions)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
context "with two messages" do
|
246
|
+
let(:message1) { "hey" }
|
247
|
+
let(:message2) { "you" }
|
248
|
+
let(:actions) { [
|
249
|
+
["index", {:_id=>nil, :_index=>"logstash"}, {"message"=> message1}],
|
250
|
+
["index", {:_id=>nil, :_index=>"logstash"}, {"message"=> message2}],
|
251
|
+
]}
|
252
|
+
it "executes one bulk_send operation" do
|
253
|
+
allow(subject).to receive(:join_bulk_responses)
|
254
|
+
expect(subject).to receive(:bulk_send).once
|
255
|
+
s = subject.send(:bulk, actions)
|
256
|
+
end
|
240
257
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
258
|
+
context "if one exceeds TARGET_BULK_BYTES" do
|
259
|
+
let(:target_bulk_bytes) { LogStash::Outputs::ElasticSearch::TARGET_BULK_BYTES }
|
260
|
+
let(:message1) { "a" * (target_bulk_bytes + 1) }
|
261
|
+
it "executes two bulk_send operations" do
|
262
|
+
allow(subject).to receive(:join_bulk_responses)
|
263
|
+
expect(subject).to receive(:bulk_send).twice
|
264
|
+
s = subject.send(:bulk, actions)
|
265
|
+
end
|
266
|
+
end
|
248
267
|
end
|
249
|
-
|
250
|
-
|
268
|
+
end
|
269
|
+
end
|
251
270
|
end
|
252
271
|
|
253
272
|
describe "sniffing" do
|
@@ -24,7 +24,7 @@ describe "Proxy option" do
|
|
24
24
|
|
25
25
|
context "when specified as a URI" do
|
26
26
|
shared_examples("hash conversion") do |hash|
|
27
|
-
let(:settings) { super.merge("proxy" => proxy)}
|
27
|
+
let(:settings) { super().merge("proxy" => proxy)}
|
28
28
|
|
29
29
|
it "should set the proxy to the correct hash value" do
|
30
30
|
expect(::Manticore::Client).to have_received(:new) do |options|
|
@@ -71,7 +71,7 @@ describe "Proxy option" do
|
|
71
71
|
end
|
72
72
|
|
73
73
|
context "when specified as ''" do
|
74
|
-
let(:settings) { super.merge("proxy" => "${A_MISSING_ENV_VARIABLE:}")}
|
74
|
+
let(:settings) { super().merge("proxy" => "${A_MISSING_ENV_VARIABLE:}")}
|
75
75
|
|
76
76
|
it "should not send the proxy option to manticore" do
|
77
77
|
expect { subject.register }.not_to raise_error
|
@@ -85,7 +85,7 @@ describe "Proxy option" do
|
|
85
85
|
end
|
86
86
|
|
87
87
|
context "when specified as invalid uri" do
|
88
|
-
let(:settings) { super.merge("proxy" => ":")}
|
88
|
+
let(:settings) { super().merge("proxy" => ":")}
|
89
89
|
|
90
90
|
it "should fail" do
|
91
91
|
# SafeURI isn't doing the proper exception wrapping for us, we can not simply :
|
@@ -4,7 +4,7 @@ require "flores/random"
|
|
4
4
|
require "logstash/outputs/elasticsearch"
|
5
5
|
|
6
6
|
describe LogStash::Outputs::ElasticSearch do
|
7
|
-
subject { described_class.new(options) }
|
7
|
+
subject(:elasticsearch_output_instance) { described_class.new(options) }
|
8
8
|
let(:options) { {} }
|
9
9
|
let(:maximum_seen_major_version) { [1,2,5,6,7,8].sample }
|
10
10
|
|
@@ -46,7 +46,7 @@ describe LogStash::Outputs::ElasticSearch do
|
|
46
46
|
|
47
47
|
describe "getting a document type" do
|
48
48
|
context "if document_type isn't set" do
|
49
|
-
let(:options) { super.merge("document_type" => nil)}
|
49
|
+
let(:options) { super().merge("document_type" => nil)}
|
50
50
|
context "for 7.x elasticsearch clusters" do
|
51
51
|
let(:maximum_seen_major_version) { 7 }
|
52
52
|
it "should return '_doc'" do
|
@@ -70,7 +70,7 @@ describe LogStash::Outputs::ElasticSearch do
|
|
70
70
|
end
|
71
71
|
|
72
72
|
context "with 'document type set'" do
|
73
|
-
let(:options) { super.merge("document_type" => "bar")}
|
73
|
+
let(:options) { super().merge("document_type" => "bar")}
|
74
74
|
it "should get the event type from the 'document_type' setting" do
|
75
75
|
expect(subject.send(:get_event_type, LogStash::Event.new())).to eql("bar")
|
76
76
|
end
|
@@ -87,7 +87,7 @@ describe LogStash::Outputs::ElasticSearch do
|
|
87
87
|
end
|
88
88
|
|
89
89
|
context "with 'document type set'" do
|
90
|
-
let(:options) { super.merge("document_type" => "bar")}
|
90
|
+
let(:options) { super().merge("document_type" => "bar")}
|
91
91
|
it "should get the event type from the 'document_type' setting" do
|
92
92
|
action_tuple = subject.send(:event_action_tuple, LogStash::Event.new("type" => "foo"))
|
93
93
|
action_params = action_tuple[1]
|
@@ -105,7 +105,7 @@ describe LogStash::Outputs::ElasticSearch do
|
|
105
105
|
end
|
106
106
|
|
107
107
|
context "with 'document type set'" do
|
108
|
-
let(:options) { super.merge("document_type" => "bar")}
|
108
|
+
let(:options) { super().merge("document_type" => "bar")}
|
109
109
|
it "should not include '_type'" do
|
110
110
|
action_tuple = subject.send(:event_action_tuple, LogStash::Event.new("type" => "foo"))
|
111
111
|
action_params = action_tuple[1]
|
@@ -127,7 +127,7 @@ describe LogStash::Outputs::ElasticSearch do
|
|
127
127
|
|
128
128
|
context "as part of a URL" do
|
129
129
|
let(:options) {
|
130
|
-
super.merge("hosts" => ["http://#{user}:#{password.value}@localhost:9200"])
|
130
|
+
super().merge("hosts" => ["http://#{user}:#{password.value}@localhost:9200"])
|
131
131
|
}
|
132
132
|
|
133
133
|
include_examples("an authenticated config")
|
@@ -135,7 +135,7 @@ describe LogStash::Outputs::ElasticSearch do
|
|
135
135
|
|
136
136
|
context "as a hash option" do
|
137
137
|
let(:options) {
|
138
|
-
super.merge!(
|
138
|
+
super().merge!(
|
139
139
|
"user" => user,
|
140
140
|
"password" => password
|
141
141
|
)
|
@@ -175,7 +175,7 @@ describe LogStash::Outputs::ElasticSearch do
|
|
175
175
|
|
176
176
|
context "with extra slashes" do
|
177
177
|
let(:path) { "/slashed-path/ "}
|
178
|
-
let(:options) { super.merge("path" => "/some-path/") }
|
178
|
+
let(:options) { super().merge("path" => "/some-path/") }
|
179
179
|
|
180
180
|
it "should properly set the path on the HTTP client without adding slashes" do
|
181
181
|
expect(manticore_url.path).to eql(options["path"])
|
@@ -234,13 +234,13 @@ describe LogStash::Outputs::ElasticSearch do
|
|
234
234
|
end
|
235
235
|
|
236
236
|
describe "without a port specified" do
|
237
|
-
let(:options) { super.merge('hosts' => 'localhost') }
|
237
|
+
let(:options) { super().merge('hosts' => 'localhost') }
|
238
238
|
it "should properly set the default port (9200) on the HTTP client" do
|
239
239
|
expect(manticore_url.port).to eql(9200)
|
240
240
|
end
|
241
241
|
end
|
242
242
|
describe "with a port other than 9200 specified" do
|
243
|
-
let(:options) { super.merge('hosts' => 'localhost:9202') }
|
243
|
+
let(:options) { super().merge('hosts' => 'localhost:9202') }
|
244
244
|
it "should properly set the specified port on the HTTP client" do
|
245
245
|
expect(manticore_url.port).to eql(9202)
|
246
246
|
end
|
@@ -265,12 +265,14 @@ describe LogStash::Outputs::ElasticSearch do
|
|
265
265
|
let(:event) { ::LogStash::Event.new("foo" => "bar") }
|
266
266
|
let(:error) do
|
267
267
|
::LogStash::Outputs::ElasticSearch::HttpClient::Pool::BadResponseCodeError.new(
|
268
|
-
429, double("url").as_null_object,
|
268
|
+
429, double("url").as_null_object, request_body, double("response body")
|
269
269
|
)
|
270
270
|
end
|
271
271
|
let(:logger) { double("logger").as_null_object }
|
272
272
|
let(:response) { { :errors => [], :items => [] } }
|
273
273
|
|
274
|
+
let(:request_body) { double(:request_body, :bytesize => 1023) }
|
275
|
+
|
274
276
|
before(:each) do
|
275
277
|
|
276
278
|
i = 0
|
@@ -296,6 +298,95 @@ describe LogStash::Outputs::ElasticSearch do
|
|
296
298
|
expect(subject.logger).to have_received(:debug).with(/Encountered a retryable error/i, anything)
|
297
299
|
end
|
298
300
|
end
|
301
|
+
|
302
|
+
context "unexpected bulk response" do
|
303
|
+
let(:options) do
|
304
|
+
{ "hosts" => "127.0.0.1:9999", "index" => "%{foo}", "manage_template" => false }
|
305
|
+
end
|
306
|
+
|
307
|
+
let(:events) { [ ::LogStash::Event.new("foo" => "bar1"), ::LogStash::Event.new("foo" => "bar2") ] }
|
308
|
+
|
309
|
+
let(:bulk_response) do
|
310
|
+
# shouldn't really happen but we've seen this happen - here ES returns more items than were sent
|
311
|
+
{ "took"=>1, "ingest_took"=>9, "errors"=>true,
|
312
|
+
"items"=>[{"index"=>{"_index"=>"bar1", "_type"=>"_doc", "_id"=>nil, "status"=>500,
|
313
|
+
"error"=>{"type" => "illegal_state_exception",
|
314
|
+
"reason" => "pipeline with id [test-ingest] could not be loaded, caused by [ElasticsearchParseException[Error updating pipeline with id [test-ingest]]; nested: ElasticsearchException[java.lang.IllegalArgumentException: no enrich index exists for policy with name [test-metadata1]]; nested: IllegalArgumentException[no enrich index exists for policy with name [test-metadata1]];; ElasticsearchException[java.lang.IllegalArgumentException: no enrich index exists for policy with name [test-metadata1]]; nested: IllegalArgumentException[no enrich index exists for policy with name [test-metadata1]];; java.lang.IllegalArgumentException: no enrich index exists for policy with name [test-metadata1]]"
|
315
|
+
}
|
316
|
+
}
|
317
|
+
},
|
318
|
+
# NOTE: this is an artificial success (usually everything fails with a 500) but even if some doc where
|
319
|
+
# to succeed due the unexpected reponse items we can not clearly identify which actions to retry ...
|
320
|
+
{"index"=>{"_index"=>"bar2", "_type"=>"_doc", "_id"=>nil, "status"=>201}},
|
321
|
+
{"index"=>{"_index"=>"bar2", "_type"=>"_doc", "_id"=>nil, "status"=>500,
|
322
|
+
"error"=>{"type" => "illegal_state_exception",
|
323
|
+
"reason" => "pipeline with id [test-ingest] could not be loaded, caused by [ElasticsearchParseException[Error updating pipeline with id [test-ingest]]; nested: ElasticsearchException[java.lang.IllegalArgumentException: no enrich index exists for policy with name [test-metadata1]];"
|
324
|
+
}
|
325
|
+
}
|
326
|
+
}]
|
327
|
+
}
|
328
|
+
end
|
329
|
+
|
330
|
+
before(:each) do
|
331
|
+
allow(subject.client).to receive(:bulk_send).with(instance_of(StringIO), instance_of(Array)) do |stream, actions|
|
332
|
+
expect( stream.string ).to include '"foo":"bar1"'
|
333
|
+
expect( stream.string ).to include '"foo":"bar2"'
|
334
|
+
end.and_return(bulk_response, {"errors"=>false}) # let's make it go away (second call) to not retry indefinitely
|
335
|
+
end
|
336
|
+
|
337
|
+
it "should retry submit" do
|
338
|
+
allow(subject.logger).to receive(:error).with(/Encountered an unexpected error/i, anything)
|
339
|
+
allow(subject.client).to receive(:bulk).and_call_original # track count
|
340
|
+
|
341
|
+
subject.multi_receive(events)
|
342
|
+
|
343
|
+
expect(subject.client).to have_received(:bulk).twice
|
344
|
+
end
|
345
|
+
|
346
|
+
it "should log specific error message" do
|
347
|
+
expect(subject.logger).to receive(:error).with(/Encountered an unexpected error/i,
|
348
|
+
hash_including(:error_message => 'Sent 2 documents but Elasticsearch returned 3 responses (likely a bug with _bulk endpoint)'))
|
349
|
+
|
350
|
+
subject.multi_receive(events)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
context '413 errors' do
|
356
|
+
let(:payload_size) { LogStash::Outputs::ElasticSearch::TARGET_BULK_BYTES + 1024 }
|
357
|
+
let(:event) { ::LogStash::Event.new("message" => ("a" * payload_size ) ) }
|
358
|
+
|
359
|
+
let(:logger_stub) { double("logger").as_null_object }
|
360
|
+
|
361
|
+
before(:each) do
|
362
|
+
allow(elasticsearch_output_instance.client).to receive(:logger).and_return(logger_stub)
|
363
|
+
|
364
|
+
allow(elasticsearch_output_instance.client).to receive(:bulk).and_call_original
|
365
|
+
|
366
|
+
max_bytes = payload_size * 3 / 4 # ensure a failure first attempt
|
367
|
+
allow(elasticsearch_output_instance.client.pool).to receive(:post) do |path, params, body|
|
368
|
+
if body.length > max_bytes
|
369
|
+
max_bytes *= 2 # ensure a successful retry
|
370
|
+
double("Response", :code => 413, :body => "")
|
371
|
+
else
|
372
|
+
double("Response", :code => 200, :body => '{"errors":false,"items":[{"index":{"status":200,"result":"created"}}]}')
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
it 'retries the 413 until it goes away' do
|
378
|
+
elasticsearch_output_instance.multi_receive([event])
|
379
|
+
|
380
|
+
expect(elasticsearch_output_instance.client).to have_received(:bulk).twice
|
381
|
+
end
|
382
|
+
|
383
|
+
it 'logs about payload quantity and size' do
|
384
|
+
elasticsearch_output_instance.multi_receive([event])
|
385
|
+
|
386
|
+
expect(logger_stub).to have_received(:warn)
|
387
|
+
.with(a_string_matching(/413 Payload Too Large/),
|
388
|
+
hash_including(:action_count => 1, :content_length => a_value > 20_000_000))
|
389
|
+
end
|
299
390
|
end
|
300
391
|
|
301
392
|
context "with timeout set" do
|
@@ -410,7 +501,7 @@ describe LogStash::Outputs::ElasticSearch do
|
|
410
501
|
let(:options) { { 'retry_on_conflict' => num_retries } }
|
411
502
|
|
412
503
|
context "with a regular index" do
|
413
|
-
let(:options) { super.merge("action" => "index") }
|
504
|
+
let(:options) { super().merge("action" => "index") }
|
414
505
|
|
415
506
|
it "should not set the retry_on_conflict parameter when creating an event_action_tuple" do
|
416
507
|
allow(subject.client).to receive(:maximum_seen_major_version).and_return(maximum_seen_major_version)
|
@@ -420,7 +511,7 @@ describe LogStash::Outputs::ElasticSearch do
|
|
420
511
|
end
|
421
512
|
|
422
513
|
context "using a plain update" do
|
423
|
-
let(:options) { super.merge("action" => "update", "retry_on_conflict" => num_retries, "document_id" => 1) }
|
514
|
+
let(:options) { super().merge("action" => "update", "retry_on_conflict" => num_retries, "document_id" => 1) }
|
424
515
|
|
425
516
|
it "should set the retry_on_conflict parameter when creating an event_action_tuple" do
|
426
517
|
action, params, event_data = subject.event_action_tuple(event)
|
@@ -429,7 +520,7 @@ describe LogStash::Outputs::ElasticSearch do
|
|
429
520
|
end
|
430
521
|
|
431
522
|
context "with a sprintf action that resolves to update" do
|
432
|
-
let(:options) { super.merge("action" => "%{myactionfield}", "retry_on_conflict" => num_retries, "document_id" => 1) }
|
523
|
+
let(:options) { super().merge("action" => "%{myactionfield}", "retry_on_conflict" => num_retries, "document_id" => 1) }
|
433
524
|
|
434
525
|
it "should set the retry_on_conflict parameter when creating an event_action_tuple" do
|
435
526
|
action, params, event_data = subject.event_action_tuple(event)
|
@@ -44,7 +44,7 @@ describe "whitelisting error types in expected behavior" do
|
|
44
44
|
end
|
45
45
|
|
46
46
|
describe "when failure logging is disabled for docuemnt exists error" do
|
47
|
-
let(:settings) { super.merge("failure_type_logging_whitelist" => ["document_already_exists_exception"]) }
|
47
|
+
let(:settings) { super().merge("failure_type_logging_whitelist" => ["document_already_exists_exception"]) }
|
48
48
|
|
49
49
|
it "should log a failure on the action" do
|
50
50
|
expect(subject.logger).not_to have_received(:warn).with("Failed action.", anything)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-output-elasticsearch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 10.8.
|
4
|
+
version: 10.8.6
|
5
5
|
platform: java
|
6
6
|
authors:
|
7
7
|
- Elastic
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-04-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|