ruby-kafka 0.7.4 → 1.1.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +168 -3
  3. data/.github/workflows/stale.yml +19 -0
  4. data/CHANGELOG.md +48 -0
  5. data/README.md +59 -0
  6. data/lib/kafka/async_producer.rb +30 -9
  7. data/lib/kafka/broker.rb +13 -1
  8. data/lib/kafka/broker_pool.rb +1 -1
  9. data/lib/kafka/client.rb +63 -6
  10. data/lib/kafka/cluster.rb +53 -1
  11. data/lib/kafka/compression.rb +13 -11
  12. data/lib/kafka/compressor.rb +1 -0
  13. data/lib/kafka/connection.rb +7 -1
  14. data/lib/kafka/connection_builder.rb +1 -1
  15. data/lib/kafka/consumer.rb +98 -17
  16. data/lib/kafka/consumer_group.rb +20 -2
  17. data/lib/kafka/datadog.rb +32 -12
  18. data/lib/kafka/fetch_operation.rb +1 -1
  19. data/lib/kafka/fetched_batch.rb +5 -1
  20. data/lib/kafka/fetched_batch_generator.rb +5 -2
  21. data/lib/kafka/fetched_message.rb +1 -0
  22. data/lib/kafka/fetched_offset_resolver.rb +1 -1
  23. data/lib/kafka/fetcher.rb +13 -6
  24. data/lib/kafka/gzip_codec.rb +4 -0
  25. data/lib/kafka/heartbeat.rb +8 -3
  26. data/lib/kafka/lz4_codec.rb +4 -0
  27. data/lib/kafka/offset_manager.rb +13 -2
  28. data/lib/kafka/produce_operation.rb +1 -1
  29. data/lib/kafka/producer.rb +33 -8
  30. data/lib/kafka/prometheus.rb +316 -0
  31. data/lib/kafka/protocol/add_offsets_to_txn_request.rb +29 -0
  32. data/lib/kafka/protocol/add_offsets_to_txn_response.rb +19 -0
  33. data/lib/kafka/protocol/join_group_request.rb +8 -2
  34. data/lib/kafka/protocol/metadata_response.rb +1 -1
  35. data/lib/kafka/protocol/offset_fetch_request.rb +3 -1
  36. data/lib/kafka/protocol/produce_request.rb +3 -1
  37. data/lib/kafka/protocol/record_batch.rb +7 -4
  38. data/lib/kafka/protocol/sasl_handshake_request.rb +1 -1
  39. data/lib/kafka/protocol/txn_offset_commit_request.rb +46 -0
  40. data/lib/kafka/protocol/txn_offset_commit_response.rb +18 -0
  41. data/lib/kafka/protocol.rb +8 -0
  42. data/lib/kafka/round_robin_assignment_strategy.rb +10 -7
  43. data/lib/kafka/sasl/gssapi.rb +1 -1
  44. data/lib/kafka/sasl/oauth.rb +64 -0
  45. data/lib/kafka/sasl/plain.rb +1 -1
  46. data/lib/kafka/sasl/scram.rb +16 -13
  47. data/lib/kafka/sasl_authenticator.rb +10 -3
  48. data/lib/kafka/snappy_codec.rb +4 -0
  49. data/lib/kafka/ssl_context.rb +5 -1
  50. data/lib/kafka/ssl_socket_with_timeout.rb +1 -0
  51. data/lib/kafka/statsd.rb +10 -1
  52. data/lib/kafka/tagged_logger.rb +77 -0
  53. data/lib/kafka/transaction_manager.rb +26 -1
  54. data/lib/kafka/transaction_state_machine.rb +1 -1
  55. data/lib/kafka/version.rb +1 -1
  56. data/lib/kafka/zstd_codec.rb +27 -0
  57. data/lib/kafka.rb +4 -0
  58. data/ruby-kafka.gemspec +5 -3
  59. metadata +50 -7
data/lib/kafka/cluster.rb CHANGED
@@ -23,7 +23,7 @@ module Kafka
23
23
  raise ArgumentError, "At least one seed broker must be configured"
24
24
  end
25
25
 
26
- @logger = logger
26
+ @logger = TaggedLogger.new(logger)
27
27
  @seed_brokers = seed_brokers
28
28
  @broker_pool = broker_pool
29
29
  @cluster_info = nil
@@ -45,6 +45,10 @@ module Kafka
45
45
  new_topics = topics - @target_topics
46
46
 
47
47
  unless new_topics.empty?
48
+ if new_topics.any? { |topic| topic.nil? or topic.empty? }
49
+ raise ArgumentError, "Topic must not be nil or empty"
50
+ end
51
+
48
52
  @logger.info "New topics added to target list: #{new_topics.to_a.join(', ')}"
49
53
 
50
54
  @target_topics.merge(new_topics)
@@ -139,6 +143,40 @@ module Kafka
139
143
  end
140
144
  end
141
145
 
146
+ def describe_configs(broker_id, configs = [])
147
+ options = {
148
+ resources: [[Kafka::Protocol::RESOURCE_TYPE_CLUSTER, broker_id.to_s, configs]]
149
+ }
150
+
151
+ info = cluster_info.brokers.find {|broker| broker.node_id == broker_id }
152
+ broker = @broker_pool.connect(info.host, info.port, node_id: info.node_id)
153
+
154
+ response = broker.describe_configs(**options)
155
+
156
+ response.resources.each do |resource|
157
+ Protocol.handle_error(resource.error_code, resource.error_message)
158
+ end
159
+
160
+ response.resources.first.configs
161
+ end
162
+
163
+ def alter_configs(broker_id, configs = [])
164
+ options = {
165
+ resources: [[Kafka::Protocol::RESOURCE_TYPE_CLUSTER, broker_id.to_s, configs]]
166
+ }
167
+
168
+ info = cluster_info.brokers.find {|broker| broker.node_id == broker_id }
169
+ broker = @broker_pool.connect(info.host, info.port, node_id: info.node_id)
170
+
171
+ response = broker.alter_configs(**options)
172
+
173
+ response.resources.each do |resource|
174
+ Protocol.handle_error(resource.error_code, resource.error_message)
175
+ end
176
+
177
+ nil
178
+ end
179
+
142
180
  def partitions_for(topic)
143
181
  add_target_topics([topic])
144
182
  refresh_metadata_if_necessary!
@@ -252,6 +290,20 @@ module Kafka
252
290
  group
253
291
  end
254
292
 
293
+ def fetch_group_offsets(group_id)
294
+ topics = get_group_coordinator(group_id: group_id)
295
+ .fetch_offsets(group_id: group_id, topics: nil)
296
+ .topics
297
+
298
+ topics.each do |_, partitions|
299
+ partitions.each do |_, response|
300
+ Protocol.handle_error(response.error_code)
301
+ end
302
+ end
303
+
304
+ topics
305
+ end
306
+
255
307
  def create_partitions_for(name, num_partitions:, timeout:)
256
308
  options = {
257
309
  topics: [[name, num_partitions, nil]],
@@ -3,27 +3,27 @@
3
3
  require "kafka/snappy_codec"
4
4
  require "kafka/gzip_codec"
5
5
  require "kafka/lz4_codec"
6
+ require "kafka/zstd_codec"
6
7
 
7
8
  module Kafka
8
9
  module Compression
9
- CODEC_NAMES = {
10
- 1 => :gzip,
11
- 2 => :snappy,
12
- 3 => :lz4,
13
- }.freeze
14
-
15
- CODECS = {
10
+ CODECS_BY_NAME = {
16
11
  :gzip => GzipCodec.new,
17
12
  :snappy => SnappyCodec.new,
18
13
  :lz4 => LZ4Codec.new,
14
+ :zstd => ZstdCodec.new,
19
15
  }.freeze
20
16
 
17
+ CODECS_BY_ID = CODECS_BY_NAME.each_with_object({}) do |(_, codec), hash|
18
+ hash[codec.codec_id] = codec
19
+ end.freeze
20
+
21
21
  def self.codecs
22
- CODECS.keys
22
+ CODECS_BY_NAME.keys
23
23
  end
24
24
 
25
25
  def self.find_codec(name)
26
- codec = CODECS.fetch(name) do
26
+ codec = CODECS_BY_NAME.fetch(name) do
27
27
  raise "Unknown compression codec #{name}"
28
28
  end
29
29
 
@@ -33,11 +33,13 @@ module Kafka
33
33
  end
34
34
 
35
35
  def self.find_codec_by_id(codec_id)
36
- codec_name = CODEC_NAMES.fetch(codec_id) do
36
+ codec = CODECS_BY_ID.fetch(codec_id) do
37
37
  raise "Unknown codec id #{codec_id}"
38
38
  end
39
39
 
40
- find_codec(codec_name)
40
+ codec.load
41
+
42
+ codec
41
43
  end
42
44
  end
43
45
  end
@@ -18,6 +18,7 @@ module Kafka
18
18
  # * `compressed_bytesize` – the byte size of the compressed data.
19
19
  #
20
20
  class Compressor
21
+ attr_reader :codec
21
22
 
22
23
  # @param codec_name [Symbol, nil]
23
24
  # @param threshold [Integer] the minimum number of messages in a message set
@@ -52,12 +52,15 @@ module Kafka
52
52
  # @return [Connection] a new connection.
53
53
  def initialize(host:, port:, client_id:, logger:, instrumenter:, connect_timeout: nil, socket_timeout: nil, ssl_context: nil)
54
54
  @host, @port, @client_id = host, port, client_id
55
- @logger = logger
55
+ @logger = TaggedLogger.new(logger)
56
56
  @instrumenter = instrumenter
57
57
 
58
58
  @connect_timeout = connect_timeout || CONNECT_TIMEOUT
59
59
  @socket_timeout = socket_timeout || SOCKET_TIMEOUT
60
60
  @ssl_context = ssl_context
61
+
62
+ @socket = nil
63
+ @last_request = nil
61
64
  end
62
65
 
63
66
  def to_s
@@ -93,6 +96,7 @@ module Kafka
93
96
 
94
97
  raise IdleConnection if idle?
95
98
 
99
+ @logger.push_tags(api_name)
96
100
  @instrumenter.instrument("request.connection", notification) do
97
101
  open unless open?
98
102
 
@@ -113,6 +117,8 @@ module Kafka
113
117
  close
114
118
 
115
119
  raise ConnectionError, "Connection error #{e.class}: #{e}"
120
+ ensure
121
+ @logger.pop_tags
116
122
  end
117
123
 
118
124
  private
@@ -4,7 +4,7 @@ module Kafka
4
4
  class ConnectionBuilder
5
5
  def initialize(client_id:, logger:, instrumenter:, connect_timeout:, socket_timeout:, ssl_context:, sasl_authenticator:)
6
6
  @client_id = client_id
7
- @logger = logger
7
+ @logger = TaggedLogger.new(logger)
8
8
  @instrumenter = instrumenter
9
9
  @connect_timeout = connect_timeout
10
10
  @socket_timeout = socket_timeout
@@ -44,15 +44,16 @@ module Kafka
44
44
  #
45
45
  class Consumer
46
46
 
47
- def initialize(cluster:, logger:, instrumenter:, group:, fetcher:, offset_manager:, session_timeout:, heartbeat:)
47
+ def initialize(cluster:, logger:, instrumenter:, group:, fetcher:, offset_manager:, session_timeout:, heartbeat:, refresh_topic_interval: 0)
48
48
  @cluster = cluster
49
- @logger = logger
49
+ @logger = TaggedLogger.new(logger)
50
50
  @instrumenter = instrumenter
51
51
  @group = group
52
52
  @offset_manager = offset_manager
53
53
  @session_timeout = session_timeout
54
54
  @fetcher = fetcher
55
55
  @heartbeat = heartbeat
56
+ @refresh_topic_interval = refresh_topic_interval
56
57
 
57
58
  @pauses = Hash.new {|h, k|
58
59
  h[k] = Hash.new {|h2, k2|
@@ -73,6 +74,15 @@ module Kafka
73
74
  # when user commits message other than last in a batch, this would make ruby-kafka refetch
74
75
  # some already consumed messages
75
76
  @current_offsets = Hash.new { |h, k| h[k] = {} }
77
+
78
+ # Map storing subscribed topics with their configuration
79
+ @subscribed_topics = Concurrent::Map.new
80
+
81
+ # Set storing topics that matched topics in @subscribed_topics
82
+ @matched_topics = Set.new
83
+
84
+ # Whether join_group must be executed again because new topics are added
85
+ @join_group_for_new_topics = false
76
86
  end
77
87
 
78
88
  # Subscribes the consumer to a topic.
@@ -82,7 +92,8 @@ module Kafka
82
92
  # messages to be written. In the former case, set `start_from_beginning`
83
93
  # to true (the default); in the latter, set it to false.
84
94
  #
85
- # @param topic [String] the name of the topic to subscribe to.
95
+ # @param topic_or_regex [String, Regexp] subscribe to single topic with a string
96
+ # or multiple topics matching a regex.
86
97
  # @param default_offset [Symbol] whether to start from the beginning or the
87
98
  # end of the topic's partitions. Deprecated.
88
99
  # @param start_from_beginning [Boolean] whether to start from the beginning
@@ -93,12 +104,15 @@ module Kafka
93
104
  # @param max_bytes_per_partition [Integer] the maximum amount of data fetched
94
105
  # from a single partition at a time.
95
106
  # @return [nil]
96
- def subscribe(topic, default_offset: nil, start_from_beginning: true, max_bytes_per_partition: 1048576)
107
+ def subscribe(topic_or_regex, default_offset: nil, start_from_beginning: true, max_bytes_per_partition: 1048576)
97
108
  default_offset ||= start_from_beginning ? :earliest : :latest
98
109
 
99
- @group.subscribe(topic)
100
- @offset_manager.set_default_offset(topic, default_offset)
101
- @fetcher.subscribe(topic, max_bytes_per_partition: max_bytes_per_partition)
110
+ @subscribed_topics[topic_or_regex] = {
111
+ default_offset: default_offset,
112
+ start_from_beginning: start_from_beginning,
113
+ max_bytes_per_partition: max_bytes_per_partition
114
+ }
115
+ scan_for_subscribing
102
116
 
103
117
  nil
104
118
  end
@@ -111,7 +125,6 @@ module Kafka
111
125
  def stop
112
126
  @running = false
113
127
  @fetcher.stop
114
- @cluster.disconnect
115
128
  end
116
129
 
117
130
  # Pause processing of a specific topic partition.
@@ -241,7 +254,7 @@ module Kafka
241
254
 
242
255
  trigger_heartbeat
243
256
 
244
- return if !@running
257
+ return if shutting_down?
245
258
  end
246
259
 
247
260
  # We've successfully processed a batch from the partition, so we can clear
@@ -280,6 +293,9 @@ module Kafka
280
293
  # without an exception. Once marked successful, the offsets of processed
281
294
  # messages can be committed to Kafka.
282
295
  # @yieldparam batch [Kafka::FetchedBatch] a message batch fetched from Kafka.
296
+ # @raise [Kafka::ProcessingError] if there was an error processing a batch.
297
+ # The original exception will be returned by calling `#cause` on the
298
+ # {Kafka::ProcessingError} instance.
283
299
  # @return [nil]
284
300
  def each_batch(min_bytes: 1, max_bytes: 10485760, max_wait_time: 1, automatically_mark_as_processed: true)
285
301
  @fetcher.configure(
@@ -300,6 +316,7 @@ module Kafka
300
316
  topic: batch.topic,
301
317
  partition: batch.partition,
302
318
  last_offset: batch.last_offset,
319
+ last_create_time: batch.messages.last.try(:create_time),
303
320
  offset_lag: batch.offset_lag,
304
321
  highwater_mark_offset: batch.highwater_mark_offset,
305
322
  message_count: batch.messages.count,
@@ -336,7 +353,7 @@ module Kafka
336
353
 
337
354
  trigger_heartbeat
338
355
 
339
- return if !@running
356
+ return if shutting_down?
340
357
  end
341
358
 
342
359
  # We may not have received any messages, but it's still a good idea to
@@ -386,22 +403,24 @@ module Kafka
386
403
 
387
404
  def consumer_loop
388
405
  @running = true
406
+ @logger.push_tags(@group.to_s)
389
407
 
390
408
  @fetcher.start
391
409
 
392
- while @running
410
+ while running?
393
411
  begin
394
412
  @instrumenter.instrument("loop.consumer") do
413
+ refresh_topic_list_if_enabled
395
414
  yield
396
415
  end
397
416
  rescue HeartbeatError
398
417
  make_final_offsets_commit!
399
- join_group
418
+ join_group if running?
400
419
  rescue OffsetCommitError
401
- join_group
420
+ join_group if running?
402
421
  rescue RebalanceInProgress
403
422
  @logger.warn "Group rebalance in progress, re-joining..."
404
- join_group
423
+ join_group if running?
405
424
  rescue FetchError, NotLeaderForPartition, UnknownTopicOrPartition
406
425
  @cluster.mark_as_stale!
407
426
  rescue LeaderNotAvailable => e
@@ -423,7 +442,9 @@ module Kafka
423
442
  # important that members explicitly tell Kafka when they're leaving.
424
443
  make_final_offsets_commit!
425
444
  @group.leave rescue nil
445
+ @cluster.disconnect
426
446
  @running = false
447
+ @logger.pop_tags
427
448
  end
428
449
 
429
450
  def make_final_offsets_commit!(attempts = 3)
@@ -442,6 +463,8 @@ module Kafka
442
463
  end
443
464
 
444
465
  def join_group
466
+ @join_group_for_new_topics = false
467
+
445
468
  old_generation_id = @group.generation_id
446
469
 
447
470
  @group.join
@@ -503,11 +526,19 @@ module Kafka
503
526
  end
504
527
  end
505
528
 
529
+ def refresh_topic_list_if_enabled
530
+ return if @refresh_topic_interval <= 0
531
+ return if @refreshed_at && @refreshed_at + @refresh_topic_interval > Time.now
532
+
533
+ scan_for_subscribing
534
+ @refreshed_at = Time.now
535
+ end
536
+
506
537
  def fetch_batches
507
538
  # Return early if the consumer has been stopped.
508
- return [] if !@running
539
+ return [] if shutting_down?
509
540
 
510
- join_group unless @group.member?
541
+ join_group if !@group.member? || @join_group_for_new_topics
511
542
 
512
543
  trigger_heartbeat
513
544
 
@@ -515,7 +546,7 @@ module Kafka
515
546
 
516
547
  if !@fetcher.data?
517
548
  @logger.debug "No batches to process"
518
- sleep 2
549
+ sleep(@fetcher.max_wait_time || 2)
519
550
  []
520
551
  else
521
552
  tag, message = @fetcher.poll
@@ -545,6 +576,14 @@ module Kafka
545
576
  @pauses[topic][partition]
546
577
  end
547
578
 
579
+ def running?
580
+ @running
581
+ end
582
+
583
+ def shutting_down?
584
+ !running?
585
+ end
586
+
548
587
  def clear_current_offsets(excluding: {})
549
588
  @current_offsets.each do |topic, partitions|
550
589
  partitions.keep_if do |partition, _|
@@ -552,5 +591,47 @@ module Kafka
552
591
  end
553
592
  end
554
593
  end
594
+
595
+ def scan_for_subscribing
596
+ @subscribed_topics.each do |topic_or_regex, config|
597
+ default_offset = config.fetch(:default_offset)
598
+ start_from_beginning = config.fetch(:start_from_beginning)
599
+ max_bytes_per_partition = config.fetch(:max_bytes_per_partition)
600
+ if topic_or_regex.is_a?(Regexp)
601
+ subscribe_to_regex(topic_or_regex, default_offset, start_from_beginning, max_bytes_per_partition)
602
+ else
603
+ subscribe_to_topic(topic_or_regex, default_offset, start_from_beginning, max_bytes_per_partition)
604
+ end
605
+ end
606
+ end
607
+
608
+ def subscribe_to_regex(topic_regex, default_offset, start_from_beginning, max_bytes_per_partition)
609
+ cluster_topics.select { |topic| topic =~ topic_regex }.each do |topic|
610
+ subscribe_to_topic(topic, default_offset, start_from_beginning, max_bytes_per_partition)
611
+ end
612
+ end
613
+
614
+ def subscribe_to_topic(topic, default_offset, start_from_beginning, max_bytes_per_partition)
615
+ return if @matched_topics.include?(topic)
616
+ @matched_topics.add(topic)
617
+ @join_group_for_new_topics = true
618
+
619
+ @group.subscribe(topic)
620
+ @offset_manager.set_default_offset(topic, default_offset)
621
+ @fetcher.subscribe(topic, max_bytes_per_partition: max_bytes_per_partition)
622
+ @cluster.mark_as_stale!
623
+ end
624
+
625
+ def cluster_topics
626
+ attempts = 0
627
+ begin
628
+ attempts += 1
629
+ @cluster.list_topics
630
+ rescue Kafka::ConnectionError
631
+ @cluster.mark_as_stale!
632
+ retry unless attempts > 1
633
+ raise
634
+ end
635
+ end
555
636
  end
556
637
  end
@@ -7,11 +7,12 @@ module Kafka
7
7
  class ConsumerGroup
8
8
  attr_reader :assigned_partitions, :generation_id, :group_id
9
9
 
10
- def initialize(cluster:, logger:, group_id:, session_timeout:, retention_time:, instrumenter:)
10
+ def initialize(cluster:, logger:, group_id:, session_timeout:, rebalance_timeout:, retention_time:, instrumenter:)
11
11
  @cluster = cluster
12
- @logger = logger
12
+ @logger = TaggedLogger.new(logger)
13
13
  @group_id = group_id
14
14
  @session_timeout = session_timeout
15
+ @rebalance_timeout = rebalance_timeout
15
16
  @instrumenter = instrumenter
16
17
  @member_id = ""
17
18
  @generation_id = nil
@@ -122,6 +123,15 @@ module Kafka
122
123
  retry
123
124
  end
124
125
 
126
+ def to_s
127
+ "[#{@group_id}] {" + assigned_partitions.map { |topic, partitions|
128
+ partition_str = partitions.size > 5 ?
129
+ "#{partitions[0..4].join(', ')}..." :
130
+ partitions.join(', ')
131
+ "#{topic}: #{partition_str}"
132
+ }.join('; ') + '}:'
133
+ end
134
+
125
135
  private
126
136
 
127
137
  def join_group
@@ -131,7 +141,9 @@ module Kafka
131
141
  response = coordinator.join_group(
132
142
  group_id: @group_id,
133
143
  session_timeout: @session_timeout,
144
+ rebalance_timeout: @rebalance_timeout,
134
145
  member_id: @member_id,
146
+ topics: @topics,
135
147
  )
136
148
 
137
149
  Protocol.handle_error(response.error_code)
@@ -149,6 +161,12 @@ module Kafka
149
161
  @member_id = ""
150
162
  sleep 1
151
163
 
164
+ retry
165
+ rescue CoordinatorLoadInProgress
166
+ @logger.error "Coordinator broker still loading, retrying in 1s..."
167
+
168
+ sleep 1
169
+
152
170
  retry
153
171
  end
154
172
 
data/lib/kafka/datadog.rb CHANGED
@@ -31,7 +31,7 @@ module Kafka
31
31
 
32
32
  class << self
33
33
  def statsd
34
- @statsd ||= ::Datadog::Statsd.new(host, port, namespace: namespace, tags: tags)
34
+ @statsd ||= ::Datadog::Statsd.new(host, port, namespace: namespace, tags: tags, socket_path: socket_path)
35
35
  end
36
36
 
37
37
  def statsd=(statsd)
@@ -40,7 +40,7 @@ module Kafka
40
40
  end
41
41
 
42
42
  def host
43
- @host ||= default_host
43
+ @host
44
44
  end
45
45
 
46
46
  def host=(host)
@@ -49,7 +49,7 @@ module Kafka
49
49
  end
50
50
 
51
51
  def port
52
- @port ||= default_port
52
+ @port
53
53
  end
54
54
 
55
55
  def port=(port)
@@ -57,6 +57,15 @@ module Kafka
57
57
  clear
58
58
  end
59
59
 
60
+ def socket_path
61
+ @socket_path
62
+ end
63
+
64
+ def socket_path=(socket_path)
65
+ @socket_path = socket_path
66
+ clear
67
+ end
68
+
60
69
  def namespace
61
70
  @namespace ||= STATSD_NAMESPACE
62
71
  end
@@ -77,14 +86,6 @@ module Kafka
77
86
 
78
87
  private
79
88
 
80
- def default_host
81
- ::Datadog::Statsd.const_defined?(:Connection) ? ::Datadog::Statsd::Connection::DEFAULT_HOST : ::Datadog::Statsd::DEFAULT_HOST
82
- end
83
-
84
- def default_port
85
- ::Datadog::Statsd.const_defined?(:Connection) ? ::Datadog::Statsd::Connection::DEFAULT_PORT : ::Datadog::Statsd::DEFAULT_PORT
86
- end
87
-
88
89
  def clear
89
90
  @statsd && @statsd.close
90
91
  @statsd = nil
@@ -167,8 +168,9 @@ module Kafka
167
168
 
168
169
  def process_batch(event)
169
170
  offset = event.payload.fetch(:last_offset)
170
- lag = event.payload.fetch(:offset_lag)
171
171
  messages = event.payload.fetch(:message_count)
172
+ create_time = event.payload.fetch(:last_create_time)
173
+ time_lag = create_time && ((Time.now - create_time) * 1000).to_i
172
174
 
173
175
  tags = {
174
176
  client: event.payload.fetch(:client_id),
@@ -185,6 +187,24 @@ module Kafka
185
187
  end
186
188
 
187
189
  gauge("consumer.offset", offset, tags: tags)
190
+
191
+ if time_lag
192
+ gauge("consumer.time_lag", time_lag, tags: tags)
193
+ end
194
+ end
195
+
196
+ def fetch_batch(event)
197
+ lag = event.payload.fetch(:offset_lag)
198
+ batch_size = event.payload.fetch(:message_count)
199
+
200
+ tags = {
201
+ client: event.payload.fetch(:client_id),
202
+ group_id: event.payload.fetch(:group_id),
203
+ topic: event.payload.fetch(:topic),
204
+ partition: event.payload.fetch(:partition),
205
+ }
206
+
207
+ histogram("consumer.batch_size", batch_size, tags: tags)
188
208
  gauge("consumer.lag", lag, tags: tags)
189
209
  end
190
210
 
@@ -23,7 +23,7 @@ module Kafka
23
23
  class FetchOperation
24
24
  def initialize(cluster:, logger:, min_bytes: 1, max_bytes: 10485760, max_wait_time: 5)
25
25
  @cluster = cluster
26
- @logger = logger
26
+ @logger = TaggedLogger.new(logger)
27
27
  @min_bytes = min_bytes
28
28
  @max_bytes = max_bytes
29
29
  @max_wait_time = max_wait_time
@@ -13,18 +13,22 @@ module Kafka
13
13
  # @return [Integer]
14
14
  attr_reader :last_offset
15
15
 
16
+ # @return [Integer]
17
+ attr_reader :leader_epoch
18
+
16
19
  # @return [Integer] the offset of the most recent message in the partition.
17
20
  attr_reader :highwater_mark_offset
18
21
 
19
22
  # @return [Array<Kafka::FetchedMessage>]
20
23
  attr_accessor :messages
21
24
 
22
- def initialize(topic:, partition:, highwater_mark_offset:, messages:, last_offset: nil)
25
+ def initialize(topic:, partition:, highwater_mark_offset:, messages:, last_offset: nil, leader_epoch: nil)
23
26
  @topic = topic
24
27
  @partition = partition
25
28
  @highwater_mark_offset = highwater_mark_offset
26
29
  @messages = messages
27
30
  @last_offset = last_offset
31
+ @leader_epoch = leader_epoch
28
32
  end
29
33
 
30
34
  def empty?
@@ -10,7 +10,7 @@ module Kafka
10
10
  def initialize(topic, fetched_partition, offset, logger:)
11
11
  @topic = topic
12
12
  @fetched_partition = fetched_partition
13
- @logger = logger
13
+ @logger = TaggedLogger.new(logger)
14
14
  @offset = offset
15
15
  end
16
16
 
@@ -48,7 +48,7 @@ module Kafka
48
48
  partition: @fetched_partition.partition
49
49
  )
50
50
  end
51
- end
51
+ end.compact
52
52
  end
53
53
  FetchedBatch.new(
54
54
  topic: @topic,
@@ -62,11 +62,13 @@ module Kafka
62
62
  def extract_records
63
63
  records = []
64
64
  last_offset = nil
65
+ leader_epoch = nil
65
66
  aborted_transactions = @fetched_partition.aborted_transactions.sort_by(&:first_offset)
66
67
  aborted_producer_ids = {}
67
68
 
68
69
  @fetched_partition.messages.each do |record_batch|
69
70
  last_offset = record_batch.last_offset if last_offset.nil? || last_offset < record_batch.last_offset
71
+ leader_epoch = record_batch.partition_leader_epoch if leader_epoch.nil? || leader_epoch < record_batch.partition_leader_epoch
70
72
  # Find the list of aborted producer IDs less than current offset
71
73
  unless aborted_transactions.empty?
72
74
  if aborted_transactions.first.first_offset <= record_batch.last_offset
@@ -99,6 +101,7 @@ module Kafka
99
101
  topic: @topic,
100
102
  partition: @fetched_partition.partition,
101
103
  last_offset: last_offset,
104
+ leader_epoch: leader_epoch,
102
105
  highwater_mark_offset: @fetched_partition.highwater_mark_offset,
103
106
  messages: records
104
107
  )
@@ -43,5 +43,6 @@ module Kafka
43
43
  def is_control_record
44
44
  @message.is_control_record
45
45
  end
46
+
46
47
  end
47
48
  end
@@ -3,7 +3,7 @@
3
3
  module Kafka
4
4
  class FetchedOffsetResolver
5
5
  def initialize(logger:)
6
- @logger = logger
6
+ @logger = TaggedLogger.new(logger)
7
7
  end
8
8
 
9
9
  def resolve!(broker, topics)