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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ci.yml +13 -4
- data/CHANGELOG.md +114 -6
- data/Gemfile +1 -0
- data/Gemfile.lock +27 -24
- data/README.md +2 -0
- data/bin/rspecs +6 -0
- data/certs/cert_chain.pem +21 -21
- data/docker-compose.yml +22 -0
- data/karafka-web.gemspec +3 -3
- data/lib/karafka/web/app.rb +6 -2
- data/lib/karafka/web/cli.rb +51 -47
- data/lib/karafka/web/config.rb +31 -9
- data/lib/karafka/web/contracts/base.rb +32 -0
- data/lib/karafka/web/contracts/config.rb +63 -0
- data/lib/karafka/web/deserializer.rb +10 -1
- data/lib/karafka/web/errors.rb +29 -7
- data/lib/karafka/web/installer.rb +58 -148
- data/lib/karafka/web/management/base.rb +34 -0
- data/lib/karafka/web/management/clean_boot_file.rb +31 -0
- data/lib/karafka/web/management/create_initial_states.rb +101 -0
- data/lib/karafka/web/management/create_topics.rb +127 -0
- data/lib/karafka/web/management/delete_topics.rb +28 -0
- data/lib/karafka/web/management/enable.rb +82 -0
- data/lib/karafka/web/management/extend_boot_file.rb +37 -0
- data/lib/karafka/web/processing/consumer.rb +73 -17
- data/lib/karafka/web/processing/consumers/aggregators/base.rb +56 -0
- data/lib/karafka/web/processing/consumers/aggregators/metrics.rb +154 -0
- data/lib/karafka/web/processing/consumers/aggregators/state.rb +180 -0
- data/lib/karafka/web/processing/consumers/contracts/aggregated_stats.rb +32 -0
- data/lib/karafka/web/processing/consumers/contracts/metrics.rb +53 -0
- data/lib/karafka/web/processing/consumers/contracts/process.rb +19 -0
- data/lib/karafka/web/processing/consumers/contracts/state.rb +49 -0
- data/lib/karafka/web/processing/consumers/contracts/topic_stats.rb +21 -0
- data/lib/karafka/web/processing/consumers/metrics.rb +29 -0
- data/lib/karafka/web/processing/consumers/schema_manager.rb +56 -0
- data/lib/karafka/web/processing/consumers/state.rb +6 -9
- data/lib/karafka/web/processing/time_series_tracker.rb +130 -0
- data/lib/karafka/web/tracking/consumers/contracts/consumer_group.rb +2 -2
- data/lib/karafka/web/tracking/consumers/contracts/job.rb +2 -1
- data/lib/karafka/web/tracking/consumers/contracts/partition.rb +14 -1
- data/lib/karafka/web/tracking/consumers/contracts/report.rb +10 -8
- data/lib/karafka/web/tracking/consumers/contracts/subscription_group.rb +2 -2
- data/lib/karafka/web/tracking/consumers/contracts/topic.rb +2 -2
- data/lib/karafka/web/tracking/consumers/listeners/processing.rb +6 -2
- data/lib/karafka/web/tracking/consumers/listeners/statistics.rb +15 -1
- data/lib/karafka/web/tracking/consumers/reporter.rb +14 -6
- data/lib/karafka/web/tracking/consumers/sampler.rb +80 -39
- data/lib/karafka/web/tracking/contracts/error.rb +2 -1
- data/lib/karafka/web/ui/app.rb +20 -10
- data/lib/karafka/web/ui/base.rb +50 -4
- data/lib/karafka/web/ui/controllers/base.rb +11 -0
- data/lib/karafka/web/ui/controllers/become_pro.rb +1 -1
- data/lib/karafka/web/ui/controllers/cluster.rb +7 -4
- data/lib/karafka/web/ui/controllers/consumers.rb +1 -1
- data/lib/karafka/web/ui/controllers/dashboard.rb +32 -0
- data/lib/karafka/web/ui/controllers/jobs.rb +1 -1
- data/lib/karafka/web/ui/controllers/requests/params.rb +18 -0
- data/lib/karafka/web/ui/controllers/responses/redirect.rb +29 -0
- data/lib/karafka/web/ui/helpers/application_helper.rb +57 -14
- data/lib/karafka/web/ui/helpers/paths_helper.rb +48 -0
- data/lib/karafka/web/ui/lib/hash_proxy.rb +18 -6
- data/lib/karafka/web/ui/lib/paginations/watermark_offsets_based.rb +75 -0
- data/lib/karafka/web/ui/lib/ttl_cache.rb +8 -0
- data/lib/karafka/web/ui/models/consumers_metrics.rb +46 -0
- data/lib/karafka/web/ui/models/{state.rb → consumers_state.rb} +6 -2
- data/lib/karafka/web/ui/models/health.rb +37 -7
- data/lib/karafka/web/ui/models/message.rb +11 -3
- data/lib/karafka/web/ui/models/metrics/aggregated.rb +196 -0
- data/lib/karafka/web/ui/models/metrics/charts/aggregated.rb +50 -0
- data/lib/karafka/web/ui/models/metrics/charts/topics.rb +109 -0
- data/lib/karafka/web/ui/models/metrics/topics.rb +101 -0
- data/lib/karafka/web/ui/models/partition.rb +27 -0
- data/lib/karafka/web/ui/models/process.rb +12 -1
- data/lib/karafka/web/ui/models/status.rb +107 -21
- data/lib/karafka/web/ui/models/visibility_filter.rb +33 -0
- data/lib/karafka/web/ui/pro/app.rb +80 -20
- data/lib/karafka/web/ui/pro/controllers/cluster.rb +11 -0
- data/lib/karafka/web/ui/pro/controllers/consumers.rb +10 -6
- data/lib/karafka/web/ui/pro/controllers/dashboard.rb +54 -0
- data/lib/karafka/web/ui/pro/controllers/errors.rb +3 -0
- data/lib/karafka/web/ui/pro/controllers/explorer.rb +94 -9
- data/lib/karafka/web/ui/pro/controllers/health.rb +10 -2
- data/lib/karafka/web/ui/pro/controllers/messages.rb +62 -0
- data/lib/karafka/web/ui/pro/controllers/routing.rb +44 -0
- data/lib/karafka/web/ui/pro/views/consumers/_breadcrumbs.erb +7 -1
- data/lib/karafka/web/ui/pro/views/consumers/_consumer.erb +1 -1
- data/lib/karafka/web/ui/pro/views/consumers/_counters.erb +7 -5
- data/lib/karafka/web/ui/pro/views/consumers/consumer/_job.erb +3 -3
- data/lib/karafka/web/ui/pro/views/consumers/consumer/_metrics.erb +5 -4
- data/lib/karafka/web/ui/pro/views/consumers/consumer/_partition.erb +13 -4
- data/lib/karafka/web/ui/pro/views/consumers/consumer/_subscription_group.erb +3 -2
- data/lib/karafka/web/ui/pro/views/consumers/consumer/_tabs.erb +7 -0
- data/lib/karafka/web/ui/pro/views/consumers/details.erb +21 -0
- data/lib/karafka/web/ui/pro/views/consumers/index.erb +4 -2
- data/lib/karafka/web/ui/pro/views/dashboard/_ranges_selector.erb +39 -0
- data/lib/karafka/web/ui/pro/views/dashboard/index.erb +82 -0
- data/lib/karafka/web/ui/pro/views/dlq/_topic.erb +1 -1
- data/lib/karafka/web/ui/pro/views/errors/_error.erb +1 -1
- data/lib/karafka/web/ui/pro/views/errors/index.erb +1 -1
- data/lib/karafka/web/ui/pro/views/errors/partition.erb +1 -1
- data/lib/karafka/web/ui/pro/views/errors/show.erb +1 -1
- data/lib/karafka/web/ui/pro/views/explorer/_breadcrumbs.erb +6 -4
- data/lib/karafka/web/ui/pro/views/explorer/_filtered.erb +16 -0
- data/lib/karafka/web/ui/pro/views/explorer/_message.erb +6 -2
- data/lib/karafka/web/ui/pro/views/explorer/_no_topics.erb +7 -0
- data/lib/karafka/web/ui/pro/views/explorer/_partition_option.erb +2 -2
- data/lib/karafka/web/ui/pro/views/explorer/_topic.erb +1 -1
- data/lib/karafka/web/ui/pro/views/explorer/index.erb +12 -8
- data/lib/karafka/web/ui/pro/views/explorer/messages/_headers.erb +15 -0
- data/lib/karafka/web/ui/pro/views/explorer/messages/_key.erb +12 -0
- data/lib/karafka/web/ui/pro/views/explorer/partition/_details.erb +35 -0
- data/lib/karafka/web/ui/pro/views/explorer/partition.erb +5 -3
- data/lib/karafka/web/ui/pro/views/explorer/show.erb +48 -5
- data/lib/karafka/web/ui/pro/views/explorer/topic/_details.erb +23 -0
- data/lib/karafka/web/ui/pro/views/explorer/topic.erb +14 -12
- data/lib/karafka/web/ui/pro/views/health/_breadcrumbs.erb +16 -0
- data/lib/karafka/web/ui/pro/views/health/_no_data.erb +9 -0
- data/lib/karafka/web/ui/pro/views/health/_partition.erb +17 -15
- data/lib/karafka/web/ui/pro/views/health/_partition_offset.erb +40 -0
- data/lib/karafka/web/ui/pro/views/health/_tabs.erb +27 -0
- data/lib/karafka/web/ui/pro/views/health/offsets.erb +71 -0
- data/lib/karafka/web/ui/pro/views/health/overview.erb +68 -0
- data/lib/karafka/web/ui/pro/views/jobs/_job.erb +6 -3
- data/lib/karafka/web/ui/pro/views/jobs/index.erb +4 -1
- data/lib/karafka/web/ui/pro/views/routing/_consumer_group.erb +37 -0
- data/lib/karafka/web/ui/pro/views/routing/_detail.erb +25 -0
- data/lib/karafka/web/ui/pro/views/routing/_topic.erb +23 -0
- data/lib/karafka/web/ui/pro/views/routing/index.erb +10 -0
- data/lib/karafka/web/ui/pro/views/routing/show.erb +26 -0
- data/lib/karafka/web/ui/pro/views/shared/_navigation.erb +6 -9
- data/lib/karafka/web/ui/public/images/logo-gray.svg +28 -0
- data/lib/karafka/web/ui/public/javascripts/application.js +30 -0
- data/lib/karafka/web/ui/public/javascripts/chart.min.js +14 -0
- data/lib/karafka/web/ui/public/javascripts/charts.js +330 -0
- data/lib/karafka/web/ui/public/javascripts/datepicker.js +6 -0
- data/lib/karafka/web/ui/public/javascripts/live_poll.js +39 -12
- data/lib/karafka/web/ui/public/javascripts/offset_datetime.js +74 -0
- data/lib/karafka/web/ui/public/javascripts/tabs.js +59 -0
- data/lib/karafka/web/ui/public/stylesheets/application.css +11 -0
- data/lib/karafka/web/ui/public/stylesheets/datepicker.min.css +12 -0
- data/lib/karafka/web/ui/views/cluster/_no_partitions.erb +3 -0
- data/lib/karafka/web/ui/views/cluster/_partition.erb +20 -22
- data/lib/karafka/web/ui/views/cluster/index.erb +6 -1
- data/lib/karafka/web/ui/views/consumers/_consumer.erb +1 -1
- data/lib/karafka/web/ui/views/consumers/_counters.erb +6 -4
- data/lib/karafka/web/ui/views/consumers/_summary.erb +3 -3
- data/lib/karafka/web/ui/views/consumers/index.erb +3 -1
- data/lib/karafka/web/ui/views/dashboard/_feature_pro.erb +3 -0
- data/lib/karafka/web/ui/views/dashboard/_not_enough_data.erb +15 -0
- data/lib/karafka/web/ui/views/dashboard/_ranges_selector.erb +23 -0
- data/lib/karafka/web/ui/views/dashboard/index.erb +95 -0
- data/lib/karafka/web/ui/views/errors/_detail.erb +12 -0
- data/lib/karafka/web/ui/views/errors/_error.erb +1 -1
- data/lib/karafka/web/ui/views/errors/show.erb +1 -1
- data/lib/karafka/web/ui/views/jobs/index.erb +3 -1
- data/lib/karafka/web/ui/views/layout.erb +10 -3
- data/lib/karafka/web/ui/views/routing/_consumer_group.erb +8 -6
- data/lib/karafka/web/ui/views/routing/_detail.erb +2 -2
- data/lib/karafka/web/ui/views/routing/_topic.erb +1 -1
- data/lib/karafka/web/ui/views/routing/show.erb +1 -1
- data/lib/karafka/web/ui/views/shared/_brand.erb +2 -2
- data/lib/karafka/web/ui/views/shared/_chart.erb +14 -0
- data/lib/karafka/web/ui/views/shared/_content.erb +2 -2
- data/lib/karafka/web/ui/views/shared/_feature_pro.erb +1 -1
- data/lib/karafka/web/ui/views/shared/_flashes.erb +9 -0
- data/lib/karafka/web/ui/views/shared/_footer.erb +22 -0
- data/lib/karafka/web/ui/views/shared/_header.erb +15 -9
- data/lib/karafka/web/ui/views/shared/_live_poll.erb +7 -0
- data/lib/karafka/web/ui/views/shared/_navigation.erb +5 -8
- data/lib/karafka/web/ui/views/shared/_no_paginated_data.erb +9 -0
- data/lib/karafka/web/ui/views/shared/_pagination.erb +1 -1
- data/lib/karafka/web/ui/views/shared/_tab_nav.erb +7 -0
- data/lib/karafka/web/ui/views/shared/exceptions/not_found.erb +34 -32
- data/lib/karafka/web/ui/views/shared/exceptions/pro_only.erb +45 -43
- data/lib/karafka/web/ui/views/status/failures/_consumers_reports_schema_state.erb +15 -0
- data/lib/karafka/web/ui/views/status/failures/_enabled.erb +8 -0
- data/lib/karafka/web/ui/views/status/failures/_initial_consumers_metrics.erb +11 -0
- data/lib/karafka/web/ui/views/status/failures/{_initial_state.erb → _initial_consumers_state.erb} +3 -3
- data/lib/karafka/web/ui/views/status/failures/_partitions.erb +14 -6
- data/lib/karafka/web/ui/views/status/info/_components.erb +21 -1
- data/lib/karafka/web/ui/views/status/show.erb +62 -5
- data/lib/karafka/web/ui/views/status/successes/_enabled.erb +1 -0
- data/lib/karafka/web/ui/views/status/warnings/_replication.erb +19 -0
- data/lib/karafka/web/version.rb +1 -1
- data/lib/karafka/web.rb +11 -0
- data.tar.gz.sig +0 -0
- metadata +109 -39
- metadata.gz.sig +0 -0
- data/lib/karafka/web/processing/consumers/aggregator.rb +0 -130
- data/lib/karafka/web/tracking/contracts/base.rb +0 -34
- data/lib/karafka/web/ui/pro/views/explorer/_encryption_enabled.erb +0 -18
- data/lib/karafka/web/ui/pro/views/explorer/partition/_watermark_offsets.erb +0 -10
- data/lib/karafka/web/ui/pro/views/explorer/topic/_partitions.erb +0 -11
- data/lib/karafka/web/ui/pro/views/health/index.erb +0 -60
- /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::
|
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.
|
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
|
-
|
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
|
@@ -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
|
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.
|
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
|
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
|
-
|
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
|