ruby-kafka-custom 0.7.7.26
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/kafka/async_producer.rb +279 -0
- data/lib/kafka/broker.rb +205 -0
- data/lib/kafka/broker_info.rb +16 -0
- data/lib/kafka/broker_pool.rb +41 -0
- data/lib/kafka/broker_uri.rb +43 -0
- data/lib/kafka/client.rb +754 -0
- data/lib/kafka/cluster.rb +455 -0
- data/lib/kafka/compression.rb +43 -0
- data/lib/kafka/compressor.rb +85 -0
- data/lib/kafka/connection.rb +220 -0
- data/lib/kafka/connection_builder.rb +33 -0
- data/lib/kafka/consumer.rb +592 -0
- data/lib/kafka/consumer_group.rb +208 -0
- data/lib/kafka/datadog.rb +413 -0
- data/lib/kafka/fetch_operation.rb +115 -0
- data/lib/kafka/fetched_batch.rb +54 -0
- data/lib/kafka/fetched_batch_generator.rb +117 -0
- data/lib/kafka/fetched_message.rb +47 -0
- data/lib/kafka/fetched_offset_resolver.rb +48 -0
- data/lib/kafka/fetcher.rb +221 -0
- data/lib/kafka/gzip_codec.rb +30 -0
- data/lib/kafka/heartbeat.rb +25 -0
- data/lib/kafka/instrumenter.rb +38 -0
- data/lib/kafka/lz4_codec.rb +23 -0
- data/lib/kafka/message_buffer.rb +87 -0
- data/lib/kafka/offset_manager.rb +248 -0
- data/lib/kafka/partitioner.rb +35 -0
- data/lib/kafka/pause.rb +92 -0
- data/lib/kafka/pending_message.rb +29 -0
- data/lib/kafka/pending_message_queue.rb +41 -0
- data/lib/kafka/produce_operation.rb +205 -0
- data/lib/kafka/producer.rb +504 -0
- data/lib/kafka/protocol.rb +217 -0
- data/lib/kafka/protocol/add_partitions_to_txn_request.rb +34 -0
- data/lib/kafka/protocol/add_partitions_to_txn_response.rb +47 -0
- data/lib/kafka/protocol/alter_configs_request.rb +44 -0
- data/lib/kafka/protocol/alter_configs_response.rb +49 -0
- data/lib/kafka/protocol/api_versions_request.rb +21 -0
- data/lib/kafka/protocol/api_versions_response.rb +53 -0
- data/lib/kafka/protocol/consumer_group_protocol.rb +19 -0
- data/lib/kafka/protocol/create_partitions_request.rb +42 -0
- data/lib/kafka/protocol/create_partitions_response.rb +28 -0
- data/lib/kafka/protocol/create_topics_request.rb +45 -0
- data/lib/kafka/protocol/create_topics_response.rb +26 -0
- data/lib/kafka/protocol/decoder.rb +175 -0
- data/lib/kafka/protocol/delete_topics_request.rb +33 -0
- data/lib/kafka/protocol/delete_topics_response.rb +26 -0
- data/lib/kafka/protocol/describe_configs_request.rb +35 -0
- data/lib/kafka/protocol/describe_configs_response.rb +73 -0
- data/lib/kafka/protocol/describe_groups_request.rb +27 -0
- data/lib/kafka/protocol/describe_groups_response.rb +73 -0
- data/lib/kafka/protocol/encoder.rb +184 -0
- data/lib/kafka/protocol/end_txn_request.rb +29 -0
- data/lib/kafka/protocol/end_txn_response.rb +19 -0
- data/lib/kafka/protocol/fetch_request.rb +70 -0
- data/lib/kafka/protocol/fetch_response.rb +136 -0
- data/lib/kafka/protocol/find_coordinator_request.rb +29 -0
- data/lib/kafka/protocol/find_coordinator_response.rb +29 -0
- data/lib/kafka/protocol/heartbeat_request.rb +27 -0
- data/lib/kafka/protocol/heartbeat_response.rb +17 -0
- data/lib/kafka/protocol/init_producer_id_request.rb +26 -0
- data/lib/kafka/protocol/init_producer_id_response.rb +27 -0
- data/lib/kafka/protocol/join_group_request.rb +41 -0
- data/lib/kafka/protocol/join_group_response.rb +33 -0
- data/lib/kafka/protocol/leave_group_request.rb +25 -0
- data/lib/kafka/protocol/leave_group_response.rb +17 -0
- data/lib/kafka/protocol/list_groups_request.rb +23 -0
- data/lib/kafka/protocol/list_groups_response.rb +35 -0
- data/lib/kafka/protocol/list_offset_request.rb +53 -0
- data/lib/kafka/protocol/list_offset_response.rb +89 -0
- data/lib/kafka/protocol/member_assignment.rb +42 -0
- data/lib/kafka/protocol/message.rb +172 -0
- data/lib/kafka/protocol/message_set.rb +55 -0
- data/lib/kafka/protocol/metadata_request.rb +31 -0
- data/lib/kafka/protocol/metadata_response.rb +185 -0
- data/lib/kafka/protocol/offset_commit_request.rb +47 -0
- data/lib/kafka/protocol/offset_commit_response.rb +29 -0
- data/lib/kafka/protocol/offset_fetch_request.rb +36 -0
- data/lib/kafka/protocol/offset_fetch_response.rb +56 -0
- data/lib/kafka/protocol/produce_request.rb +92 -0
- data/lib/kafka/protocol/produce_response.rb +63 -0
- data/lib/kafka/protocol/record.rb +88 -0
- data/lib/kafka/protocol/record_batch.rb +222 -0
- data/lib/kafka/protocol/request_message.rb +26 -0
- data/lib/kafka/protocol/sasl_handshake_request.rb +33 -0
- data/lib/kafka/protocol/sasl_handshake_response.rb +28 -0
- data/lib/kafka/protocol/sync_group_request.rb +33 -0
- data/lib/kafka/protocol/sync_group_response.rb +23 -0
- data/lib/kafka/round_robin_assignment_strategy.rb +54 -0
- data/lib/kafka/sasl/gssapi.rb +76 -0
- data/lib/kafka/sasl/oauth.rb +64 -0
- data/lib/kafka/sasl/plain.rb +39 -0
- data/lib/kafka/sasl/scram.rb +177 -0
- data/lib/kafka/sasl_authenticator.rb +61 -0
- data/lib/kafka/snappy_codec.rb +25 -0
- data/lib/kafka/socket_with_timeout.rb +96 -0
- data/lib/kafka/ssl_context.rb +66 -0
- data/lib/kafka/ssl_socket_with_timeout.rb +187 -0
- data/lib/kafka/statsd.rb +296 -0
- data/lib/kafka/tagged_logger.rb +72 -0
- data/lib/kafka/transaction_manager.rb +261 -0
- data/lib/kafka/transaction_state_machine.rb +72 -0
- data/lib/kafka/version.rb +5 -0
- metadata +461 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kafka/sasl/plain'
|
4
|
+
require 'kafka/sasl/gssapi'
|
5
|
+
require 'kafka/sasl/scram'
|
6
|
+
require 'kafka/sasl/oauth'
|
7
|
+
|
8
|
+
module Kafka
|
9
|
+
class SaslAuthenticator
|
10
|
+
def initialize(logger:, sasl_gssapi_principal:, sasl_gssapi_keytab:,
|
11
|
+
sasl_plain_authzid:, sasl_plain_username:, sasl_plain_password:,
|
12
|
+
sasl_scram_username:, sasl_scram_password:, sasl_scram_mechanism:,
|
13
|
+
sasl_oauth_token_provider:)
|
14
|
+
@logger = TaggedLogger.new(logger)
|
15
|
+
|
16
|
+
@plain = Sasl::Plain.new(
|
17
|
+
authzid: sasl_plain_authzid,
|
18
|
+
username: sasl_plain_username,
|
19
|
+
password: sasl_plain_password,
|
20
|
+
logger: @logger,
|
21
|
+
)
|
22
|
+
|
23
|
+
@gssapi = Sasl::Gssapi.new(
|
24
|
+
principal: sasl_gssapi_principal,
|
25
|
+
keytab: sasl_gssapi_keytab,
|
26
|
+
logger: @logger,
|
27
|
+
)
|
28
|
+
|
29
|
+
@scram = Sasl::Scram.new(
|
30
|
+
username: sasl_scram_username,
|
31
|
+
password: sasl_scram_password,
|
32
|
+
mechanism: sasl_scram_mechanism,
|
33
|
+
logger: @logger,
|
34
|
+
)
|
35
|
+
|
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?)
|
42
|
+
end
|
43
|
+
|
44
|
+
def enabled?
|
45
|
+
!@mechanism.nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
def authenticate!(connection)
|
49
|
+
return unless enabled?
|
50
|
+
|
51
|
+
ident = @mechanism.ident
|
52
|
+
response = connection.send_request(Kafka::Protocol::SaslHandshakeRequest.new(ident))
|
53
|
+
|
54
|
+
unless response.error_code == 0 && response.enabled_mechanisms.include?(ident)
|
55
|
+
raise Kafka::Error, "#{ident} is not supported."
|
56
|
+
end
|
57
|
+
|
58
|
+
@mechanism.authenticate!(connection.to_s, connection.encoder, connection.decoder)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kafka
|
4
|
+
class SnappyCodec
|
5
|
+
def codec_id
|
6
|
+
2
|
7
|
+
end
|
8
|
+
|
9
|
+
def load
|
10
|
+
require "snappy"
|
11
|
+
rescue LoadError
|
12
|
+
raise LoadError,
|
13
|
+
"Using snappy compression requires adding a dependency on the `snappy` gem to your Gemfile."
|
14
|
+
end
|
15
|
+
|
16
|
+
def compress(data)
|
17
|
+
Snappy.deflate(data)
|
18
|
+
end
|
19
|
+
|
20
|
+
def decompress(data)
|
21
|
+
buffer = StringIO.new(data)
|
22
|
+
Snappy::Reader.new(buffer).read
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
|
5
|
+
module Kafka
|
6
|
+
|
7
|
+
# Opens sockets in a non-blocking fashion, ensuring that we're not stalling
|
8
|
+
# for long periods of time.
|
9
|
+
#
|
10
|
+
# It's possible to set timeouts for connecting to the server, for reading data,
|
11
|
+
# and for writing data. Whenever a timeout is exceeded, Errno::ETIMEDOUT is
|
12
|
+
# raised.
|
13
|
+
#
|
14
|
+
class SocketWithTimeout
|
15
|
+
|
16
|
+
# Opens a socket.
|
17
|
+
#
|
18
|
+
# @param host [String]
|
19
|
+
# @param port [Integer]
|
20
|
+
# @param connect_timeout [Integer] the connection timeout, in seconds.
|
21
|
+
# @param timeout [Integer] the read and write timeout, in seconds.
|
22
|
+
# @raise [Errno::ETIMEDOUT] if the timeout is exceeded.
|
23
|
+
def initialize(host, port, connect_timeout: nil, timeout: nil)
|
24
|
+
addr = Socket.getaddrinfo(host, nil)
|
25
|
+
sockaddr = Socket.pack_sockaddr_in(port, addr[0][3])
|
26
|
+
|
27
|
+
@timeout = timeout
|
28
|
+
|
29
|
+
@socket = Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
|
30
|
+
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
31
|
+
|
32
|
+
begin
|
33
|
+
# Initiate the socket connection in the background. If it doesn't fail
|
34
|
+
# immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS)
|
35
|
+
# indicating the connection is in progress.
|
36
|
+
@socket.connect_nonblock(sockaddr)
|
37
|
+
rescue IO::WaitWritable
|
38
|
+
# IO.select will block until the socket is writable or the timeout
|
39
|
+
# is exceeded, whichever comes first.
|
40
|
+
unless IO.select(nil, [@socket], nil, connect_timeout)
|
41
|
+
# IO.select returns nil when the socket is not ready before timeout
|
42
|
+
# seconds have elapsed
|
43
|
+
@socket.close
|
44
|
+
raise Errno::ETIMEDOUT
|
45
|
+
end
|
46
|
+
|
47
|
+
begin
|
48
|
+
# Verify there is now a good connection.
|
49
|
+
@socket.connect_nonblock(sockaddr)
|
50
|
+
rescue Errno::EISCONN
|
51
|
+
# The socket is connected, we're good!
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Reads bytes from the socket, possible with a timeout.
|
57
|
+
#
|
58
|
+
# @param num_bytes [Integer] the number of bytes to read.
|
59
|
+
# @raise [Errno::ETIMEDOUT] if the timeout is exceeded.
|
60
|
+
# @return [String] the data that was read from the socket.
|
61
|
+
def read(num_bytes)
|
62
|
+
unless IO.select([@socket], nil, nil, @timeout)
|
63
|
+
raise Errno::ETIMEDOUT
|
64
|
+
end
|
65
|
+
|
66
|
+
@socket.read(num_bytes)
|
67
|
+
rescue IO::EAGAINWaitReadable
|
68
|
+
retry
|
69
|
+
end
|
70
|
+
|
71
|
+
# Writes bytes to the socket, possible with a timeout.
|
72
|
+
#
|
73
|
+
# @param bytes [String] the data that should be written to the socket.
|
74
|
+
# @raise [Errno::ETIMEDOUT] if the timeout is exceeded.
|
75
|
+
# @return [Integer] the number of bytes written.
|
76
|
+
def write(bytes)
|
77
|
+
unless IO.select(nil, [@socket], nil, @timeout)
|
78
|
+
raise Errno::ETIMEDOUT
|
79
|
+
end
|
80
|
+
|
81
|
+
@socket.write(bytes)
|
82
|
+
end
|
83
|
+
|
84
|
+
def close
|
85
|
+
@socket.close
|
86
|
+
end
|
87
|
+
|
88
|
+
def closed?
|
89
|
+
@socket.closed?
|
90
|
+
end
|
91
|
+
|
92
|
+
def set_encoding(encoding)
|
93
|
+
@socket.set_encoding(encoding)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
|
5
|
+
module Kafka
|
6
|
+
module SslContext
|
7
|
+
CLIENT_CERT_DELIMITER = "\n-----END CERTIFICATE-----\n"
|
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)
|
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
|
+
|
12
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
13
|
+
|
14
|
+
if client_cert && client_cert_key
|
15
|
+
if client_cert_key_password
|
16
|
+
cert_key = OpenSSL::PKey.read(client_cert_key, client_cert_key_password)
|
17
|
+
else
|
18
|
+
cert_key = OpenSSL::PKey.read(client_cert_key)
|
19
|
+
end
|
20
|
+
context_params = {
|
21
|
+
cert: OpenSSL::X509::Certificate.new(client_cert),
|
22
|
+
key: cert_key
|
23
|
+
}
|
24
|
+
if client_cert_chain
|
25
|
+
certs = []
|
26
|
+
client_cert_chain.split(CLIENT_CERT_DELIMITER).each do |cert|
|
27
|
+
cert += CLIENT_CERT_DELIMITER
|
28
|
+
certs << OpenSSL::X509::Certificate.new(cert)
|
29
|
+
end
|
30
|
+
context_params[:extra_chain_cert] = certs
|
31
|
+
end
|
32
|
+
ssl_context.set_params(context_params)
|
33
|
+
elsif client_cert && !client_cert_key
|
34
|
+
raise ArgumentError, "Kafka client initialized with `ssl_client_cert` but no `ssl_client_cert_key`. Please provide both."
|
35
|
+
elsif !client_cert && client_cert_key
|
36
|
+
raise ArgumentError, "Kafka client initialized with `ssl_client_cert_key`, but no `ssl_client_cert`. Please provide both."
|
37
|
+
elsif client_cert_chain && !client_cert
|
38
|
+
raise ArgumentError, "Kafka client initialized with `ssl_client_cert_chain`, but no `ssl_client_cert`. Please provide cert, key and chain."
|
39
|
+
elsif client_cert_chain && !client_cert_key
|
40
|
+
raise ArgumentError, "Kafka client initialized with `ssl_client_cert_chain`, but no `ssl_client_cert_key`. Please provide cert, key and chain."
|
41
|
+
elsif client_cert_key_password && !client_cert_key
|
42
|
+
raise ArgumentError, "Kafka client initialized with `ssl_client_cert_key_password`, but no `ssl_client_cert_key`. Please provide both."
|
43
|
+
end
|
44
|
+
|
45
|
+
puts "----------------------------------"
|
46
|
+
puts OpenSSL::OPENSSL_LIBRARY_VERSION
|
47
|
+
puts "----------------------------------"
|
48
|
+
|
49
|
+
if ca_cert || ca_cert_file_path || ca_certs_from_system
|
50
|
+
store = OpenSSL::X509::Store.new
|
51
|
+
Array(ca_cert).each do |cert|
|
52
|
+
store.add_cert(OpenSSL::X509::Certificate.new(cert))
|
53
|
+
end
|
54
|
+
if ca_cert_file_path
|
55
|
+
store.add_file(ca_cert_file_path)
|
56
|
+
end
|
57
|
+
if ca_certs_from_system
|
58
|
+
store.set_default_paths
|
59
|
+
end
|
60
|
+
ssl_context.cert_store = store
|
61
|
+
end
|
62
|
+
|
63
|
+
ssl_context
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
|
5
|
+
module Kafka
|
6
|
+
|
7
|
+
# Opens sockets in a non-blocking fashion, ensuring that we're not stalling
|
8
|
+
# for long periods of time.
|
9
|
+
#
|
10
|
+
# It's possible to set timeouts for connecting to the server, for reading data,
|
11
|
+
# and for writing data. Whenever a timeout is exceeded, Errno::ETIMEDOUT is
|
12
|
+
# raised.
|
13
|
+
#
|
14
|
+
class SSLSocketWithTimeout
|
15
|
+
|
16
|
+
# Opens a socket.
|
17
|
+
#
|
18
|
+
# @param host [String]
|
19
|
+
# @param port [Integer]
|
20
|
+
# @param connect_timeout [Integer] the connection timeout, in seconds.
|
21
|
+
# @param timeout [Integer] the read and write timeout, in seconds.
|
22
|
+
# @param ssl_context [OpenSSL::SSL::SSLContext] which SSLContext the ssl connection should use
|
23
|
+
# @raise [Errno::ETIMEDOUT] if the timeout is exceeded.
|
24
|
+
def initialize(host, port, connect_timeout: nil, timeout: nil, ssl_context:)
|
25
|
+
addr = Socket.getaddrinfo(host, nil)
|
26
|
+
sockaddr = Socket.pack_sockaddr_in(port, addr[0][3])
|
27
|
+
|
28
|
+
@connect_timeout = connect_timeout
|
29
|
+
@timeout = timeout
|
30
|
+
|
31
|
+
@tcp_socket = Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
|
32
|
+
@tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
33
|
+
|
34
|
+
# first initiate the TCP socket
|
35
|
+
begin
|
36
|
+
# Initiate the socket connection in the background. If it doesn't fail
|
37
|
+
# immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS)
|
38
|
+
# indicating the connection is in progress.
|
39
|
+
@tcp_socket.connect_nonblock(sockaddr)
|
40
|
+
rescue IO::WaitWritable
|
41
|
+
# select will block until the socket is writable or the timeout
|
42
|
+
# is exceeded, whichever comes first.
|
43
|
+
unless select_with_timeout(@tcp_socket, :connect_write)
|
44
|
+
# select returns nil when the socket is not ready before timeout
|
45
|
+
# seconds have elapsed
|
46
|
+
@tcp_socket.close
|
47
|
+
raise Errno::ETIMEDOUT
|
48
|
+
end
|
49
|
+
|
50
|
+
begin
|
51
|
+
# Verify there is now a good connection.
|
52
|
+
@tcp_socket.connect_nonblock(sockaddr)
|
53
|
+
rescue Errno::EISCONN
|
54
|
+
# The socket is connected, we're good!
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# once that's connected, we can start initiating the ssl socket
|
59
|
+
@ssl_socket = OpenSSL::SSL::SSLSocket.new(@tcp_socket, ssl_context)
|
60
|
+
|
61
|
+
begin
|
62
|
+
# Initiate the socket connection in the background. If it doesn't fail
|
63
|
+
# immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS)
|
64
|
+
# indicating the connection is in progress.
|
65
|
+
# Unlike waiting for a tcp socket to connect, you can't time out ssl socket
|
66
|
+
# connections during the connect phase properly, because IO.select only partially works.
|
67
|
+
# Instead, you have to retry.
|
68
|
+
@ssl_socket.connect_nonblock
|
69
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
|
70
|
+
if select_with_timeout(@ssl_socket, :connect_read)
|
71
|
+
retry
|
72
|
+
else
|
73
|
+
@ssl_socket.close
|
74
|
+
close
|
75
|
+
raise Errno::ETIMEDOUT
|
76
|
+
end
|
77
|
+
rescue IO::WaitWritable
|
78
|
+
if select_with_timeout(@ssl_socket, :connect_write)
|
79
|
+
retry
|
80
|
+
else
|
81
|
+
close
|
82
|
+
raise Errno::ETIMEDOUT
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Reads bytes from the socket, possible with a timeout.
|
88
|
+
#
|
89
|
+
# @param num_bytes [Integer] the number of bytes to read.
|
90
|
+
# @raise [Errno::ETIMEDOUT] if the timeout is exceeded.
|
91
|
+
# @return [String] the data that was read from the socket.
|
92
|
+
def read(num_bytes)
|
93
|
+
buffer = String.new
|
94
|
+
|
95
|
+
until buffer.length >= num_bytes
|
96
|
+
begin
|
97
|
+
# Unlike plain TCP sockets, SSL sockets don't support IO.select
|
98
|
+
# properly.
|
99
|
+
# Instead, timeouts happen on a per read basis, and we have to
|
100
|
+
# catch exceptions from read_nonblock and gradually build up
|
101
|
+
# our read buffer.
|
102
|
+
buffer << @ssl_socket.read_nonblock(num_bytes - buffer.length)
|
103
|
+
rescue IO::WaitReadable
|
104
|
+
if select_with_timeout(@ssl_socket, :read)
|
105
|
+
retry
|
106
|
+
else
|
107
|
+
raise Errno::ETIMEDOUT
|
108
|
+
end
|
109
|
+
rescue IO::WaitWritable
|
110
|
+
if select_with_timeout(@ssl_socket, :write)
|
111
|
+
retry
|
112
|
+
else
|
113
|
+
raise Errno::ETIMEDOUT
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
buffer
|
119
|
+
end
|
120
|
+
|
121
|
+
# Writes bytes to the socket, possible with a timeout.
|
122
|
+
#
|
123
|
+
# @param bytes [String] the data that should be written to the socket.
|
124
|
+
# @raise [Errno::ETIMEDOUT] if the timeout is exceeded.
|
125
|
+
# @return [Integer] the number of bytes written.
|
126
|
+
def write(bytes)
|
127
|
+
loop do
|
128
|
+
written = 0
|
129
|
+
begin
|
130
|
+
# unlike plain tcp sockets, ssl sockets don't support IO.select
|
131
|
+
# properly.
|
132
|
+
# Instead, timeouts happen on a per write basis, and we have to
|
133
|
+
# catch exceptions from write_nonblock, and gradually build up
|
134
|
+
# our write buffer.
|
135
|
+
written += @ssl_socket.write_nonblock(bytes)
|
136
|
+
rescue Errno::EFAULT => error
|
137
|
+
raise error
|
138
|
+
rescue OpenSSL::SSL::SSLError, Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable => error
|
139
|
+
if error.is_a?(OpenSSL::SSL::SSLError) && error.message == 'write would block'
|
140
|
+
if select_with_timeout(@ssl_socket, :write)
|
141
|
+
retry
|
142
|
+
else
|
143
|
+
raise Errno::ETIMEDOUT
|
144
|
+
end
|
145
|
+
else
|
146
|
+
raise error
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Fast, common case.
|
151
|
+
break if written == bytes.size
|
152
|
+
|
153
|
+
# This takes advantage of the fact that most ruby implementations
|
154
|
+
# have Copy-On-Write strings. Thusly why requesting a subrange
|
155
|
+
# of data, we actually don't copy data because the new string
|
156
|
+
# simply references a subrange of the original.
|
157
|
+
bytes = bytes[written, bytes.size]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def close
|
162
|
+
@tcp_socket.close
|
163
|
+
@ssl_socket.close
|
164
|
+
end
|
165
|
+
|
166
|
+
def closed?
|
167
|
+
@tcp_socket.closed? || @ssl_socket.closed?
|
168
|
+
end
|
169
|
+
|
170
|
+
def set_encoding(encoding)
|
171
|
+
@tcp_socket.set_encoding(encoding)
|
172
|
+
end
|
173
|
+
|
174
|
+
def select_with_timeout(socket, type)
|
175
|
+
case type
|
176
|
+
when :connect_read
|
177
|
+
IO.select([socket], nil, nil, @connect_timeout)
|
178
|
+
when :connect_write
|
179
|
+
IO.select(nil, [socket], nil, @connect_timeout)
|
180
|
+
when :read
|
181
|
+
IO.select([socket], nil, nil, @timeout)
|
182
|
+
when :write
|
183
|
+
IO.select(nil, [socket], nil, @timeout)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
data/lib/kafka/statsd.rb
ADDED
@@ -0,0 +1,296 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "statsd"
|
5
|
+
rescue LoadError
|
6
|
+
$stderr.puts "In order to report Kafka client metrics to Statsd you need to install the `statsd-ruby` gem."
|
7
|
+
raise
|
8
|
+
end
|
9
|
+
|
10
|
+
require "active_support/subscriber"
|
11
|
+
|
12
|
+
module Kafka
|
13
|
+
# Reports operational metrics to a Statsd agent.
|
14
|
+
#
|
15
|
+
# require "kafka/statsd"
|
16
|
+
#
|
17
|
+
# # Default is "ruby_kafka".
|
18
|
+
# Kafka::Statsd.namespace = "custom-namespace"
|
19
|
+
#
|
20
|
+
# # Default is "127.0.0.1".
|
21
|
+
# Kafka::Statsd.host = "statsd.something.com"
|
22
|
+
#
|
23
|
+
# # Default is 8125.
|
24
|
+
# Kafka::Statsd.port = 1234
|
25
|
+
#
|
26
|
+
# Once the file has been required, no further configuration is needed – all operational
|
27
|
+
# metrics are automatically emitted.
|
28
|
+
module Statsd
|
29
|
+
DEFAULT_NAMESPACE = "ruby_kafka"
|
30
|
+
DEFAULT_HOST = '127.0.0.1'
|
31
|
+
DEFAULT_PORT = 8125
|
32
|
+
|
33
|
+
def self.statsd
|
34
|
+
@statsd ||= ::Statsd.new(DEFAULT_HOST, DEFAULT_PORT).tap { |sd| sd.namespace = DEFAULT_NAMESPACE }
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.host=(host)
|
38
|
+
statsd.host = host
|
39
|
+
statsd.connect if statsd.respond_to?(:connect)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.port=(port)
|
43
|
+
statsd.port = port
|
44
|
+
statsd.connect if statsd.respond_to?(:connect)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.namespace=(namespace)
|
48
|
+
statsd.namespace = namespace
|
49
|
+
end
|
50
|
+
|
51
|
+
class StatsdSubscriber < ActiveSupport::Subscriber
|
52
|
+
private
|
53
|
+
|
54
|
+
%w[increment count timing gauge].each do |type|
|
55
|
+
define_method(type) do |*args|
|
56
|
+
Kafka::Statsd.statsd.send(type, *args)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class ConnectionSubscriber < StatsdSubscriber
|
62
|
+
def request(event)
|
63
|
+
client = event.payload.fetch(:client_id)
|
64
|
+
api = event.payload.fetch(:api, "unknown")
|
65
|
+
request_size = event.payload.fetch(:request_size, 0)
|
66
|
+
response_size = event.payload.fetch(:response_size, 0)
|
67
|
+
broker = event.payload.fetch(:broker_host)
|
68
|
+
|
69
|
+
timing("api.#{client}.#{api}.#{broker}.latency", event.duration)
|
70
|
+
increment("api.#{client}.#{api}.#{broker}.calls")
|
71
|
+
|
72
|
+
timing("api.#{client}.#{api}.#{broker}.request_size", request_size)
|
73
|
+
timing("api.#{client}.#{api}.#{broker}.response_size", response_size)
|
74
|
+
|
75
|
+
if event.payload.key?(:exception)
|
76
|
+
increment("api.#{client}.#{api}.#{broker}.errors")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
attach_to "connection.kafka"
|
81
|
+
end
|
82
|
+
|
83
|
+
class ConsumerSubscriber < StatsdSubscriber
|
84
|
+
def process_message(event)
|
85
|
+
offset_lag = event.payload.fetch(:offset_lag)
|
86
|
+
create_time = event.payload.fetch(:create_time)
|
87
|
+
client = event.payload.fetch(:client_id)
|
88
|
+
group_id = event.payload.fetch(:group_id)
|
89
|
+
topic = event.payload.fetch(:topic)
|
90
|
+
partition = event.payload.fetch(:partition)
|
91
|
+
|
92
|
+
time_lag = create_time && ((Time.now - create_time) * 1000).to_i
|
93
|
+
|
94
|
+
if event.payload.key?(:exception)
|
95
|
+
increment("consumer.#{client}.#{group_id}.#{topic}.#{partition}.process_message.errors")
|
96
|
+
else
|
97
|
+
timing("consumer.#{client}.#{group_id}.#{topic}.#{partition}.process_message.latency", event.duration)
|
98
|
+
increment("consumer.#{client}.#{group_id}.#{topic}.#{partition}.messages")
|
99
|
+
end
|
100
|
+
|
101
|
+
gauge("consumer.#{client}.#{group_id}.#{topic}.#{partition}.lag", offset_lag)
|
102
|
+
|
103
|
+
# Not all messages have timestamps.
|
104
|
+
if time_lag
|
105
|
+
gauge("consumer.#{client}.#{group_id}.#{topic}.#{partition}.time_lag", time_lag)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def process_batch(event)
|
110
|
+
messages = event.payload.fetch(:message_count)
|
111
|
+
client = event.payload.fetch(:client_id)
|
112
|
+
group_id = event.payload.fetch(:group_id)
|
113
|
+
topic = event.payload.fetch(:topic)
|
114
|
+
partition = event.payload.fetch(:partition)
|
115
|
+
|
116
|
+
if event.payload.key?(:exception)
|
117
|
+
increment("consumer.#{client}.#{group_id}.#{topic}.#{partition}.process_batch.errors")
|
118
|
+
else
|
119
|
+
timing("consumer.#{client}.#{group_id}.#{topic}.#{partition}.process_batch.latency", event.duration)
|
120
|
+
count("consumer.#{client}.#{group_id}.#{topic}.#{partition}.messages", messages)
|
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)
|
131
|
+
|
132
|
+
count("consumer.#{client}.#{group_id}.#{topic}.#{partition}.batch_size", batch_size)
|
133
|
+
gauge("consumer.#{client}.#{group_id}.#{topic}.#{partition}.lag", lag)
|
134
|
+
end
|
135
|
+
|
136
|
+
def join_group(event)
|
137
|
+
client = event.payload.fetch(:client_id)
|
138
|
+
group_id = event.payload.fetch(:group_id)
|
139
|
+
|
140
|
+
timing("consumer.#{client}.#{group_id}.join_group", event.duration)
|
141
|
+
|
142
|
+
if event.payload.key?(:exception)
|
143
|
+
increment("consumer.#{client}.#{group_id}.join_group.errors")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def sync_group(event)
|
148
|
+
client = event.payload.fetch(:client_id)
|
149
|
+
group_id = event.payload.fetch(:group_id)
|
150
|
+
|
151
|
+
timing("consumer.#{client}.#{group_id}.sync_group", event.duration)
|
152
|
+
|
153
|
+
if event.payload.key?(:exception)
|
154
|
+
increment("consumer.#{client}.#{group_id}.sync_group.errors")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def leave_group(event)
|
159
|
+
client = event.payload.fetch(:client_id)
|
160
|
+
group_id = event.payload.fetch(:group_id)
|
161
|
+
|
162
|
+
timing("consumer.#{client}.#{group_id}.leave_group", event.duration)
|
163
|
+
|
164
|
+
if event.payload.key?(:exception)
|
165
|
+
increment("consumer.#{client}.#{group_id}.leave_group.errors")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def pause_status(event)
|
170
|
+
client = event.payload.fetch(:client_id)
|
171
|
+
group_id = event.payload.fetch(:group_id)
|
172
|
+
topic = event.payload.fetch(:topic)
|
173
|
+
partition = event.payload.fetch(:partition)
|
174
|
+
|
175
|
+
duration = event.payload.fetch(:duration)
|
176
|
+
|
177
|
+
gauge("consumer.#{client}.#{group_id}.#{topic}.#{partition}.pause.duration", duration)
|
178
|
+
end
|
179
|
+
|
180
|
+
attach_to "consumer.kafka"
|
181
|
+
end
|
182
|
+
|
183
|
+
class ProducerSubscriber < StatsdSubscriber
|
184
|
+
def produce_message(event)
|
185
|
+
client = event.payload.fetch(:client_id)
|
186
|
+
topic = event.payload.fetch(:topic)
|
187
|
+
message_size = event.payload.fetch(:message_size)
|
188
|
+
buffer_size = event.payload.fetch(:buffer_size)
|
189
|
+
max_buffer_size = event.payload.fetch(:max_buffer_size)
|
190
|
+
buffer_fill_ratio = buffer_size.to_f / max_buffer_size.to_f
|
191
|
+
buffer_fill_percentage = buffer_fill_ratio * 100.0
|
192
|
+
|
193
|
+
# This gets us the write rate.
|
194
|
+
increment("producer.#{client}.#{topic}.produce.messages")
|
195
|
+
|
196
|
+
timing("producer.#{client}.#{topic}.produce.message_size", message_size)
|
197
|
+
|
198
|
+
# This gets us the avg/max buffer size per producer.
|
199
|
+
timing("producer.#{client}.buffer.size", buffer_size)
|
200
|
+
|
201
|
+
# This gets us the avg/max buffer fill ratio per producer.
|
202
|
+
timing("producer.#{client}.buffer.fill_ratio", buffer_fill_ratio)
|
203
|
+
timing("producer.#{client}.buffer.fill_percentage", buffer_fill_percentage)
|
204
|
+
end
|
205
|
+
|
206
|
+
def buffer_overflow(event)
|
207
|
+
client = event.payload.fetch(:client_id)
|
208
|
+
topic = event.payload.fetch(:topic)
|
209
|
+
|
210
|
+
increment("producer.#{client}.#{topic}.produce.errors")
|
211
|
+
end
|
212
|
+
|
213
|
+
def deliver_messages(event)
|
214
|
+
client = event.payload.fetch(:client_id)
|
215
|
+
message_count = event.payload.fetch(:delivered_message_count)
|
216
|
+
attempts = event.payload.fetch(:attempts)
|
217
|
+
|
218
|
+
if event.payload.key?(:exception)
|
219
|
+
increment("producer.#{client}.deliver.errors")
|
220
|
+
end
|
221
|
+
|
222
|
+
timing("producer.#{client}.deliver.latency", event.duration)
|
223
|
+
|
224
|
+
# Messages delivered to Kafka:
|
225
|
+
count("producer.#{client}.deliver.messages", message_count)
|
226
|
+
|
227
|
+
# Number of attempts to deliver messages:
|
228
|
+
timing("producer.#{client}.deliver.attempts", attempts)
|
229
|
+
end
|
230
|
+
|
231
|
+
def ack_message(event)
|
232
|
+
client = event.payload.fetch(:client_id)
|
233
|
+
topic = event.payload.fetch(:topic)
|
234
|
+
|
235
|
+
# Number of messages ACK'd for the topic.
|
236
|
+
increment("producer.#{client}.#{topic}.ack.messages")
|
237
|
+
|
238
|
+
# Histogram of delay between a message being produced and it being ACK'd.
|
239
|
+
timing("producer.#{client}.#{topic}.ack.delay", event.payload.fetch(:delay))
|
240
|
+
end
|
241
|
+
|
242
|
+
def topic_error(event)
|
243
|
+
client = event.payload.fetch(:client_id)
|
244
|
+
topic = event.payload.fetch(:topic)
|
245
|
+
|
246
|
+
increment("producer.#{client}.#{topic}.ack.errors")
|
247
|
+
end
|
248
|
+
|
249
|
+
attach_to "producer.kafka"
|
250
|
+
end
|
251
|
+
|
252
|
+
class AsyncProducerSubscriber < StatsdSubscriber
|
253
|
+
def enqueue_message(event)
|
254
|
+
client = event.payload.fetch(:client_id)
|
255
|
+
topic = event.payload.fetch(:topic)
|
256
|
+
queue_size = event.payload.fetch(:queue_size)
|
257
|
+
max_queue_size = event.payload.fetch(:max_queue_size)
|
258
|
+
queue_fill_ratio = queue_size.to_f / max_queue_size.to_f
|
259
|
+
|
260
|
+
# This gets us the avg/max queue size per producer.
|
261
|
+
timing("async_producer.#{client}.#{topic}.queue.size", queue_size)
|
262
|
+
|
263
|
+
# This gets us the avg/max queue fill ratio per producer.
|
264
|
+
timing("async_producer.#{client}.#{topic}.queue.fill_ratio", queue_fill_ratio)
|
265
|
+
end
|
266
|
+
|
267
|
+
def buffer_overflow(event)
|
268
|
+
client = event.payload.fetch(:client_id)
|
269
|
+
topic = event.payload.fetch(:topic)
|
270
|
+
|
271
|
+
increment("async_producer.#{client}.#{topic}.produce.errors")
|
272
|
+
end
|
273
|
+
|
274
|
+
def drop_messages(event)
|
275
|
+
client = event.payload.fetch(:client_id)
|
276
|
+
message_count = event.payload.fetch(:message_count)
|
277
|
+
|
278
|
+
count("async_producer.#{client}.dropped_messages", message_count)
|
279
|
+
end
|
280
|
+
|
281
|
+
attach_to "async_producer.kafka"
|
282
|
+
end
|
283
|
+
|
284
|
+
class FetcherSubscriber < StatsdSubscriber
|
285
|
+
def loop(event)
|
286
|
+
queue_size = event.payload.fetch(:queue_size)
|
287
|
+
client = event.payload.fetch(:client_id)
|
288
|
+
group_id = event.payload.fetch(:group_id)
|
289
|
+
|
290
|
+
gauge("fetcher.#{client}.#{group_id}.queue_size", queue_size)
|
291
|
+
end
|
292
|
+
|
293
|
+
attach_to "fetcher.kafka"
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|