logstash-output-kafka 7.0.1 → 7.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5a612af7a54364ed111f32cb4917e1628d97911b
4
- data.tar.gz: b5f48497dbb62d9523ebe196ffd25b12f9065fff
3
+ metadata.gz: a5ccedfbfedaaf72bad1ec4696411dd1859e1922
4
+ data.tar.gz: c4180be5c8a6e66d49b421ebf46e962948c03c35
5
5
  SHA512:
6
- metadata.gz: 89ac63b16b8ea24fefcfdf37135585c980095fa032e3b6fe093fc86bb4576514e89765712aafe668692820c5bf67458b72ec807e603e517f99617e7cab07e688
7
- data.tar.gz: 03806af2fac8d5fa49d8916cdcba45ebb1f9f00a4c8d91b5aa5bca8eaa4ac33d2c303cb3fdf372fdbb040bdb9049f5434b65ba8a93e691355ffd46daceff3a26
6
+ metadata.gz: 3fe5acfc009c895074de3d3c84fc188c5bbaba6297f5a0b1c2286fbd25da7688f6f56ad28d9f60c5c4d6ac01ba0bafccb4e5bdccc5b960e8d0c934c21a5312b9
7
+ data.tar.gz: 4916c1b9c9aac493d83e63a5448b3776603ec2f4dbcaca58e37850e8a19a8e8a0acb8de7e68b6c61a6fc5304c8ad2a92b0b6dce1f48fff8e1cf4d3da3e3abdf1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 7.0.3
2
+ - Bugfix: Sends are now retried until successful. Previously, failed transmissions to Kafka
3
+ could have been lost by the KafkaProducer library. Now we verify transmission explicitly.
4
+ This changes the default 'retry' from 0 to retry-forever. It was a bug that we defaulted
5
+ to a retry count of 0.
6
+ https://github.com/logstash-plugins/logstash-output-kafka/pull/151
7
+
8
+ ## 7.0.2
9
+ - Docs: Fix misleading info about the default codec
10
+
1
11
  ## 7.0.1
2
12
  - Fix some documentation issues
3
13
 
data/docs/index.asciidoc CHANGED
@@ -48,19 +48,19 @@ This output supports connecting to Kafka over:
48
48
 
49
49
  By default security is disabled but can be turned on as needed.
50
50
 
51
- The only required configuration is the topic_id. The default codec is plain,
52
- so events will be persisted on the broker in plain format. Logstash will encode your messages with not
53
- only the message but also with a timestamp and hostname. If you do not want anything but your message
54
- passing through, you should make the output configuration something like:
51
+ The only required configuration is the topic_id.
52
+
53
+ The default codec is plain. Logstash will encode your events with not only the message field but also with a timestamp and hostname.
54
+
55
+ If you want the full content of your events to be sent as json, you should set the codec in the output configuration like this:
55
56
  [source,ruby]
56
57
  output {
57
58
  kafka {
58
- codec => plain {
59
- format => "%{message}"
60
- }
59
+ codec => json
61
60
  topic_id => "mytopic"
62
61
  }
63
62
  }
63
+
64
64
  For more information see http://kafka.apache.org/documentation.html#theproducer
65
65
 
66
66
  Kafka producer configuration: http://kafka.apache.org/documentation.html#newproducerconfigs
@@ -291,10 +291,17 @@ retries are exhausted.
291
291
  ===== `retries`
292
292
 
293
293
  * Value type is <<number,number>>
294
- * Default value is `0`
294
+ * There is no default value for this setting.
295
+
296
+ The default retry behavior is to retry until successful. To prevent data loss,
297
+ the use of this setting is discouraged.
298
+
299
+ If you choose to set `retries`, a value greater than zero will cause the
300
+ client to only retry a fixed number of times. This will result in data loss
301
+ if a transport fault exists for longer than your retry count (network outage,
302
+ Kafka down, etc).
295
303
 
296
- Setting a value greater than zero will cause the client to
297
- resend any record whose send fails with a potentially transient error.
304
+ A value less than zero is a configuration error.
298
305
 
299
306
  [id="plugins-{type}s-{plugin}-retry_backoff_ms"]
300
307
  ===== `retry_backoff_ms`
@@ -109,9 +109,15 @@ class LogStash::Outputs::Kafka < LogStash::Outputs::Base
109
109
  # elapses the client will resend the request if necessary or fail the request if
110
110
  # retries are exhausted.
111
111
  config :request_timeout_ms, :validate => :string
112
- # Setting a value greater than zero will cause the client to
113
- # resend any record whose send fails with a potentially transient error.
114
- config :retries, :validate => :number, :default => 0
112
+ # The default retry behavior is to retry until successful. To prevent data loss,
113
+ # the use of this setting is discouraged.
114
+ #
115
+ # If you choose to set `retries`, a value greater than zero will cause the
116
+ # client to only retry a fixed number of times. This will result in data loss
117
+ # if a transient error outlasts your retry count.
118
+ #
119
+ # A value less than zero is a configuration error.
120
+ config :retries, :validate => :number
115
121
  # The amount of time to wait before attempting to retry a failed produce request to a given topic partition.
116
122
  config :retry_backoff_ms, :validate => :number, :default => 100
117
123
  # The size of the TCP send buffer to use when sending data.
@@ -170,6 +176,17 @@ class LogStash::Outputs::Kafka < LogStash::Outputs::Base
170
176
 
171
177
  public
172
178
  def register
179
+ @thread_batch_map = Concurrent::Hash.new
180
+
181
+ if !@retries.nil?
182
+ if @retries < 0
183
+ raise ConfigurationError, "A negative retry count (#{@retries}) is not valid. Must be a value >= 0"
184
+ end
185
+
186
+ @logger.warn("Kafka output is configured with finite retry. This instructs Logstash to LOSE DATA after a set number of send attempts fails. If you do not want to lose data if Kafka is down, then you must remove the retry setting.", :retries => @retries)
187
+ end
188
+
189
+
173
190
  @producer = create_producer
174
191
  @codec.on_event do |event, data|
175
192
  begin
@@ -178,7 +195,7 @@ class LogStash::Outputs::Kafka < LogStash::Outputs::Base
178
195
  else
179
196
  record = org.apache.kafka.clients.producer.ProducerRecord.new(event.sprintf(@topic_id), event.sprintf(@message_key), data)
180
197
  end
181
- @producer.send(record)
198
+ prepare(record)
182
199
  rescue LogStash::ShutdownSignal
183
200
  @logger.debug('Kafka producer got shutdown signal')
184
201
  rescue => e
@@ -186,14 +203,89 @@ class LogStash::Outputs::Kafka < LogStash::Outputs::Base
186
203
  :exception => e)
187
204
  end
188
205
  end
189
-
190
206
  end # def register
191
207
 
192
- def receive(event)
193
- if event == LogStash::SHUTDOWN
194
- return
208
+ def prepare(record)
209
+ # This output is threadsafe, so we need to keep a batch per thread.
210
+ @thread_batch_map[Thread.current].add(record)
211
+ end
212
+
213
+ def multi_receive(events)
214
+ t = Thread.current
215
+ if !@thread_batch_map.include?(t)
216
+ @thread_batch_map[t] = java.util.ArrayList.new(events.size)
217
+ end
218
+
219
+ events.each do |event|
220
+ break if event == LogStash::SHUTDOWN
221
+ @codec.encode(event)
222
+ end
223
+
224
+ batch = @thread_batch_map[t]
225
+ if batch.any?
226
+ retrying_send(batch)
227
+ batch.clear
195
228
  end
196
- @codec.encode(event)
229
+ end
230
+
231
+ def retrying_send(batch)
232
+ remaining = @retries;
233
+
234
+ while batch.any?
235
+ if !remaining.nil?
236
+ if remaining < 0
237
+ # TODO(sissel): Offer to DLQ? Then again, if it's a transient fault,
238
+ # DLQing would make things worse (you dlq data that would be successful
239
+ # after the fault is repaired)
240
+ logger.info("Exhausted user-configured retry count when sending to Kafka. Dropping these events.",
241
+ :max_retries => @retries, :drop_count => batch.count)
242
+ break
243
+ end
244
+
245
+ remaining -= 1
246
+ end
247
+
248
+ failures = []
249
+
250
+ futures = batch.collect do |record|
251
+ begin
252
+ # send() can throw an exception even before the future is created.
253
+ @producer.send(record)
254
+ rescue org.apache.kafka.common.errors.TimeoutException => e
255
+ failures << record
256
+ nil
257
+ rescue org.apache.kafka.common.errors.InterruptException => e
258
+ failures << record
259
+ nil
260
+ rescue org.apache.kafka.common.errors.SerializationException => e
261
+ # TODO(sissel): Retrying will fail because the data itself has a problem serializing.
262
+ # TODO(sissel): Let's add DLQ here.
263
+ failures << record
264
+ nil
265
+ end
266
+ end.compact
267
+
268
+ futures.each_with_index do |future, i|
269
+ begin
270
+ result = future.get()
271
+ rescue => e
272
+ # TODO(sissel): Add metric to count failures, possibly by exception type.
273
+ logger.debug? && logger.debug("KafkaProducer.send() failed: #{e}", :exception => e);
274
+ failures << batch[i]
275
+ end
276
+ end
277
+
278
+ # No failures? Cool. Let's move on.
279
+ break if failures.empty?
280
+
281
+ # Otherwise, retry with any failed transmissions
282
+ batch = failures
283
+ delay = 1.0 / @retry_backoff_ms
284
+ logger.info("Sending batch to Kafka failed. Will retry after a delay.", :batch_size => batch.size,
285
+ :failures => failures.size, :sleep => delay);
286
+ sleep(delay)
287
+ end
288
+
197
289
  end
198
290
 
199
291
  def close
@@ -217,8 +309,8 @@ class LogStash::Outputs::Kafka < LogStash::Outputs::Base
217
309
  props.put(kafka::MAX_REQUEST_SIZE_CONFIG, max_request_size.to_s)
218
310
  props.put(kafka::RECONNECT_BACKOFF_MS_CONFIG, reconnect_backoff_ms) unless reconnect_backoff_ms.nil?
219
311
  props.put(kafka::REQUEST_TIMEOUT_MS_CONFIG, request_timeout_ms) unless request_timeout_ms.nil?
220
- props.put(kafka::RETRIES_CONFIG, retries.to_s)
221
- props.put(kafka::RETRY_BACKOFF_MS_CONFIG, retry_backoff_ms.to_s)
312
+ props.put(kafka::RETRIES_CONFIG, retries.to_s) unless retries.nil?
313
+ props.put(kafka::RETRY_BACKOFF_MS_CONFIG, retry_backoff_ms.to_s)
222
314
  props.put(kafka::SEND_BUFFER_CONFIG, send_buffer_bytes.to_s)
223
315
  props.put(kafka::VALUE_SERIALIZER_CLASS_CONFIG, value_serializer)
224
316
 
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
 
3
3
  s.name = 'logstash-output-kafka'
4
- s.version = '7.0.1'
4
+ s.version = '7.0.3'
5
5
  s.licenses = ['Apache License (2.0)']
6
6
  s.summary = 'Output events to a Kafka topic. This uses the Kafka Producer API to write messages to a topic on the broker'
7
7
  s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
@@ -157,7 +157,7 @@ describe "outputs/kafka", :integration => true do
157
157
  def load_kafka_data(config)
158
158
  kafka = LogStash::Outputs::Kafka.new(config)
159
159
  kafka.register
160
- num_events.times do kafka.receive(event) end
160
+ kafka.multi_receive(num_events.times.collect { event })
161
161
  kafka.close
162
162
  end
163
163
 
@@ -25,34 +25,118 @@ describe "outputs/kafka" do
25
25
  context 'when outputting messages' do
26
26
  it 'should send logstash event to kafka broker' do
27
27
  expect_any_instance_of(org.apache.kafka.clients.producer.KafkaProducer).to receive(:send)
28
- .with(an_instance_of(org.apache.kafka.clients.producer.ProducerRecord))
28
+ .with(an_instance_of(org.apache.kafka.clients.producer.ProducerRecord)).and_call_original
29
29
  kafka = LogStash::Outputs::Kafka.new(simple_kafka_config)
30
30
  kafka.register
31
- kafka.receive(event)
31
+ kafka.multi_receive([event])
32
32
  end
33
33
 
34
34
  it 'should support Event#sprintf placeholders in topic_id' do
35
35
  topic_field = 'topic_name'
36
36
  expect(org.apache.kafka.clients.producer.ProducerRecord).to receive(:new)
37
- .with("my_topic", event.to_s)
38
- expect_any_instance_of(org.apache.kafka.clients.producer.KafkaProducer).to receive(:send)
37
+ .with("my_topic", event.to_s).and_call_original
38
+ expect_any_instance_of(org.apache.kafka.clients.producer.KafkaProducer).to receive(:send).and_call_original
39
39
  kafka = LogStash::Outputs::Kafka.new({'topic_id' => "%{#{topic_field}}"})
40
40
  kafka.register
41
- kafka.receive(event)
41
+ kafka.multi_receive([event])
42
42
  end
43
43
 
44
44
  it 'should support field referenced message_keys' do
45
45
  expect(org.apache.kafka.clients.producer.ProducerRecord).to receive(:new)
46
- .with("test", "172.0.0.1", event.to_s)
47
- expect_any_instance_of(org.apache.kafka.clients.producer.KafkaProducer).to receive(:send)
46
+ .with("test", "172.0.0.1", event.to_s).and_call_original
47
+ expect_any_instance_of(org.apache.kafka.clients.producer.KafkaProducer).to receive(:send).and_call_original
48
48
  kafka = LogStash::Outputs::Kafka.new(simple_kafka_config.merge({"message_key" => "%{host}"}))
49
49
  kafka.register
50
- kafka.receive(event)
50
+ kafka.multi_receive([event])
51
51
  end
52
52
 
53
53
  it 'should raise config error when truststore location is not set and ssl is enabled' do
54
- kafka = LogStash::Outputs::Kafka.new(simple_kafka_config.merge({"ssl" => "true"}))
54
+ kafka = LogStash::Outputs::Kafka.new(simple_kafka_config.merge("security_protocol" => "SSL"))
55
55
  expect { kafka.register }.to raise_error(LogStash::ConfigurationError, /ssl_truststore_location must be set when SSL is enabled/)
56
56
  end
57
57
  end
58
+
59
+ context "when KafkaProducer#send() raises an exception" do
60
+ let(:failcount) { (rand * 10).to_i }
61
+ let(:sendcount) { failcount + 1 }
62
+
63
+ let(:exception_classes) { [
64
+ org.apache.kafka.common.errors.TimeoutException,
65
+ org.apache.kafka.common.errors.InterruptException,
66
+ org.apache.kafka.common.errors.SerializationException
67
+ ] }
68
+
69
+ before do
70
+ count = 0
71
+ expect_any_instance_of(org.apache.kafka.clients.producer.KafkaProducer).to receive(:send)
72
+ .exactly(sendcount).times
73
+ .and_wrap_original do |m, *args|
74
+ if count < failcount # fail 'failcount' times in a row.
75
+ count += 1
76
+ # Pick an exception at random
77
+ raise exception_classes.shuffle.first.new("injected exception for testing")
78
+ else
79
+ m.call(*args) # call original
80
+ end
81
+ end
82
+ end
83
+
84
+ it "should retry until successful" do
85
+ kafka = LogStash::Outputs::Kafka.new(simple_kafka_config)
86
+ kafka.register
87
+ kafka.multi_receive([event])
88
+ end
89
+ end
90
+
91
+ context "when a send fails" do
92
+ context "and the default retries behavior is used" do
93
+ # Fail this many times and then finally succeed.
94
+ let(:failcount) { (rand * 10).to_i }
95
+
96
+ # Expect KafkaProducer.send() to get called again after every failure, plus the successful one.
97
+ let(:sendcount) { failcount + 1 }
98
+
99
+ it "should retry until successful" do
100
+ count = 0;
101
+
102
+ expect_any_instance_of(org.apache.kafka.clients.producer.KafkaProducer).to receive(:send)
103
+ .exactly(sendcount).times
104
+ .and_wrap_original do |m, *args|
105
+ if count < failcount
106
+ count += 1
107
+ # inject some failures.
108
+
109
+ # Return a custom Future that will raise an exception to simulate a Kafka send() problem.
110
+ future = java.util.concurrent.FutureTask.new { raise "Failed" }
111
+ future.run
112
+ future
113
+ else
114
+ m.call(*args)
115
+ end
116
+ end
117
+ kafka = LogStash::Outputs::Kafka.new(simple_kafka_config)
118
+ kafka.register
119
+ kafka.multi_receive([event])
120
+ end
121
+ end
122
+
123
+ context "and when retries is set by the user" do
124
+ let(:retries) { (rand * 10).to_i }
125
+ let(:max_sends) { retries + 1 }
126
+
127
+ it "should give up after retries are exhausted" do
128
+ expect_any_instance_of(org.apache.kafka.clients.producer.KafkaProducer).to receive(:send)
129
+ .at_most(max_sends).times
130
+ .and_wrap_original do |m, *args|
131
+ # Always fail.
132
+ future = java.util.concurrent.FutureTask.new { raise "Failed" }
133
+ future.run
134
+ future
135
+ end
136
+ kafka = LogStash::Outputs::Kafka.new(simple_kafka_config.merge("retries" => retries))
137
+ kafka.register
138
+ kafka.multi_receive([event])
139
+ end
140
+ end
141
+ end
58
142
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-output-kafka
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.1
4
+ version: 7.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elasticsearch
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-08-16 00:00:00.000000000 Z
11
+ date: 2017-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement