kafka_command 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +179 -0
  3. data/.env +1 -0
  4. data/.env.test +1 -0
  5. data/.gitignore +41 -0
  6. data/.rspec +1 -0
  7. data/.rubocop.yml +12 -0
  8. data/.ruby-version +1 -0
  9. data/Gemfile +17 -0
  10. data/Gemfile.lock +194 -0
  11. data/LICENSE +21 -0
  12. data/README.md +138 -0
  13. data/Rakefile +34 -0
  14. data/app/assets/config/manifest.js +3 -0
  15. data/app/assets/images/.keep +0 -0
  16. data/app/assets/images/kafka_command/cluster_view.png +0 -0
  17. data/app/assets/images/kafka_command/kafka.png +0 -0
  18. data/app/assets/images/kafka_command/topic_view.png +0 -0
  19. data/app/assets/javascripts/kafka_command/application.js +14 -0
  20. data/app/assets/stylesheets/kafka_command/application.css +27 -0
  21. data/app/assets/stylesheets/kafka_command/clusters.css +8 -0
  22. data/app/assets/stylesheets/kafka_command/topics.css +3 -0
  23. data/app/channels/application_cable/channel.rb +6 -0
  24. data/app/channels/application_cable/connection.rb +6 -0
  25. data/app/controllers/kafka_command/application_controller.rb +96 -0
  26. data/app/controllers/kafka_command/brokers_controller.rb +26 -0
  27. data/app/controllers/kafka_command/clusters_controller.rb +46 -0
  28. data/app/controllers/kafka_command/consumer_groups_controller.rb +44 -0
  29. data/app/controllers/kafka_command/topics_controller.rb +187 -0
  30. data/app/helpers/kafka_command/application_helper.rb +29 -0
  31. data/app/helpers/kafka_command/consumer_group_helper.rb +13 -0
  32. data/app/jobs/application_job.rb +6 -0
  33. data/app/mailers/application_mailer.rb +8 -0
  34. data/app/models/kafka_command/broker.rb +47 -0
  35. data/app/models/kafka_command/client.rb +102 -0
  36. data/app/models/kafka_command/cluster.rb +172 -0
  37. data/app/models/kafka_command/consumer_group.rb +142 -0
  38. data/app/models/kafka_command/consumer_group_partition.rb +23 -0
  39. data/app/models/kafka_command/group_member.rb +18 -0
  40. data/app/models/kafka_command/partition.rb +36 -0
  41. data/app/models/kafka_command/topic.rb +153 -0
  42. data/app/views/kafka_command/brokers/index.html.erb +38 -0
  43. data/app/views/kafka_command/clusters/_tabs.html.erb +9 -0
  44. data/app/views/kafka_command/clusters/index.html.erb +54 -0
  45. data/app/views/kafka_command/clusters/new.html.erb +115 -0
  46. data/app/views/kafka_command/configuration_error.html.erb +1 -0
  47. data/app/views/kafka_command/consumer_groups/index.html.erb +32 -0
  48. data/app/views/kafka_command/consumer_groups/show.html.erb +115 -0
  49. data/app/views/kafka_command/shared/_alert.html.erb +13 -0
  50. data/app/views/kafka_command/shared/_search_bar.html.erb +31 -0
  51. data/app/views/kafka_command/shared/_title.html.erb +6 -0
  52. data/app/views/kafka_command/topics/_form_fields.html.erb +49 -0
  53. data/app/views/kafka_command/topics/edit.html.erb +17 -0
  54. data/app/views/kafka_command/topics/index.html.erb +46 -0
  55. data/app/views/kafka_command/topics/new.html.erb +36 -0
  56. data/app/views/kafka_command/topics/show.html.erb +126 -0
  57. data/app/views/layouts/kafka_command/application.html.erb +50 -0
  58. data/bin/rails +16 -0
  59. data/config/initializers/kafka.rb +13 -0
  60. data/config/initializers/kafka_command.rb +11 -0
  61. data/config/routes.rb +11 -0
  62. data/docker-compose.yml +18 -0
  63. data/kafka_command.gemspec +27 -0
  64. data/lib/assets/.keep +0 -0
  65. data/lib/core_extensions/kafka/broker/attr_readers.rb +11 -0
  66. data/lib/core_extensions/kafka/broker_pool/attr_readers.rb +11 -0
  67. data/lib/core_extensions/kafka/client/attr_readers.rb +11 -0
  68. data/lib/core_extensions/kafka/cluster/attr_readers.rb +11 -0
  69. data/lib/core_extensions/kafka/protocol/metadata_response/partition_metadata/attr_readers.rb +15 -0
  70. data/lib/kafka_command/configuration.rb +150 -0
  71. data/lib/kafka_command/engine.rb +11 -0
  72. data/lib/kafka_command/errors.rb +6 -0
  73. data/lib/kafka_command/version.rb +5 -0
  74. data/lib/kafka_command.rb +13 -0
  75. data/lib/tasks/.keep +0 -0
  76. data/spec/dummy/Rakefile +6 -0
  77. data/spec/dummy/app/assets/config/manifest.js +4 -0
  78. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  79. data/spec/dummy/app/assets/javascripts/cable.js +13 -0
  80. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  81. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  82. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  83. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  84. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  85. data/spec/dummy/app/jobs/application_job.rb +2 -0
  86. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  87. data/spec/dummy/app/models/application_record.rb +3 -0
  88. data/spec/dummy/app/views/layouts/application.html.erb +15 -0
  89. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  90. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  91. data/spec/dummy/bin/bundle +3 -0
  92. data/spec/dummy/bin/rails +4 -0
  93. data/spec/dummy/bin/rake +4 -0
  94. data/spec/dummy/bin/setup +36 -0
  95. data/spec/dummy/bin/update +31 -0
  96. data/spec/dummy/bin/yarn +11 -0
  97. data/spec/dummy/config/application.rb +19 -0
  98. data/spec/dummy/config/boot.rb +5 -0
  99. data/spec/dummy/config/cable.yml +10 -0
  100. data/spec/dummy/config/database.yml +25 -0
  101. data/spec/dummy/config/environment.rb +5 -0
  102. data/spec/dummy/config/environments/development.rb +61 -0
  103. data/spec/dummy/config/environments/production.rb +94 -0
  104. data/spec/dummy/config/environments/test.rb +46 -0
  105. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  106. data/spec/dummy/config/initializers/assets.rb +14 -0
  107. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  108. data/spec/dummy/config/initializers/content_security_policy.rb +25 -0
  109. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  110. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  111. data/spec/dummy/config/initializers/inflections.rb +16 -0
  112. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  113. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  114. data/spec/dummy/config/kafka_command.yml +18 -0
  115. data/spec/dummy/config/locales/en.yml +33 -0
  116. data/spec/dummy/config/puma.rb +34 -0
  117. data/spec/dummy/config/routes.rb +3 -0
  118. data/spec/dummy/config/spring.rb +6 -0
  119. data/spec/dummy/config/ssl/test_ca_cert +1 -0
  120. data/spec/dummy/config/ssl/test_client_cert +1 -0
  121. data/spec/dummy/config/ssl/test_client_cert_key +1 -0
  122. data/spec/dummy/config/storage.yml +34 -0
  123. data/spec/dummy/config.ru +5 -0
  124. data/spec/dummy/db/schema.rb +42 -0
  125. data/spec/dummy/db/test.sqlite3 +0 -0
  126. data/spec/dummy/log/development.log +0 -0
  127. data/spec/dummy/log/hey.log +0 -0
  128. data/spec/dummy/log/test.log +2227 -0
  129. data/spec/dummy/package.json +5 -0
  130. data/spec/dummy/public/404.html +67 -0
  131. data/spec/dummy/public/422.html +67 -0
  132. data/spec/dummy/public/500.html +66 -0
  133. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  134. data/spec/dummy/public/apple-touch-icon.png +0 -0
  135. data/spec/dummy/public/favicon.ico +0 -0
  136. data/spec/examples.txt +165 -0
  137. data/spec/fast_helper.rb +20 -0
  138. data/spec/fixtures/files/kafka_command_sasl.yml +10 -0
  139. data/spec/fixtures/files/kafka_command_ssl.yml +10 -0
  140. data/spec/fixtures/files/kafka_command_ssl_file_paths.yml +11 -0
  141. data/spec/fixtures/files/kafka_command_staging.yml +8 -0
  142. data/spec/lib/kafka_command/configuration_spec.rb +311 -0
  143. data/spec/models/kafka_command/broker_spec.rb +83 -0
  144. data/spec/models/kafka_command/client_spec.rb +306 -0
  145. data/spec/models/kafka_command/cluster_spec.rb +163 -0
  146. data/spec/models/kafka_command/consumer_group_partition_spec.rb +43 -0
  147. data/spec/models/kafka_command/consumer_group_spec.rb +236 -0
  148. data/spec/models/kafka_command/partition_spec.rb +95 -0
  149. data/spec/models/kafka_command/topic_spec.rb +311 -0
  150. data/spec/rails_helper.rb +63 -0
  151. data/spec/requests/json/brokers_spec.rb +50 -0
  152. data/spec/requests/json/clusters_spec.rb +58 -0
  153. data/spec/requests/json/consumer_groups_spec.rb +139 -0
  154. data/spec/requests/json/topics_spec.rb +274 -0
  155. data/spec/spec_helper.rb +109 -0
  156. data/spec/support/factory_bot.rb +5 -0
  157. data/spec/support/json_helper.rb +13 -0
  158. data/spec/support/kafka_helper.rb +93 -0
  159. metadata +326 -0
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module KafkaCommand
6
+ class Cluster
7
+ extend Forwardable
8
+ DEFAULT_PROTOCOL = 'PLAINTEXT'
9
+
10
+ attr_reader :client,
11
+ :name,
12
+ :description,
13
+ :protocol,
14
+ :sasl_scram_username,
15
+ :sasl_scram_password,
16
+ :version,
17
+ :connect_timeout,
18
+ :socket_timeout
19
+
20
+ alias_method :id, :name
21
+
22
+ def_delegators :@client, :topics, :groups, :brokers
23
+
24
+ def initialize(name:, seed_brokers:, description: nil, protocol: DEFAULT_PROTOCOL,
25
+ sasl_scram_username: nil, sasl_scram_password: nil, ssl_ca_cert_file_path: nil,
26
+ ssl_client_cert_file_path: nil, ssl_client_cert_key_file_path: nil, version: nil,
27
+ ssl_ca_cert: nil, ssl_client_cert: nil, ssl_client_cert_key: nil, connect_timeout: nil,
28
+ socket_timeout: nil
29
+ )
30
+ @name = name
31
+ @seed_brokers = seed_brokers
32
+ @description = description
33
+ @sasl_scram_username = sasl_scram_username
34
+ @sasl_scram_password = sasl_scram_password
35
+ @ssl_ca_cert = ssl_ca_cert
36
+ @ssl_ca_cert_file_path = ssl_ca_cert_file_path
37
+ @ssl_client_cert = ssl_client_cert
38
+ @ssl_client_cert_file_path = ssl_client_cert_file_path
39
+ @ssl_client_cert_key = ssl_client_cert_key
40
+ @ssl_client_cert_key_file_path = ssl_client_cert_key_file_path
41
+ @version = version
42
+ @connect_timeout = connect_timeout
43
+ @socket_timeout = socket_timeout
44
+ @client = initialize_client
45
+ end
46
+
47
+ def connected?
48
+ # Tried using all?(&:connected?) here, but was getting some weird behavior with the views
49
+ brokers.map(&:connected?).all?
50
+ end
51
+
52
+ def create_topic(name, **kwargs)
53
+ client.create_topic(name, **kwargs)
54
+
55
+ # Give the cluster time to know about the topic
56
+ 3.times do
57
+ client.refresh_topics!
58
+ topic = topics.find { |t| t.name == name }
59
+ return topic if topic
60
+ end
61
+ end
62
+
63
+ def to_human
64
+ name.humanize.capitalize
65
+ end
66
+
67
+ def to_s
68
+ name
69
+ end
70
+
71
+ def ssl?
72
+ get_ssl_ca_cert.present?
73
+ end
74
+
75
+ def sasl?
76
+ sasl_scram_username.present? && sasl_scram_password.present?
77
+ end
78
+
79
+ def ==(other)
80
+ name == other.name
81
+ end
82
+
83
+ def self.find(cluster_name)
84
+ all.find { |c| c.name == cluster_name }
85
+ end
86
+
87
+ def self.none?
88
+ all.none?
89
+ end
90
+
91
+ def self.count
92
+ all.count
93
+ end
94
+
95
+ def self.all
96
+ KafkaCommand.config.clusters.map do |name, cluster_info|
97
+ new(
98
+ name: name,
99
+ seed_brokers: cluster_info['seed_brokers'],
100
+ protocol: cluster_info['protocol'],
101
+ description: cluster_info['description'],
102
+ connect_timeout: cluster_info['connect_timeout'],
103
+ socket_timeout: cluster_info['socket_timeout'],
104
+ sasl_scram_username: cluster_info['sasl_scram_username'],
105
+ sasl_scram_password: cluster_info['sasl_scram_password'],
106
+ ssl_ca_cert: cluster_info['ssl_ca_cert'],
107
+ ssl_ca_cert_file_path: cluster_info['ssl_ca_cert_file_path'],
108
+ ssl_client_cert: cluster_info['ssl_client_cert'],
109
+ ssl_client_cert_file_path: cluster_info['ssl_client_cert_file_path'],
110
+ ssl_client_cert_key: cluster_info['ssl_client_cert_key'],
111
+ ssl_client_cert_key_file_path: cluster_info['ssl_client_cert_key_file_path'],
112
+ )
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def get_ssl_ca_cert
119
+ return @ssl_ca_cert if @ssl_ca_cert
120
+
121
+ if @ssl_ca_cert_file_path && File.exists?(@ssl_ca_cert_file_path)
122
+ File.read(@ssl_ca_cert_file_path).strip
123
+ end
124
+ end
125
+
126
+ def get_ssl_client_cert
127
+ return @ssl_client_cert if @ssl_client_cert
128
+
129
+ if @ssl_client_cert_file_path && File.exists?(@ssl_client_cert_file_path)
130
+ File.read(@ssl_client_cert_file_path).strip
131
+ end
132
+ end
133
+
134
+ def get_ssl_client_cert_key
135
+ return @ssl_client_cert_key if @ssl_client_cert_key
136
+
137
+ if @ssl_client_cert_key_file_path && File.exists?(@ssl_client_cert_key_file_path)
138
+ File.read(@ssl_client_cert_key_file_path).strip
139
+ end
140
+ end
141
+
142
+ def seed_brokers
143
+ return @seed_brokers.split(',') if @seed_brokers.is_a?(String)
144
+ @seed_brokers
145
+ end
146
+
147
+ def initialize_client
148
+ @client ||= begin
149
+ client_kwargs = {
150
+ brokers: seed_brokers,
151
+ client_id: name
152
+ }
153
+
154
+ if sasl?
155
+ client_kwargs[:sasl_scram_username] = sasl_scram_username
156
+ client_kwargs[:sasl_scram_password] = sasl_scram_password
157
+ client_kwargs[:sasl_scram_mechanism] = 'sha256'
158
+ client_kwargs[:ssl_ca_cert] = get_ssl_ca_cert
159
+ elsif ssl?
160
+ client_kwargs[:ssl_ca_cert] = get_ssl_ca_cert
161
+ client_kwargs[:ssl_client_cert] = get_ssl_client_cert
162
+ client_kwargs[:ssl_client_cert_key] = get_ssl_client_cert_key
163
+ end
164
+
165
+ client_kwargs[:connect_timeout] = connect_timeout if connect_timeout
166
+ client_kwargs[:socket_timeout] = socket_timeout if socket_timeout
167
+
168
+ Client.new(**client_kwargs)
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KafkaCommand
4
+ class ConsumerGroup
5
+ attr_reader :group_id
6
+
7
+ def initialize(group_id, client)
8
+ @client = client
9
+ @group_id = group_id
10
+ end
11
+
12
+ def refresh!
13
+ clear_group_metadata!
14
+ end
15
+
16
+ def stable?
17
+ state.match?(/stable/i)
18
+ end
19
+
20
+ def empty?
21
+ state.match?(/empty/i) || members.none?
22
+ end
23
+
24
+ def partitions_for(topic_name)
25
+ topic = @client.find_topic(topic_name)
26
+ partition_lag = lag_for(topic.name)
27
+
28
+ topic.partitions.map do |p|
29
+ ConsumerGroupPartition.new(
30
+ lag: partition_lag[p.partition_id][:lag],
31
+ offset: partition_lag[p.partition_id][:offset],
32
+ group_id: @group_id,
33
+ topic_name: topic.name,
34
+ partition_id: p.partition_id
35
+ )
36
+ end
37
+ end
38
+
39
+ def total_lag_for(topic_name)
40
+ lag_for(topic_name).values.map { |lag_hash| lag_hash[:lag] || 0 }.reduce(:+)
41
+ end
42
+
43
+ def as_json(*)
44
+ topics_json = consumed_topics.map do |topic|
45
+ {
46
+ name: topic.name,
47
+ partitions: partitions_for(topic.name).map(&:as_json)
48
+ }
49
+ end
50
+
51
+ {
52
+ group_id: @group_id,
53
+ state: state,
54
+ topics: topics_json
55
+ }
56
+ end
57
+
58
+ def consumed_topics
59
+ topic_names = members.flat_map(&:topic_names).uniq
60
+
61
+ @client.topics.select do |t|
62
+ topic_names.include?(t.name)
63
+ end
64
+ end
65
+
66
+ def coordinator
67
+ @coordinator ||= @client.get_group_coordinator(group_id: @group_id)
68
+ end
69
+
70
+ def group_metadata
71
+ @group_metadata ||= initialize_group_metadata
72
+ end
73
+
74
+ def state
75
+ group_metadata.state
76
+ end
77
+
78
+ def members
79
+ group_metadata.members.map do |member|
80
+ GroupMember.new(member)
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def lag_for(topic_name)
87
+ topic = @client.find_topic(topic_name)
88
+ topic_offsets = topic.offsets
89
+ group_offsets = offsets_for(topic_name)
90
+
91
+ lag_hash = compute_lag(topic_offsets, group_offsets)
92
+ lag_hash.each_with_object({}) do |(partition_id, lag), return_hash|
93
+ return_hash[partition_id] = {
94
+ offset: group_offsets[partition_id],
95
+ lag: lag
96
+ }
97
+ end
98
+ end
99
+
100
+ def offsets_for(topic_name)
101
+ topic = @client.find_topic(topic_name)
102
+
103
+ offsets = coordinator.fetch_offsets(
104
+ group_id: @group_id,
105
+ topics: { topic.name => topic.partitions.map(&:partition_id) }
106
+ ).topics[topic.name]
107
+
108
+ offsets.keys.each do |partition_id|
109
+ if offsets[partition_id].offset == -1
110
+ offsets[partition_id] = nil
111
+ else
112
+ offsets[partition_id] = offsets[partition_id].offset
113
+ end
114
+ end
115
+
116
+ offsets
117
+ end
118
+
119
+ def compute_lag(topic_offsets, group_offsets)
120
+ topic_offsets.each_with_object({}) do |(partition_id, latest_offset), lag_hash|
121
+ lag =
122
+ if group_offsets[partition_id].nil?
123
+ nil
124
+ elsif group_offsets[partition_id] >= latest_offset
125
+ 0
126
+ else
127
+ latest_offset - group_offsets[partition_id]
128
+ end
129
+
130
+ lag_hash[partition_id] = lag
131
+ end
132
+ end
133
+
134
+ def initialize_group_metadata
135
+ @client.describe_group(@group_id)
136
+ end
137
+
138
+ def clear_group_metadata!
139
+ @group_metadata = nil
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KafkaCommand
4
+ class ConsumerGroupPartition
5
+ attr_reader :lag, :topic_name, :offset, :partition_id, :group_id
6
+
7
+ def initialize(lag:, topic_name:, offset:, group_id:, partition_id:)
8
+ @group_id = group_id
9
+ @lag = lag
10
+ @topic_name = topic_name
11
+ @offset = offset
12
+ @partition_id = partition_id
13
+ end
14
+
15
+ def as_json(*)
16
+ {
17
+ lag: @lag,
18
+ offset: @offset,
19
+ partition_id: @partition_id
20
+ }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KafkaCommand
4
+ class GroupMember
5
+ attr_reader :member_id, :client_host, :client_id, :topic_assignment
6
+
7
+ def initialize(member_metadata)
8
+ @member_id = member_metadata.member_id
9
+ @client_host = member_metadata.client_host
10
+ @client_id = member_metadata.client_id
11
+ @topic_assignment = member_metadata.member_assignment.topics
12
+ end
13
+
14
+ def topic_names
15
+ @topic_assignment.keys
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module KafkaCommand
6
+ class Partition
7
+ extend Forwardable
8
+ attr_reader :topic
9
+ def_delegators :@partition_metadata, :isr, :leader, :partition_id, :replicas
10
+
11
+ def initialize(partition_metadata, topic)
12
+ @topic = topic
13
+ @partition_metadata = partition_metadata
14
+ end
15
+
16
+ def highwater_mark_offset
17
+ @topic.offset_for(self)
18
+ end
19
+ alias offset highwater_mark_offset
20
+
21
+ def as_json(*)
22
+ {
23
+ isr: isr,
24
+ leader: leader,
25
+ id: partition_id,
26
+ highwater_mark_offset: offset
27
+ }
28
+ end
29
+
30
+ # TODO
31
+ #
32
+ # implement describe to retrieve important configs
33
+ # def describe
34
+ # end
35
+ end
36
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KafkaCommand
4
+ class Topic
5
+ attr_reader :name,
6
+ :partitions,
7
+ :replication_factor,
8
+ :max_message_bytes,
9
+ :retention_ms,
10
+ :retention_bytes
11
+
12
+ alias_method :id, :name
13
+
14
+ API_TIMEOUT = 10
15
+ CONSUMER_OFFSET_TOPIC = '__consumer_offsets'
16
+ DEFAULT_MAX_MESSAGE_BYTES = 1000012
17
+ DEFAULT_RETENTION_MS = 604800000
18
+ DEFAULT_RETENTION_BYTES = -1
19
+ TOPIC_CONFIGS = %w[
20
+ max.message.bytes
21
+ retention.bytes
22
+ retention.ms
23
+ ].freeze
24
+
25
+ class DeletionError < StandardError; end
26
+
27
+ def initialize(topic_metadata, client)
28
+ @client = client
29
+ @topic_metadata = topic_metadata
30
+ initialize_from_metadata
31
+ end
32
+
33
+ def brokers_spread
34
+ ((replication_factor.to_f / @client.brokers.count.to_f) * 100).round
35
+ end
36
+
37
+ def destroy
38
+ if name == CONSUMER_OFFSET_TOPIC
39
+ raise DeletionError, "Cannot delete the #{CONSUMER_OFFSET_TOPIC} topic"
40
+ end
41
+
42
+ @client.delete_topic(@name, timeout: API_TIMEOUT)
43
+ end
44
+
45
+ def set_configs!(max_message_bytes: nil, retention_ms: nil, retention_bytes: nil)
46
+ config = {}
47
+ config['max.message.bytes'] = max_message_bytes if max_message_bytes
48
+ config['retention.ms'] = retention_ms if retention_ms
49
+ config['retention.bytes'] = retention_bytes if retention_bytes
50
+
51
+ @client.alter_topic(@name, config) unless config.empty?
52
+ refresh!
53
+ end
54
+
55
+ def set_partitions!(num_partitions)
56
+ unless @client.supports_api?(Kafka::Protocol::CREATE_PARTITIONS_API)
57
+ raise UnsupportedApiError, 'This version of Kafka does not support the create partitions API.'
58
+ end
59
+
60
+ @client.create_partitions_for(
61
+ @name,
62
+ num_partitions: num_partitions,
63
+ timeout: API_TIMEOUT
64
+ )
65
+
66
+ refresh!
67
+ end
68
+
69
+ def refresh!
70
+ @topic_metadata = @client.fetch_metadata(topics: [@name]).topics.first
71
+ initialize_from_metadata
72
+ end
73
+
74
+ def offset_for(partition)
75
+ tries = 0
76
+ @client.resolve_offset(@name, partition.partition_id, :latest)
77
+ rescue Kafka::UnknownTopicOrPartition
78
+ raise if tries >= 3
79
+ tries += 1
80
+ retry
81
+ end
82
+
83
+ def offsets(partition_ids = nil)
84
+ partition_ids ||= @partitions.map(&:partition_id)
85
+ @client.resolve_offsets(@name, partition_ids, :latest)
86
+ end
87
+
88
+ def offset_sum
89
+ offsets.values.reduce(:+)
90
+ end
91
+
92
+ def consumer_offset_topic?
93
+ name == CONSUMER_OFFSET_TOPIC
94
+ end
95
+
96
+ def groups
97
+ @client.groups.select do |g|
98
+ g.consumed_topics.include?(self)
99
+ end
100
+ end
101
+
102
+ # Needs arguments to be compatible with rails as_json calls
103
+ def as_json(include_config: false, **kwargs)
104
+ json = {
105
+ name: @name,
106
+ replication_factor: @replication_factor,
107
+ partitions: @partitions.sort_by(&:partition_id).map(&:as_json)
108
+ }
109
+
110
+ if include_config
111
+ json[:config] = {
112
+ max_message_bytes: max_message_bytes,
113
+ retention_ms: retention_ms,
114
+ retention_bytes: retention_bytes
115
+ }
116
+ end
117
+
118
+ json
119
+ end
120
+
121
+ def ==(other)
122
+ @name == other.name
123
+ end
124
+
125
+ def topic_configs
126
+ @topic_configs ||= describe
127
+ end
128
+
129
+ def retention_ms
130
+ topic_configs['retention.ms'].to_i
131
+ end
132
+
133
+ def retention_bytes
134
+ topic_configs['retention.bytes'].to_i
135
+ end
136
+
137
+ def max_message_bytes
138
+ topic_configs['max.message.bytes'].to_i
139
+ end
140
+
141
+ private
142
+
143
+ def describe
144
+ @client.describe_topic(@name, TOPIC_CONFIGS)
145
+ end
146
+
147
+ def initialize_from_metadata
148
+ @name = @topic_metadata.topic_name
149
+ @partitions = @topic_metadata.partitions.map { |pm| Partition.new(pm, self) }
150
+ @replication_factor = @partitions.map(&:isr).map(&:length).max
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,38 @@
1
+ <%= render partial: 'kafka_command/shared/title', locals: { title: @cluster.to_human, subtitle: 'Brokers' } %>
2
+
3
+ <div class="level"a>
4
+ <div class="level-left">
5
+ <div class="level-item" style="margin-left: 0.2rem">
6
+ <p class="subtitle is-5">
7
+ <strong><%= @brokers.count %></strong> brokers
8
+ </p>
9
+ </div>
10
+ </div>
11
+ </div>
12
+
13
+ <table class="table is-striped is-fullwidth is-bordered">
14
+ <thead>
15
+ <th>Id</th>
16
+ <th>Host</th>
17
+ <th>Connection Status</th>
18
+ </thead>
19
+ <tbody>
20
+ <% @brokers.each do |broker| %>
21
+ <tr>
22
+ <td><%= broker.kafka_broker_id %></td>
23
+ <td><%= broker.host %></td>
24
+ <td>
25
+ <% if broker.connected? %>
26
+ <span class="icon has-text-success">
27
+ <i class="fas fa-check-circle"></i>
28
+ </span>
29
+ <% else %>
30
+ <span class="icon has-text-danger">
31
+ <i class="fas fa-times-circle"></i>
32
+ </span>
33
+ <% end %>
34
+ </td>
35
+ </tr>
36
+ <% end %>
37
+ </tbody>
38
+ </table>
@@ -0,0 +1,9 @@
1
+ <%= render partial: 'kafka_command/shared/title', locals: { title: @cluster.to_human, subtitle: @cluster.description } %>
2
+
3
+ <div class="tabs">
4
+ <ul>
5
+ <li class="<%= tab_class('topics') %>"><%= link_to 'Topics', cluster_topics_path(@cluster) %></li>
6
+ <li class="<%= tab_class('consumer_groups') %>"><%= link_to 'Consumer Groups', cluster_consumer_groups_path(@cluster) %></li>
7
+ <li class="<%= tab_class('brokers') %>"><%= link_to 'Brokers', cluster_brokers_path(@cluster) %></li>
8
+ </ul>
9
+ </div>
@@ -0,0 +1,54 @@
1
+ <%= render partial: 'kafka_command/shared/title', locals: { title: 'Clusters', subtitle: 'Manage multiple clusters' } %>
2
+
3
+ <div class="level"a>
4
+ <%=
5
+ render(
6
+ partial: 'kafka_command/shared/search_bar',
7
+ locals: {
8
+ resources: @clusters,
9
+ resource_name: 'cluster',
10
+ resource_path: clusters_path,
11
+ filter_property: :name
12
+ }
13
+ )
14
+ %>
15
+ </div>
16
+
17
+ <table class="table is-striped is-fullwidth is-bordered">
18
+ <thead>
19
+ <th>Name</th>
20
+ <th>Description</th>
21
+ <th>Brokers</th>
22
+ <th>Protocol</th>
23
+ <th>Connection Status</th>
24
+ </thead>
25
+ <tbody>
26
+ <% @clusters.each do |cluster| %>
27
+ <tr>
28
+ <td><%= link_to cluster.name, cluster_topics_path(cluster) %></td>
29
+ <td><%= cluster.description %></td>
30
+ <td><%= cluster.brokers.count %></td>
31
+ <td>
32
+ <% if cluster.ssl? %>
33
+ SSL
34
+ <% elsif cluster.sasl? %>
35
+ SASL/SCRAM
36
+ <% else %>
37
+ PLAINTEXT
38
+ <% end %>
39
+ </td>
40
+ <td>
41
+ <% if cluster.connected? %>
42
+ <span class="icon has-text-success">
43
+ <i class="fas fa-check-circle"></i>
44
+ </span>
45
+ <% else %>
46
+ <span class="icon has-text-danger">
47
+ <i class="fas fa-times-circle"></i>
48
+ </span>
49
+ <% end %>
50
+ </td>
51
+ </tr>
52
+ <% end %>
53
+ </tbody>
54
+ </table>