karafka-web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +3 -0
  3. data/.coditsu/ci.yml +3 -0
  4. data/.diffend.yml +3 -0
  5. data/.github/FUNDING.yml +1 -0
  6. data/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
  7. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  8. data/.github/workflows/ci.yml +49 -0
  9. data/.gitignore +69 -0
  10. data/.ruby-gemset +1 -0
  11. data/.ruby-version +1 -0
  12. data/CHANGELOG.md +9 -0
  13. data/CODE_OF_CONDUCT.md +46 -0
  14. data/Gemfile +7 -0
  15. data/Gemfile.lock +52 -0
  16. data/LICENSE +17 -0
  17. data/README.md +29 -0
  18. data/bin/karafka-web +33 -0
  19. data/certs/cert_chain.pem +26 -0
  20. data/config/locales/errors.yml +9 -0
  21. data/karafka-web.gemspec +44 -0
  22. data/lib/karafka/web/app.rb +17 -0
  23. data/lib/karafka/web/config.rb +80 -0
  24. data/lib/karafka/web/deserializer.rb +20 -0
  25. data/lib/karafka/web/errors.rb +25 -0
  26. data/lib/karafka/web/installer.rb +124 -0
  27. data/lib/karafka/web/processing/consumer.rb +66 -0
  28. data/lib/karafka/web/processing/consumers/aggregator.rb +130 -0
  29. data/lib/karafka/web/processing/consumers/state.rb +32 -0
  30. data/lib/karafka/web/tracking/base_contract.rb +31 -0
  31. data/lib/karafka/web/tracking/consumers/contracts/consumer_group.rb +33 -0
  32. data/lib/karafka/web/tracking/consumers/contracts/job.rb +26 -0
  33. data/lib/karafka/web/tracking/consumers/contracts/partition.rb +22 -0
  34. data/lib/karafka/web/tracking/consumers/contracts/report.rb +95 -0
  35. data/lib/karafka/web/tracking/consumers/contracts/topic.rb +29 -0
  36. data/lib/karafka/web/tracking/consumers/listeners/base.rb +33 -0
  37. data/lib/karafka/web/tracking/consumers/listeners/errors.rb +107 -0
  38. data/lib/karafka/web/tracking/consumers/listeners/pausing.rb +45 -0
  39. data/lib/karafka/web/tracking/consumers/listeners/processing.rb +157 -0
  40. data/lib/karafka/web/tracking/consumers/listeners/statistics.rb +123 -0
  41. data/lib/karafka/web/tracking/consumers/listeners/status.rb +58 -0
  42. data/lib/karafka/web/tracking/consumers/sampler.rb +216 -0
  43. data/lib/karafka/web/tracking/memoized_shell.rb +48 -0
  44. data/lib/karafka/web/tracking/reporter.rb +144 -0
  45. data/lib/karafka/web/tracking/ttl_array.rb +59 -0
  46. data/lib/karafka/web/tracking/ttl_hash.rb +16 -0
  47. data/lib/karafka/web/ui/app.rb +78 -0
  48. data/lib/karafka/web/ui/base.rb +77 -0
  49. data/lib/karafka/web/ui/controllers/base.rb +40 -0
  50. data/lib/karafka/web/ui/controllers/become_pro.rb +17 -0
  51. data/lib/karafka/web/ui/controllers/cluster.rb +24 -0
  52. data/lib/karafka/web/ui/controllers/consumers.rb +27 -0
  53. data/lib/karafka/web/ui/controllers/errors.rb +43 -0
  54. data/lib/karafka/web/ui/controllers/jobs.rb +33 -0
  55. data/lib/karafka/web/ui/controllers/requests/params.rb +30 -0
  56. data/lib/karafka/web/ui/controllers/responses/data.rb +26 -0
  57. data/lib/karafka/web/ui/controllers/routing.rb +30 -0
  58. data/lib/karafka/web/ui/helpers/application_helper.rb +144 -0
  59. data/lib/karafka/web/ui/lib/hash_proxy.rb +66 -0
  60. data/lib/karafka/web/ui/lib/paginate_array.rb +38 -0
  61. data/lib/karafka/web/ui/models/consumer_group.rb +20 -0
  62. data/lib/karafka/web/ui/models/health.rb +44 -0
  63. data/lib/karafka/web/ui/models/job.rb +13 -0
  64. data/lib/karafka/web/ui/models/message.rb +99 -0
  65. data/lib/karafka/web/ui/models/partition.rb +13 -0
  66. data/lib/karafka/web/ui/models/process.rb +56 -0
  67. data/lib/karafka/web/ui/models/processes.rb +86 -0
  68. data/lib/karafka/web/ui/models/state.rb +67 -0
  69. data/lib/karafka/web/ui/models/topic.rb +19 -0
  70. data/lib/karafka/web/ui/pro/app.rb +120 -0
  71. data/lib/karafka/web/ui/pro/controllers/cluster.rb +16 -0
  72. data/lib/karafka/web/ui/pro/controllers/consumers.rb +54 -0
  73. data/lib/karafka/web/ui/pro/controllers/dlq.rb +44 -0
  74. data/lib/karafka/web/ui/pro/controllers/errors.rb +57 -0
  75. data/lib/karafka/web/ui/pro/controllers/explorer.rb +79 -0
  76. data/lib/karafka/web/ui/pro/controllers/health.rb +33 -0
  77. data/lib/karafka/web/ui/pro/controllers/jobs.rb +26 -0
  78. data/lib/karafka/web/ui/pro/controllers/routing.rb +26 -0
  79. data/lib/karafka/web/ui/pro/views/consumers/_breadcrumbs.erb +27 -0
  80. data/lib/karafka/web/ui/pro/views/consumers/_consumer.erb +60 -0
  81. data/lib/karafka/web/ui/pro/views/consumers/_counters.erb +50 -0
  82. data/lib/karafka/web/ui/pro/views/consumers/_summary.erb +81 -0
  83. data/lib/karafka/web/ui/pro/views/consumers/consumer/_consumer_group.erb +109 -0
  84. data/lib/karafka/web/ui/pro/views/consumers/consumer/_job.erb +26 -0
  85. data/lib/karafka/web/ui/pro/views/consumers/consumer/_metrics.erb +126 -0
  86. data/lib/karafka/web/ui/pro/views/consumers/consumer/_no_jobs.erb +9 -0
  87. data/lib/karafka/web/ui/pro/views/consumers/consumer/_no_subscriptions.erb +9 -0
  88. data/lib/karafka/web/ui/pro/views/consumers/consumer/_partition.erb +32 -0
  89. data/lib/karafka/web/ui/pro/views/consumers/consumer/_stopped.erb +10 -0
  90. data/lib/karafka/web/ui/pro/views/consumers/consumer/_tabs.erb +20 -0
  91. data/lib/karafka/web/ui/pro/views/consumers/index.erb +30 -0
  92. data/lib/karafka/web/ui/pro/views/consumers/jobs.erb +42 -0
  93. data/lib/karafka/web/ui/pro/views/consumers/subscriptions.erb +23 -0
  94. data/lib/karafka/web/ui/pro/views/dlq/_breadcrumbs.erb +5 -0
  95. data/lib/karafka/web/ui/pro/views/dlq/_no_topics.erb +9 -0
  96. data/lib/karafka/web/ui/pro/views/dlq/_topic.erb +12 -0
  97. data/lib/karafka/web/ui/pro/views/dlq/index.erb +16 -0
  98. data/lib/karafka/web/ui/pro/views/errors/_breadcrumbs.erb +25 -0
  99. data/lib/karafka/web/ui/pro/views/errors/_detail.erb +29 -0
  100. data/lib/karafka/web/ui/pro/views/errors/_error.erb +26 -0
  101. data/lib/karafka/web/ui/pro/views/errors/_partition_option.erb +7 -0
  102. data/lib/karafka/web/ui/pro/views/errors/index.erb +58 -0
  103. data/lib/karafka/web/ui/pro/views/errors/show.erb +56 -0
  104. data/lib/karafka/web/ui/pro/views/explorer/_breadcrumbs.erb +29 -0
  105. data/lib/karafka/web/ui/pro/views/explorer/_detail.erb +21 -0
  106. data/lib/karafka/web/ui/pro/views/explorer/_encryption_enabled.erb +18 -0
  107. data/lib/karafka/web/ui/pro/views/explorer/_failed_deserialization.erb +4 -0
  108. data/lib/karafka/web/ui/pro/views/explorer/_message.erb +16 -0
  109. data/lib/karafka/web/ui/pro/views/explorer/_partition_option.erb +7 -0
  110. data/lib/karafka/web/ui/pro/views/explorer/_topic.erb +12 -0
  111. data/lib/karafka/web/ui/pro/views/explorer/index.erb +17 -0
  112. data/lib/karafka/web/ui/pro/views/explorer/partition.erb +56 -0
  113. data/lib/karafka/web/ui/pro/views/explorer/show.erb +65 -0
  114. data/lib/karafka/web/ui/pro/views/health/_breadcrumbs.erb +5 -0
  115. data/lib/karafka/web/ui/pro/views/health/_partition.erb +35 -0
  116. data/lib/karafka/web/ui/pro/views/health/index.erb +60 -0
  117. data/lib/karafka/web/ui/pro/views/jobs/_breadcrumbs.erb +5 -0
  118. data/lib/karafka/web/ui/pro/views/jobs/_job.erb +31 -0
  119. data/lib/karafka/web/ui/pro/views/jobs/_no_jobs.erb +9 -0
  120. data/lib/karafka/web/ui/pro/views/jobs/index.erb +34 -0
  121. data/lib/karafka/web/ui/pro/views/shared/_navigation.erb +57 -0
  122. data/lib/karafka/web/ui/public/images/favicon.ico +0 -0
  123. data/lib/karafka/web/ui/public/images/logo.svg +28 -0
  124. data/lib/karafka/web/ui/public/javascripts/application.js +41 -0
  125. data/lib/karafka/web/ui/public/javascripts/bootstrap.min.js +7 -0
  126. data/lib/karafka/web/ui/public/javascripts/highlight.min.js +337 -0
  127. data/lib/karafka/web/ui/public/javascripts/live_poll.js +124 -0
  128. data/lib/karafka/web/ui/public/javascripts/timeago.min.js +1 -0
  129. data/lib/karafka/web/ui/public/stylesheets/application.css +106 -0
  130. data/lib/karafka/web/ui/public/stylesheets/bootstrap.min.css +7 -0
  131. data/lib/karafka/web/ui/public/stylesheets/bootstrap.min.css.map +1 -0
  132. data/lib/karafka/web/ui/public/stylesheets/highlight.min.css +10 -0
  133. data/lib/karafka/web/ui/views/cluster/_breadcrumbs.erb +5 -0
  134. data/lib/karafka/web/ui/views/cluster/_broker.erb +5 -0
  135. data/lib/karafka/web/ui/views/cluster/_partition.erb +22 -0
  136. data/lib/karafka/web/ui/views/cluster/index.erb +72 -0
  137. data/lib/karafka/web/ui/views/consumers/_breadcrumbs.erb +27 -0
  138. data/lib/karafka/web/ui/views/consumers/_consumer.erb +43 -0
  139. data/lib/karafka/web/ui/views/consumers/_counters.erb +44 -0
  140. data/lib/karafka/web/ui/views/consumers/_summary.erb +81 -0
  141. data/lib/karafka/web/ui/views/consumers/consumer/_consumer_group.erb +109 -0
  142. data/lib/karafka/web/ui/views/consumers/consumer/_job.erb +26 -0
  143. data/lib/karafka/web/ui/views/consumers/consumer/_metrics.erb +126 -0
  144. data/lib/karafka/web/ui/views/consumers/consumer/_no_jobs.erb +9 -0
  145. data/lib/karafka/web/ui/views/consumers/consumer/_no_subscriptions.erb +9 -0
  146. data/lib/karafka/web/ui/views/consumers/consumer/_partition.erb +32 -0
  147. data/lib/karafka/web/ui/views/consumers/consumer/_stopped.erb +10 -0
  148. data/lib/karafka/web/ui/views/consumers/consumer/_tabs.erb +20 -0
  149. data/lib/karafka/web/ui/views/consumers/index.erb +29 -0
  150. data/lib/karafka/web/ui/views/errors/_breadcrumbs.erb +19 -0
  151. data/lib/karafka/web/ui/views/errors/_detail.erb +29 -0
  152. data/lib/karafka/web/ui/views/errors/_error.erb +26 -0
  153. data/lib/karafka/web/ui/views/errors/index.erb +38 -0
  154. data/lib/karafka/web/ui/views/errors/show.erb +30 -0
  155. data/lib/karafka/web/ui/views/jobs/_breadcrumbs.erb +5 -0
  156. data/lib/karafka/web/ui/views/jobs/_job.erb +22 -0
  157. data/lib/karafka/web/ui/views/jobs/_no_jobs.erb +9 -0
  158. data/lib/karafka/web/ui/views/jobs/index.erb +31 -0
  159. data/lib/karafka/web/ui/views/layout.erb +23 -0
  160. data/lib/karafka/web/ui/views/routing/_breadcrumbs.erb +15 -0
  161. data/lib/karafka/web/ui/views/routing/_consumer_group.erb +34 -0
  162. data/lib/karafka/web/ui/views/routing/_detail.erb +25 -0
  163. data/lib/karafka/web/ui/views/routing/_topic.erb +18 -0
  164. data/lib/karafka/web/ui/views/routing/index.erb +10 -0
  165. data/lib/karafka/web/ui/views/routing/show.erb +26 -0
  166. data/lib/karafka/web/ui/views/shared/_become_pro.erb +13 -0
  167. data/lib/karafka/web/ui/views/shared/_brand.erb +3 -0
  168. data/lib/karafka/web/ui/views/shared/_content.erb +31 -0
  169. data/lib/karafka/web/ui/views/shared/_header.erb +20 -0
  170. data/lib/karafka/web/ui/views/shared/_navigation.erb +57 -0
  171. data/lib/karafka/web/ui/views/shared/_pagination.erb +21 -0
  172. data/lib/karafka/web/ui/views/shared/exceptions/not_found.erb +39 -0
  173. data/lib/karafka/web/ui/views/shared/exceptions/pro_only.erb +52 -0
  174. data/lib/karafka/web/version.rb +8 -0
  175. data/lib/karafka/web.rb +60 -0
  176. data.tar.gz.sig +0 -0
  177. metadata +328 -0
  178. metadata.gz.sig +0 -0
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Controllers
7
+ # Selects cluster info and topics basic info
8
+ class Cluster < Base
9
+ # List cluster info data
10
+ def index
11
+ @cluster_info = Karafka::Admin.cluster_info
12
+
13
+ @topics = @cluster_info
14
+ .topics
15
+ .reject { |topic| topic[:topic_name] == '__consumer_offsets' }
16
+ .sort_by { |topic| topic[:topic_name] }
17
+
18
+ respond
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Controllers
7
+ # Consumers (consuming processes - `karafka server`) processes display consumer
8
+ class Consumers < Base
9
+ # List page with consumers
10
+ # @note For now we load all and paginate over the squashed data.
11
+ def index
12
+ @current_state = Models::State.current!
13
+ processes_total = Models::Processes.active(@current_state)
14
+
15
+ @counters = Lib::HashProxy.new(@current_state[:stats])
16
+ @processes, @next_page = Lib::PaginateArray.new.call(
17
+ processes_total,
18
+ @params.current_page
19
+ )
20
+
21
+ respond
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Controllers
7
+ # Errors displaying controller
8
+ # It supports only scenarios with a single partition for errors
9
+ # If you have high load of errors, consider going Pro
10
+ class Errors < Base
11
+ # Lists first page of the errors
12
+ def index
13
+ @previous_page, @error_messages, @next_page, = Models::Message.page(
14
+ errors_topic,
15
+ 0,
16
+ @params.current_page
17
+ )
18
+
19
+ respond
20
+ end
21
+
22
+ # @param offset [Integer] given error message offset
23
+ def show(offset)
24
+ @error_message = Models::Message.find(
25
+ errors_topic,
26
+ 0,
27
+ offset
28
+ )
29
+
30
+ respond
31
+ end
32
+
33
+ private
34
+
35
+ # @return [String] errors topic
36
+ def errors_topic
37
+ ::Karafka::Web.config.topics.errors
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Controllers
7
+ # Active jobs (work) reporting controller
8
+ class Jobs < Base
9
+ # Lists jobs
10
+ def index
11
+ current_state = Models::State.current!
12
+ processes = Models::Processes.active(current_state)
13
+
14
+ # Aggregate jobs and inject the process info into them for better reporting
15
+ jobs_total = processes.flat_map do |process|
16
+ process.jobs.map do |job|
17
+ job.to_h[:process] = process
18
+ job
19
+ end
20
+ end
21
+
22
+ @jobs, @next_page = Ui::Lib::PaginateArray.new.call(
23
+ jobs_total,
24
+ @params.current_page
25
+ )
26
+
27
+ respond
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Controllers
7
+ # Namespace for request related components
8
+ module Requests
9
+ # Internal representation of params with sane sanitization
10
+ class Params
11
+ # @param request_params [Hash] raw hash with params
12
+ def initialize(request_params)
13
+ @request_params = request_params
14
+ end
15
+
16
+ # @return [Integer] current page for paginated views
17
+ # @note It does basic sanitization
18
+ def current_page
19
+ @current_page ||= begin
20
+ page = @request_params['page'].to_i
21
+
22
+ page.positive? ? page : 1
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Controllers
7
+ # Response related components
8
+ module Responses
9
+ # Response data object. It is used to transfer attributes assigned in controllers into
10
+ # views
11
+ # It acts as a simplification / transport layer for assigned attributes
12
+ class Data
13
+ attr_reader :path, :attributes
14
+
15
+ # @param path [String] render path
16
+ # @param attributes [Hash] attributes assigned in the controller
17
+ def initialize(path, attributes)
18
+ @path = path
19
+ @attributes = attributes
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Controllers
7
+ # Routing presentation controller
8
+ class Routing < Base
9
+ # Routing list
10
+ def index
11
+ @routes = Karafka::App.routes
12
+
13
+ respond
14
+ end
15
+
16
+ # Given route details
17
+ #
18
+ # @param topic_id [String] topic id
19
+ def show(topic_id)
20
+ @topic = Karafka::Routing::Router.find_by(id: topic_id)
21
+
22
+ @topic || raise(::Karafka::Web::Errors::Ui::NotFoundError, topic_id)
23
+
24
+ respond
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @see https://github.com/mperham/sidekiq/blob/main/lib/sidekiq/web/helpers.rb
4
+ module Karafka
5
+ module Web
6
+ module Ui
7
+ # Namespace for helpers used by the Web UI
8
+ module Helpers
9
+ # Main application helper
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
+ # Adds active class to the current location in the nav if needed
24
+ # @param location [Hash]
25
+ def nav_class(location)
26
+ comparator, value = location.to_a.first
27
+
28
+ local_location = request.path.gsub(env.fetch('SCRIPT_NAME'), '')
29
+ local_location.public_send(:"#{comparator}?", value) ? 'active' : ''
30
+ end
31
+
32
+ # Converts object into a string and for objects that would anyhow return their
33
+ # stringified instance value, it replaces it with the class name instead.
34
+ # Useful for deserializers, etc presentation.
35
+ #
36
+ # @param object [Object]
37
+ # @return [String]
38
+ def object_value_to_s(object)
39
+ object.to_s.include?('#<') ? object.class.to_s : object.to_s
40
+ end
41
+
42
+ # Renders per scope breadcrumbs
43
+ def render_breadcrumbs
44
+ scope = request.path.gsub(root_path, '').split('/')[0]
45
+
46
+ render "#{scope}/_breadcrumbs"
47
+ end
48
+
49
+ # Takes a status and recommends background style color
50
+ #
51
+ # @param status [String] status
52
+ # @return [String] background style
53
+ def status_bg(status)
54
+ case status
55
+ when 'initialized' then 'bg-success'
56
+ when 'running' then 'bg-success'
57
+ when 'quieting' then 'bg-warning'
58
+ when 'quiet' then 'bg-warning text-dark'
59
+ when 'stopping' then 'bg-warning text-dark'
60
+ when 'stopped' then 'bg-danger'
61
+ else
62
+ raise ::Karafka::Errors::UnsupportedCaseError, status
63
+ end
64
+ end
65
+
66
+ # Takes the lag trend and gives it appropriate background style color for badge
67
+ #
68
+ # @param trend [Numeric] lag trend
69
+ # @return [String] bg classes
70
+ def lag_trend_bg(trend)
71
+ bg = 'bg-success' if trend.negative?
72
+ bg ||= 'bg-warning text-dark' if trend.positive?
73
+ bg ||= 'bg-secondary'
74
+ bg
75
+ end
76
+
77
+ # Takes a kafka report state and recommends background style color
78
+ # @param state [String] state
79
+ # @return [String] background style
80
+ def kafka_state_bg(state)
81
+ case state
82
+ when 'up' then 'bg-success text-white'
83
+ when 'active' then 'bg-success text-white'
84
+ when 'steady' then 'bg-success text-white'
85
+ else
86
+ 'bg-warning text-dark'
87
+ end
88
+ end
89
+
90
+ # @param mem_kb [Integer] memory used in KB
91
+ # @return [String] formatted memory usage
92
+ def format_memory(mem_kb)
93
+ return '0' if !mem_kb || mem_kb.zero?
94
+
95
+ if mem_kb < 10_240
96
+ "#{number_with_delimiter(mem_kb)} KB"
97
+ elsif mem_kb < 1_000_000
98
+ "#{number_with_delimiter((mem_kb / 1024.0).to_i)} MB"
99
+ else
100
+ "#{number_with_delimiter((mem_kb / (1024.0 * 1024.0)).round(1))} GB"
101
+ end
102
+ end
103
+
104
+ # Converts number to a more friendly delimiter based version
105
+ # @param number [Numeric]
106
+ # @return [String] number with delimiter
107
+ def number_with_delimiter(number)
108
+ return '' unless number
109
+
110
+ parts = number.to_s.to_str.split('.')
111
+ parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, '\1,')
112
+ parts.join('.')
113
+ end
114
+
115
+ # @param time [Float] UTC time float
116
+ # @return [String] relative time tag for timeago.js
117
+ def relative_time(time)
118
+ stamp = Time.at(time).getutc.iso8601
119
+ %(<time class="ltr" dir="ltr" title="#{stamp}" datetime="#{stamp}">#{time}</time>)
120
+ end
121
+
122
+ # Returns the view title html code
123
+ #
124
+ # @param title [String] page title
125
+ # @param hr [Boolean] should we add the hr tag at the end
126
+ # @return [String] title html
127
+ def view_title(title, hr: false)
128
+ <<-HTML
129
+ <div class="container mb-5">
130
+ <div class="row">
131
+ <h3>
132
+ #{title}
133
+ </h3>
134
+ </div>
135
+
136
+ #{hr ? '<hr/>' : ''}
137
+ </div>
138
+ HTML
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ # Non info related extra components used in the UI
7
+ module Lib
8
+ # Proxy for hashes we use across UI.
9
+ # Often we have nested values we want to extract or just values we want to reference and
10
+ # this object drastically simplifies that.
11
+ #
12
+ # It is mostly used for flat hashes.
13
+ #
14
+ # It is in a way similar to openstruct but has abilities to dive deep into objects
15
+ class HashProxy
16
+ # @param hash [Hash] hash we want to convert to a proxy
17
+ def initialize(hash)
18
+ @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
+ end
26
+
27
+ # @return [Original hash]
28
+ def to_h
29
+ @hash
30
+ end
31
+
32
+ # @param method_name [String] method name
33
+ # @param args [Object] all the args of the method
34
+ # @param block [Proc] block for the method
35
+ def method_missing(method_name, *args, &block)
36
+ return super unless args.empty? && block.nil?
37
+
38
+ result = deep_find(@hash, method_name.to_sym)
39
+ result.nil? ? super : result
40
+ end
41
+
42
+ # @param method_name [String] method name
43
+ # @param include_private [Boolean]
44
+ def respond_to_missing?(method_name, include_private = false)
45
+ result = deep_find(@hash, method_name.to_sym)
46
+ result.nil? ? super : true
47
+ end
48
+
49
+ private
50
+
51
+ # @param obj [Object] local scope of iterating
52
+ # @param key [Symbol, String] key we are looking for
53
+ def deep_find(obj, key)
54
+ if obj.respond_to?(:key?) && obj.key?(key)
55
+ obj[key]
56
+ elsif obj.respond_to?(:each)
57
+ r = nil
58
+ obj.find { |*a| r = deep_find(a.last, key) }
59
+ r
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Lib
7
+ # A simple wrapper for paginating array related data structures
8
+ class PaginateArray
9
+ # @param array [Array] array we want to paginate
10
+ # @param current_page [Integer] page we want to be on
11
+ # @return [Array<Array, <Integer, nil>>] Array with two elements: first is the array with
12
+ # data of the given page and second is the next page number of nil in case there is
13
+ # no next page (end of data)
14
+ def call(array, current_page)
15
+ slices = array.each_slice(per_page).to_a
16
+
17
+ current_data = slices[current_page - 1] || []
18
+
19
+ if slices.count >= current_page - 1 && current_data.size >= per_page
20
+ next_page = current_page + 1
21
+ else
22
+ next_page = nil
23
+ end
24
+
25
+ [current_data, next_page]
26
+ end
27
+
28
+ private
29
+
30
+ # @return [Integer] how many elements should we display in the UI
31
+ def per_page
32
+ ::Karafka::Web.config.ui.per_page
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ # Namespace for models representing pieces of data about Karafka setup
7
+ module Models
8
+ # Representation of data of a Karafka consumer group
9
+ class ConsumerGroup < Lib::HashProxy
10
+ # @return [Array<Topic>] Data of topics belonging to this consumer group
11
+ def topics
12
+ super.values.map do |topic_hash|
13
+ Topic.new(topic_hash)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Models
7
+ # Aggregated health data statistics representation
8
+ class Health
9
+ class << self
10
+ # @param state [State] current system state
11
+ # @return [Hash] has with aggregated statistics
12
+ def current(state)
13
+ stats = {}
14
+
15
+ processes = Processes.active(state)
16
+
17
+ processes.each do |process|
18
+ process.consumer_groups.each do |details|
19
+ name = details.id
20
+
21
+ stats[name] ||= {}
22
+
23
+ details.topics.each do |details2|
24
+ t_name = details2.name
25
+
26
+ stats[name][t_name] ||= {}
27
+ details2.partitions.each do |partition|
28
+ partition_id = partition.id
29
+ stats[name][t_name] ||= {}
30
+ stats[name][t_name][partition_id] = partition.to_h
31
+ stats[name][t_name][partition_id][:process] = process
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ stats
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Models
7
+ # Single job data representation model
8
+ class Job < Lib::HashProxy
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Models
7
+ # A proxy between `::Karafka::Messages::Message` and web UI
8
+ # We work with the Karafka messages but use this model to wrap the work needed.
9
+ class Message
10
+ class << self
11
+ # Looks for a message from a given topic partition
12
+ #
13
+ # @param topic_id [String]
14
+ # @param partition_id [Integer]
15
+ # @param offset [Integer]
16
+ # @raise [::Karafka::Web::Errors::Ui::NotFoundError] when not found
17
+ def find(topic_id, partition_id, offset)
18
+ message = Karafka::Admin.read_topic(
19
+ topic_id,
20
+ partition_id,
21
+ 1,
22
+ offset
23
+ ).first
24
+
25
+ return message if message
26
+
27
+ raise(
28
+ ::Karafka::Web::Errors::Ui::NotFoundError,
29
+ [topic_id, partition_id, offset].join(', ')
30
+ )
31
+ end
32
+
33
+ # Fetches requested page of Kafka messages.
34
+ #
35
+ # @param topic_id [String]
36
+ # @param partition_id [Integer]
37
+ # @param page [Integer]
38
+ # @return [Array] We return both page data as well as all the details needed to build
39
+ # the pagination details.
40
+ def page(topic_id, partition_id, page)
41
+ # Establish the leading offset
42
+ lead = Karafka::Admin.read_topic(topic_id, partition_id, 1).first
43
+
44
+ partitions_count = fetch_partition_count(topic_id)
45
+
46
+ # If there is not even one message, we need to early exit
47
+ return [false, [], false, partitions_count] unless lead
48
+
49
+ # We add plus one because we compute previous offset from which we want to start and
50
+ # not previous page leading offset
51
+ previous_offset = lead.offset - (per_page * page) + 1
52
+
53
+ if previous_offset.negative?
54
+ count = per_page + previous_offset
55
+ previous_page = page < 2 ? false : page - 1
56
+ next_page = false
57
+ previous_offset = 0
58
+ else
59
+ previous_page = page < 2 ? false : page - 1
60
+ next_page = page + 1
61
+ count = per_page
62
+ end
63
+
64
+ [
65
+ previous_page,
66
+ read_topic(topic_id, partition_id, count, previous_offset).reverse,
67
+ next_page,
68
+ partitions_count
69
+ ]
70
+ end
71
+
72
+ private
73
+
74
+ # @param args [Object] anything required by the admin `#read_topic`
75
+ # @return [Array<Karafka::Messages::Message>] topic partition messages
76
+ def read_topic(*args)
77
+ ::Karafka::Admin.read_topic(*args)
78
+ end
79
+
80
+ # @param topic_id [String] id of the topic
81
+ # @return [Integer] number of partitions this topic has
82
+ def fetch_partition_count(topic_id)
83
+ ::Karafka::Admin
84
+ .cluster_info
85
+ .topics
86
+ .find { |topic| topic[:topic_name] == topic_id }
87
+ .fetch(:partition_count)
88
+ end
89
+
90
+ # @return [Integer] elements per page
91
+ def per_page
92
+ ::Karafka::Web.config.ui.per_page
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Models
7
+ # Single topic partition data representation model
8
+ class Partition < Lib::HashProxy
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end