ruby-kafka 0.7.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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)