karafka-web 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (197) 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 +114 -6
  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 +31 -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 +50 -4
  53. data/lib/karafka/web/ui/controllers/base.rb +11 -0
  54. data/lib/karafka/web/ui/controllers/become_pro.rb +1 -1
  55. data/lib/karafka/web/ui/controllers/cluster.rb +7 -4
  56. data/lib/karafka/web/ui/controllers/consumers.rb +1 -1
  57. data/lib/karafka/web/ui/controllers/dashboard.rb +32 -0
  58. data/lib/karafka/web/ui/controllers/jobs.rb +1 -1
  59. data/lib/karafka/web/ui/controllers/requests/params.rb +18 -0
  60. data/lib/karafka/web/ui/controllers/responses/redirect.rb +29 -0
  61. data/lib/karafka/web/ui/helpers/application_helper.rb +57 -14
  62. data/lib/karafka/web/ui/helpers/paths_helper.rb +48 -0
  63. data/lib/karafka/web/ui/lib/hash_proxy.rb +18 -6
  64. data/lib/karafka/web/ui/lib/paginations/watermark_offsets_based.rb +75 -0
  65. data/lib/karafka/web/ui/lib/ttl_cache.rb +8 -0
  66. data/lib/karafka/web/ui/models/consumers_metrics.rb +46 -0
  67. data/lib/karafka/web/ui/models/{state.rb → consumers_state.rb} +6 -2
  68. data/lib/karafka/web/ui/models/health.rb +37 -7
  69. data/lib/karafka/web/ui/models/message.rb +11 -3
  70. data/lib/karafka/web/ui/models/metrics/aggregated.rb +196 -0
  71. data/lib/karafka/web/ui/models/metrics/charts/aggregated.rb +50 -0
  72. data/lib/karafka/web/ui/models/metrics/charts/topics.rb +109 -0
  73. data/lib/karafka/web/ui/models/metrics/topics.rb +101 -0
  74. data/lib/karafka/web/ui/models/partition.rb +27 -0
  75. data/lib/karafka/web/ui/models/process.rb +12 -1
  76. data/lib/karafka/web/ui/models/status.rb +107 -21
  77. data/lib/karafka/web/ui/models/visibility_filter.rb +33 -0
  78. data/lib/karafka/web/ui/pro/app.rb +80 -20
  79. data/lib/karafka/web/ui/pro/controllers/cluster.rb +11 -0
  80. data/lib/karafka/web/ui/pro/controllers/consumers.rb +10 -6
  81. data/lib/karafka/web/ui/pro/controllers/dashboard.rb +54 -0
  82. data/lib/karafka/web/ui/pro/controllers/errors.rb +3 -0
  83. data/lib/karafka/web/ui/pro/controllers/explorer.rb +94 -9
  84. data/lib/karafka/web/ui/pro/controllers/health.rb +10 -2
  85. data/lib/karafka/web/ui/pro/controllers/messages.rb +62 -0
  86. data/lib/karafka/web/ui/pro/controllers/routing.rb +44 -0
  87. data/lib/karafka/web/ui/pro/views/consumers/_breadcrumbs.erb +7 -1
  88. data/lib/karafka/web/ui/pro/views/consumers/_consumer.erb +1 -1
  89. data/lib/karafka/web/ui/pro/views/consumers/_counters.erb +7 -5
  90. data/lib/karafka/web/ui/pro/views/consumers/consumer/_job.erb +3 -3
  91. data/lib/karafka/web/ui/pro/views/consumers/consumer/_metrics.erb +5 -4
  92. data/lib/karafka/web/ui/pro/views/consumers/consumer/_partition.erb +13 -4
  93. data/lib/karafka/web/ui/pro/views/consumers/consumer/_subscription_group.erb +3 -2
  94. data/lib/karafka/web/ui/pro/views/consumers/consumer/_tabs.erb +7 -0
  95. data/lib/karafka/web/ui/pro/views/consumers/details.erb +21 -0
  96. data/lib/karafka/web/ui/pro/views/consumers/index.erb +4 -2
  97. data/lib/karafka/web/ui/pro/views/dashboard/_ranges_selector.erb +39 -0
  98. data/lib/karafka/web/ui/pro/views/dashboard/index.erb +82 -0
  99. data/lib/karafka/web/ui/pro/views/dlq/_topic.erb +1 -1
  100. data/lib/karafka/web/ui/pro/views/errors/_error.erb +1 -1
  101. data/lib/karafka/web/ui/pro/views/errors/index.erb +1 -1
  102. data/lib/karafka/web/ui/pro/views/errors/partition.erb +1 -1
  103. data/lib/karafka/web/ui/pro/views/errors/show.erb +1 -1
  104. data/lib/karafka/web/ui/pro/views/explorer/_breadcrumbs.erb +6 -4
  105. data/lib/karafka/web/ui/pro/views/explorer/_filtered.erb +16 -0
  106. data/lib/karafka/web/ui/pro/views/explorer/_message.erb +6 -2
  107. data/lib/karafka/web/ui/pro/views/explorer/_no_topics.erb +7 -0
  108. data/lib/karafka/web/ui/pro/views/explorer/_partition_option.erb +2 -2
  109. data/lib/karafka/web/ui/pro/views/explorer/_topic.erb +1 -1
  110. data/lib/karafka/web/ui/pro/views/explorer/index.erb +12 -8
  111. data/lib/karafka/web/ui/pro/views/explorer/messages/_headers.erb +15 -0
  112. data/lib/karafka/web/ui/pro/views/explorer/messages/_key.erb +12 -0
  113. data/lib/karafka/web/ui/pro/views/explorer/partition/_details.erb +35 -0
  114. data/lib/karafka/web/ui/pro/views/explorer/partition.erb +5 -3
  115. data/lib/karafka/web/ui/pro/views/explorer/show.erb +48 -5
  116. data/lib/karafka/web/ui/pro/views/explorer/topic/_details.erb +23 -0
  117. data/lib/karafka/web/ui/pro/views/explorer/topic.erb +14 -12
  118. data/lib/karafka/web/ui/pro/views/health/_breadcrumbs.erb +16 -0
  119. data/lib/karafka/web/ui/pro/views/health/_no_data.erb +9 -0
  120. data/lib/karafka/web/ui/pro/views/health/_partition.erb +17 -15
  121. data/lib/karafka/web/ui/pro/views/health/_partition_offset.erb +40 -0
  122. data/lib/karafka/web/ui/pro/views/health/_tabs.erb +27 -0
  123. data/lib/karafka/web/ui/pro/views/health/offsets.erb +71 -0
  124. data/lib/karafka/web/ui/pro/views/health/overview.erb +68 -0
  125. data/lib/karafka/web/ui/pro/views/jobs/_job.erb +6 -3
  126. data/lib/karafka/web/ui/pro/views/jobs/index.erb +4 -1
  127. data/lib/karafka/web/ui/pro/views/routing/_consumer_group.erb +37 -0
  128. data/lib/karafka/web/ui/pro/views/routing/_detail.erb +25 -0
  129. data/lib/karafka/web/ui/pro/views/routing/_topic.erb +23 -0
  130. data/lib/karafka/web/ui/pro/views/routing/index.erb +10 -0
  131. data/lib/karafka/web/ui/pro/views/routing/show.erb +26 -0
  132. data/lib/karafka/web/ui/pro/views/shared/_navigation.erb +6 -9
  133. data/lib/karafka/web/ui/public/images/logo-gray.svg +28 -0
  134. data/lib/karafka/web/ui/public/javascripts/application.js +30 -0
  135. data/lib/karafka/web/ui/public/javascripts/chart.min.js +14 -0
  136. data/lib/karafka/web/ui/public/javascripts/charts.js +330 -0
  137. data/lib/karafka/web/ui/public/javascripts/datepicker.js +6 -0
  138. data/lib/karafka/web/ui/public/javascripts/live_poll.js +39 -12
  139. data/lib/karafka/web/ui/public/javascripts/offset_datetime.js +74 -0
  140. data/lib/karafka/web/ui/public/javascripts/tabs.js +59 -0
  141. data/lib/karafka/web/ui/public/stylesheets/application.css +11 -0
  142. data/lib/karafka/web/ui/public/stylesheets/datepicker.min.css +12 -0
  143. data/lib/karafka/web/ui/views/cluster/_no_partitions.erb +3 -0
  144. data/lib/karafka/web/ui/views/cluster/_partition.erb +20 -22
  145. data/lib/karafka/web/ui/views/cluster/index.erb +6 -1
  146. data/lib/karafka/web/ui/views/consumers/_consumer.erb +1 -1
  147. data/lib/karafka/web/ui/views/consumers/_counters.erb +6 -4
  148. data/lib/karafka/web/ui/views/consumers/_summary.erb +3 -3
  149. data/lib/karafka/web/ui/views/consumers/index.erb +3 -1
  150. data/lib/karafka/web/ui/views/dashboard/_feature_pro.erb +3 -0
  151. data/lib/karafka/web/ui/views/dashboard/_not_enough_data.erb +15 -0
  152. data/lib/karafka/web/ui/views/dashboard/_ranges_selector.erb +23 -0
  153. data/lib/karafka/web/ui/views/dashboard/index.erb +95 -0
  154. data/lib/karafka/web/ui/views/errors/_detail.erb +12 -0
  155. data/lib/karafka/web/ui/views/errors/_error.erb +1 -1
  156. data/lib/karafka/web/ui/views/errors/show.erb +1 -1
  157. data/lib/karafka/web/ui/views/jobs/index.erb +3 -1
  158. data/lib/karafka/web/ui/views/layout.erb +10 -3
  159. data/lib/karafka/web/ui/views/routing/_consumer_group.erb +8 -6
  160. data/lib/karafka/web/ui/views/routing/_detail.erb +2 -2
  161. data/lib/karafka/web/ui/views/routing/_topic.erb +1 -1
  162. data/lib/karafka/web/ui/views/routing/show.erb +1 -1
  163. data/lib/karafka/web/ui/views/shared/_brand.erb +2 -2
  164. data/lib/karafka/web/ui/views/shared/_chart.erb +14 -0
  165. data/lib/karafka/web/ui/views/shared/_content.erb +2 -2
  166. data/lib/karafka/web/ui/views/shared/_feature_pro.erb +1 -1
  167. data/lib/karafka/web/ui/views/shared/_flashes.erb +9 -0
  168. data/lib/karafka/web/ui/views/shared/_footer.erb +22 -0
  169. data/lib/karafka/web/ui/views/shared/_header.erb +15 -9
  170. data/lib/karafka/web/ui/views/shared/_live_poll.erb +7 -0
  171. data/lib/karafka/web/ui/views/shared/_navigation.erb +5 -8
  172. data/lib/karafka/web/ui/views/shared/_no_paginated_data.erb +9 -0
  173. data/lib/karafka/web/ui/views/shared/_pagination.erb +1 -1
  174. data/lib/karafka/web/ui/views/shared/_tab_nav.erb +7 -0
  175. data/lib/karafka/web/ui/views/shared/exceptions/not_found.erb +34 -32
  176. data/lib/karafka/web/ui/views/shared/exceptions/pro_only.erb +45 -43
  177. data/lib/karafka/web/ui/views/status/failures/_consumers_reports_schema_state.erb +15 -0
  178. data/lib/karafka/web/ui/views/status/failures/_enabled.erb +8 -0
  179. data/lib/karafka/web/ui/views/status/failures/_initial_consumers_metrics.erb +11 -0
  180. data/lib/karafka/web/ui/views/status/failures/{_initial_state.erb → _initial_consumers_state.erb} +3 -3
  181. data/lib/karafka/web/ui/views/status/failures/_partitions.erb +14 -6
  182. data/lib/karafka/web/ui/views/status/info/_components.erb +21 -1
  183. data/lib/karafka/web/ui/views/status/show.erb +62 -5
  184. data/lib/karafka/web/ui/views/status/successes/_enabled.erb +1 -0
  185. data/lib/karafka/web/ui/views/status/warnings/_replication.erb +19 -0
  186. data/lib/karafka/web/version.rb +1 -1
  187. data/lib/karafka/web.rb +11 -0
  188. data.tar.gz.sig +0 -0
  189. metadata +109 -39
  190. metadata.gz.sig +0 -0
  191. data/lib/karafka/web/processing/consumers/aggregator.rb +0 -130
  192. data/lib/karafka/web/tracking/contracts/base.rb +0 -34
  193. data/lib/karafka/web/ui/pro/views/explorer/_encryption_enabled.erb +0 -18
  194. data/lib/karafka/web/ui/pro/views/explorer/partition/_watermark_offsets.erb +0 -10
  195. data/lib/karafka/web/ui/pro/views/explorer/topic/_partitions.erb +0 -11
  196. data/lib/karafka/web/ui/pro/views/health/index.erb +0 -60
  197. /data/lib/karafka/web/ui/pro/views/explorer/{_detail.erb → messages/_detail.erb} +0 -0
@@ -8,7 +8,7 @@ module Karafka
8
8
  class Jobs < Base
9
9
  # Lists jobs
10
10
  def index
11
- current_state = Models::State.current!
11
+ current_state = Models::ConsumersState.current!
12
12
  processes = Models::Processes.active(current_state)
13
13
 
14
14
  # Aggregate jobs and inject the process info into them for better reporting
@@ -8,6 +8,17 @@ module Karafka
8
8
  module Requests
9
9
  # Internal representation of params with sane sanitization
10
10
  class Params
11
+ # What ranges we support for charts
12
+ # Anything else will be rejected
13
+ ALLOWED_RANGES = %w[
14
+ seconds
15
+ minutes
16
+ hours
17
+ days
18
+ ].freeze
19
+
20
+ private_constant :ALLOWED_RANGES
21
+
11
22
  # @param request_params [Hash] raw hash with params
12
23
  def initialize(request_params)
13
24
  @request_params = request_params
@@ -23,6 +34,13 @@ module Karafka
23
34
  end
24
35
  end
25
36
 
37
+ # @return [String] Range type for charts we want to fetch
38
+ def current_range
39
+ candidate = @request_params.fetch('range', 'seconds')
40
+ candidate = ALLOWED_RANGES.first unless ALLOWED_RANGES.include?(candidate)
41
+ candidate.to_sym
42
+ end
43
+
26
44
  # @return [Integer] offset from which we want to start. `-1` indicates, that we want
27
45
  # to show the first page discovered based on the high watermark offset. If no offset
28
46
  # is provided, we go with the high offset first page approach
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Controllers
7
+ module Responses
8
+ # Representation of a redirect response with optional flash messages
9
+ class Redirect
10
+ attr_reader :path, :flashes
11
+
12
+ # @param path [String, Symbol] relative (without root path) path where we want to be
13
+ # redirected or `:back` to use referer back
14
+ # @param flashes [Hash] hash where key is the flash type and value is the message
15
+ def initialize(path = :back, flashes = {})
16
+ @path = path
17
+ @flashes = flashes
18
+ end
19
+
20
+ # @return [Boolean] are we going back via referer and not explicit path
21
+ def back?
22
+ @path == :back
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -8,18 +8,6 @@ module Karafka
8
8
  module Helpers
9
9
  # Main application helper
10
10
  module ApplicationHelper
11
- # Generates a full path with the root path out of the provided arguments
12
- #
13
- # @param args [Array<String, Numeric>] arguments that will make the path
14
- # @return [String] path from the root
15
- #
16
- # @note This needs to be done that way with the `#root_path` because the web UI can be
17
- # mounted in a sub-path and we need to make sure our all paths are relative to "our"
18
- # root, not the root of the app in which it was mounted.
19
- def root_path(*args)
20
- "#{env.fetch('SCRIPT_NAME')}/#{args.join('/')}"
21
- end
22
-
23
11
  # Adds active class to the current location in the nav if needed
24
12
  # @param location [Hash]
25
13
  def nav_class(location)
@@ -41,7 +29,7 @@ module Karafka
41
29
 
42
30
  # Renders per scope breadcrumbs
43
31
  def render_breadcrumbs
44
- scope = request.path.gsub(root_path, '').split('/')[0]
32
+ scope = request.path.delete_prefix(root_path).split('/')[0]
45
33
 
46
34
  render "#{scope}/_breadcrumbs"
47
35
  end
@@ -137,19 +125,53 @@ module Karafka
137
125
  %(<span title="#{stamp}">#{time}</span>)
138
126
  end
139
127
 
128
+ # @param lag [Integer] lag
129
+ # @return [String] lag if correct or `N/A` with labeled explanation
130
+ # @see #offset_with_label
131
+ def lag_with_label(lag)
132
+ if lag.negative?
133
+ title = 'Not available until first offset commit'
134
+ %(<span class="badge bg-secondary" title="#{title}">N/A</span>)
135
+ else
136
+ lag.to_s
137
+ end
138
+ end
139
+
140
+ # @param topic_name [String] name of the topic for explorer path
141
+ # @param partition_id [Integer] partition for the explorer path
140
142
  # @param offset [Integer] offset
143
+ # @param explore [Boolean] should we generate (when allowed) a link to message explorer
141
144
  # @return [String] offset if correct or `N/A` with labeled explanation for offsets
142
145
  # that are less than 0. Offset with less than 0 indicates, that the offset was not
143
146
  # yet committed and there is no value we know of
144
- def offset_with_label(offset)
147
+ def offset_with_label(topic_name, partition_id, offset, explore: false)
145
148
  if offset.negative?
146
149
  title = 'Not available until first offset commit'
147
150
  %(<span class="badge bg-secondary" title="#{title}">N/A</span>)
151
+ elsif explore
152
+ path = explorer_path(topic_name, partition_id, offset)
153
+ %(<a href="#{path}">#{offset}</a>)
148
154
  else
149
155
  offset.to_s
150
156
  end
151
157
  end
152
158
 
159
+ # @param details [::Karafka::Web::Ui::Models::Partition] partition information with
160
+ # lso risk state info
161
+ # @return [String] background classes for row marking
162
+ def lso_risk_state_bg(details)
163
+ case details.lso_risk_state
164
+ when :active
165
+ ''
166
+ when :at_risk
167
+ 'bg-warning bg-opacity-25'
168
+ when :stopped
169
+ 'bg-danger bg-opacity-25'
170
+ else
171
+ raise ::Karafka::Errors::UnsupportedCaseError
172
+ end
173
+ end
174
+
153
175
  # Returns the view title html code
154
176
  #
155
177
  # @param title [String] page title
@@ -168,6 +190,27 @@ module Karafka
168
190
  </div>
169
191
  HTML
170
192
  end
193
+
194
+ # @param hash [Hash] we want to flatten
195
+ # @param parent_key [String] key for recursion
196
+ # @param result [Hash] result for recursion
197
+ # @return [Hash]
198
+ def flat_hash(hash, parent_key = nil, result = {})
199
+ hash.each do |key, value|
200
+ current_key = parent_key ? "#{parent_key}.#{key}" : key.to_s
201
+ if value.is_a?(Hash)
202
+ flat_hash(value, current_key, result)
203
+ elsif value.is_a?(Array)
204
+ value.each_with_index do |item, index|
205
+ flat_hash({ index => item }, current_key, result)
206
+ end
207
+ else
208
+ result[current_key] = value
209
+ end
210
+ end
211
+
212
+ result
213
+ end
171
214
  end
172
215
  end
173
216
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Helpers
7
+ # Helper for web ui paths builders
8
+ module PathsHelper
9
+ # Generates a full path with the root path out of the provided arguments
10
+ #
11
+ # @param args [Array<String, Numeric>] arguments that will make the path
12
+ # @return [String] path from the root
13
+ #
14
+ # @note This needs to be done that way with the `#root_path` because the web UI can be
15
+ # mounted in a sub-path and we need to make sure our all paths are relative to "our"
16
+ # root, not the root of the app in which it was mounted.
17
+ def root_path(*args)
18
+ "#{env.fetch('SCRIPT_NAME')}/#{args.join('/')}"
19
+ end
20
+
21
+ # Generates a full path to any asset with our web-ui version. We ship all assets with
22
+ # the version in the url to prevent those assets from being used after update. After
23
+ # each web-ui update, assets are going to be re-fetched as the url will change
24
+ #
25
+ # @param local_path [String] local path to the asset
26
+ # @return [String] full path to the asst including correct root path
27
+ def asset_path(local_path)
28
+ root_path("assets/#{Karafka::Web::VERSION}/#{local_path}")
29
+ end
30
+
31
+ # Helps build explorer paths. We often link offsets to proper messages, etc so this
32
+ # allows us to short-track this
33
+ # @param topic_name [String, nil] name of the topic where we want to go within the
34
+ # explorer or nil if we want to just go to the explorer root
35
+ # @param partition_id [Integer, nil] partition we want to display in the explorer or nil
36
+ # if we want to go to the topic root
37
+ # @param offset [Integer, nil] offset of particular message or nil of we want to just go
38
+ # to the partition root
39
+ # @param action [String, nil] specific routed action or nil
40
+ # @return [String] path to the expected location
41
+ def explorer_path(topic_name = nil, partition_id = nil, offset = nil, action = nil)
42
+ root_path(*['explorer', topic_name, partition_id, offset, action].compact)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -12,16 +12,17 @@ module Karafka
12
12
  # It is mostly used for flat hashes.
13
13
  #
14
14
  # It is in a way similar to openstruct but has abilities to dive deep into objects
15
+ #
16
+ # It is not super fast but it is enough for the UI and how deep structures we have.
15
17
  class HashProxy
18
+ extend Forwardable
19
+
20
+ def_delegators :@hash, :[], :[]=, :key?, :each, :find
21
+
16
22
  # @param hash [Hash] hash we want to convert to a proxy
17
23
  def initialize(hash)
18
24
  @hash = hash
19
- end
20
-
21
- # @param key [Object] hash key
22
- # @return [Object] key content or nil if missing
23
- def [](key)
24
- @hash[key]
25
+ @visited = []
25
26
  end
26
27
 
27
28
  # @return [Original hash]
@@ -35,7 +36,12 @@ module Karafka
35
36
  def method_missing(method_name, *args, &block)
36
37
  return super unless args.empty? && block.nil?
37
38
 
39
+ @visited.clear
40
+
38
41
  result = deep_find(@hash, method_name.to_sym)
42
+
43
+ @visited.clear
44
+
39
45
  result.nil? ? super : result
40
46
  end
41
47
 
@@ -51,6 +57,12 @@ module Karafka
51
57
  # @param obj [Object] local scope of iterating
52
58
  # @param key [Symbol, String] key we are looking for
53
59
  def deep_find(obj, key)
60
+ # Prevent circular dependency lookups by making sure we do not check the same object
61
+ # multiple times
62
+ return nil if @visited.include?(obj)
63
+
64
+ @visited << obj
65
+
54
66
  if obj.respond_to?(:key?) && obj.key?(key)
55
67
  obj[key]
56
68
  elsif obj.respond_to?(:each)
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Lib
7
+ module Paginations
8
+ # Watermark offsets single message pagination engine
9
+ #
10
+ # It is used to provide pagination for single message displays (explorer, errors)
11
+ class WatermarkOffsetsBased < Base
12
+ # @param current_offset [Integer] current message offset
13
+ # @param low_watermark_offset [Integer]
14
+ # @param high_watermark_offset [Integer]
15
+ def initialize(
16
+ current_offset,
17
+ low_watermark_offset,
18
+ high_watermark_offset
19
+ )
20
+ @low_watermark_offset = low_watermark_offset
21
+ @high_watermark_offset = high_watermark_offset
22
+ @previous_offset = current_offset + 1
23
+ @current_offset = current_offset
24
+ @next_offset = current_offset - 1
25
+ super()
26
+ end
27
+
28
+ # @return [Boolean] show pagination only when there are other things to present
29
+ def paginate?
30
+ return true if @current_offset > @low_watermark_offset
31
+ return true if @current_offset < @high_watermark_offset - 1
32
+
33
+ false
34
+ end
35
+
36
+ # @return [Boolean] provide link to the first (newest)
37
+ def first_offset?
38
+ @current_offset < @high_watermark_offset - 1
39
+ end
40
+
41
+ # @return [Integer] highest available offset
42
+ def first_offset
43
+ @high_watermark_offset - 1
44
+ end
45
+
46
+ # @return [Boolean]
47
+ def previous_offset?
48
+ @current_offset < @high_watermark_offset - 1
49
+ end
50
+
51
+ # @return [Boolean] We always show current offset
52
+ def current_offset?
53
+ true
54
+ end
55
+
56
+ # @return [String] shows as current page pagination the offset
57
+ def current_label
58
+ @current_offset.to_s
59
+ end
60
+
61
+ # @return [Boolean] if not lowest, show
62
+ def next_offset?
63
+ @current_offset > @low_watermark_offset
64
+ end
65
+
66
+ # @return [String] params offset key
67
+ def offset_key
68
+ 'offset'
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -56,6 +56,14 @@ module Karafka
56
56
  end
57
57
  end
58
58
 
59
+ # Clears the whole cache
60
+ def clear
61
+ @mutex.synchronize do
62
+ @times.clear
63
+ @values.clear
64
+ end
65
+ end
66
+
59
67
  private
60
68
 
61
69
  # Removes expired elements from the cache
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Models
7
+ # Model representing the current consumers metrics most recent state
8
+ class ConsumersMetrics < Lib::HashProxy
9
+ class << self
10
+ # @return [State, false] current consumers metrics or false if not found
11
+ def current
12
+ state = fetch
13
+
14
+ return false unless state
15
+
16
+ # Do not return the state in case web-ui is not enabled because we need our
17
+ # internal deserializer for it to operate. False will force user to go to the
18
+ # status page
19
+ return false unless Models::Status.new.enabled.success?
20
+
21
+ state = state.payload
22
+ new(state)
23
+ end
24
+
25
+ # @return [State] current consumers metrics
26
+ # @raise [::Karafka::Web::Errors::Ui::NotFoundError] raised when there is no metrics.
27
+ def current!
28
+ current || raise(::Karafka::Web::Errors::Ui::NotFoundError)
29
+ end
30
+
31
+ private
32
+
33
+ # @return [::Karafka::Messages::Message, nil] most recent state or nil if none
34
+ def fetch
35
+ ::Karafka::Admin.read_topic(
36
+ Karafka::Web.config.topics.consumers.metrics,
37
+ 0,
38
+ 1
39
+ ).last
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -7,7 +7,7 @@ module Karafka
7
7
  # Represents the current consumer processes aggregated state
8
8
  # This state is the core of Karafka reporting. It holds the most important aggregated data
9
9
  # as well as pointers to states of particular consumers and their details.
10
- class State < Lib::HashProxy
10
+ class ConsumersState < Lib::HashProxy
11
11
  extend ::Karafka::Core::Helpers::Time
12
12
 
13
13
  class << self
@@ -18,6 +18,10 @@ module Karafka
18
18
  state = fetch
19
19
 
20
20
  return false unless state
21
+ # Do not return the state in case web-ui is not enabled because we need our
22
+ # internal deserializer for it to operate. False will force user to go to the
23
+ # status page
24
+ return false unless Models::Status.new.enabled.success?
21
25
 
22
26
  state = state.payload
23
27
  evict_expired_processes(state)
@@ -28,7 +32,7 @@ module Karafka
28
32
 
29
33
  # @return [State] current aggregated state
30
34
  # @raise [::Karafka::Web::Errors::Ui::NotFoundError] raised when there is no current
31
- # state. Probably because `karafka server` was never executed
35
+ # state.
32
36
  def current!
33
37
  current || raise(::Karafka::Web::Errors::Ui::NotFoundError)
34
38
  end
@@ -12,21 +12,51 @@ module Karafka
12
12
  def current(state)
13
13
  stats = {}
14
14
 
15
+ fetch_topics_data(state, stats)
16
+ fetch_rebalance_ages(state, stats)
17
+
18
+ stats
19
+ end
20
+
21
+ private
22
+
23
+ # Aggregates data on a per topic basis (in the context of a consumer group)
24
+ # @param state [Hash]
25
+ # @param stats [Hash] hash where we will store all the aggregated data
26
+ def fetch_topics_data(state, stats)
15
27
  iterate_partitions(state) do |process, consumer_group, topic, partition|
16
28
  cg_name = consumer_group.id
17
29
  t_name = topic.name
18
30
  pt_id = partition.id
19
31
 
20
- stats[cg_name] ||= {}
21
- stats[cg_name][t_name] ||= {}
22
- stats[cg_name][t_name][pt_id] = partition.to_h
23
- stats[cg_name][t_name][pt_id][:process] = process
32
+ stats[cg_name] ||= { topics: {} }
33
+ stats[cg_name][:topics][t_name] ||= {}
34
+ stats[cg_name][:topics][t_name][pt_id] = partition
35
+ stats[cg_name][:topics][t_name][pt_id][:process] = process
24
36
  end
25
-
26
- stats
27
37
  end
28
38
 
29
- private
39
+ # Aggregates rebalances ages data
40
+ # @param state [Hash]
41
+ # @param stats [Hash] hash where we will store all the aggregated data
42
+ def fetch_rebalance_ages(state, stats)
43
+ iterate_partitions(state) do |process, consumer_group|
44
+ cg_name = consumer_group.id
45
+ dispatched_at = process.dispatched_at
46
+
47
+ ages = consumer_group[:subscription_groups].values.map do |sub_group_details|
48
+ rebalance_age_ms = sub_group_details[:state][:rebalance_age] || 0
49
+ dispatched_at - rebalance_age_ms / 1_000
50
+ end
51
+
52
+ stats[cg_name][:rebalance_ages] ||= []
53
+ stats[cg_name][:rebalance_ages] += ages
54
+ end
55
+
56
+ stats.each_value do |details|
57
+ details[:rebalanced_at] = details[:rebalance_ages].max
58
+ end
59
+ end
30
60
 
31
61
  # Iterates over all partitions, yielding with extra expanded details
32
62
  #
@@ -104,7 +104,7 @@ module Karafka
104
104
  # If there is a potential previous page with more recent data, compute its
105
105
  # offset
106
106
  previous_offset >= high_offset ? false : previous_offset,
107
- fill_compacted(messages, partition_id, context_offset, context_count).reverse,
107
+ fill_compacted(messages, partition_id, context_offset, context_count, high_offset).reverse,
108
108
  next_offset
109
109
  ]
110
110
  end
@@ -215,14 +215,22 @@ module Karafka
215
215
  # @param start_offset [Integer] offset of the first message (lowest) that we received
216
216
  # @param count [Integer] how many messages we wanted - we need that to fill spots to
217
217
  # have exactly the number that was requested and not more
218
+ # @param high_offset [Integer] high watermark offset
218
219
  # @return [Array<Karafka::Messages::Message, Integer>] array with gaps filled with the
219
220
  # missing offset
220
- def fill_compacted(messages, partition_id, start_offset, count)
221
- Array.new(count) do |index|
221
+ def fill_compacted(messages, partition_id, start_offset, count, high_offset)
222
+ filled = Array.new(count) do |index|
222
223
  messages.find do |message|
223
224
  (message.offset - start_offset) == index
224
225
  end || [partition_id, start_offset + index]
225
226
  end
227
+
228
+ # Remove dummies provisioned over the high offset
229
+ filled.delete_if do |message|
230
+ message.is_a?(Array) && message.last >= high_offset
231
+ end
232
+
233
+ filled
226
234
  end
227
235
  end
228
236
  end