karafka-web 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +3 -0
- data/.coditsu/ci.yml +3 -0
- data/.diffend.yml +3 -0
- data/.github/FUNDING.yml +1 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.github/workflows/ci.yml +49 -0
- data/.gitignore +69 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +52 -0
- data/LICENSE +17 -0
- data/README.md +29 -0
- data/bin/karafka-web +33 -0
- data/certs/cert_chain.pem +26 -0
- data/config/locales/errors.yml +9 -0
- data/karafka-web.gemspec +44 -0
- data/lib/karafka/web/app.rb +17 -0
- data/lib/karafka/web/config.rb +80 -0
- data/lib/karafka/web/deserializer.rb +20 -0
- data/lib/karafka/web/errors.rb +25 -0
- data/lib/karafka/web/installer.rb +124 -0
- data/lib/karafka/web/processing/consumer.rb +66 -0
- data/lib/karafka/web/processing/consumers/aggregator.rb +130 -0
- data/lib/karafka/web/processing/consumers/state.rb +32 -0
- data/lib/karafka/web/tracking/base_contract.rb +31 -0
- data/lib/karafka/web/tracking/consumers/contracts/consumer_group.rb +33 -0
- data/lib/karafka/web/tracking/consumers/contracts/job.rb +26 -0
- data/lib/karafka/web/tracking/consumers/contracts/partition.rb +22 -0
- data/lib/karafka/web/tracking/consumers/contracts/report.rb +95 -0
- data/lib/karafka/web/tracking/consumers/contracts/topic.rb +29 -0
- data/lib/karafka/web/tracking/consumers/listeners/base.rb +33 -0
- data/lib/karafka/web/tracking/consumers/listeners/errors.rb +107 -0
- data/lib/karafka/web/tracking/consumers/listeners/pausing.rb +45 -0
- data/lib/karafka/web/tracking/consumers/listeners/processing.rb +157 -0
- data/lib/karafka/web/tracking/consumers/listeners/statistics.rb +123 -0
- data/lib/karafka/web/tracking/consumers/listeners/status.rb +58 -0
- data/lib/karafka/web/tracking/consumers/sampler.rb +216 -0
- data/lib/karafka/web/tracking/memoized_shell.rb +48 -0
- data/lib/karafka/web/tracking/reporter.rb +144 -0
- data/lib/karafka/web/tracking/ttl_array.rb +59 -0
- data/lib/karafka/web/tracking/ttl_hash.rb +16 -0
- data/lib/karafka/web/ui/app.rb +78 -0
- data/lib/karafka/web/ui/base.rb +77 -0
- data/lib/karafka/web/ui/controllers/base.rb +40 -0
- data/lib/karafka/web/ui/controllers/become_pro.rb +17 -0
- data/lib/karafka/web/ui/controllers/cluster.rb +24 -0
- data/lib/karafka/web/ui/controllers/consumers.rb +27 -0
- data/lib/karafka/web/ui/controllers/errors.rb +43 -0
- data/lib/karafka/web/ui/controllers/jobs.rb +33 -0
- data/lib/karafka/web/ui/controllers/requests/params.rb +30 -0
- data/lib/karafka/web/ui/controllers/responses/data.rb +26 -0
- data/lib/karafka/web/ui/controllers/routing.rb +30 -0
- data/lib/karafka/web/ui/helpers/application_helper.rb +144 -0
- data/lib/karafka/web/ui/lib/hash_proxy.rb +66 -0
- data/lib/karafka/web/ui/lib/paginate_array.rb +38 -0
- data/lib/karafka/web/ui/models/consumer_group.rb +20 -0
- data/lib/karafka/web/ui/models/health.rb +44 -0
- data/lib/karafka/web/ui/models/job.rb +13 -0
- data/lib/karafka/web/ui/models/message.rb +99 -0
- data/lib/karafka/web/ui/models/partition.rb +13 -0
- data/lib/karafka/web/ui/models/process.rb +56 -0
- data/lib/karafka/web/ui/models/processes.rb +86 -0
- data/lib/karafka/web/ui/models/state.rb +67 -0
- data/lib/karafka/web/ui/models/topic.rb +19 -0
- data/lib/karafka/web/ui/pro/app.rb +120 -0
- data/lib/karafka/web/ui/pro/controllers/cluster.rb +16 -0
- data/lib/karafka/web/ui/pro/controllers/consumers.rb +54 -0
- data/lib/karafka/web/ui/pro/controllers/dlq.rb +44 -0
- data/lib/karafka/web/ui/pro/controllers/errors.rb +57 -0
- data/lib/karafka/web/ui/pro/controllers/explorer.rb +79 -0
- data/lib/karafka/web/ui/pro/controllers/health.rb +33 -0
- data/lib/karafka/web/ui/pro/controllers/jobs.rb +26 -0
- data/lib/karafka/web/ui/pro/controllers/routing.rb +26 -0
- data/lib/karafka/web/ui/pro/views/consumers/_breadcrumbs.erb +27 -0
- data/lib/karafka/web/ui/pro/views/consumers/_consumer.erb +60 -0
- data/lib/karafka/web/ui/pro/views/consumers/_counters.erb +50 -0
- data/lib/karafka/web/ui/pro/views/consumers/_summary.erb +81 -0
- data/lib/karafka/web/ui/pro/views/consumers/consumer/_consumer_group.erb +109 -0
- data/lib/karafka/web/ui/pro/views/consumers/consumer/_job.erb +26 -0
- data/lib/karafka/web/ui/pro/views/consumers/consumer/_metrics.erb +126 -0
- data/lib/karafka/web/ui/pro/views/consumers/consumer/_no_jobs.erb +9 -0
- data/lib/karafka/web/ui/pro/views/consumers/consumer/_no_subscriptions.erb +9 -0
- data/lib/karafka/web/ui/pro/views/consumers/consumer/_partition.erb +32 -0
- data/lib/karafka/web/ui/pro/views/consumers/consumer/_stopped.erb +10 -0
- data/lib/karafka/web/ui/pro/views/consumers/consumer/_tabs.erb +20 -0
- data/lib/karafka/web/ui/pro/views/consumers/index.erb +30 -0
- data/lib/karafka/web/ui/pro/views/consumers/jobs.erb +42 -0
- data/lib/karafka/web/ui/pro/views/consumers/subscriptions.erb +23 -0
- data/lib/karafka/web/ui/pro/views/dlq/_breadcrumbs.erb +5 -0
- data/lib/karafka/web/ui/pro/views/dlq/_no_topics.erb +9 -0
- data/lib/karafka/web/ui/pro/views/dlq/_topic.erb +12 -0
- data/lib/karafka/web/ui/pro/views/dlq/index.erb +16 -0
- data/lib/karafka/web/ui/pro/views/errors/_breadcrumbs.erb +25 -0
- data/lib/karafka/web/ui/pro/views/errors/_detail.erb +29 -0
- data/lib/karafka/web/ui/pro/views/errors/_error.erb +26 -0
- data/lib/karafka/web/ui/pro/views/errors/_partition_option.erb +7 -0
- data/lib/karafka/web/ui/pro/views/errors/index.erb +58 -0
- data/lib/karafka/web/ui/pro/views/errors/show.erb +56 -0
- data/lib/karafka/web/ui/pro/views/explorer/_breadcrumbs.erb +29 -0
- data/lib/karafka/web/ui/pro/views/explorer/_detail.erb +21 -0
- data/lib/karafka/web/ui/pro/views/explorer/_encryption_enabled.erb +18 -0
- data/lib/karafka/web/ui/pro/views/explorer/_failed_deserialization.erb +4 -0
- data/lib/karafka/web/ui/pro/views/explorer/_message.erb +16 -0
- data/lib/karafka/web/ui/pro/views/explorer/_partition_option.erb +7 -0
- data/lib/karafka/web/ui/pro/views/explorer/_topic.erb +12 -0
- data/lib/karafka/web/ui/pro/views/explorer/index.erb +17 -0
- data/lib/karafka/web/ui/pro/views/explorer/partition.erb +56 -0
- data/lib/karafka/web/ui/pro/views/explorer/show.erb +65 -0
- data/lib/karafka/web/ui/pro/views/health/_breadcrumbs.erb +5 -0
- data/lib/karafka/web/ui/pro/views/health/_partition.erb +35 -0
- data/lib/karafka/web/ui/pro/views/health/index.erb +60 -0
- data/lib/karafka/web/ui/pro/views/jobs/_breadcrumbs.erb +5 -0
- data/lib/karafka/web/ui/pro/views/jobs/_job.erb +31 -0
- data/lib/karafka/web/ui/pro/views/jobs/_no_jobs.erb +9 -0
- data/lib/karafka/web/ui/pro/views/jobs/index.erb +34 -0
- data/lib/karafka/web/ui/pro/views/shared/_navigation.erb +57 -0
- data/lib/karafka/web/ui/public/images/favicon.ico +0 -0
- data/lib/karafka/web/ui/public/images/logo.svg +28 -0
- data/lib/karafka/web/ui/public/javascripts/application.js +41 -0
- data/lib/karafka/web/ui/public/javascripts/bootstrap.min.js +7 -0
- data/lib/karafka/web/ui/public/javascripts/highlight.min.js +337 -0
- data/lib/karafka/web/ui/public/javascripts/live_poll.js +124 -0
- data/lib/karafka/web/ui/public/javascripts/timeago.min.js +1 -0
- data/lib/karafka/web/ui/public/stylesheets/application.css +106 -0
- data/lib/karafka/web/ui/public/stylesheets/bootstrap.min.css +7 -0
- data/lib/karafka/web/ui/public/stylesheets/bootstrap.min.css.map +1 -0
- data/lib/karafka/web/ui/public/stylesheets/highlight.min.css +10 -0
- data/lib/karafka/web/ui/views/cluster/_breadcrumbs.erb +5 -0
- data/lib/karafka/web/ui/views/cluster/_broker.erb +5 -0
- data/lib/karafka/web/ui/views/cluster/_partition.erb +22 -0
- data/lib/karafka/web/ui/views/cluster/index.erb +72 -0
- data/lib/karafka/web/ui/views/consumers/_breadcrumbs.erb +27 -0
- data/lib/karafka/web/ui/views/consumers/_consumer.erb +43 -0
- data/lib/karafka/web/ui/views/consumers/_counters.erb +44 -0
- data/lib/karafka/web/ui/views/consumers/_summary.erb +81 -0
- data/lib/karafka/web/ui/views/consumers/consumer/_consumer_group.erb +109 -0
- data/lib/karafka/web/ui/views/consumers/consumer/_job.erb +26 -0
- data/lib/karafka/web/ui/views/consumers/consumer/_metrics.erb +126 -0
- data/lib/karafka/web/ui/views/consumers/consumer/_no_jobs.erb +9 -0
- data/lib/karafka/web/ui/views/consumers/consumer/_no_subscriptions.erb +9 -0
- data/lib/karafka/web/ui/views/consumers/consumer/_partition.erb +32 -0
- data/lib/karafka/web/ui/views/consumers/consumer/_stopped.erb +10 -0
- data/lib/karafka/web/ui/views/consumers/consumer/_tabs.erb +20 -0
- data/lib/karafka/web/ui/views/consumers/index.erb +29 -0
- data/lib/karafka/web/ui/views/errors/_breadcrumbs.erb +19 -0
- data/lib/karafka/web/ui/views/errors/_detail.erb +29 -0
- data/lib/karafka/web/ui/views/errors/_error.erb +26 -0
- data/lib/karafka/web/ui/views/errors/index.erb +38 -0
- data/lib/karafka/web/ui/views/errors/show.erb +30 -0
- data/lib/karafka/web/ui/views/jobs/_breadcrumbs.erb +5 -0
- data/lib/karafka/web/ui/views/jobs/_job.erb +22 -0
- data/lib/karafka/web/ui/views/jobs/_no_jobs.erb +9 -0
- data/lib/karafka/web/ui/views/jobs/index.erb +31 -0
- data/lib/karafka/web/ui/views/layout.erb +23 -0
- data/lib/karafka/web/ui/views/routing/_breadcrumbs.erb +15 -0
- data/lib/karafka/web/ui/views/routing/_consumer_group.erb +34 -0
- data/lib/karafka/web/ui/views/routing/_detail.erb +25 -0
- data/lib/karafka/web/ui/views/routing/_topic.erb +18 -0
- data/lib/karafka/web/ui/views/routing/index.erb +10 -0
- data/lib/karafka/web/ui/views/routing/show.erb +26 -0
- data/lib/karafka/web/ui/views/shared/_become_pro.erb +13 -0
- data/lib/karafka/web/ui/views/shared/_brand.erb +3 -0
- data/lib/karafka/web/ui/views/shared/_content.erb +31 -0
- data/lib/karafka/web/ui/views/shared/_header.erb +20 -0
- data/lib/karafka/web/ui/views/shared/_navigation.erb +57 -0
- data/lib/karafka/web/ui/views/shared/_pagination.erb +21 -0
- data/lib/karafka/web/ui/views/shared/exceptions/not_found.erb +39 -0
- data/lib/karafka/web/ui/views/shared/exceptions/pro_only.erb +52 -0
- data/lib/karafka/web/version.rb +8 -0
- data/lib/karafka/web.rb +60 -0
- data.tar.gz.sig +0 -0
- metadata +328 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,216 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Web
|
5
|
+
module Tracking
|
6
|
+
# Namespace for all the things related to tracking consumers and consuming processes
|
7
|
+
module Consumers
|
8
|
+
# Samples for fetching and storing metrics samples about the consumer process
|
9
|
+
class Sampler
|
10
|
+
include ::Karafka::Core::Helpers::Time
|
11
|
+
|
12
|
+
attr_reader :counters, :consumer_groups, :errors, :times, :pauses, :jobs
|
13
|
+
|
14
|
+
# 60 seconds window for time tracked window-based metrics
|
15
|
+
TIMES_TTL = 60
|
16
|
+
|
17
|
+
# Times ttl in ms
|
18
|
+
TIMES_TTL_MS = TIMES_TTL * 1_000
|
19
|
+
|
20
|
+
private_constant :TIMES_TTL, :TIMES_TTL_MS
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@counters = {
|
24
|
+
batches: 0,
|
25
|
+
messages: 0,
|
26
|
+
errors: 0,
|
27
|
+
retries: 0,
|
28
|
+
dead: 0
|
29
|
+
}
|
30
|
+
@times = TtlHash.new(TIMES_TTL_MS)
|
31
|
+
@consumer_groups = {}
|
32
|
+
@errors = []
|
33
|
+
@started_at = float_now
|
34
|
+
@pauses = Set.new
|
35
|
+
@jobs = {}
|
36
|
+
@shell = MemoizedShell.new
|
37
|
+
end
|
38
|
+
|
39
|
+
# We cannot report and track the same time, that is why we use mutex here. To make sure
|
40
|
+
# that samples aggregations and counting does not interact with reporter flushing.
|
41
|
+
def track
|
42
|
+
Reporter::MUTEX.synchronize do
|
43
|
+
yield(self)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Hash] report hash with all the details about consumer operations
|
48
|
+
def to_report
|
49
|
+
{
|
50
|
+
schema_version: '1.0.0',
|
51
|
+
type: 'consumer',
|
52
|
+
dispatched_at: float_now,
|
53
|
+
|
54
|
+
process: {
|
55
|
+
started_at: @started_at,
|
56
|
+
name: process_name,
|
57
|
+
status: ::Karafka::App.config.internal.status.to_s,
|
58
|
+
listeners: Karafka::Server.listeners.count,
|
59
|
+
concurrency: concurrency,
|
60
|
+
memory_usage: memory_usage,
|
61
|
+
memory_total_usage: memory_total_usage,
|
62
|
+
memory_size: memory_size,
|
63
|
+
cpu_count: cpu_count,
|
64
|
+
cpu_usage: cpu_usage
|
65
|
+
},
|
66
|
+
|
67
|
+
versions: {
|
68
|
+
ruby: ruby_version,
|
69
|
+
karafka: ::Karafka::VERSION,
|
70
|
+
waterdrop: ::WaterDrop::VERSION
|
71
|
+
},
|
72
|
+
|
73
|
+
stats: Karafka::Server.jobs_queue.statistics.merge(
|
74
|
+
utilization: utilization
|
75
|
+
).merge(total: @counters),
|
76
|
+
|
77
|
+
consumer_groups: @consumer_groups,
|
78
|
+
jobs: jobs.values
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
# Clears counters and errors.
|
83
|
+
# Used after data is reported by reported to start collecting new samples
|
84
|
+
# @note We do not clear processing or pauses or other things like this because we track
|
85
|
+
# their states and not values, so they need to be tracked between flushes.
|
86
|
+
def clear
|
87
|
+
@counters.each { |k, _| @counters[k] = 0 }
|
88
|
+
|
89
|
+
@errors.clear
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# @return [Numeric] % utilization of all the threads. 100% means all the threads are
|
95
|
+
# utilized all the time within the given time window. 0% means, nothing is happening
|
96
|
+
# most if not all the time.
|
97
|
+
def utilization
|
98
|
+
return 0 if times[:total].empty?
|
99
|
+
|
100
|
+
# Max times ttl
|
101
|
+
timefactor = float_now - @started_at
|
102
|
+
timefactor = timefactor > TIMES_TTL ? TIMES_TTL : timefactor
|
103
|
+
|
104
|
+
# We divide by 1_000 to convert from milliseconds
|
105
|
+
# We multiply by 100 to have it in % scale
|
106
|
+
times[:total].sum / 1_000 / concurrency / timefactor * 100
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [String] Unique process name
|
110
|
+
def process_name
|
111
|
+
@process_name ||= "#{Socket.gethostname}:#{::Process.pid}:#{SecureRandom.hex(6)}"
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [Integer] memory used by this process in kilobytes
|
115
|
+
def memory_usage
|
116
|
+
pid = ::Process.pid
|
117
|
+
|
118
|
+
case RUBY_PLATFORM
|
119
|
+
# Reading this that way is cheaper than running a shell command
|
120
|
+
when /linux/
|
121
|
+
IO.readlines("/proc/#{pid}/status").each do |line|
|
122
|
+
next unless line.start_with?('VmRSS:')
|
123
|
+
|
124
|
+
break line.split[1].to_i
|
125
|
+
end
|
126
|
+
when /darwin|bsd/
|
127
|
+
@shell
|
128
|
+
.call("ps -o pid,rss -p #{pid}")
|
129
|
+
.lines
|
130
|
+
.last
|
131
|
+
.split
|
132
|
+
.last
|
133
|
+
.to_i
|
134
|
+
else
|
135
|
+
0
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Total memory used in the OS
|
140
|
+
def memory_total_usage
|
141
|
+
case RUBY_PLATFORM
|
142
|
+
when /darwin|bsd|linux/
|
143
|
+
@shell
|
144
|
+
.call('ps -A -o rss=')
|
145
|
+
.split("\n")
|
146
|
+
.inject { |a, e| a.to_i + e.strip.to_i }
|
147
|
+
else
|
148
|
+
0
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# @return [Integer] total amount of memory
|
153
|
+
def memory_size
|
154
|
+
@memory_size ||= case RUBY_PLATFORM
|
155
|
+
when /linux/
|
156
|
+
@shell
|
157
|
+
.call('grep MemTotal /proc/meminfo')
|
158
|
+
.match(/(\d+)/)
|
159
|
+
.to_s
|
160
|
+
.to_i
|
161
|
+
when /darwin|bsd/
|
162
|
+
@shell
|
163
|
+
.call('sysctl -a')
|
164
|
+
.split("\n")
|
165
|
+
.find { |line| line.start_with?('hw.memsize:') }
|
166
|
+
.to_s
|
167
|
+
.split(' ')
|
168
|
+
.last
|
169
|
+
.to_i
|
170
|
+
else
|
171
|
+
0
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# @return [Array<Float>] load averages for last 1, 5 and 15 minutes
|
176
|
+
def cpu_usage
|
177
|
+
case RUBY_PLATFORM
|
178
|
+
when /darwin|bsd|linux/
|
179
|
+
@shell
|
180
|
+
.call('w | head -1')
|
181
|
+
.strip
|
182
|
+
.split(' ')
|
183
|
+
.map(&:to_f)
|
184
|
+
.last(3)
|
185
|
+
else
|
186
|
+
[-1, -1, -1]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# @return [Integer] CPU count
|
191
|
+
def cpu_count
|
192
|
+
@cpu_count ||= Etc.nprocessors
|
193
|
+
end
|
194
|
+
|
195
|
+
# @return [String] currently used ruby version with details
|
196
|
+
def ruby_version
|
197
|
+
if defined?(JRUBY_VERSION)
|
198
|
+
revision = JRUBY_REVISION.to_s
|
199
|
+
version = JRUBY_VERSION
|
200
|
+
else
|
201
|
+
revision = RUBY_REVISION.to_s
|
202
|
+
version = RUBY_ENGINE_VERSION
|
203
|
+
end
|
204
|
+
|
205
|
+
"#{RUBY_ENGINE} #{version}-#{RUBY_PATCHLEVEL} #{revision[0..5]}"
|
206
|
+
end
|
207
|
+
|
208
|
+
# @return [Integer] number of threads that process work
|
209
|
+
def concurrency
|
210
|
+
@concurrency ||= Karafka::App.config.concurrency
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Web
|
5
|
+
# Namespace used to encapsulate all components needed to track and report states of particular
|
6
|
+
# processes
|
7
|
+
module Tracking
|
8
|
+
# Class used to run shell command that also returns previous result in case of a failure
|
9
|
+
# This is used because children cat get signals when performing stat fetches and then
|
10
|
+
# fetch is stopped. This can cause invalid results from sub-shell commands.
|
11
|
+
#
|
12
|
+
# This will return last result as log as there was one.
|
13
|
+
class MemoizedShell
|
14
|
+
# Hpw many tries do we want to perform before giving up on the shell command
|
15
|
+
MAX_ATTEMPTS = 4
|
16
|
+
|
17
|
+
private_constant :MAX_ATTEMPTS
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@accu = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param cmd [String]
|
24
|
+
# @return [String, nil] sub-shell evaluation string result or nil if we were not able to
|
25
|
+
# run or re-run the call.
|
26
|
+
def call(cmd)
|
27
|
+
attempt ||= 0
|
28
|
+
|
29
|
+
while attempt < MAX_ATTEMPTS
|
30
|
+
attempt += 1
|
31
|
+
|
32
|
+
stdout_str, status = Open3.capture2(cmd)
|
33
|
+
|
34
|
+
if status.success?
|
35
|
+
@accu[cmd] = stdout_str
|
36
|
+
return stdout_str
|
37
|
+
else
|
38
|
+
return stdout_str if attempt > MAX_ATTEMPTS
|
39
|
+
return @accu[cmd] if @accu.key?(cmd)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
@accu[cmd]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Web
|
5
|
+
module Tracking
|
6
|
+
# Reports the collected data about the process and sends it, so we can use it in the UI
|
7
|
+
class Reporter
|
8
|
+
include ::Karafka::Core::Helpers::Time
|
9
|
+
include ::Karafka::Helpers::Async
|
10
|
+
|
11
|
+
# Minimum number of messages to produce to produce then in sync mode
|
12
|
+
# This acts as a small back-off not to overload the system in case we would have extremely
|
13
|
+
# big number of errors happening
|
14
|
+
PRODUCE_SYNC_THRESHOLD = 25
|
15
|
+
|
16
|
+
private_constant :PRODUCE_SYNC_THRESHOLD
|
17
|
+
|
18
|
+
# This mutex is shared between tracker and samplers so there is no case where metrics
|
19
|
+
# would be collected same time tracker reports
|
20
|
+
MUTEX = Mutex.new
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
# Move back so first report is dispatched fast to indicate, that the process is alive
|
24
|
+
@tracked_at = monotonic_now - 10_000
|
25
|
+
@consumer_contract = Consumers::Contracts::Report.new
|
26
|
+
end
|
27
|
+
|
28
|
+
# Dispatches the current state from sampler to appropriate topics
|
29
|
+
#
|
30
|
+
# @param forced [Boolean] should we report bypassing the time frequency or should we report
|
31
|
+
# only in case we would not send the report for long enough time.
|
32
|
+
def report(forced: false)
|
33
|
+
MUTEX.synchronize do
|
34
|
+
# Start background thread only when needed
|
35
|
+
# This prevents us from starting it too early or for non-consumer processes where
|
36
|
+
# Karafka is being included
|
37
|
+
async_call unless @running
|
38
|
+
|
39
|
+
return unless report?(forced)
|
40
|
+
|
41
|
+
@tracked_at = monotonic_now
|
42
|
+
|
43
|
+
consumer_report = consumer_sampler.to_report
|
44
|
+
|
45
|
+
@consumer_contract.validate!(consumer_report)
|
46
|
+
|
47
|
+
# Report consumers statuses
|
48
|
+
messages = [
|
49
|
+
{
|
50
|
+
topic: ::Karafka::Web.config.topics.consumers.reports,
|
51
|
+
payload: consumer_report.to_json,
|
52
|
+
key: consumer_report[:process][:name]
|
53
|
+
}
|
54
|
+
]
|
55
|
+
|
56
|
+
# Report errors that occurred (if any)
|
57
|
+
messages += consumer_sampler.errors.map do |error|
|
58
|
+
{
|
59
|
+
topic: Karafka::Web.config.topics.errors,
|
60
|
+
# Inject process name into the error details for easier tracing
|
61
|
+
payload: error.merge(
|
62
|
+
process_name: consumer_report[:process][:name]
|
63
|
+
).to_json,
|
64
|
+
# Always dispatch errors from the same process to the same partition
|
65
|
+
key: consumer_report[:process][:name]
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
produce(messages)
|
70
|
+
|
71
|
+
# Clear the sampler so it tracks new state changes without previous once impacting
|
72
|
+
# the data
|
73
|
+
consumer_sampler.clear
|
74
|
+
end
|
75
|
+
# Since we run this in a background thread, there may be a case upon shutdown, where the
|
76
|
+
# producer is closed right before a potential dispatch. It is not worth dealing with this
|
77
|
+
# and we can just safely ignore this
|
78
|
+
rescue WaterDrop::Errors::ProducerClosedError
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
# Reports bypassing frequency check. This can be used to report when state changes in the
|
83
|
+
# process drastically. For example when process is stopping, we want to indicate this as
|
84
|
+
# fast as possible in the UI, etc.
|
85
|
+
def report!
|
86
|
+
report(forced: true)
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# Reports the process state once in a while
|
92
|
+
def call
|
93
|
+
@running = true
|
94
|
+
|
95
|
+
loop do
|
96
|
+
report
|
97
|
+
|
98
|
+
# We won't track more often anyhow but want to try frequently not to miss a window
|
99
|
+
# We need to convert the sleep interval into seconds for sleep
|
100
|
+
sleep(::Karafka::Web.config.tracking.interval / 1_000 / 10)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# @param forced [Boolean] is this report forced. Forced means that as long as we can flush
|
105
|
+
# we will flush
|
106
|
+
# @return [Boolean] Should we report or is it not yet time to do so
|
107
|
+
def report?(forced)
|
108
|
+
# We never report in initializing phase because things are not yet fully configured
|
109
|
+
return false if ::Karafka::App.initializing?
|
110
|
+
# We never report in the initialized because server is not yet ready until Karafka is
|
111
|
+
# fully running and some of the things like listeners are not yet available
|
112
|
+
return false if ::Karafka::App.initialized?
|
113
|
+
|
114
|
+
return true if forced
|
115
|
+
|
116
|
+
(monotonic_now - @tracked_at) >= ::Karafka::Web.config.tracking.interval
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [Object] sampler for the metrics
|
120
|
+
def consumer_sampler
|
121
|
+
@consumer_sampler ||= ::Karafka::Web.config.tracking.consumers.sampler
|
122
|
+
end
|
123
|
+
|
124
|
+
# Produces messages to Kafka.
|
125
|
+
#
|
126
|
+
# @param messages [Array<Hash>]
|
127
|
+
#
|
128
|
+
# @note We pick either sync or async dependent on number of messages. The trick here is,
|
129
|
+
# that we do not want to end up overloading the internal queue with messages in case
|
130
|
+
# someone has a lot of errors from processing or other errors. Producing sync will wait
|
131
|
+
# for the delivery, hence will slow things down a little bit. On the other hand during
|
132
|
+
# normal operations we should not have that many messages to dispatch and it should not
|
133
|
+
# slowdown any processing.
|
134
|
+
def produce(messages)
|
135
|
+
if messages.count >= PRODUCE_SYNC_THRESHOLD
|
136
|
+
::Karafka.producer.produce_many_sync(messages)
|
137
|
+
else
|
138
|
+
::Karafka.producer.produce_many_async(messages)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Web
|
5
|
+
module Tracking
|
6
|
+
# Array that allows us to store data points that expire over time automatically.
|
7
|
+
class TtlArray
|
8
|
+
include ::Karafka::Core::Helpers::Time
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
# @param ttl [Integer] milliseconds ttl
|
12
|
+
def initialize(ttl)
|
13
|
+
@ttl = ttl
|
14
|
+
@accu = []
|
15
|
+
end
|
16
|
+
|
17
|
+
# Iterates over only active elements
|
18
|
+
def each
|
19
|
+
clear
|
20
|
+
|
21
|
+
@accu.each do |sample|
|
22
|
+
yield sample[:value]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param value [Object] adds value to the array
|
27
|
+
# @return [Object] added element
|
28
|
+
def <<(value)
|
29
|
+
@accu << { value: value, added_at: monotonic_now }
|
30
|
+
|
31
|
+
clear
|
32
|
+
|
33
|
+
value
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Boolean] is the array empty
|
37
|
+
def empty?
|
38
|
+
clear
|
39
|
+
@accu.empty?
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Array] pure array version with only active elements
|
43
|
+
def to_a
|
44
|
+
clear
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# Evicts outdated samples
|
51
|
+
def clear
|
52
|
+
@accu.delete_if do |sample|
|
53
|
+
monotonic_now - sample[:added_at] > @ttl
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Web
|
5
|
+
module Tracking
|
6
|
+
# Hash that accumulates data that has an expiration date (ttl)
|
7
|
+
# Used to keep track of metrics in a window
|
8
|
+
class TtlHash < Hash
|
9
|
+
# @param ttl [Integer] milliseconds ttl
|
10
|
+
def initialize(ttl)
|
11
|
+
super() { |k, v| k[v] = TtlArray.new(ttl) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Web
|
5
|
+
# Web UI namespace
|
6
|
+
module Ui
|
7
|
+
# Main Roda Web App that servers all the metrics and stats
|
8
|
+
class App < Base
|
9
|
+
# Use the gem views and assets location
|
10
|
+
opts[:root] = Karafka::Web.gem_root.join('lib/karafka/web/ui')
|
11
|
+
|
12
|
+
instance_exec(&CONTEXT_DETAILS)
|
13
|
+
|
14
|
+
route do |r|
|
15
|
+
r.root { r.redirect root_path('consumers') }
|
16
|
+
|
17
|
+
@current_page = params.current_page
|
18
|
+
|
19
|
+
r.on 'consumers' do
|
20
|
+
r.get String, 'subscriptions' do |_process_id|
|
21
|
+
raise Errors::Ui::ProOnlyError
|
22
|
+
end
|
23
|
+
|
24
|
+
r.get do
|
25
|
+
@breadcrumbs = false
|
26
|
+
controller = Controllers::Consumers.new(params)
|
27
|
+
render_response controller.index
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
%w[
|
32
|
+
health
|
33
|
+
explorer
|
34
|
+
dlq
|
35
|
+
].each do |route|
|
36
|
+
r.get route, [String, true], [String, true] do
|
37
|
+
raise Errors::Ui::ProOnlyError
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
r.get 'jobs' do
|
42
|
+
controller = Controllers::Jobs.new(params)
|
43
|
+
render_response controller.index
|
44
|
+
end
|
45
|
+
|
46
|
+
r.on 'routing' do
|
47
|
+
controller = Controllers::Routing.new(params)
|
48
|
+
|
49
|
+
r.get String do |topic_id|
|
50
|
+
render_response controller.show(topic_id)
|
51
|
+
end
|
52
|
+
|
53
|
+
r.get do
|
54
|
+
render_response controller.index
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
r.get 'cluster' do
|
59
|
+
controller = Controllers::Cluster.new(params)
|
60
|
+
render_response controller.index
|
61
|
+
end
|
62
|
+
|
63
|
+
r.on 'errors' do
|
64
|
+
controller = Controllers::Errors.new(params)
|
65
|
+
|
66
|
+
r.get Integer do |offset|
|
67
|
+
render_response controller.show(offset)
|
68
|
+
end
|
69
|
+
|
70
|
+
r.get do
|
71
|
+
render_response controller.index
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Web
|
5
|
+
module Ui
|
6
|
+
# Base Roda application
|
7
|
+
class Base < Roda
|
8
|
+
include Helpers::ApplicationHelper
|
9
|
+
|
10
|
+
# Details that need to be evaluated in the context of OSS or Pro web UI.
|
11
|
+
# If those would be evaluated in the base, they would not be initialized as expected
|
12
|
+
CONTEXT_DETAILS = lambda do
|
13
|
+
plugin(
|
14
|
+
:static,
|
15
|
+
%w[/javascripts /stylesheets /images],
|
16
|
+
root: Karafka::Web.gem_root.join('lib/karafka/web/ui/public')
|
17
|
+
)
|
18
|
+
plugin :render_each
|
19
|
+
plugin :partials
|
20
|
+
end
|
21
|
+
|
22
|
+
plugin :render, escape: true, engine: 'erb'
|
23
|
+
plugin :run_append_slash
|
24
|
+
plugin :error_handler
|
25
|
+
plugin :not_found
|
26
|
+
plugin :path
|
27
|
+
|
28
|
+
# Display appropriate error specific to a given error type
|
29
|
+
plugin :error_handler, classes: [
|
30
|
+
::Karafka::Web::Errors::Ui::NotFoundError,
|
31
|
+
::Rdkafka::RdkafkaError,
|
32
|
+
Errors::Ui::ProOnlyError
|
33
|
+
] do |e|
|
34
|
+
@error = true
|
35
|
+
|
36
|
+
if e.is_a?(Errors::Ui::ProOnlyError)
|
37
|
+
view 'shared/exceptions/pro_only'
|
38
|
+
else
|
39
|
+
response.status = 404
|
40
|
+
view 'shared/exceptions/not_found'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
not_found do
|
45
|
+
@error = true
|
46
|
+
response.status = 404
|
47
|
+
view 'shared/exceptions/not_found'
|
48
|
+
end
|
49
|
+
|
50
|
+
# Allows us to build current path with additional params
|
51
|
+
# @param query_data [Hash] query params we want to add to the current path
|
52
|
+
path :current do |query_data = {}|
|
53
|
+
q = query_data.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
|
54
|
+
"#{request.path}?#{q}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Sets appropriate template variables based on the response object and renders the
|
58
|
+
# expected view
|
59
|
+
# @param response [Karafka::Web::Ui::Controllers::Responses::Data] response data object
|
60
|
+
def render_response(response)
|
61
|
+
response.attributes.each do |key, value|
|
62
|
+
instance_variable_set(
|
63
|
+
"@#{key}", value
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
view(response.path)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Karafka::Web::Ui::Controllers::Requests::Params] curated params
|
71
|
+
def params
|
72
|
+
Controllers::Requests::Params.new(request.params)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Web
|
5
|
+
module Ui
|
6
|
+
# Namespace for controller related components in the Web UI app.
|
7
|
+
module Controllers
|
8
|
+
# Base controller from which all the controllers should inherit.
|
9
|
+
class Base
|
10
|
+
# @param params [Karafka::Web::Ui::Controllers::Requests::Params] request parameters
|
11
|
+
def initialize(params)
|
12
|
+
@params = params
|
13
|
+
end
|
14
|
+
|
15
|
+
# Builds the respond data object with assigned attributes based on instance variables.
|
16
|
+
#
|
17
|
+
# @return [Responses::Data] data that should be used to render appropriate view
|
18
|
+
def respond
|
19
|
+
attributes = {}
|
20
|
+
|
21
|
+
scope = self.class.to_s.split('::').last.gsub(/(.)([A-Z])/, '\1_\2').downcase
|
22
|
+
action = caller_locations(1, 1)[0].label
|
23
|
+
|
24
|
+
instance_variables.each do |iv|
|
25
|
+
next if iv.to_s.start_with?('@_')
|
26
|
+
next if iv.to_s.start_with?('@params')
|
27
|
+
|
28
|
+
attributes[iv.to_s.delete('@').to_sym] = instance_variable_get(iv)
|
29
|
+
end
|
30
|
+
|
31
|
+
Responses::Data.new(
|
32
|
+
"#{scope}/#{action}",
|
33
|
+
attributes
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Web
|
5
|
+
module Ui
|
6
|
+
module Controllers
|
7
|
+
# Pro message reporting info controller
|
8
|
+
class BecomePro < Base
|
9
|
+
# Display a message, that a give feature is available only in Pro
|
10
|
+
def show
|
11
|
+
respond
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|