karafka-web 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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