karafka-web 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|