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
@@ -24,9 +24,12 @@ module Karafka
24
24
  def index
25
25
  @topics = Models::ClusterInfo
26
26
  .topics
27
- .reject { |topic| topic[:topic_name] == '__consumer_offsets' }
28
27
  .sort_by { |topic| topic[:topic_name] }
29
28
 
29
+ unless ::Karafka::Web.config.ui.show_internal_topics
30
+ @topics.reject! { |topic| topic[:topic_name].start_with?('__') }
31
+ end
32
+
30
33
  respond
31
34
  end
32
35
 
@@ -42,6 +45,8 @@ module Karafka
42
45
  # @note We cannot use offset references here because each of the partitions may have
43
46
  # completely different values
44
47
  def topic(topic_id)
48
+ @visibility_filter = ::Karafka::Web.config.ui.visibility_filter
49
+
45
50
  @topic_id = topic_id
46
51
  @partitions_count = Models::ClusterInfo.partitions_count(topic_id)
47
52
 
@@ -63,6 +68,7 @@ module Karafka
63
68
  # @param topic_id [String]
64
69
  # @param partition_id [Integer]
65
70
  def partition(topic_id, partition_id)
71
+ @visibility_filter = ::Karafka::Web.config.ui.visibility_filter
66
72
  @topic_id = topic_id
67
73
  @partition_id = partition_id
68
74
  @watermark_offsets = Ui::Models::WatermarkOffsets.find(topic_id, partition_id)
@@ -74,7 +80,8 @@ module Karafka
74
80
  previous_offset,
75
81
  @params.current_offset,
76
82
  next_offset,
77
- @messages.map(&:offset)
83
+ # If message is an array, it means it's a compacted dummy offset representation
84
+ @messages.map { |message| message.is_a?(Array) ? message.last : message.offset }
78
85
  )
79
86
 
80
87
  respond
@@ -85,28 +92,106 @@ module Karafka
85
92
  # @param topic_id [String]
86
93
  # @param partition_id [Integer]
87
94
  # @param offset [Integer] offset of the message we want to display
88
- def show(topic_id, partition_id, offset)
95
+ # @param paginate [Boolean] do we want to have pagination
96
+ def show(topic_id, partition_id, offset, paginate: true)
97
+ @visibility_filter = ::Karafka::Web.config.ui.visibility_filter
89
98
  @topic_id = topic_id
90
99
  @partition_id = partition_id
91
100
  @offset = offset
92
101
  @message = Ui::Models::Message.find(@topic_id, @partition_id, @offset)
93
102
  @payload_error = false
94
103
 
95
- @decrypt = if ::Karafka::App.config.encryption.active
96
- ::Karafka::Web.config.ui.decrypt
97
- else
98
- true
99
- end
100
-
101
104
  begin
102
105
  @pretty_payload = JSON.pretty_generate(@message.payload)
103
106
  rescue StandardError => e
104
107
  @payload_error = e
105
108
  end
106
109
 
110
+ # This may be off for certain views like recent view where we are interested only
111
+ # in the most recent all the time. It does not make any sense to display pagination
112
+ # there
113
+ if paginate
114
+ # We need watermark offsets to decide if we can paginate left and right
115
+ watermark_offsets = Ui::Models::WatermarkOffsets.find(topic_id, partition_id)
116
+ paginate(offset, watermark_offsets.low, watermark_offsets.high)
117
+ end
118
+
107
119
  respond
108
120
  end
109
121
 
122
+ # Displays the most recent message on a topic/partition
123
+ #
124
+ # @param topic_id [String]
125
+ # @param partition_id [Integer, nil] partition we're interested in or nil if we are
126
+ # interested in the most recent message from all the partitions
127
+ def recent(topic_id, partition_id)
128
+ if partition_id
129
+ active_partitions = [partition_id]
130
+ else
131
+ partitions_count = Models::ClusterInfo.partitions_count(topic_id)
132
+ active_partitions, = Paginators::Partitions.call(partitions_count, 1)
133
+ end
134
+
135
+ # This selects first page with most recent messages
136
+ messages, = Models::Message.topic_page(topic_id, active_partitions, 1)
137
+
138
+ # Selects newest out of all partitions
139
+ recent = messages.max_by(&:timestamp)
140
+
141
+ recent || raise(::Karafka::Web::Errors::Ui::NotFoundError)
142
+
143
+ show(topic_id, recent.partition, recent.offset, paginate: false)
144
+ end
145
+
146
+ # Computes a page on which the given offset is in the middle of the page (if possible)
147
+ # Useful often when debugging to be able to quickly jump to the historical location
148
+ # of message and its surrounding to understand failure
149
+ #
150
+ # @param topic_id [String]
151
+ # @param partition_id [Integer]
152
+ # @param offset [Integer] offset of the message we want to display
153
+ def surrounding(topic_id, partition_id, offset)
154
+ watermark_offsets = Ui::Models::WatermarkOffsets.find(topic_id, partition_id)
155
+
156
+ raise ::Karafka::Web::Errors::Ui::NotFoundError if offset < watermark_offsets.low
157
+ raise ::Karafka::Web::Errors::Ui::NotFoundError if offset >= watermark_offsets.high
158
+
159
+ # Assume we start from this offset
160
+ shift = 0
161
+ elements = 0
162
+
163
+ # Position the offset as close to the middle of offset based page as possible
164
+ ::Karafka::Web.config.ui.per_page.times do
165
+ break if elements >= ::Karafka::Web.config.ui.per_page
166
+
167
+ elements += 1 if offset + shift < watermark_offsets.high
168
+
169
+ if offset - shift > watermark_offsets.low
170
+ shift += 1
171
+ elements += 1
172
+ end
173
+ end
174
+
175
+ target = offset - shift
176
+
177
+ redirect("explorer/#{topic_id}/#{partition_id}?offset=#{target}")
178
+ end
179
+
180
+ # Finds the closest offset matching the requested time and redirects to this location
181
+ # Note, that it redirects to closest but always younger.
182
+ #
183
+ # @param topic_id [String]
184
+ # @param partition_id [Integer]
185
+ # @param time [Time] time of the message
186
+ def closest(topic_id, partition_id, time)
187
+ target = ::Karafka::Admin.read_topic(topic_id, partition_id, 1, time).first
188
+
189
+ partition_path = "explorer/#{topic_id}/#{partition_id}"
190
+ partition_path += "?offset=#{target.offset}" if target
191
+
192
+ redirect(partition_path)
193
+ end
194
+
110
195
  private
111
196
 
112
197
  # Fetches current page data
@@ -19,12 +19,20 @@ module Karafka
19
19
  # Health state controller
20
20
  class Health < Ui::Controllers::Base
21
21
  # Displays the current system state
22
- def index
23
- current_state = Models::State.current!
22
+ def overview
23
+ current_state = Models::ConsumersState.current!
24
24
  @stats = Models::Health.current(current_state)
25
25
 
26
26
  respond
27
27
  end
28
+
29
+ # Displays details about offsets and their progression/statuses
30
+ def offsets
31
+ # Same data as overview but presented differently
32
+ overview
33
+
34
+ respond
35
+ end
28
36
  end
29
37
  end
30
38
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This Karafka component is a Pro component under a commercial license.
4
+ # This Karafka component is NOT licensed under LGPL.
5
+ #
6
+ # All of the commercial components are present in the lib/karafka/pro directory of this
7
+ # repository and their usage requires commercial license agreement.
8
+ #
9
+ # Karafka has also commercial-friendly license, commercial support and commercial components.
10
+ #
11
+ # By sending a pull request to the pro components, you are agreeing to transfer the copyright of
12
+ # your code to Maciej Mensfeld.
13
+
14
+ module Karafka
15
+ module Web
16
+ module Ui
17
+ module Pro
18
+ module Controllers
19
+ # Controller for working with messages
20
+ # While part of messages operations is done via explorer (exploring), this controller
21
+ # handles other cases not related to viewing data
22
+ class Messages < Ui::Controllers::Base
23
+ # Takes a requested message content and republishes it again
24
+ #
25
+ # @param topic_id [String]
26
+ # @param partition_id [Integer]
27
+ # @param offset [Integer] offset of the message we want to republish
28
+ def republish(topic_id, partition_id, offset)
29
+ message = Ui::Models::Message.find(topic_id, partition_id, offset)
30
+
31
+ delivery = ::Karafka.producer.produce_sync(
32
+ topic: topic_id,
33
+ partition: partition_id,
34
+ payload: message.raw_payload,
35
+ headers: message.headers,
36
+ key: message.key
37
+ )
38
+
39
+ redirect(
40
+ :back,
41
+ success: reproduced(message, delivery)
42
+ )
43
+ end
44
+
45
+ private
46
+
47
+ # @param message [Karafka::Messages::Message]
48
+ # @param delivery [Rdkafka::Producer::DeliveryReport]
49
+ # @return [String] flash message about message reproducing
50
+ def reproduced(message, delivery)
51
+ <<~MSG
52
+ Message with offset #{message.offset}
53
+ has been sent again to #{message.topic}##{message.partition}
54
+ and received offset #{delivery.offset}.
55
+ MSG
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -18,6 +18,50 @@ module Karafka
18
18
  module Controllers
19
19
  # Routing details - same as in OSS
20
20
  class Routing < Ui::Controllers::Routing
21
+ # Routing list
22
+ def index
23
+ detect_patterns_routes
24
+
25
+ @routes = Karafka::App.routes
26
+
27
+ respond
28
+ end
29
+
30
+ # Given route details
31
+ #
32
+ # @param topic_id [String] topic id
33
+ def show(topic_id)
34
+ detect_patterns_routes
35
+
36
+ @topic = Karafka::Routing::Router.find_by(id: topic_id)
37
+
38
+ @topic || raise(::Karafka::Web::Errors::Ui::NotFoundError, topic_id)
39
+
40
+ respond
41
+ end
42
+
43
+ private
44
+
45
+ # Checks list of topics and tries to match them against the available patterns
46
+ # Uses the Pro detector to expand routes in the Web-UI so we include topics that are
47
+ # or will be matched using our regular expressions
48
+ def detect_patterns_routes
49
+ detector = ::Karafka::Pro::Routing::Features::Patterns::Detector.new
50
+ topics_names = Models::ClusterInfo.topics.map(&:topic_name)
51
+
52
+ Karafka::App
53
+ .routes
54
+ .flat_map(&:subscription_groups)
55
+ .each do |subscription_group|
56
+ sg_topics = subscription_group.topics
57
+
58
+ # Reject topics that are already part of routing for given subscription groups
59
+ # and then for remaining try to apply patterns and expand routes
60
+ topics_names
61
+ .reject { |t_name| sg_topics.any? { |rtopic| rtopic.name == t_name } }
62
+ .each { |t_name| detector.expand(sg_topics, t_name) }
63
+ end
64
+ end
21
65
  end
22
66
  end
23
67
  end
@@ -17,11 +17,17 @@
17
17
  Running jobs
18
18
  </a>
19
19
  </li>
20
- <% else %>
20
+ <% elsif current_path.include?('/subscriptions') %>
21
21
  <li class="breadcrumb-item">
22
22
  <a href="<%= root_path('consumers', @process.id, 'subscriptions') %>">
23
23
  Active subscriptions
24
24
  </a>
25
25
  </li>
26
+ <% else %>
27
+ <li class="breadcrumb-item">
28
+ <a href="<%= root_path('consumers', @process.id, 'details') %>">
29
+ Details
30
+ </a>
31
+ </li>
26
32
  <% end %>
27
33
  <% end %>
@@ -47,7 +47,7 @@
47
47
  <%= process.utilization.round(1) %>%
48
48
  </span>
49
49
  <span class="badge bg-primary badge-topic">
50
- <%= process.concurrency %> /
50
+ <%= process.workers %> /
51
51
  <%= process.busy %>
52
52
  </span>
53
53
  </td>
@@ -14,19 +14,21 @@
14
14
  <div class="desc">Batches</div>
15
15
  </li>
16
16
  <li class="col-sm">
17
- <div class="count mb-1"><%= @processes.sum(&:lag_stored) %></div>
18
- <div class="desc">Lag</div>
17
+ <div class="count mb-1"><%= @counters.lag_stored %></div>
18
+ <div class="desc">Lag stored</div>
19
19
  </li>
20
20
  <li class="col-sm">
21
- <div class="count mb-1"><%= @counters.busy %></div>
22
- <div class="desc">Busy</div>
21
+ <a href="<%= root_path('jobs') %>">
22
+ <div class="count mb-1"><%= @counters.busy %></div>
23
+ <div class="desc">Busy</div>
24
+ </a>
23
25
  </li>
24
26
  <li class="col-sm">
25
27
  <div class="count mb-1"><%= @counters.enqueued %></div>
26
28
  <div class="desc">Enqueued</div>
27
29
  </li>
28
30
  <li class="col-sm">
29
- <a href="<%= root_path('errors/0') %>">
31
+ <a href="<%= root_path('errors') %>">
30
32
  <div class="count mb-1"><%= @counters.errors %></div>
31
33
  <div class="desc">Errors</div>
32
34
  </a>
@@ -17,13 +17,13 @@
17
17
  <code>#<%= job.type %></code>
18
18
  </td>
19
19
  <td>
20
- <%== offset_with_label job.first_offset %>
20
+ <%== offset_with_label job.topic, job.partition, job.first_offset, explore: true %>
21
21
  </td>
22
22
  <td>
23
- <%== offset_with_label job.last_offset %>
23
+ <%== offset_with_label job.topic, job.partition, job.last_offset, explore: true %>
24
24
  </td>
25
25
  <td>
26
- <%== offset_with_label job.committed_offset %>
26
+ <%== offset_with_label job.topic, job.partition, job.committed_offset, explore: true %>
27
27
  </td>
28
28
  <td>
29
29
  <%== relative_time job.started_at %>
@@ -19,6 +19,7 @@
19
19
  <%== relative_time @process.started_at %>
20
20
  </span>
21
21
  </li>
22
+
22
23
  <li class="align-items-center d-flex justify-content-between">
23
24
  State from:
24
25
  <span class="badge bg-secondary">
@@ -41,13 +42,13 @@
41
42
  <p class="card-text">
42
43
  <ul style="list-style: square !important;">
43
44
  <li class="align-items-center d-flex justify-content-between">
44
- Threads:
45
+ Workers:
45
46
  <span class="badge bg-primary">
46
- <%= @process.concurrency %>
47
+ <%= @process.workers %>
47
48
  </span>
48
49
  </li>
49
50
  <li class="align-items-center d-flex justify-content-between">
50
- Threads utilization:
51
+ Utilization:
51
52
  <span class="badge bg-primary">
52
53
  <%= @process.utilization.round(2) %>%
53
54
  </span>
@@ -55,7 +56,7 @@
55
56
  <li class="align-items-center d-flex justify-content-between">
56
57
  CPUs:
57
58
  <span class="badge bg-primary">
58
- <%= @process.cpu_count %>
59
+ <%= @process.cpus %>
59
60
  </span>
60
61
  </li>
61
62
  <li class="align-items-center d-flex justify-content-between">
@@ -1,4 +1,4 @@
1
- <tr>
1
+ <tr class="align-middle <%= lso_risk_state_bg(partition) %>">
2
2
  <td>
3
3
  <%= topic.name %>
4
4
  </td>
@@ -6,7 +6,7 @@
6
6
  <%= partition.id %>
7
7
  </td>
8
8
  <td>
9
- <%== offset_with_label partition.lag_stored.to_i %>
9
+ <%== lag_with_label partition.lag_stored %>
10
10
  </td>
11
11
  <td>
12
12
  <span class="badge <%= lag_trend_bg(partition.lag_stored_d) %>">
@@ -14,10 +14,14 @@
14
14
  </span>
15
15
  </td>
16
16
  <td>
17
- <%== offset_with_label partition.committed_offset.to_i %>
17
+ <% if partition.stored_offset.negative? %>
18
+ <%== offset_with_label topic.name, partition.id, partition.committed_offset - 1 %>
19
+ <% else %>
20
+ <%== offset_with_label topic.name, partition.id, partition.committed_offset %>
21
+ <% end %>
18
22
  </td>
19
23
  <td>
20
- <%== offset_with_label partition.stored_offset.to_i %>
24
+ <%== offset_with_label topic.name, partition.id, partition.stored_offset %>
21
25
  </td>
22
26
  <td>
23
27
  <span class="badge <%= kafka_state_bg(partition.fetch_state) %> mt-1 mb-1">
@@ -29,4 +33,9 @@
29
33
  <%= partition.poll_state %>
30
34
  </span>
31
35
  </td>
36
+ <td>
37
+ <span class="badge bg-success <%= lso_risk_state_bg(partition) %> bg-opacity-100">
38
+ <%= partition.lso_risk_state %>
39
+ </span>
40
+ </td>
32
41
  </tr>
@@ -65,7 +65,7 @@
65
65
  </div>
66
66
 
67
67
  <% if subscription_group.topics.empty? %>
68
- <div class="row">
68
+ <div class="row mb-4">
69
69
  <div class="col-lg-12">
70
70
  <div class="alert alert-info" role="alert">
71
71
  This process does not consume any messages from any topics of this consumer group.
@@ -81,11 +81,12 @@
81
81
  <th>Topic</th>
82
82
  <th>Partition</th>
83
83
  <th>Lag stored</th>
84
- <th>Lag trend</th>
84
+ <th>Lag stored trend</th>
85
85
  <th>Committed offset</th>
86
86
  <th>Stored offset</th>
87
87
  <th>Fetch state</th>
88
88
  <th>Poll state</th>
89
+ <th>LSO state</th>
89
90
  </tr>
90
91
  </thead>
91
92
  <tbody>
@@ -9,12 +9,19 @@
9
9
  (<%= @process.subscribed_partitions_count %>)
10
10
  </a>
11
11
  </li>
12
+
12
13
  <li class="nav-item">
13
14
  <a class="nav-link <%= nav_class(include: 'jobs') %>" href="<%= root_path('consumers', @process.id, 'jobs') %>">
14
15
  Running jobs
15
16
  (<%= @process.jobs.count %>)
16
17
  </a>
17
18
  </li>
19
+
20
+ <li class="nav-item">
21
+ <a class="nav-link <%= nav_class(include: 'details') %>" href="<%= root_path('consumers', @process.id, 'details') %>">
22
+ Details
23
+ </a>
24
+ </li>
18
25
  </ul>
19
26
 
20
27
  </div>
@@ -0,0 +1,21 @@
1
+ <%== view_title(@process.name) %>
2
+
3
+ <% if @process.status == 'stopped' %>
4
+ <%== partial 'consumers/consumer/stopped' %>
5
+ <% end %>
6
+
7
+ <%== partial 'consumers/consumer/metrics' %>
8
+
9
+ <%== partial 'consumers/consumer/tabs' %>
10
+
11
+ <div class="container">
12
+ <div class="row">
13
+ <div class="col-sm-12">
14
+ <div class="card">
15
+ <div class="card-body">
16
+ <pre class="m-0 p-0"><code class="wrapped json p-0 m-0"><%= JSON.pretty_generate(@process.to_h) %></code></pre>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </div>
@@ -4,8 +4,10 @@
4
4
  <div class="container">
5
5
  <div class="row">
6
6
  <div class="col-sm-12">
7
- <% if @processes.empty? %>
7
+ <% if @processes.empty? && params.current_page <= 1 %>
8
8
  <%== partial 'consumers/no_consumers' %>
9
+ <% elsif @processes.empty? %>
10
+ <%== partial 'shared/no_paginated_data' %>
9
11
  <% else %>
10
12
  <table class="processes bg-white table table-hover table-bordered table-striped mb-0 align-middle">
11
13
  <thead>
@@ -15,7 +17,7 @@
15
17
  <th class="col-sm-1">Memory</th>
16
18
  <th class="col-sm-1">Performance</th>
17
19
  <th class="col-sm-1">Load</th>
18
- <th class="col-sm-1">Total lag</th>
20
+ <th class="col-sm-1">Lag stored</th>
19
21
  </tr>
20
22
  </thead>
21
23
  <tbody>
@@ -0,0 +1,39 @@
1
+ <div class="container">
2
+ <div class="row">
3
+ <div class="col-sm-12 text-end">
4
+ <div class="btn-group btn-group-sm" role="group" aria-label="Small button group">
5
+ <%
6
+ path = root_path('dashboard?range=seconds')
7
+ active = params.current_range == :seconds ? 'active' : false
8
+ %>
9
+ <a type="button" href="<%= path %>" class="btn btn-outline-primary <%= active %>">
10
+ 5 minutes
11
+ </a>
12
+
13
+ <%
14
+ path = root_path('dashboard?range=minutes')
15
+ active = params.current_range == :minutes ? 'active' : false
16
+ %>
17
+ <a type="button" href="<%= path %>" class="btn btn-outline-primary <%= active %>">
18
+ 1 hour
19
+ </a>
20
+
21
+ <%
22
+ path = root_path('dashboard?range=hours')
23
+ active = params.current_range == :hours ? 'active' : false
24
+ %>
25
+ <a type="button" href="<%= path %>" class="btn btn-outline-primary <%= active %>">
26
+ 24 hours
27
+ </a>
28
+
29
+ <%
30
+ path = root_path('dashboard?range=days')
31
+ active = params.current_range == :days ? 'active' : false
32
+ %>
33
+ <a type="button" href="<%= path %>" class="btn btn-outline-primary <%= active %>">
34
+ 7 days
35
+ </a>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </div>
@@ -0,0 +1,82 @@
1
+ <%== partial 'consumers/counters' %>
2
+
3
+ <% if @aggregated.sufficient? %>
4
+ <%== partial 'dashboard/ranges_selector' %>
5
+
6
+ <div class="container">
7
+ <div class="row">
8
+ <div class="col-sm-12">
9
+ <h5 class="mb-3">Processing metrics</h5>
10
+
11
+ <hr class="mb-4"/>
12
+
13
+ <ul class="nav nav-tabs" id="graphs1" role="tablist">
14
+ <%== partial 'shared/tab_nav', locals: { title: 'Messages', id: 'messages', active: true } %>
15
+ <%== partial 'shared/tab_nav', locals: { title: 'Batches', id: 'batches' } %>
16
+ <%== partial 'shared/tab_nav', locals: { title: 'Lags stored', id: 'lags-stored' } %>
17
+ <%== partial 'shared/tab_nav', locals: { title: 'Topics pace', id: 'topics-pace' } %>
18
+ <%== partial 'shared/tab_nav', locals: { title: 'Max LSO time', id: 'max-lso-time' } %>
19
+ </ul>
20
+
21
+ <div class="tab-content">
22
+ <div class="tab-pane show active" id="messages" role="tabpanel">
23
+ <% data = @aggregated_charts.with(:messages, :errors, :dead, :retries, :batch_size) %>
24
+ <%== partial 'shared/chart', locals: { data: data, id: 'messages' } %>
25
+ </div>
26
+
27
+ <div class="tab-pane" id="batches" role="tabpanel">
28
+ <% data = @aggregated_charts.with(:batches, :errors, :dead, :retries) %>
29
+ <%== partial 'shared/chart', locals: { data: data, id: 'batches' } %>
30
+ </div>
31
+
32
+ <div class="tab-pane" id="lags-stored" role="tabpanel">
33
+ <%== partial 'shared/chart', locals: { data: @topics_charts.lags_stored, id: 'lags-stored' } %>
34
+ </div>
35
+
36
+ <div class="tab-pane" id="topics-pace" role="tabpanel">
37
+ <%== partial 'shared/chart', locals: { data: @topics_charts.topics_pace, id: 'topics-pace' } %>
38
+ </div>
39
+
40
+ <div class="tab-pane" id="max-lso-time" role="tabpanel">
41
+ <%== partial 'shared/chart', locals: { data: @topics_charts.max_lso_time, id: 'max-lso-time' } %>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+
48
+ <div class="container">
49
+ <div class="row">
50
+ <div class="col-sm-12">
51
+ <h5 class="mb-3">Utilization metrics</h5>
52
+
53
+ <hr class="mb-4"/>
54
+
55
+ <ul class="nav nav-tabs" id="graphs2" role="tablist">
56
+ <%== partial 'shared/tab_nav', locals: { title: 'Utilization', id: 'utilization', active: true } %>
57
+ <%== partial 'shared/tab_nav', locals: { title: 'RSS', id: 'rss' } %>
58
+ <%== partial 'shared/tab_nav', locals: { title: 'Concurrency', id: 'concurrency' } %>
59
+ </ul>
60
+
61
+ <div class="tab-content">
62
+ <div class="tab-pane show active" id="utilization" role="tabpanel">
63
+ <% data = @aggregated_charts.with(:utilization) %>
64
+ <%== partial 'shared/chart', locals: { data: data, id: 'utilization', label_type_y: 'percentage' } %>
65
+ </div>
66
+
67
+ <div class="tab-pane show" id="rss" role="tabpanel">
68
+ <% data = @aggregated_charts.with(:rss, :process_rss) %>
69
+ <%== partial 'shared/chart', locals: { data: data, id: 'rss', label_type_y: 'memory' } %>
70
+ </div>
71
+
72
+ <div class="tab-pane show" id="concurrency" role="tabpanel">
73
+ <% data = @aggregated_charts.with(:processes, :workers, :listeners) %>
74
+ <%== partial 'shared/chart', locals: { data: data, id: 'concurrency' } %>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ <% else %>
81
+ <%== partial 'dashboard/not_enough_data' %>
82
+ <% end %>
@@ -2,7 +2,7 @@
2
2
  <div class="card" >
3
3
  <div class="card-body p-2">
4
4
  <p class="card-text mb-0 p-2">
5
- <a href="<%= root_path('explorer', topic[:topic_name], 0) %>">
5
+ <a href="<%= explorer_path(topic[:topic_name]) %>">
6
6
  <%= topic[:topic_name] %> /
7
7
  <%= topic[:partition_count] %>
8
8
  </a>