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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +168 -3
- data/.github/workflows/stale.yml +19 -0
- data/CHANGELOG.md +48 -0
- data/README.md +59 -0
- data/lib/kafka/async_producer.rb +30 -9
- data/lib/kafka/broker.rb +13 -1
- data/lib/kafka/broker_pool.rb +1 -1
- data/lib/kafka/client.rb +63 -6
- data/lib/kafka/cluster.rb +53 -1
- data/lib/kafka/compression.rb +13 -11
- data/lib/kafka/compressor.rb +1 -0
- data/lib/kafka/connection.rb +7 -1
- data/lib/kafka/connection_builder.rb +1 -1
- data/lib/kafka/consumer.rb +98 -17
- data/lib/kafka/consumer_group.rb +20 -2
- data/lib/kafka/datadog.rb +32 -12
- data/lib/kafka/fetch_operation.rb +1 -1
- data/lib/kafka/fetched_batch.rb +5 -1
- data/lib/kafka/fetched_batch_generator.rb +5 -2
- data/lib/kafka/fetched_message.rb +1 -0
- data/lib/kafka/fetched_offset_resolver.rb +1 -1
- data/lib/kafka/fetcher.rb +13 -6
- data/lib/kafka/gzip_codec.rb +4 -0
- data/lib/kafka/heartbeat.rb +8 -3
- data/lib/kafka/lz4_codec.rb +4 -0
- data/lib/kafka/offset_manager.rb +13 -2
- data/lib/kafka/produce_operation.rb +1 -1
- data/lib/kafka/producer.rb +33 -8
- data/lib/kafka/prometheus.rb +316 -0
- data/lib/kafka/protocol/add_offsets_to_txn_request.rb +29 -0
- data/lib/kafka/protocol/add_offsets_to_txn_response.rb +19 -0
- data/lib/kafka/protocol/join_group_request.rb +8 -2
- data/lib/kafka/protocol/metadata_response.rb +1 -1
- data/lib/kafka/protocol/offset_fetch_request.rb +3 -1
- data/lib/kafka/protocol/produce_request.rb +3 -1
- data/lib/kafka/protocol/record_batch.rb +7 -4
- data/lib/kafka/protocol/sasl_handshake_request.rb +1 -1
- data/lib/kafka/protocol/txn_offset_commit_request.rb +46 -0
- data/lib/kafka/protocol/txn_offset_commit_response.rb +18 -0
- data/lib/kafka/protocol.rb +8 -0
- data/lib/kafka/round_robin_assignment_strategy.rb +10 -7
- data/lib/kafka/sasl/gssapi.rb +1 -1
- data/lib/kafka/sasl/oauth.rb +64 -0
- data/lib/kafka/sasl/plain.rb +1 -1
- data/lib/kafka/sasl/scram.rb +16 -13
- data/lib/kafka/sasl_authenticator.rb +10 -3
- data/lib/kafka/snappy_codec.rb +4 -0
- data/lib/kafka/ssl_context.rb +5 -1
- data/lib/kafka/ssl_socket_with_timeout.rb +1 -0
- data/lib/kafka/statsd.rb +10 -1
- data/lib/kafka/tagged_logger.rb +77 -0
- data/lib/kafka/transaction_manager.rb +26 -1
- data/lib/kafka/transaction_state_machine.rb +1 -1
- data/lib/kafka/version.rb +1 -1
- data/lib/kafka/zstd_codec.rb +27 -0
- data/lib/kafka.rb +4 -0
- data/ruby-kafka.gemspec +5 -3
- metadata +50 -7
| @@ -27,6 +27,8 @@ module Kafka | |
| 27 27 | 
             
                #         Value => bytes
         | 
| 28 28 | 
             
                #
         | 
| 29 29 | 
             
                class ProduceRequest
         | 
| 30 | 
            +
                  API_MIN_VERSION = 3
         | 
| 31 | 
            +
             | 
| 30 32 | 
             
                  attr_reader :transactional_id, :required_acks, :timeout, :messages_for_topics, :compressor
         | 
| 31 33 |  | 
| 32 34 | 
             
                  # @param required_acks [Integer]
         | 
| @@ -45,7 +47,7 @@ module Kafka | |
| 45 47 | 
             
                  end
         | 
| 46 48 |  | 
| 47 49 | 
             
                  def api_version
         | 
| 48 | 
            -
                     | 
| 50 | 
            +
                    compressor.codec.nil? ? API_MIN_VERSION : [compressor.codec.produce_api_min_version, API_MIN_VERSION].max
         | 
| 49 51 | 
             
                  end
         | 
| 50 52 |  | 
| 51 53 | 
             
                  def response_class
         | 
| @@ -1,3 +1,4 @@ | |
| 1 | 
            +
            require 'bigdecimal'
         | 
| 1 2 | 
             
            require 'digest/crc32'
         | 
| 2 3 | 
             
            require 'kafka/protocol/record'
         | 
| 3 4 |  | 
| @@ -11,6 +12,7 @@ module Kafka | |
| 11 12 | 
             
                  CODEC_ID_MASK = 0b00000111
         | 
| 12 13 | 
             
                  IN_TRANSACTION_MASK = 0b00010000
         | 
| 13 14 | 
             
                  IS_CONTROL_BATCH_MASK = 0b00100000
         | 
| 15 | 
            +
                  TIMESTAMP_TYPE_MASK = 0b001000
         | 
| 14 16 |  | 
| 15 17 | 
             
                  attr_reader :records, :first_offset, :first_timestamp, :partition_leader_epoch, :in_transaction, :is_control_batch, :last_offset_delta, :max_timestamp, :producer_id, :producer_epoch, :first_sequence
         | 
| 16 18 |  | 
| @@ -130,7 +132,7 @@ module Kafka | |
| 130 132 |  | 
| 131 133 | 
             
                    records.each_with_index do |record, index|
         | 
| 132 134 | 
             
                      record.offset_delta = index
         | 
| 133 | 
            -
                      record.timestamp_delta = (record.create_time - first_timestamp).to_i
         | 
| 135 | 
            +
                      record.timestamp_delta = ((record.create_time - first_timestamp) * 1000).to_i
         | 
| 134 136 | 
             
                    end
         | 
| 135 137 | 
             
                    @last_offset_delta = records.length - 1
         | 
| 136 138 | 
             
                  end
         | 
| @@ -163,10 +165,11 @@ module Kafka | |
| 163 165 | 
             
                    codec_id = attributes & CODEC_ID_MASK
         | 
| 164 166 | 
             
                    in_transaction = (attributes & IN_TRANSACTION_MASK) > 0
         | 
| 165 167 | 
             
                    is_control_batch = (attributes & IS_CONTROL_BATCH_MASK) > 0
         | 
| 168 | 
            +
                    log_append_time = (attributes & TIMESTAMP_TYPE_MASK) != 0
         | 
| 166 169 |  | 
| 167 170 | 
             
                    last_offset_delta = record_batch_decoder.int32
         | 
| 168 | 
            -
                    first_timestamp = Time.at(record_batch_decoder.int64 / 1000)
         | 
| 169 | 
            -
                    max_timestamp = Time.at(record_batch_decoder.int64 / 1000)
         | 
| 171 | 
            +
                    first_timestamp = Time.at(record_batch_decoder.int64 / BigDecimal(1000))
         | 
| 172 | 
            +
                    max_timestamp = Time.at(record_batch_decoder.int64 / BigDecimal(1000))
         | 
| 170 173 |  | 
| 171 174 | 
             
                    producer_id = record_batch_decoder.int64
         | 
| 172 175 | 
             
                    producer_epoch = record_batch_decoder.int16
         | 
| @@ -186,7 +189,7 @@ module Kafka | |
| 186 189 | 
             
                    until records_array_decoder.eof?
         | 
| 187 190 | 
             
                      record = Record.decode(records_array_decoder)
         | 
| 188 191 | 
             
                      record.offset = first_offset + record.offset_delta
         | 
| 189 | 
            -
                      record.create_time = first_timestamp + record.timestamp_delta
         | 
| 192 | 
            +
                      record.create_time = log_append_time && max_timestamp ? max_timestamp : first_timestamp + record.timestamp_delta / BigDecimal(1000)
         | 
| 190 193 | 
             
                      records_array << record
         | 
| 191 194 | 
             
                    end
         | 
| 192 195 |  | 
| @@ -8,7 +8,7 @@ module Kafka | |
| 8 8 |  | 
| 9 9 | 
             
                class SaslHandshakeRequest
         | 
| 10 10 |  | 
| 11 | 
            -
                  SUPPORTED_MECHANISMS = %w(GSSAPI PLAIN SCRAM-SHA-256 SCRAM-SHA-512)
         | 
| 11 | 
            +
                  SUPPORTED_MECHANISMS = %w(GSSAPI PLAIN SCRAM-SHA-256 SCRAM-SHA-512 OAUTHBEARER)
         | 
| 12 12 |  | 
| 13 13 | 
             
                  def initialize(mechanism)
         | 
| 14 14 | 
             
                    unless SUPPORTED_MECHANISMS.include?(mechanism)
         | 
| @@ -0,0 +1,46 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Kafka
         | 
| 4 | 
            +
              module Protocol
         | 
| 5 | 
            +
                class TxnOffsetCommitRequest
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  def api_key
         | 
| 8 | 
            +
                    TXN_OFFSET_COMMIT_API
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def api_version
         | 
| 12 | 
            +
                    2
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def response_class
         | 
| 16 | 
            +
                    TxnOffsetCommitResponse
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def initialize(transactional_id:, group_id:, producer_id:, producer_epoch:, offsets:)
         | 
| 20 | 
            +
                    @transactional_id = transactional_id
         | 
| 21 | 
            +
                    @producer_id = producer_id
         | 
| 22 | 
            +
                    @producer_epoch = producer_epoch
         | 
| 23 | 
            +
                    @group_id = group_id
         | 
| 24 | 
            +
                    @offsets = offsets
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def encode(encoder)
         | 
| 28 | 
            +
                    encoder.write_string(@transactional_id.to_s)
         | 
| 29 | 
            +
                    encoder.write_string(@group_id)
         | 
| 30 | 
            +
                    encoder.write_int64(@producer_id)
         | 
| 31 | 
            +
                    encoder.write_int16(@producer_epoch)
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    encoder.write_array(@offsets) do |topic, partitions|
         | 
| 34 | 
            +
                      encoder.write_string(topic)
         | 
| 35 | 
            +
                      encoder.write_array(partitions) do |partition, offset|
         | 
| 36 | 
            +
                        encoder.write_int32(partition)
         | 
| 37 | 
            +
                        encoder.write_int64(offset[:offset])
         | 
| 38 | 
            +
                        encoder.write_string(nil) # metadata
         | 
| 39 | 
            +
                        encoder.write_int32(offset[:leader_epoch])
         | 
| 40 | 
            +
                      end
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
            end
         | 
| @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            module Kafka
         | 
| 2 | 
            +
              module Protocol
         | 
| 3 | 
            +
                class TxnOffsetCommitResponse
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  attr_reader :error_code
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  def initialize(error_code:)
         | 
| 8 | 
            +
                    @error_code = error_code
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def self.decode(decoder)
         | 
| 12 | 
            +
                    _throttle_time_ms = decoder.int32
         | 
| 13 | 
            +
                    error_code = decoder.int16
         | 
| 14 | 
            +
                    new(error_code: error_code)
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
    
        data/lib/kafka/protocol.rb
    CHANGED
    
    | @@ -33,7 +33,9 @@ module Kafka | |
| 33 33 | 
             
                DELETE_TOPICS_API         = 20
         | 
| 34 34 | 
             
                INIT_PRODUCER_ID_API      = 22
         | 
| 35 35 | 
             
                ADD_PARTITIONS_TO_TXN_API = 24
         | 
| 36 | 
            +
                ADD_OFFSETS_TO_TXN_API    = 25
         | 
| 36 37 | 
             
                END_TXN_API               = 26
         | 
| 38 | 
            +
                TXN_OFFSET_COMMIT_API     = 28
         | 
| 37 39 | 
             
                DESCRIBE_CONFIGS_API      = 32
         | 
| 38 40 | 
             
                ALTER_CONFIGS_API         = 33
         | 
| 39 41 | 
             
                CREATE_PARTITIONS_API     = 37
         | 
| @@ -57,7 +59,9 @@ module Kafka | |
| 57 59 | 
             
                  DELETE_TOPICS_API         => :delete_topics,
         | 
| 58 60 | 
             
                  INIT_PRODUCER_ID_API      => :init_producer_id_api,
         | 
| 59 61 | 
             
                  ADD_PARTITIONS_TO_TXN_API => :add_partitions_to_txn_api,
         | 
| 62 | 
            +
                  ADD_OFFSETS_TO_TXN_API    => :add_offsets_to_txn_api,
         | 
| 60 63 | 
             
                  END_TXN_API               => :end_txn_api,
         | 
| 64 | 
            +
                  TXN_OFFSET_COMMIT_API     => :txn_offset_commit_api,
         | 
| 61 65 | 
             
                  DESCRIBE_CONFIGS_API      => :describe_configs_api,
         | 
| 62 66 | 
             
                  CREATE_PARTITIONS_API     => :create_partitions
         | 
| 63 67 | 
             
                }
         | 
| @@ -177,6 +181,10 @@ require "kafka/protocol/fetch_request" | |
| 177 181 | 
             
            require "kafka/protocol/fetch_response"
         | 
| 178 182 | 
             
            require "kafka/protocol/list_offset_request"
         | 
| 179 183 | 
             
            require "kafka/protocol/list_offset_response"
         | 
| 184 | 
            +
            require "kafka/protocol/add_offsets_to_txn_request"
         | 
| 185 | 
            +
            require "kafka/protocol/add_offsets_to_txn_response"
         | 
| 186 | 
            +
            require "kafka/protocol/txn_offset_commit_request"
         | 
| 187 | 
            +
            require "kafka/protocol/txn_offset_commit_response"
         | 
| 180 188 | 
             
            require "kafka/protocol/find_coordinator_request"
         | 
| 181 189 | 
             
            require "kafka/protocol/find_coordinator_response"
         | 
| 182 190 | 
             
            require "kafka/protocol/join_group_request"
         | 
| @@ -24,20 +24,23 @@ module Kafka | |
| 24 24 | 
             
                    group_assignment[member_id] = Protocol::MemberAssignment.new
         | 
| 25 25 | 
             
                  end
         | 
| 26 26 |  | 
| 27 | 
            -
                  topics. | 
| 27 | 
            +
                  topic_partitions = topics.flat_map do |topic|
         | 
| 28 28 | 
             
                    begin
         | 
| 29 29 | 
             
                      partitions = @cluster.partitions_for(topic).map(&:partition_id)
         | 
| 30 30 | 
             
                    rescue UnknownTopicOrPartition
         | 
| 31 31 | 
             
                      raise UnknownTopicOrPartition, "unknown topic #{topic}"
         | 
| 32 32 | 
             
                    end
         | 
| 33 | 
            +
                    Array.new(partitions.count) { topic }.zip(partitions)
         | 
| 34 | 
            +
                  end
         | 
| 33 35 |  | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 36 | 
            +
                  partitions_per_member = topic_partitions.group_by.with_index do |_, index|
         | 
| 37 | 
            +
                    index % members.count
         | 
| 38 | 
            +
                  end.values
         | 
| 37 39 |  | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 40 | 
            +
                  members.zip(partitions_per_member).each do |member_id, member_partitions|
         | 
| 41 | 
            +
                    unless member_partitions.nil?
         | 
| 42 | 
            +
                      member_partitions.each do |topic, partition|
         | 
| 43 | 
            +
                        group_assignment[member_id].assign(topic, [partition])
         | 
| 41 44 | 
             
                      end
         | 
| 42 45 | 
             
                    end
         | 
| 43 46 | 
             
                  end
         | 
    
        data/lib/kafka/sasl/gssapi.rb
    CHANGED
    
    
| @@ -0,0 +1,64 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Kafka
         | 
| 4 | 
            +
              module Sasl
         | 
| 5 | 
            +
                class OAuth
         | 
| 6 | 
            +
                  OAUTH_IDENT = "OAUTHBEARER"
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  # token_provider: THE FOLLOWING INTERFACE MUST BE FULFILLED:
         | 
| 9 | 
            +
                  #
         | 
| 10 | 
            +
                  # [REQUIRED] TokenProvider#token      - Returns an ID/Access Token to be sent to the Kafka client.
         | 
| 11 | 
            +
                  #   The implementation should ensure token reuse so that multiple calls at connect time do not
         | 
| 12 | 
            +
                  #   create multiple tokens. The implementation should also periodically refresh the token in
         | 
| 13 | 
            +
                  #   order to guarantee that each call returns an unexpired token. A timeout error should
         | 
| 14 | 
            +
                  #   be returned after a short period of inactivity so that the broker can log debugging
         | 
| 15 | 
            +
                  #   info and retry.
         | 
| 16 | 
            +
                  #
         | 
| 17 | 
            +
                  # [OPTIONAL] TokenProvider#extensions - Returns a map of key-value pairs that can be sent with the
         | 
| 18 | 
            +
                  #   SASL/OAUTHBEARER initial client response. If not provided, the values are ignored. This feature
         | 
| 19 | 
            +
                  #   is only available in Kafka >= 2.1.0.
         | 
| 20 | 
            +
                  #
         | 
| 21 | 
            +
                  def initialize(logger:, token_provider:)
         | 
| 22 | 
            +
                    @logger = TaggedLogger.new(logger)
         | 
| 23 | 
            +
                    @token_provider = token_provider
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  def ident
         | 
| 27 | 
            +
                    OAUTH_IDENT
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  def configured?
         | 
| 31 | 
            +
                    @token_provider
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  def authenticate!(host, encoder, decoder)
         | 
| 35 | 
            +
                    # Send SASLOauthBearerClientResponse with token
         | 
| 36 | 
            +
                    @logger.debug "Authenticating to #{host} with SASL #{OAUTH_IDENT}"
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    encoder.write_bytes(initial_client_response)
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    begin
         | 
| 41 | 
            +
                      # receive SASL OAuthBearer Server Response
         | 
| 42 | 
            +
                      msg = decoder.bytes
         | 
| 43 | 
            +
                      raise Kafka::Error, "SASL #{OAUTH_IDENT} authentication failed: unknown error" unless msg
         | 
| 44 | 
            +
                    rescue Errno::ETIMEDOUT, EOFError => e
         | 
| 45 | 
            +
                      raise Kafka::Error, "SASL #{OAUTH_IDENT} authentication failed: #{e.message}"
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    @logger.debug "SASL #{OAUTH_IDENT} authentication successful."
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  private
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def initial_client_response
         | 
| 54 | 
            +
                    raise Kafka::TokenMethodNotImplementedError, "Token provider doesn't define 'token'" unless @token_provider.respond_to? :token
         | 
| 55 | 
            +
                    "n,,\x01auth=Bearer #{@token_provider.token}#{token_extensions}\x01\x01"
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def token_extensions
         | 
| 59 | 
            +
                    return nil unless @token_provider.respond_to? :extensions
         | 
| 60 | 
            +
                    "\x01#{@token_provider.extensions.map {|e| e.join("=")}.join("\x01")}"
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
              end
         | 
| 64 | 
            +
            end
         | 
    
        data/lib/kafka/sasl/plain.rb
    CHANGED
    
    
    
        data/lib/kafka/sasl/scram.rb
    CHANGED
    
    | @@ -12,9 +12,10 @@ module Kafka | |
| 12 12 | 
             
                  }.freeze
         | 
| 13 13 |  | 
| 14 14 | 
             
                  def initialize(username:, password:, mechanism: 'sha256', logger:)
         | 
| 15 | 
            +
                    @semaphore = Mutex.new
         | 
| 15 16 | 
             
                    @username = username
         | 
| 16 17 | 
             
                    @password = password
         | 
| 17 | 
            -
                    @logger = logger
         | 
| 18 | 
            +
                    @logger = TaggedLogger.new(logger)
         | 
| 18 19 |  | 
| 19 20 | 
             
                    if mechanism
         | 
| 20 21 | 
             
                      @mechanism = MECHANISMS.fetch(mechanism) do
         | 
| @@ -35,22 +36,24 @@ module Kafka | |
| 35 36 | 
             
                    @logger.debug "Authenticating #{@username} with SASL #{@mechanism}"
         | 
| 36 37 |  | 
| 37 38 | 
             
                    begin
         | 
| 38 | 
            -
                       | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 39 | 
            +
                      @semaphore.synchronize do
         | 
| 40 | 
            +
                        msg = first_message
         | 
| 41 | 
            +
                        @logger.debug "Sending first client SASL SCRAM message: #{msg}"
         | 
| 42 | 
            +
                        encoder.write_bytes(msg)
         | 
| 41 43 |  | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            +
                        @server_first_message = decoder.bytes
         | 
| 45 | 
            +
                        @logger.debug "Received first server SASL SCRAM message: #{@server_first_message}"
         | 
| 44 46 |  | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 47 | 
            +
                        msg = final_message
         | 
| 48 | 
            +
                        @logger.debug "Sending final client SASL SCRAM message: #{msg}"
         | 
| 49 | 
            +
                        encoder.write_bytes(msg)
         | 
| 48 50 |  | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            +
                        response = parse_response(decoder.bytes)
         | 
| 52 | 
            +
                        @logger.debug "Received last server SASL SCRAM message: #{response}"
         | 
| 51 53 |  | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            +
                        raise FailedScramAuthentication, response['e'] if response['e']
         | 
| 55 | 
            +
                        raise FailedScramAuthentication, "Invalid server signature" if response['v'] != server_signature
         | 
| 56 | 
            +
                      end
         | 
| 54 57 | 
             
                    rescue EOFError => e
         | 
| 55 58 | 
             
                      raise FailedScramAuthentication, e.message
         | 
| 56 59 | 
             
                    end
         | 
| @@ -3,13 +3,15 @@ | |
| 3 3 | 
             
            require 'kafka/sasl/plain'
         | 
| 4 4 | 
             
            require 'kafka/sasl/gssapi'
         | 
| 5 5 | 
             
            require 'kafka/sasl/scram'
         | 
| 6 | 
            +
            require 'kafka/sasl/oauth'
         | 
| 6 7 |  | 
| 7 8 | 
             
            module Kafka
         | 
| 8 9 | 
             
              class SaslAuthenticator
         | 
| 9 10 | 
             
                def initialize(logger:, sasl_gssapi_principal:, sasl_gssapi_keytab:,
         | 
| 10 11 | 
             
                               sasl_plain_authzid:, sasl_plain_username:, sasl_plain_password:,
         | 
| 11 | 
            -
                               sasl_scram_username:, sasl_scram_password:, sasl_scram_mechanism | 
| 12 | 
            -
             | 
| 12 | 
            +
                               sasl_scram_username:, sasl_scram_password:, sasl_scram_mechanism:,
         | 
| 13 | 
            +
                               sasl_oauth_token_provider:)
         | 
| 14 | 
            +
                  @logger = TaggedLogger.new(logger)
         | 
| 13 15 |  | 
| 14 16 | 
             
                  @plain = Sasl::Plain.new(
         | 
| 15 17 | 
             
                    authzid: sasl_plain_authzid,
         | 
| @@ -31,7 +33,12 @@ module Kafka | |
| 31 33 | 
             
                    logger: @logger,
         | 
| 32 34 | 
             
                  )
         | 
| 33 35 |  | 
| 34 | 
            -
                  @ | 
| 36 | 
            +
                  @oauth = Sasl::OAuth.new(
         | 
| 37 | 
            +
                    token_provider: sasl_oauth_token_provider,
         | 
| 38 | 
            +
                    logger: @logger,
         | 
| 39 | 
            +
                  )
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  @mechanism = [@gssapi, @plain, @scram, @oauth].find(&:configured?)
         | 
| 35 42 | 
             
                end
         | 
| 36 43 |  | 
| 37 44 | 
             
                def enabled?
         | 
    
        data/lib/kafka/snappy_codec.rb
    CHANGED
    
    
    
        data/lib/kafka/ssl_context.rb
    CHANGED
    
    | @@ -6,7 +6,7 @@ module Kafka | |
| 6 6 | 
             
              module SslContext
         | 
| 7 7 | 
             
                CLIENT_CERT_DELIMITER = "\n-----END CERTIFICATE-----\n"
         | 
| 8 8 |  | 
| 9 | 
            -
                def self.build(ca_cert_file_path: nil, ca_cert: nil, client_cert: nil, client_cert_key: nil, client_cert_key_password: nil, client_cert_chain: nil, ca_certs_from_system: nil)
         | 
| 9 | 
            +
                def self.build(ca_cert_file_path: nil, ca_cert: nil, client_cert: nil, client_cert_key: nil, client_cert_key_password: nil, client_cert_chain: nil, ca_certs_from_system: nil, verify_hostname: true)
         | 
| 10 10 | 
             
                  return nil unless ca_cert_file_path || ca_cert || client_cert || client_cert_key || client_cert_key_password || client_cert_chain || ca_certs_from_system
         | 
| 11 11 |  | 
| 12 12 | 
             
                  ssl_context = OpenSSL::SSL::SSLContext.new
         | 
| @@ -56,6 +56,10 @@ module Kafka | |
| 56 56 | 
             
                    ssl_context.cert_store = store
         | 
| 57 57 | 
             
                  end
         | 
| 58 58 |  | 
| 59 | 
            +
                  ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
         | 
| 60 | 
            +
                  # Verify certificate hostname if supported (ruby >= 2.4.0)
         | 
| 61 | 
            +
                  ssl_context.verify_hostname = verify_hostname if ssl_context.respond_to?(:verify_hostname=)
         | 
| 62 | 
            +
             | 
| 59 63 | 
             
                  ssl_context
         | 
| 60 64 | 
             
                end
         | 
| 61 65 | 
             
              end
         | 
| @@ -57,6 +57,7 @@ module Kafka | |
| 57 57 |  | 
| 58 58 | 
             
                  # once that's connected, we can start initiating the ssl socket
         | 
| 59 59 | 
             
                  @ssl_socket = OpenSSL::SSL::SSLSocket.new(@tcp_socket, ssl_context)
         | 
| 60 | 
            +
                  @ssl_socket.hostname = host
         | 
| 60 61 |  | 
| 61 62 | 
             
                  begin
         | 
| 62 63 | 
             
                    # Initiate the socket connection in the background. If it doesn't fail
         | 
    
        data/lib/kafka/statsd.rb
    CHANGED
    
    | @@ -107,7 +107,6 @@ module Kafka | |
| 107 107 | 
             
                  end
         | 
| 108 108 |  | 
| 109 109 | 
             
                  def process_batch(event)
         | 
| 110 | 
            -
                    lag = event.payload.fetch(:offset_lag)
         | 
| 111 110 | 
             
                    messages = event.payload.fetch(:message_count)
         | 
| 112 111 | 
             
                    client = event.payload.fetch(:client_id)
         | 
| 113 112 | 
             
                    group_id = event.payload.fetch(:group_id)
         | 
| @@ -120,7 +119,17 @@ module Kafka | |
| 120 119 | 
             
                      timing("consumer.#{client}.#{group_id}.#{topic}.#{partition}.process_batch.latency", event.duration)
         | 
| 121 120 | 
             
                      count("consumer.#{client}.#{group_id}.#{topic}.#{partition}.messages", messages)
         | 
| 122 121 | 
             
                    end
         | 
| 122 | 
            +
                  end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                  def fetch_batch(event)
         | 
| 125 | 
            +
                    lag = event.payload.fetch(:offset_lag)
         | 
| 126 | 
            +
                    batch_size = event.payload.fetch(:message_count)
         | 
| 127 | 
            +
                    client = event.payload.fetch(:client_id)
         | 
| 128 | 
            +
                    group_id = event.payload.fetch(:group_id)
         | 
| 129 | 
            +
                    topic = event.payload.fetch(:topic)
         | 
| 130 | 
            +
                    partition = event.payload.fetch(:partition)
         | 
| 123 131 |  | 
| 132 | 
            +
                    count("consumer.#{client}.#{group_id}.#{topic}.#{partition}.batch_size", batch_size)
         | 
| 124 133 | 
             
                    gauge("consumer.#{client}.#{group_id}.#{topic}.#{partition}.lag", lag)
         | 
| 125 134 | 
             
                  end
         | 
| 126 135 |  | 
| @@ -0,0 +1,77 @@ | |
| 1 | 
            +
            # Basic implementation of a tagged logger that matches the API of
         | 
| 2 | 
            +
            # ActiveSupport::TaggedLogging.
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require 'delegate'
         | 
| 5 | 
            +
            require 'logger'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Kafka
         | 
| 8 | 
            +
              class TaggedLogger < SimpleDelegator
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                %i(debug info warn error).each do |method|
         | 
| 11 | 
            +
                  define_method method do |msg_or_progname, &block|
         | 
| 12 | 
            +
                    if block_given?
         | 
| 13 | 
            +
                      super(msg_or_progname, &block)
         | 
| 14 | 
            +
                    else
         | 
| 15 | 
            +
                      super("#{tags_text}#{msg_or_progname}")
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def tagged(*tags)
         | 
| 21 | 
            +
                  new_tags = push_tags(*tags)
         | 
| 22 | 
            +
                  yield self
         | 
| 23 | 
            +
                ensure
         | 
| 24 | 
            +
                  pop_tags(new_tags.size)
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def push_tags(*tags)
         | 
| 28 | 
            +
                  tags.flatten.reject { |t| t.nil? || t.empty? }.tap do |new_tags|
         | 
| 29 | 
            +
                    current_tags.concat new_tags
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def pop_tags(size = 1)
         | 
| 34 | 
            +
                  current_tags.pop size
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def clear_tags!
         | 
| 38 | 
            +
                  current_tags.clear
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def current_tags
         | 
| 42 | 
            +
                  # We use our object ID here to avoid conflicting with other instances
         | 
| 43 | 
            +
                  thread_key = @thread_key ||= "kafka_tagged_logging_tags:#{object_id}".freeze
         | 
| 44 | 
            +
                  Thread.current[thread_key] ||= []
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def tags_text
         | 
| 48 | 
            +
                  tags = current_tags
         | 
| 49 | 
            +
                  if tags.any?
         | 
| 50 | 
            +
                    tags.collect { |tag| "[#{tag}] " }.join
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                def self.new(logger_or_stream = nil)
         | 
| 55 | 
            +
                  # don't keep wrapping the same logger over and over again
         | 
| 56 | 
            +
                  return logger_or_stream if logger_or_stream.is_a?(TaggedLogger)
         | 
| 57 | 
            +
                  super
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                def initialize(logger_or_stream = nil)
         | 
| 61 | 
            +
                  logger = if %w(info debug warn error).all? { |s| logger_or_stream.respond_to?(s) }
         | 
| 62 | 
            +
                    logger_or_stream
         | 
| 63 | 
            +
                  elsif logger_or_stream
         | 
| 64 | 
            +
                    ::Logger.new(logger_or_stream)
         | 
| 65 | 
            +
                  else
         | 
| 66 | 
            +
                    ::Logger.new(nil)
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
                  super(logger)
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def flush
         | 
| 72 | 
            +
                  clear_tags!
         | 
| 73 | 
            +
                  super if defined?(super)
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
              end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
            end
         | 
| @@ -19,7 +19,7 @@ module Kafka | |
| 19 19 | 
             
                  transactional_timeout: DEFAULT_TRANSACTION_TIMEOUT
         | 
| 20 20 | 
             
                )
         | 
| 21 21 | 
             
                  @cluster = cluster
         | 
| 22 | 
            -
                  @logger = logger
         | 
| 22 | 
            +
                  @logger = TaggedLogger.new(logger)
         | 
| 23 23 |  | 
| 24 24 | 
             
                  @transactional = transactional
         | 
| 25 25 | 
             
                  @transactional_id = transactional_id
         | 
| @@ -217,6 +217,31 @@ module Kafka | |
| 217 217 | 
             
                  raise
         | 
| 218 218 | 
             
                end
         | 
| 219 219 |  | 
| 220 | 
            +
                def send_offsets_to_txn(offsets:, group_id:)
         | 
| 221 | 
            +
                  force_transactional!
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                  unless @transaction_state.in_transaction?
         | 
| 224 | 
            +
                    raise 'Transaction is not valid to send offsets'
         | 
| 225 | 
            +
                  end
         | 
| 226 | 
            +
             | 
| 227 | 
            +
                  add_response = transaction_coordinator.add_offsets_to_txn(
         | 
| 228 | 
            +
                    transactional_id: @transactional_id,
         | 
| 229 | 
            +
                    producer_id: @producer_id,
         | 
| 230 | 
            +
                    producer_epoch: @producer_epoch,
         | 
| 231 | 
            +
                    group_id: group_id
         | 
| 232 | 
            +
                  )
         | 
| 233 | 
            +
                  Protocol.handle_error(add_response.error_code)
         | 
| 234 | 
            +
             | 
| 235 | 
            +
                  send_response = transaction_coordinator.txn_offset_commit(
         | 
| 236 | 
            +
                    transactional_id: @transactional_id,
         | 
| 237 | 
            +
                    group_id: group_id,
         | 
| 238 | 
            +
                    producer_id: @producer_id,
         | 
| 239 | 
            +
                    producer_epoch: @producer_epoch,
         | 
| 240 | 
            +
                    offsets: offsets
         | 
| 241 | 
            +
                  )
         | 
| 242 | 
            +
                  Protocol.handle_error(send_response.error_code)
         | 
| 243 | 
            +
                end
         | 
| 244 | 
            +
             | 
| 220 245 | 
             
                def in_transaction?
         | 
| 221 246 | 
             
                  @transaction_state.in_transaction?
         | 
| 222 247 | 
             
                end
         | 
    
        data/lib/kafka/version.rb
    CHANGED
    
    
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Kafka
         | 
| 4 | 
            +
              class ZstdCodec
         | 
| 5 | 
            +
                def codec_id
         | 
| 6 | 
            +
                  4
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def produce_api_min_version
         | 
| 10 | 
            +
                  7
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def load
         | 
| 14 | 
            +
                  require "zstd-ruby"
         | 
| 15 | 
            +
                rescue LoadError
         | 
| 16 | 
            +
                  raise LoadError, "using zstd compression requires adding a dependency on the `zstd-ruby` gem to your Gemfile."
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def compress(data)
         | 
| 20 | 
            +
                  Zstd.compress(data)
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def decompress(data)
         | 
| 24 | 
            +
                  Zstd.decompress(data)
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
    
        data/lib/kafka.rb
    CHANGED
    
    | @@ -351,6 +351,10 @@ module Kafka | |
| 351 351 | 
             
              class FailedScramAuthentication < SaslScramError
         | 
| 352 352 | 
             
              end
         | 
| 353 353 |  | 
| 354 | 
            +
              # The Token Provider object used for SASL OAuthBearer does not implement the method `token`
         | 
| 355 | 
            +
              class TokenMethodNotImplementedError < Error
         | 
| 356 | 
            +
              end
         | 
| 357 | 
            +
             | 
| 354 358 | 
             
              # Initializes a new Kafka client.
         | 
| 355 359 | 
             
              #
         | 
| 356 360 | 
             
              # @see Client#initialize
         | 
    
        data/ruby-kafka.gemspec
    CHANGED
    
    | @@ -18,7 +18,7 @@ Gem::Specification.new do |spec| | |
| 18 18 | 
             
              DESC
         | 
| 19 19 |  | 
| 20 20 | 
             
              spec.homepage      = "https://github.com/zendesk/ruby-kafka"
         | 
| 21 | 
            -
              spec.license       = "Apache | 
| 21 | 
            +
              spec.license       = "Apache-2.0"
         | 
| 22 22 |  | 
| 23 23 | 
             
              spec.required_ruby_version = '>= 2.1.0'
         | 
| 24 24 |  | 
| @@ -36,13 +36,15 @@ Gem::Specification.new do |spec| | |
| 36 36 | 
             
              spec.add_development_dependency "dotenv"
         | 
| 37 37 | 
             
              spec.add_development_dependency "docker-api"
         | 
| 38 38 | 
             
              spec.add_development_dependency "rspec-benchmark"
         | 
| 39 | 
            -
              spec.add_development_dependency "activesupport"
         | 
| 39 | 
            +
              spec.add_development_dependency "activesupport", ">= 4.0", "< 6.1"
         | 
| 40 40 | 
             
              spec.add_development_dependency "snappy"
         | 
| 41 41 | 
             
              spec.add_development_dependency "extlz4"
         | 
| 42 | 
            +
              spec.add_development_dependency "zstd-ruby"
         | 
| 42 43 | 
             
              spec.add_development_dependency "colored"
         | 
| 43 44 | 
             
              spec.add_development_dependency "rspec_junit_formatter", "0.2.2"
         | 
| 44 | 
            -
              spec.add_development_dependency "dogstatsd-ruby", ">=  | 
| 45 | 
            +
              spec.add_development_dependency "dogstatsd-ruby", ">= 4.0.0", "< 5.0.0"
         | 
| 45 46 | 
             
              spec.add_development_dependency "statsd-ruby"
         | 
| 47 | 
            +
              spec.add_development_dependency "prometheus-client", "~> 0.10.0"
         | 
| 46 48 | 
             
              spec.add_development_dependency "ruby-prof"
         | 
| 47 49 | 
             
              spec.add_development_dependency "timecop"
         | 
| 48 50 | 
             
              spec.add_development_dependency "rubocop", "~> 0.49.1"
         |