karafka-web 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (178) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +3 -0
  3. data/.coditsu/ci.yml +3 -0
  4. data/.diffend.yml +3 -0
  5. data/.github/FUNDING.yml +1 -0
  6. data/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
  7. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  8. data/.github/workflows/ci.yml +49 -0
  9. data/.gitignore +69 -0
  10. data/.ruby-gemset +1 -0
  11. data/.ruby-version +1 -0
  12. data/CHANGELOG.md +9 -0
  13. data/CODE_OF_CONDUCT.md +46 -0
  14. data/Gemfile +7 -0
  15. data/Gemfile.lock +52 -0
  16. data/LICENSE +17 -0
  17. data/README.md +29 -0
  18. data/bin/karafka-web +33 -0
  19. data/certs/cert_chain.pem +26 -0
  20. data/config/locales/errors.yml +9 -0
  21. data/karafka-web.gemspec +44 -0
  22. data/lib/karafka/web/app.rb +17 -0
  23. data/lib/karafka/web/config.rb +80 -0
  24. data/lib/karafka/web/deserializer.rb +20 -0
  25. data/lib/karafka/web/errors.rb +25 -0
  26. data/lib/karafka/web/installer.rb +124 -0
  27. data/lib/karafka/web/processing/consumer.rb +66 -0
  28. data/lib/karafka/web/processing/consumers/aggregator.rb +130 -0
  29. data/lib/karafka/web/processing/consumers/state.rb +32 -0
  30. data/lib/karafka/web/tracking/base_contract.rb +31 -0
  31. data/lib/karafka/web/tracking/consumers/contracts/consumer_group.rb +33 -0
  32. data/lib/karafka/web/tracking/consumers/contracts/job.rb +26 -0
  33. data/lib/karafka/web/tracking/consumers/contracts/partition.rb +22 -0
  34. data/lib/karafka/web/tracking/consumers/contracts/report.rb +95 -0
  35. data/lib/karafka/web/tracking/consumers/contracts/topic.rb +29 -0
  36. data/lib/karafka/web/tracking/consumers/listeners/base.rb +33 -0
  37. data/lib/karafka/web/tracking/consumers/listeners/errors.rb +107 -0
  38. data/lib/karafka/web/tracking/consumers/listeners/pausing.rb +45 -0
  39. data/lib/karafka/web/tracking/consumers/listeners/processing.rb +157 -0
  40. data/lib/karafka/web/tracking/consumers/listeners/statistics.rb +123 -0
  41. data/lib/karafka/web/tracking/consumers/listeners/status.rb +58 -0
  42. data/lib/karafka/web/tracking/consumers/sampler.rb +216 -0
  43. data/lib/karafka/web/tracking/memoized_shell.rb +48 -0
  44. data/lib/karafka/web/tracking/reporter.rb +144 -0
  45. data/lib/karafka/web/tracking/ttl_array.rb +59 -0
  46. data/lib/karafka/web/tracking/ttl_hash.rb +16 -0
  47. data/lib/karafka/web/ui/app.rb +78 -0
  48. data/lib/karafka/web/ui/base.rb +77 -0
  49. data/lib/karafka/web/ui/controllers/base.rb +40 -0
  50. data/lib/karafka/web/ui/controllers/become_pro.rb +17 -0
  51. data/lib/karafka/web/ui/controllers/cluster.rb +24 -0
  52. data/lib/karafka/web/ui/controllers/consumers.rb +27 -0
  53. data/lib/karafka/web/ui/controllers/errors.rb +43 -0
  54. data/lib/karafka/web/ui/controllers/jobs.rb +33 -0
  55. data/lib/karafka/web/ui/controllers/requests/params.rb +30 -0
  56. data/lib/karafka/web/ui/controllers/responses/data.rb +26 -0
  57. data/lib/karafka/web/ui/controllers/routing.rb +30 -0
  58. data/lib/karafka/web/ui/helpers/application_helper.rb +144 -0
  59. data/lib/karafka/web/ui/lib/hash_proxy.rb +66 -0
  60. data/lib/karafka/web/ui/lib/paginate_array.rb +38 -0
  61. data/lib/karafka/web/ui/models/consumer_group.rb +20 -0
  62. data/lib/karafka/web/ui/models/health.rb +44 -0
  63. data/lib/karafka/web/ui/models/job.rb +13 -0
  64. data/lib/karafka/web/ui/models/message.rb +99 -0
  65. data/lib/karafka/web/ui/models/partition.rb +13 -0
  66. data/lib/karafka/web/ui/models/process.rb +56 -0
  67. data/lib/karafka/web/ui/models/processes.rb +86 -0
  68. data/lib/karafka/web/ui/models/state.rb +67 -0
  69. data/lib/karafka/web/ui/models/topic.rb +19 -0
  70. data/lib/karafka/web/ui/pro/app.rb +120 -0
  71. data/lib/karafka/web/ui/pro/controllers/cluster.rb +16 -0
  72. data/lib/karafka/web/ui/pro/controllers/consumers.rb +54 -0
  73. data/lib/karafka/web/ui/pro/controllers/dlq.rb +44 -0
  74. data/lib/karafka/web/ui/pro/controllers/errors.rb +57 -0
  75. data/lib/karafka/web/ui/pro/controllers/explorer.rb +79 -0
  76. data/lib/karafka/web/ui/pro/controllers/health.rb +33 -0
  77. data/lib/karafka/web/ui/pro/controllers/jobs.rb +26 -0
  78. data/lib/karafka/web/ui/pro/controllers/routing.rb +26 -0
  79. data/lib/karafka/web/ui/pro/views/consumers/_breadcrumbs.erb +27 -0
  80. data/lib/karafka/web/ui/pro/views/consumers/_consumer.erb +60 -0
  81. data/lib/karafka/web/ui/pro/views/consumers/_counters.erb +50 -0
  82. data/lib/karafka/web/ui/pro/views/consumers/_summary.erb +81 -0
  83. data/lib/karafka/web/ui/pro/views/consumers/consumer/_consumer_group.erb +109 -0
  84. data/lib/karafka/web/ui/pro/views/consumers/consumer/_job.erb +26 -0
  85. data/lib/karafka/web/ui/pro/views/consumers/consumer/_metrics.erb +126 -0
  86. data/lib/karafka/web/ui/pro/views/consumers/consumer/_no_jobs.erb +9 -0
  87. data/lib/karafka/web/ui/pro/views/consumers/consumer/_no_subscriptions.erb +9 -0
  88. data/lib/karafka/web/ui/pro/views/consumers/consumer/_partition.erb +32 -0
  89. data/lib/karafka/web/ui/pro/views/consumers/consumer/_stopped.erb +10 -0
  90. data/lib/karafka/web/ui/pro/views/consumers/consumer/_tabs.erb +20 -0
  91. data/lib/karafka/web/ui/pro/views/consumers/index.erb +30 -0
  92. data/lib/karafka/web/ui/pro/views/consumers/jobs.erb +42 -0
  93. data/lib/karafka/web/ui/pro/views/consumers/subscriptions.erb +23 -0
  94. data/lib/karafka/web/ui/pro/views/dlq/_breadcrumbs.erb +5 -0
  95. data/lib/karafka/web/ui/pro/views/dlq/_no_topics.erb +9 -0
  96. data/lib/karafka/web/ui/pro/views/dlq/_topic.erb +12 -0
  97. data/lib/karafka/web/ui/pro/views/dlq/index.erb +16 -0
  98. data/lib/karafka/web/ui/pro/views/errors/_breadcrumbs.erb +25 -0
  99. data/lib/karafka/web/ui/pro/views/errors/_detail.erb +29 -0
  100. data/lib/karafka/web/ui/pro/views/errors/_error.erb +26 -0
  101. data/lib/karafka/web/ui/pro/views/errors/_partition_option.erb +7 -0
  102. data/lib/karafka/web/ui/pro/views/errors/index.erb +58 -0
  103. data/lib/karafka/web/ui/pro/views/errors/show.erb +56 -0
  104. data/lib/karafka/web/ui/pro/views/explorer/_breadcrumbs.erb +29 -0
  105. data/lib/karafka/web/ui/pro/views/explorer/_detail.erb +21 -0
  106. data/lib/karafka/web/ui/pro/views/explorer/_encryption_enabled.erb +18 -0
  107. data/lib/karafka/web/ui/pro/views/explorer/_failed_deserialization.erb +4 -0
  108. data/lib/karafka/web/ui/pro/views/explorer/_message.erb +16 -0
  109. data/lib/karafka/web/ui/pro/views/explorer/_partition_option.erb +7 -0
  110. data/lib/karafka/web/ui/pro/views/explorer/_topic.erb +12 -0
  111. data/lib/karafka/web/ui/pro/views/explorer/index.erb +17 -0
  112. data/lib/karafka/web/ui/pro/views/explorer/partition.erb +56 -0
  113. data/lib/karafka/web/ui/pro/views/explorer/show.erb +65 -0
  114. data/lib/karafka/web/ui/pro/views/health/_breadcrumbs.erb +5 -0
  115. data/lib/karafka/web/ui/pro/views/health/_partition.erb +35 -0
  116. data/lib/karafka/web/ui/pro/views/health/index.erb +60 -0
  117. data/lib/karafka/web/ui/pro/views/jobs/_breadcrumbs.erb +5 -0
  118. data/lib/karafka/web/ui/pro/views/jobs/_job.erb +31 -0
  119. data/lib/karafka/web/ui/pro/views/jobs/_no_jobs.erb +9 -0
  120. data/lib/karafka/web/ui/pro/views/jobs/index.erb +34 -0
  121. data/lib/karafka/web/ui/pro/views/shared/_navigation.erb +57 -0
  122. data/lib/karafka/web/ui/public/images/favicon.ico +0 -0
  123. data/lib/karafka/web/ui/public/images/logo.svg +28 -0
  124. data/lib/karafka/web/ui/public/javascripts/application.js +41 -0
  125. data/lib/karafka/web/ui/public/javascripts/bootstrap.min.js +7 -0
  126. data/lib/karafka/web/ui/public/javascripts/highlight.min.js +337 -0
  127. data/lib/karafka/web/ui/public/javascripts/live_poll.js +124 -0
  128. data/lib/karafka/web/ui/public/javascripts/timeago.min.js +1 -0
  129. data/lib/karafka/web/ui/public/stylesheets/application.css +106 -0
  130. data/lib/karafka/web/ui/public/stylesheets/bootstrap.min.css +7 -0
  131. data/lib/karafka/web/ui/public/stylesheets/bootstrap.min.css.map +1 -0
  132. data/lib/karafka/web/ui/public/stylesheets/highlight.min.css +10 -0
  133. data/lib/karafka/web/ui/views/cluster/_breadcrumbs.erb +5 -0
  134. data/lib/karafka/web/ui/views/cluster/_broker.erb +5 -0
  135. data/lib/karafka/web/ui/views/cluster/_partition.erb +22 -0
  136. data/lib/karafka/web/ui/views/cluster/index.erb +72 -0
  137. data/lib/karafka/web/ui/views/consumers/_breadcrumbs.erb +27 -0
  138. data/lib/karafka/web/ui/views/consumers/_consumer.erb +43 -0
  139. data/lib/karafka/web/ui/views/consumers/_counters.erb +44 -0
  140. data/lib/karafka/web/ui/views/consumers/_summary.erb +81 -0
  141. data/lib/karafka/web/ui/views/consumers/consumer/_consumer_group.erb +109 -0
  142. data/lib/karafka/web/ui/views/consumers/consumer/_job.erb +26 -0
  143. data/lib/karafka/web/ui/views/consumers/consumer/_metrics.erb +126 -0
  144. data/lib/karafka/web/ui/views/consumers/consumer/_no_jobs.erb +9 -0
  145. data/lib/karafka/web/ui/views/consumers/consumer/_no_subscriptions.erb +9 -0
  146. data/lib/karafka/web/ui/views/consumers/consumer/_partition.erb +32 -0
  147. data/lib/karafka/web/ui/views/consumers/consumer/_stopped.erb +10 -0
  148. data/lib/karafka/web/ui/views/consumers/consumer/_tabs.erb +20 -0
  149. data/lib/karafka/web/ui/views/consumers/index.erb +29 -0
  150. data/lib/karafka/web/ui/views/errors/_breadcrumbs.erb +19 -0
  151. data/lib/karafka/web/ui/views/errors/_detail.erb +29 -0
  152. data/lib/karafka/web/ui/views/errors/_error.erb +26 -0
  153. data/lib/karafka/web/ui/views/errors/index.erb +38 -0
  154. data/lib/karafka/web/ui/views/errors/show.erb +30 -0
  155. data/lib/karafka/web/ui/views/jobs/_breadcrumbs.erb +5 -0
  156. data/lib/karafka/web/ui/views/jobs/_job.erb +22 -0
  157. data/lib/karafka/web/ui/views/jobs/_no_jobs.erb +9 -0
  158. data/lib/karafka/web/ui/views/jobs/index.erb +31 -0
  159. data/lib/karafka/web/ui/views/layout.erb +23 -0
  160. data/lib/karafka/web/ui/views/routing/_breadcrumbs.erb +15 -0
  161. data/lib/karafka/web/ui/views/routing/_consumer_group.erb +34 -0
  162. data/lib/karafka/web/ui/views/routing/_detail.erb +25 -0
  163. data/lib/karafka/web/ui/views/routing/_topic.erb +18 -0
  164. data/lib/karafka/web/ui/views/routing/index.erb +10 -0
  165. data/lib/karafka/web/ui/views/routing/show.erb +26 -0
  166. data/lib/karafka/web/ui/views/shared/_become_pro.erb +13 -0
  167. data/lib/karafka/web/ui/views/shared/_brand.erb +3 -0
  168. data/lib/karafka/web/ui/views/shared/_content.erb +31 -0
  169. data/lib/karafka/web/ui/views/shared/_header.erb +20 -0
  170. data/lib/karafka/web/ui/views/shared/_navigation.erb +57 -0
  171. data/lib/karafka/web/ui/views/shared/_pagination.erb +21 -0
  172. data/lib/karafka/web/ui/views/shared/exceptions/not_found.erb +39 -0
  173. data/lib/karafka/web/ui/views/shared/exceptions/pro_only.erb +52 -0
  174. data/lib/karafka/web/version.rb +8 -0
  175. data/lib/karafka/web.rb +60 -0
  176. data.tar.gz.sig +0 -0
  177. metadata +328 -0
  178. metadata.gz.sig +0 -0
@@ -0,0 +1,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