karafka-web 0.6.3 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (214) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/ci.yml +13 -4
  4. data/CHANGELOG.md +119 -5
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +27 -24
  7. data/README.md +2 -0
  8. data/bin/rspecs +6 -0
  9. data/certs/cert_chain.pem +21 -21
  10. data/docker-compose.yml +22 -0
  11. data/karafka-web.gemspec +3 -3
  12. data/lib/karafka/web/app.rb +6 -2
  13. data/lib/karafka/web/cli.rb +51 -47
  14. data/lib/karafka/web/config.rb +33 -9
  15. data/lib/karafka/web/contracts/base.rb +32 -0
  16. data/lib/karafka/web/contracts/config.rb +63 -0
  17. data/lib/karafka/web/deserializer.rb +10 -1
  18. data/lib/karafka/web/errors.rb +29 -7
  19. data/lib/karafka/web/installer.rb +58 -148
  20. data/lib/karafka/web/management/base.rb +34 -0
  21. data/lib/karafka/web/management/clean_boot_file.rb +31 -0
  22. data/lib/karafka/web/management/create_initial_states.rb +101 -0
  23. data/lib/karafka/web/management/create_topics.rb +127 -0
  24. data/lib/karafka/web/management/delete_topics.rb +28 -0
  25. data/lib/karafka/web/management/enable.rb +82 -0
  26. data/lib/karafka/web/management/extend_boot_file.rb +37 -0
  27. data/lib/karafka/web/processing/consumer.rb +73 -17
  28. data/lib/karafka/web/processing/consumers/aggregators/base.rb +56 -0
  29. data/lib/karafka/web/processing/consumers/aggregators/metrics.rb +154 -0
  30. data/lib/karafka/web/processing/consumers/aggregators/state.rb +180 -0
  31. data/lib/karafka/web/processing/consumers/contracts/aggregated_stats.rb +32 -0
  32. data/lib/karafka/web/processing/consumers/contracts/metrics.rb +53 -0
  33. data/lib/karafka/web/processing/consumers/contracts/process.rb +19 -0
  34. data/lib/karafka/web/processing/consumers/contracts/state.rb +49 -0
  35. data/lib/karafka/web/processing/consumers/contracts/topic_stats.rb +21 -0
  36. data/lib/karafka/web/processing/consumers/metrics.rb +29 -0
  37. data/lib/karafka/web/processing/consumers/schema_manager.rb +56 -0
  38. data/lib/karafka/web/processing/consumers/state.rb +6 -9
  39. data/lib/karafka/web/processing/time_series_tracker.rb +130 -0
  40. data/lib/karafka/web/tracking/consumers/contracts/consumer_group.rb +2 -2
  41. data/lib/karafka/web/tracking/consumers/contracts/job.rb +2 -1
  42. data/lib/karafka/web/tracking/consumers/contracts/partition.rb +14 -1
  43. data/lib/karafka/web/tracking/consumers/contracts/report.rb +10 -8
  44. data/lib/karafka/web/tracking/consumers/contracts/subscription_group.rb +2 -2
  45. data/lib/karafka/web/tracking/consumers/contracts/topic.rb +2 -2
  46. data/lib/karafka/web/tracking/consumers/listeners/processing.rb +6 -2
  47. data/lib/karafka/web/tracking/consumers/listeners/statistics.rb +15 -1
  48. data/lib/karafka/web/tracking/consumers/reporter.rb +14 -6
  49. data/lib/karafka/web/tracking/consumers/sampler.rb +80 -39
  50. data/lib/karafka/web/tracking/contracts/error.rb +2 -1
  51. data/lib/karafka/web/ui/app.rb +20 -10
  52. data/lib/karafka/web/ui/base.rb +56 -6
  53. data/lib/karafka/web/ui/controllers/base.rb +28 -0
  54. data/lib/karafka/web/ui/controllers/become_pro.rb +1 -1
  55. data/lib/karafka/web/ui/controllers/cluster.rb +12 -6
  56. data/lib/karafka/web/ui/controllers/consumers.rb +4 -2
  57. data/lib/karafka/web/ui/controllers/dashboard.rb +32 -0
  58. data/lib/karafka/web/ui/controllers/errors.rb +19 -6
  59. data/lib/karafka/web/ui/controllers/jobs.rb +4 -2
  60. data/lib/karafka/web/ui/controllers/requests/params.rb +28 -0
  61. data/lib/karafka/web/ui/controllers/responses/redirect.rb +29 -0
  62. data/lib/karafka/web/ui/helpers/application_helper.rb +57 -14
  63. data/lib/karafka/web/ui/helpers/paths_helper.rb +48 -0
  64. data/lib/karafka/web/ui/lib/hash_proxy.rb +18 -6
  65. data/lib/karafka/web/ui/lib/paginations/base.rb +61 -0
  66. data/lib/karafka/web/ui/lib/paginations/offset_based.rb +96 -0
  67. data/lib/karafka/web/ui/lib/paginations/page_based.rb +70 -0
  68. data/lib/karafka/web/ui/lib/paginations/paginators/arrays.rb +33 -0
  69. data/lib/karafka/web/ui/lib/paginations/paginators/base.rb +23 -0
  70. data/lib/karafka/web/ui/lib/paginations/paginators/partitions.rb +52 -0
  71. data/lib/karafka/web/ui/lib/paginations/paginators/sets.rb +85 -0
  72. data/lib/karafka/web/ui/lib/paginations/watermark_offsets_based.rb +75 -0
  73. data/lib/karafka/web/ui/lib/ttl_cache.rb +82 -0
  74. data/lib/karafka/web/ui/models/cluster_info.rb +59 -0
  75. data/lib/karafka/web/ui/models/consumers_metrics.rb +46 -0
  76. data/lib/karafka/web/ui/models/{state.rb → consumers_state.rb} +6 -2
  77. data/lib/karafka/web/ui/models/health.rb +37 -7
  78. data/lib/karafka/web/ui/models/message.rb +123 -39
  79. data/lib/karafka/web/ui/models/metrics/aggregated.rb +196 -0
  80. data/lib/karafka/web/ui/models/metrics/charts/aggregated.rb +50 -0
  81. data/lib/karafka/web/ui/models/metrics/charts/topics.rb +109 -0
  82. data/lib/karafka/web/ui/models/metrics/topics.rb +101 -0
  83. data/lib/karafka/web/ui/models/partition.rb +27 -0
  84. data/lib/karafka/web/ui/models/process.rb +12 -1
  85. data/lib/karafka/web/ui/models/status.rb +110 -22
  86. data/lib/karafka/web/ui/models/visibility_filter.rb +33 -0
  87. data/lib/karafka/web/ui/pro/app.rb +87 -19
  88. data/lib/karafka/web/ui/pro/controllers/cluster.rb +11 -0
  89. data/lib/karafka/web/ui/pro/controllers/consumers.rb +13 -7
  90. data/lib/karafka/web/ui/pro/controllers/dashboard.rb +54 -0
  91. data/lib/karafka/web/ui/pro/controllers/dlq.rb +1 -2
  92. data/lib/karafka/web/ui/pro/controllers/errors.rb +46 -10
  93. data/lib/karafka/web/ui/pro/controllers/explorer.rb +145 -15
  94. data/lib/karafka/web/ui/pro/controllers/health.rb +10 -2
  95. data/lib/karafka/web/ui/pro/controllers/messages.rb +62 -0
  96. data/lib/karafka/web/ui/pro/controllers/routing.rb +44 -0
  97. data/lib/karafka/web/ui/pro/views/consumers/_breadcrumbs.erb +7 -1
  98. data/lib/karafka/web/ui/pro/views/consumers/_consumer.erb +1 -1
  99. data/lib/karafka/web/ui/pro/views/consumers/_counters.erb +7 -5
  100. data/lib/karafka/web/ui/pro/views/consumers/consumer/_job.erb +3 -3
  101. data/lib/karafka/web/ui/pro/views/consumers/consumer/_metrics.erb +5 -4
  102. data/lib/karafka/web/ui/pro/views/consumers/consumer/_partition.erb +13 -4
  103. data/lib/karafka/web/ui/pro/views/consumers/consumer/_subscription_group.erb +3 -2
  104. data/lib/karafka/web/ui/pro/views/consumers/consumer/_tabs.erb +7 -0
  105. data/lib/karafka/web/ui/pro/views/consumers/details.erb +21 -0
  106. data/lib/karafka/web/ui/pro/views/consumers/index.erb +4 -2
  107. data/lib/karafka/web/ui/pro/views/dashboard/_ranges_selector.erb +39 -0
  108. data/lib/karafka/web/ui/pro/views/dashboard/index.erb +82 -0
  109. data/lib/karafka/web/ui/pro/views/dlq/_topic.erb +1 -1
  110. data/lib/karafka/web/ui/pro/views/errors/_breadcrumbs.erb +8 -6
  111. data/lib/karafka/web/ui/pro/views/errors/_error.erb +2 -2
  112. data/lib/karafka/web/ui/pro/views/errors/_partition_option.erb +1 -1
  113. data/lib/karafka/web/ui/pro/views/errors/_table.erb +21 -0
  114. data/lib/karafka/web/ui/pro/views/errors/_title_with_select.erb +31 -0
  115. data/lib/karafka/web/ui/pro/views/errors/index.erb +9 -56
  116. data/lib/karafka/web/ui/pro/views/errors/partition.erb +17 -0
  117. data/lib/karafka/web/ui/pro/views/errors/show.erb +1 -1
  118. data/lib/karafka/web/ui/pro/views/explorer/_breadcrumbs.erb +6 -4
  119. data/lib/karafka/web/ui/pro/views/explorer/_filtered.erb +16 -0
  120. data/lib/karafka/web/ui/pro/views/explorer/_message.erb +14 -4
  121. data/lib/karafka/web/ui/pro/views/explorer/_no_topics.erb +7 -0
  122. data/lib/karafka/web/ui/pro/views/explorer/_partition_option.erb +3 -3
  123. data/lib/karafka/web/ui/pro/views/explorer/_topic.erb +1 -1
  124. data/lib/karafka/web/ui/pro/views/explorer/index.erb +12 -8
  125. data/lib/karafka/web/ui/pro/views/explorer/messages/_headers.erb +15 -0
  126. data/lib/karafka/web/ui/pro/views/explorer/messages/_key.erb +12 -0
  127. data/lib/karafka/web/ui/pro/views/explorer/partition/_details.erb +35 -0
  128. data/lib/karafka/web/ui/pro/views/explorer/partition/_messages.erb +1 -0
  129. data/lib/karafka/web/ui/pro/views/explorer/partition.erb +6 -4
  130. data/lib/karafka/web/ui/pro/views/explorer/show.erb +48 -5
  131. data/lib/karafka/web/ui/pro/views/explorer/topic/_details.erb +23 -0
  132. data/lib/karafka/web/ui/pro/views/explorer/topic/_empty.erb +3 -0
  133. data/lib/karafka/web/ui/pro/views/explorer/topic/_limited.erb +4 -0
  134. data/lib/karafka/web/ui/pro/views/explorer/topic.erb +51 -0
  135. data/lib/karafka/web/ui/pro/views/health/_breadcrumbs.erb +16 -0
  136. data/lib/karafka/web/ui/pro/views/health/_no_data.erb +9 -0
  137. data/lib/karafka/web/ui/pro/views/health/_partition.erb +17 -15
  138. data/lib/karafka/web/ui/pro/views/health/_partition_offset.erb +40 -0
  139. data/lib/karafka/web/ui/pro/views/health/_tabs.erb +27 -0
  140. data/lib/karafka/web/ui/pro/views/health/offsets.erb +71 -0
  141. data/lib/karafka/web/ui/pro/views/health/overview.erb +68 -0
  142. data/lib/karafka/web/ui/pro/views/jobs/_job.erb +6 -3
  143. data/lib/karafka/web/ui/pro/views/jobs/index.erb +4 -1
  144. data/lib/karafka/web/ui/pro/views/routing/_consumer_group.erb +37 -0
  145. data/lib/karafka/web/ui/pro/views/routing/_detail.erb +25 -0
  146. data/lib/karafka/web/ui/pro/views/routing/_topic.erb +23 -0
  147. data/lib/karafka/web/ui/pro/views/routing/index.erb +10 -0
  148. data/lib/karafka/web/ui/pro/views/routing/show.erb +26 -0
  149. data/lib/karafka/web/ui/pro/views/shared/_navigation.erb +7 -10
  150. data/lib/karafka/web/ui/public/images/logo-gray.svg +28 -0
  151. data/lib/karafka/web/ui/public/javascripts/application.js +30 -0
  152. data/lib/karafka/web/ui/public/javascripts/chart.min.js +14 -0
  153. data/lib/karafka/web/ui/public/javascripts/charts.js +330 -0
  154. data/lib/karafka/web/ui/public/javascripts/datepicker.js +6 -0
  155. data/lib/karafka/web/ui/public/javascripts/live_poll.js +39 -12
  156. data/lib/karafka/web/ui/public/javascripts/offset_datetime.js +74 -0
  157. data/lib/karafka/web/ui/public/javascripts/tabs.js +59 -0
  158. data/lib/karafka/web/ui/public/stylesheets/application.css +11 -0
  159. data/lib/karafka/web/ui/public/stylesheets/datepicker.min.css +12 -0
  160. data/lib/karafka/web/ui/views/cluster/_no_partitions.erb +3 -0
  161. data/lib/karafka/web/ui/views/cluster/_partition.erb +20 -22
  162. data/lib/karafka/web/ui/views/cluster/index.erb +6 -1
  163. data/lib/karafka/web/ui/views/consumers/_consumer.erb +1 -1
  164. data/lib/karafka/web/ui/views/consumers/_counters.erb +6 -4
  165. data/lib/karafka/web/ui/views/consumers/_summary.erb +3 -3
  166. data/lib/karafka/web/ui/views/consumers/index.erb +3 -1
  167. data/lib/karafka/web/ui/views/dashboard/_feature_pro.erb +3 -0
  168. data/lib/karafka/web/ui/views/dashboard/_not_enough_data.erb +15 -0
  169. data/lib/karafka/web/ui/views/dashboard/_ranges_selector.erb +23 -0
  170. data/lib/karafka/web/ui/views/dashboard/index.erb +95 -0
  171. data/lib/karafka/web/ui/views/errors/_detail.erb +12 -0
  172. data/lib/karafka/web/ui/views/errors/_error.erb +2 -2
  173. data/lib/karafka/web/ui/views/errors/show.erb +1 -1
  174. data/lib/karafka/web/ui/views/jobs/index.erb +3 -1
  175. data/lib/karafka/web/ui/views/layout.erb +10 -3
  176. data/lib/karafka/web/ui/views/routing/_consumer_group.erb +8 -6
  177. data/lib/karafka/web/ui/views/routing/_detail.erb +2 -2
  178. data/lib/karafka/web/ui/views/routing/_topic.erb +1 -1
  179. data/lib/karafka/web/ui/views/routing/show.erb +1 -1
  180. data/lib/karafka/web/ui/views/shared/_brand.erb +2 -2
  181. data/lib/karafka/web/ui/views/shared/_chart.erb +14 -0
  182. data/lib/karafka/web/ui/views/shared/_content.erb +2 -2
  183. data/lib/karafka/web/ui/views/shared/_feature_pro.erb +1 -1
  184. data/lib/karafka/web/ui/views/shared/_flashes.erb +9 -0
  185. data/lib/karafka/web/ui/views/shared/_footer.erb +22 -0
  186. data/lib/karafka/web/ui/views/shared/_header.erb +15 -9
  187. data/lib/karafka/web/ui/views/shared/_live_poll.erb +7 -0
  188. data/lib/karafka/web/ui/views/shared/_navigation.erb +5 -8
  189. data/lib/karafka/web/ui/views/shared/_no_paginated_data.erb +9 -0
  190. data/lib/karafka/web/ui/views/shared/_pagination.erb +17 -13
  191. data/lib/karafka/web/ui/views/shared/_tab_nav.erb +7 -0
  192. data/lib/karafka/web/ui/views/shared/exceptions/not_found.erb +34 -32
  193. data/lib/karafka/web/ui/views/shared/exceptions/pro_only.erb +45 -43
  194. data/lib/karafka/web/ui/views/status/failures/_consumers_reports_schema_state.erb +15 -0
  195. data/lib/karafka/web/ui/views/status/failures/_enabled.erb +8 -0
  196. data/lib/karafka/web/ui/views/status/failures/_initial_consumers_metrics.erb +11 -0
  197. data/lib/karafka/web/ui/views/status/failures/{_initial_state.erb → _initial_consumers_state.erb} +3 -3
  198. data/lib/karafka/web/ui/views/status/failures/_partitions.erb +14 -6
  199. data/lib/karafka/web/ui/views/status/info/_components.erb +21 -1
  200. data/lib/karafka/web/ui/views/status/show.erb +62 -5
  201. data/lib/karafka/web/ui/views/status/successes/_enabled.erb +1 -0
  202. data/lib/karafka/web/ui/views/status/warnings/_replication.erb +19 -0
  203. data/lib/karafka/web/version.rb +1 -1
  204. data/lib/karafka/web.rb +11 -0
  205. data.tar.gz.sig +0 -0
  206. metadata +124 -39
  207. metadata.gz.sig +0 -0
  208. data/lib/karafka/web/processing/consumers/aggregator.rb +0 -130
  209. data/lib/karafka/web/tracking/contracts/base.rb +0 -34
  210. data/lib/karafka/web/ui/lib/paginate_array.rb +0 -38
  211. data/lib/karafka/web/ui/pro/views/explorer/_encryption_enabled.erb +0 -18
  212. data/lib/karafka/web/ui/pro/views/explorer/partition/_watermark_offsets.erb +0 -10
  213. data/lib/karafka/web/ui/pro/views/health/index.erb +0 -60
  214. /data/lib/karafka/web/ui/pro/views/explorer/{_detail.erb → messages/_detail.erb} +0 -0
@@ -9,7 +9,7 @@ module Karafka
9
9
  #
10
10
  # Any outgoing reporting needs to match this format for it to work with the statuses
11
11
  # consumer.
12
- class Report < Tracking::Contracts::Base
12
+ class Report < Web::Contracts::Base
13
13
  configure
14
14
 
15
15
  required(:schema_version) { |val| val.is_a?(String) && !val.empty? }
@@ -21,12 +21,14 @@ module Karafka
21
21
  nested(:process) do
22
22
  required(:started_at) { |val| val.is_a?(Numeric) && val.positive? }
23
23
  required(:name) { |val| val.is_a?(String) && val.count(':') >= 2 }
24
+ required(:cpus) { |val| val.is_a?(Integer) && val >= 1 }
24
25
  required(:memory_usage) { |val| val.is_a?(Integer) && val >= 0 }
25
26
  required(:memory_total_usage) { |val| val.is_a?(Integer) && val >= 0 }
26
27
  required(:memory_size) { |val| val.is_a?(Integer) && val >= 0 }
27
28
  required(:status) { |val| ::Karafka::Status::STATES.key?(val.to_s.to_sym) }
29
+ required(:threads) { |val| val.is_a?(Integer) && val >= 0 }
28
30
  required(:listeners) { |val| val.is_a?(Integer) && val >= 0 }
29
- required(:concurrency) { |val| val.is_a?(Integer) && val.positive? }
31
+ required(:workers) { |val| val.is_a?(Integer) && val.positive? }
30
32
  required(:tags) { |val| val.is_a?(Karafka::Core::Taggable::Tags) }
31
33
 
32
34
  required(:cpu_usage) do |val|
@@ -53,11 +55,11 @@ module Karafka
53
55
  required(:utilization) { |val| val.is_a?(Numeric) && val >= 0 }
54
56
 
55
57
  nested(:total) do
56
- required(:batches) { |val| val.is_a?(Numeric) && val >= 0 }
57
- required(:messages) { |val| val.is_a?(Numeric) && val >= 0 }
58
- required(:errors) { |val| val.is_a?(Numeric) && val >= 0 }
59
- required(:retries) { |val| val.is_a?(Numeric) && val >= 0 }
60
- required(:dead) { |val| val.is_a?(Numeric) && val >= 0 }
58
+ required(:batches) { |val| val.is_a?(Integer) && val >= 0 }
59
+ required(:messages) { |val| val.is_a?(Integer) && val >= 0 }
60
+ required(:errors) { |val| val.is_a?(Integer) && val >= 0 }
61
+ required(:retries) { |val| val.is_a?(Integer) && val >= 0 }
62
+ required(:dead) { |val| val.is_a?(Integer) && val >= 0 }
61
63
  end
62
64
  end
63
65
 
@@ -73,7 +75,7 @@ module Karafka
73
75
  cg_contract = ConsumerGroup.new
74
76
 
75
77
  # Consumer group id (key) is irrelevant because it is also in the details
76
- data.fetch(:consumer_groups).each do |_, details|
78
+ data.fetch(:consumer_groups).each_value do |details|
77
79
  cg_contract.validate!(details)
78
80
  end
79
81
 
@@ -7,7 +7,7 @@ module Karafka
7
7
  module Contracts
8
8
  # Expected data for each subscription group
9
9
  # It's mostly about topics details
10
- class SubscriptionGroup < Tracking::Contracts::Base
10
+ class SubscriptionGroup < Web::Contracts::Base
11
11
  configure
12
12
 
13
13
  required(:id) { |val| val.is_a?(String) && !val.empty? }
@@ -19,7 +19,7 @@ module Karafka
19
19
 
20
20
  topic_contract = Topic.new
21
21
 
22
- data.fetch(:topics).each do |_topic_name, details|
22
+ data.fetch(:topics).each_value do |details|
23
23
  topic_contract.validate!(details)
24
24
  end
25
25
 
@@ -6,7 +6,7 @@ module Karafka
6
6
  module Consumers
7
7
  module Contracts
8
8
  # Expected topic information that needs to go out
9
- class Topic < Tracking::Contracts::Base
9
+ class Topic < Web::Contracts::Base
10
10
  configure
11
11
 
12
12
  required(:name) { |val| val.is_a?(String) && !val.empty? }
@@ -17,7 +17,7 @@ module Karafka
17
17
 
18
18
  partition_contract = Partition.new
19
19
 
20
- data.fetch(:partitions).each do |_partition_id, details|
20
+ data.fetch(:partitions).each_value do |details|
21
21
  partition_contract.validate!(details)
22
22
  end
23
23
 
@@ -21,7 +21,7 @@ module Karafka
21
21
  # @param event [Karafka::Core::Monitoring::Event]
22
22
  def on_consumer_consume(event)
23
23
  consumer = event.payload[:caller]
24
- messages_count = consumer.messages.count
24
+ messages_count = consumer.messages.size
25
25
  jid = job_id(consumer, 'consume')
26
26
  job_details = job_details(consumer, 'consume')
27
27
 
@@ -73,7 +73,7 @@ module Karafka
73
73
  consumer = event.payload[:caller]
74
74
  topic = consumer.topic
75
75
  consumer_group_id = topic.consumer_group.id
76
- messages_count = consumer.messages.count
76
+ messages_count = consumer.messages.size
77
77
  time = event[:time]
78
78
  jid = job_id(consumer, 'consume')
79
79
 
@@ -160,6 +160,10 @@ module Karafka
160
160
  processing_lag: consumer.messages.metadata.processing_lag,
161
161
  consumption_lag: consumer.messages.metadata.consumption_lag,
162
162
  committed_offset: consumer.coordinator.seek_offset - 1,
163
+ # In theory this is redundant because we have first and last offset, but it is
164
+ # needed because VPs do not have linear count. For VPs first and last offset
165
+ # will be further away than the total messages count for a particular VP
166
+ messages: consumer.messages.size,
163
167
  consumer: consumer.class.to_s,
164
168
  consumer_group: consumer.topic.consumer_group.id,
165
169
  type: type,
@@ -104,11 +104,25 @@ module Karafka
104
104
  # @return [Hash] extracted partition metrics
105
105
  def extract_partition_metrics(pt_stats)
106
106
  metrics = pt_stats.slice(
107
+ 'consumer_lag',
108
+ 'consumer_lag_d',
107
109
  'consumer_lag_stored',
108
110
  'consumer_lag_stored_d',
109
111
  'committed_offset',
112
+ # Can be useful to track the frequency of flushes when there is progress
113
+ 'committed_offset_fd',
110
114
  'stored_offset',
111
- 'fetch_state'
115
+ # Can be useful to track the frequency of flushes when there is progress
116
+ 'stored_offset_fd',
117
+ 'fetch_state',
118
+ 'hi_offset',
119
+ 'hi_offset_fd',
120
+ 'lo_offset',
121
+ 'eof_offset',
122
+ 'ls_offset',
123
+ # Two below can be useful for detection of hanging transactions
124
+ 'ls_offset_d',
125
+ 'ls_offset_fd'
112
126
  )
113
127
 
114
128
  # Rename as we do not need `consumer_` prefix
@@ -32,6 +32,14 @@ module Karafka
32
32
  # @param forced [Boolean] should we report bypassing the time frequency or should we
33
33
  # report only in case we would not send the report for long enough time.
34
34
  def report(forced: false)
35
+ # Do not even mutex if not needed
36
+ return unless report?(forced)
37
+
38
+ # We run this sampling before the mutex so sampling does not stop things in case
39
+ # other threads would need this mutex. This can take up to 25ms and we do not want to
40
+ # block during this time
41
+ sampler.sample
42
+
35
43
  MUTEX.synchronize do
36
44
  # Start background thread only when needed
37
45
  # This prevents us from starting it too early or for non-consumer processes where
@@ -52,9 +60,10 @@ module Karafka
52
60
  messages = [
53
61
  {
54
62
  topic: ::Karafka::Web.config.topics.consumers.reports,
55
- payload: report.to_json,
63
+ payload: Zlib::Deflate.deflate(report.to_json),
56
64
  key: process_name,
57
- partition: 0
65
+ partition: 0,
66
+ headers: { 'zlib' => 'true' }
58
67
  }
59
68
  ]
60
69
 
@@ -64,14 +73,13 @@ module Karafka
64
73
 
65
74
  {
66
75
  topic: Karafka::Web.config.topics.errors,
67
- payload: error.to_json,
76
+ payload: Zlib::Deflate.deflate(error.to_json),
68
77
  # Always dispatch errors from the same process to the same partition
69
- key: process_name
78
+ key: process_name,
79
+ headers: { 'zlib' => 'true' }
70
80
  }
71
81
  end
72
82
 
73
- return if messages.empty?
74
-
75
83
  produce(messages)
76
84
 
77
85
  # Clear the sampler so it tracks new state changes without previous once impacting
@@ -14,7 +14,7 @@ module Karafka
14
14
  # Current schema version
15
15
  # This can be used in the future for detecting incompatible changes and writing
16
16
  # migrations
17
- SCHEMA_VERSION = '1.2.0'
17
+ SCHEMA_VERSION = '1.2.3'
18
18
 
19
19
  # 60 seconds window for time tracked window-based metrics
20
20
  TIMES_TTL = 60
@@ -22,18 +22,26 @@ module Karafka
22
22
  # Times ttl in ms
23
23
  TIMES_TTL_MS = TIMES_TTL * 1_000
24
24
 
25
- private_constant :TIMES_TTL, :TIMES_TTL_MS, :SCHEMA_VERSION
25
+ # Counters that count events occurrences during the given window
26
+ COUNTERS_BASE = {
27
+ # Number of processed batches
28
+ batches: 0,
29
+ # Number of processed messages
30
+ messages: 0,
31
+ # Number of errors that occurred
32
+ errors: 0,
33
+ # Number of retries that occurred
34
+ retries: 0,
35
+ # Number of messages considered dead
36
+ dead: 0
37
+ }.freeze
38
+
39
+ private_constant :TIMES_TTL, :TIMES_TTL_MS, :COUNTERS_BASE
26
40
 
27
41
  def initialize
28
42
  super
29
43
 
30
- @counters = {
31
- batches: 0,
32
- messages: 0,
33
- errors: 0,
34
- retries: 0,
35
- dead: 0
36
- }
44
+ @counters = COUNTERS_BASE.dup
37
45
  @times = TtlHash.new(TIMES_TTL_MS)
38
46
  @consumer_groups = {}
39
47
  @errors = []
@@ -41,6 +49,9 @@ module Karafka
41
49
  @pauses = Set.new
42
50
  @jobs = {}
43
51
  @shell = MemoizedShell.new
52
+ @memory_total_usage = 0
53
+ @memory_usage = 0
54
+ @cpu_usage = [-1, -1, -1]
44
55
  end
45
56
 
46
57
  # We cannot report and track the same time, that is why we use mutex here. To make sure
@@ -63,12 +74,13 @@ module Karafka
63
74
  name: process_name,
64
75
  status: ::Karafka::App.config.internal.status.to_s,
65
76
  listeners: listeners,
66
- concurrency: concurrency,
67
- memory_usage: memory_usage,
68
- memory_total_usage: memory_total_usage,
77
+ workers: workers,
78
+ memory_usage: @memory_usage,
79
+ memory_total_usage: @memory_total_usage,
69
80
  memory_size: memory_size,
70
- cpu_count: cpu_count,
71
- cpu_usage: cpu_usage,
81
+ cpus: cpus,
82
+ threads: threads,
83
+ cpu_usage: @cpu_usage,
72
84
  tags: Karafka::Process.tags
73
85
  },
74
86
 
@@ -101,6 +113,17 @@ module Karafka
101
113
  @errors.clear
102
114
  end
103
115
 
116
+ # @note This should run before any mutex, so other threads can continue as those
117
+ # operations may invoke shell commands
118
+ def sample
119
+ memory_threads_ps
120
+
121
+ @memory_usage = memory_usage
122
+ @memory_total_usage = memory_total_usage
123
+ @cpu_usage = cpu_usage
124
+ @threads = threads
125
+ end
126
+
104
127
  private
105
128
 
106
129
  # @return [Numeric] % utilization of all the threads. 100% means all the threads are
@@ -115,7 +138,7 @@ module Karafka
115
138
 
116
139
  # We divide by 1_000 to convert from milliseconds
117
140
  # We multiply by 100 to have it in % scale
118
- times[:total].sum / 1_000 / concurrency / timefactor * 100
141
+ times[:total].sum / 1_000 / workers / timefactor * 100
119
142
  end
120
143
 
121
144
  # @return [Integer] number of listeners
@@ -152,20 +175,16 @@ module Karafka
152
175
  # @return [Hash] job queue statistics
153
176
  def jobs_queue_statistics
154
177
  # We return empty stats in case jobs queue is not yet initialized
178
+ # busy - represents number of jobs that are being executed currently
179
+ # enqueued - represents number of jobs that are enqueued to be processed
155
180
  Karafka::Server.jobs_queue&.statistics || { busy: 0, enqueued: 0 }
156
181
  end
157
182
 
158
183
  # Total memory used in the OS
159
184
  def memory_total_usage
160
- case RUBY_PLATFORM
161
- when /darwin|bsd|linux/
162
- @shell
163
- .call('ps -A -o rss=')
164
- .split("\n")
165
- .inject { |a, e| a.to_i + e.strip.to_i }
166
- else
167
- 0
168
- end
185
+ return 0 unless @memory_threads_ps
186
+
187
+ @memory_threads_ps.map(&:first).sum
169
188
  end
170
189
 
171
190
  # @return [Integer] total amount of memory
@@ -173,19 +192,19 @@ module Karafka
173
192
  @memory_size ||= case RUBY_PLATFORM
174
193
  when /linux/
175
194
  @shell
176
- .call('grep MemTotal /proc/meminfo')
177
- .match(/(\d+)/)
178
- .to_s
179
- .to_i
195
+ .call('grep MemTotal /proc/meminfo')
196
+ .match(/(\d+)/)
197
+ .to_s
198
+ .to_i
180
199
  when /darwin|bsd/
181
200
  @shell
182
- .call('sysctl -a')
183
- .split("\n")
184
- .find { |line| line.start_with?('hw.memsize:') }
185
- .to_s
186
- .split(' ')
187
- .last
188
- .to_i
201
+ .call('sysctl -a')
202
+ .split("\n")
203
+ .find { |line| line.start_with?('hw.memsize:') }
204
+ .to_s
205
+ .split(' ')
206
+ .last
207
+ .to_i
189
208
  else
190
209
  0
191
210
  end
@@ -206,14 +225,36 @@ module Karafka
206
225
  end
207
226
  end
208
227
 
228
+ # @return [Integer] number of process threads.
229
+ # @note This returns total number of threads from the OS perspective including native
230
+ # extensions threads, etc.
231
+ def threads
232
+ return 0 unless @memory_threads_ps
233
+
234
+ @memory_threads_ps.find { |row| row.last == ::Process.pid }[1]
235
+ end
236
+
209
237
  # @return [Integer] CPU count
210
- def cpu_count
211
- @cpu_count ||= Etc.nprocessors
238
+ def cpus
239
+ @cpus ||= Etc.nprocessors
212
240
  end
213
241
 
214
242
  # @return [Integer] number of threads that process work
215
- def concurrency
216
- @concurrency ||= Karafka::App.config.concurrency
243
+ def workers
244
+ @workers ||= Karafka::App.config.concurrency
245
+ end
246
+
247
+ # Loads our ps results into memory so we can extract from them whatever we need
248
+ def memory_threads_ps
249
+ @memory_threads_ps = case RUBY_PLATFORM
250
+ when /darwin|bsd|linux/
251
+ @shell
252
+ .call('ps -A -o rss=,thcount,pid')
253
+ .split("\n")
254
+ .map { |row| row.strip.split(' ').map(&:to_i) }
255
+ else
256
+ @memory_threads_ps = false
257
+ end
217
258
  end
218
259
  end
219
260
  end
@@ -3,11 +3,12 @@
3
3
  module Karafka
4
4
  module Web
5
5
  module Tracking
6
+ # Namespace for all tracking related contracts
6
7
  module Contracts
7
8
  # Contract for error reporting
8
9
  # Since producers and consumers report their errors to the same topic, we need to have
9
10
  # a unified contract for both
10
- class Error < Base
11
+ class Error < Web::Contracts::Base
11
12
  configure
12
13
 
13
14
  required(:schema_version) { |val| val.is_a?(String) }
@@ -12,9 +12,19 @@ module Karafka
12
12
  instance_exec(&CONTEXT_DETAILS)
13
13
 
14
14
  route do |r|
15
- r.root { r.redirect root_path('consumers') }
15
+ r.root { r.redirect root_path('dashboard') }
16
16
 
17
- @current_page = params.current_page
17
+ # Serve current version specific assets to prevent users from fetching old assets
18
+ # after upgrade
19
+ r.on(:assets, Karafka::Web::VERSION) do
20
+ r.public
21
+ end
22
+
23
+ r.get 'dashboard' do
24
+ @breadcrumbs = false
25
+ controller = Controllers::Dashboard.new(params)
26
+ controller.index
27
+ end
18
28
 
19
29
  r.on 'consumers' do
20
30
  r.get String, 'subscriptions' do |_process_id|
@@ -24,7 +34,7 @@ module Karafka
24
34
  r.get do
25
35
  @breadcrumbs = false
26
36
  controller = Controllers::Consumers.new(params)
27
- render_response controller.index
37
+ controller.index
28
38
  end
29
39
  end
30
40
 
@@ -40,41 +50,41 @@ module Karafka
40
50
 
41
51
  r.get 'jobs' do
42
52
  controller = Controllers::Jobs.new(params)
43
- render_response controller.index
53
+ controller.index
44
54
  end
45
55
 
46
56
  r.on 'routing' do
47
57
  controller = Controllers::Routing.new(params)
48
58
 
49
59
  r.get String do |topic_id|
50
- render_response controller.show(topic_id)
60
+ controller.show(topic_id)
51
61
  end
52
62
 
53
63
  r.get do
54
- render_response controller.index
64
+ controller.index
55
65
  end
56
66
  end
57
67
 
58
68
  r.get 'cluster' do
59
69
  controller = Controllers::Cluster.new(params)
60
- render_response controller.index
70
+ controller.index
61
71
  end
62
72
 
63
73
  r.on 'errors' do
64
74
  controller = Controllers::Errors.new(params)
65
75
 
66
76
  r.get Integer do |offset|
67
- render_response controller.show(offset)
77
+ controller.show(offset)
68
78
  end
69
79
 
70
80
  r.get do
71
- render_response controller.index
81
+ controller.index
72
82
  end
73
83
  end
74
84
 
75
85
  r.get 'status' do
76
86
  controller = Controllers::Status.new(params)
77
- render_response controller.show
87
+ controller.show
78
88
  end
79
89
  end
80
90
  end
@@ -5,24 +5,34 @@ module Karafka
5
5
  module Ui
6
6
  # Base Roda application
7
7
  class Base < Roda
8
+ include Helpers::PathsHelper
8
9
  include Helpers::ApplicationHelper
9
10
 
10
11
  # Details that need to be evaluated in the context of OSS or Pro web UI.
11
12
  # If those would be evaluated in the base, they would not be initialized as expected
12
13
  CONTEXT_DETAILS = lambda do
13
14
  plugin(
14
- :static,
15
- %w[/javascripts /stylesheets /images],
16
- root: Karafka::Web.gem_root.join('lib/karafka/web/ui/public')
15
+ :public,
16
+ root: Karafka::Web.gem_root.join('lib/karafka/web/ui/public'),
17
+ # Cache all static files for the end user for as long as possible
18
+ # We can do it because we ship per version assets so they invalidate with gem bumps
19
+ headers: { 'Cache-Control' => 'max-age=604800' }
17
20
  )
18
21
  plugin :render_each
19
22
  plugin :partials
23
+ # The secret here will be reconfigured after Web UI configuration setup
24
+ # This is why we assign here a random value as it will have to be changed by the end
25
+ # user to make the Web UI work.
26
+ plugin :sessions, key: '_karafka_session', secret: SecureRandom.hex(64)
27
+ plugin :route_csrf
20
28
  end
21
29
 
22
30
  plugin :render, escape: true, engine: 'erb'
23
31
  plugin :run_append_slash
24
32
  plugin :error_handler
25
33
  plugin :not_found
34
+ plugin :hooks
35
+ plugin :flash
26
36
  plugin :path
27
37
 
28
38
  # Based on
@@ -43,15 +53,30 @@ module Karafka
43
53
  csp.base_uri "'self'"
44
54
  end
45
55
 
56
+ plugin :custom_block_results
57
+
58
+ handle_block_result Controllers::Responses::Data do |result|
59
+ render_response(result)
60
+ end
61
+
62
+ # Redirect either to referer back or to the desired path
63
+ handle_block_result Controllers::Responses::Redirect do |result|
64
+ # Map redirect flashes (if any) to Roda flash messages
65
+ result.flashes.each { |key, value| flash[key] = value }
66
+
67
+ response.redirect result.back? ? request.referer : root_path(result.path)
68
+ end
69
+
46
70
  # Display appropriate error specific to a given error type
47
71
  plugin :error_handler, classes: [
48
- ::Karafka::Web::Errors::Ui::NotFoundError,
49
72
  ::Rdkafka::RdkafkaError,
73
+ Errors::Ui::NotFoundError,
50
74
  Errors::Ui::ProOnlyError
51
75
  ] do |e|
52
76
  @error = true
53
77
 
54
78
  if e.is_a?(Errors::Ui::ProOnlyError)
79
+ response.status = 402
55
80
  view 'shared/exceptions/pro_only'
56
81
  else
57
82
  response.status = 404
@@ -65,11 +90,36 @@ module Karafka
65
90
  view 'shared/exceptions/not_found'
66
91
  end
67
92
 
93
+ before do
94
+ check_csrf!
95
+ end
96
+
97
+ plugin :class_matchers
98
+
99
+ # Time matcher with optional hours, minutes and seconds
100
+ TIME_MATCHER = %r{(\d{4}-\d{2}-\d{2}/?(\d{2})?(:\d{2})?(:\d{2})?)}
101
+
102
+ private_constant :TIME_MATCHER
103
+
104
+ # Match a date-time. Useful for time-related routes
105
+ # @note In case the date-time is invalid, raise and render 404
106
+ # @note The time component is optional as `Time#parse` will fallback to lowest time
107
+ # available, so we can build only date based lookups
108
+ class_matcher(Time, TIME_MATCHER) do |datetime|
109
+ [Time.parse(datetime)]
110
+ rescue ArgumentError
111
+ raise Errors::Ui::NotFoundError
112
+ end
113
+
68
114
  # Allows us to build current path with additional params
69
115
  # @param query_data [Hash] query params we want to add to the current path
70
116
  path :current do |query_data = {}|
71
- q = query_data.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
72
- "#{request.path}?#{q}"
117
+ q = query_data
118
+ .select { |_, v| v }
119
+ .map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }
120
+ .join('&')
121
+
122
+ [request.path, q].compact.delete_if(&:empty?).join('?')
73
123
  end
74
124
 
75
125
  # Sets appropriate template variables based on the response object and renders the
@@ -12,6 +12,8 @@ module Karafka
12
12
  @params = params
13
13
  end
14
14
 
15
+ private
16
+
15
17
  # Builds the respond data object with assigned attributes based on instance variables.
16
18
  #
17
19
  # @return [Responses::Data] data that should be used to render appropriate view
@@ -33,6 +35,32 @@ module Karafka
33
35
  attributes
34
36
  )
35
37
  end
38
+
39
+ # Builds a redirect data object with assigned flashes (if any)
40
+ # @param path [String, Symbol] relative (without root path) path where we want to be
41
+ # redirected or `:back` to use referer back
42
+ # @param flashes [Hash] hash where key is the flash type and value is the message
43
+ # @return [Responses::Redirect] redirect result
44
+ def redirect(path = :back, flashes = {})
45
+ Responses::Redirect.new(path, flashes)
46
+ end
47
+
48
+ # Initializes the expected pagination engine and assigns expected arguments
49
+ # @param args Any arguments accepted by the selected pagination engine
50
+ def paginate(*args)
51
+ engine = case args.count
52
+ when 2
53
+ Ui::Lib::Paginations::PageBased
54
+ when 3
55
+ Ui::Lib::Paginations::WatermarkOffsetsBased
56
+ when 4
57
+ Ui::Lib::Paginations::OffsetBased
58
+ else
59
+ raise ::Karafka::Errors::UnsupportedCaseError, args.count
60
+ end
61
+
62
+ @pagination = engine.new(*args)
63
+ end
36
64
  end
37
65
  end
38
66
  end
@@ -6,7 +6,7 @@ module Karafka
6
6
  module Controllers
7
7
  # Pro message reporting info controller
8
8
  class BecomePro < Base
9
- # Display a message, that a give feature is available only in Pro
9
+ # Display a message, that a given feature is available only in Pro
10
10
  def show
11
11
  respond
12
12
  end