logstash-output-kafka 7.0.1 → 7.0.3

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