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.
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,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Tracking
6
+ module Consumers
7
+ module Listeners
8
+ # Listener related to tracking errors, DLQs, and retries metrics for the Web UI
9
+ class Errors < Base
10
+ # Collects errors info and counts errors
11
+ #
12
+ # @param event [Karafka::Core::Monitoring::Event]
13
+ def on_error_occurred(event)
14
+ track do |sampler|
15
+ # Collect extra info if it was a consumer related error.
16
+ # Those come from user code
17
+ details = if event[:caller].is_a?(Karafka::BaseConsumer)
18
+ extract_consumer_info(event[:caller])
19
+ else
20
+ {}
21
+ end
22
+
23
+ error_class, error_message, backtrace = extract_error_info(event[:error])
24
+
25
+ sampler.errors << {
26
+ type: event[:type],
27
+ error_class: error_class,
28
+ error_message: error_message,
29
+ backtrace: backtrace,
30
+ details: details,
31
+ occurred_at: float_now
32
+ }
33
+
34
+ sampler.counters[:errors] += 1
35
+ end
36
+ end
37
+
38
+ # Count dead letter queue messages dispatches
39
+ #
40
+ # @param _event [Karafka::Core::Monitoring::Event]
41
+ def on_dead_letter_queue_dispatched(_event)
42
+ track do |sampler|
43
+ sampler.counters[:dead] += 1
44
+ end
45
+ end
46
+
47
+ # Count retries
48
+ #
49
+ # @param _event [Karafka::Core::Monitoring::Event]
50
+ def on_consumer_consuming_retry(_event)
51
+ track do |sampler|
52
+ sampler.counters[:retries] += 1
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # @param consumer [::Karafka::BaseConsumer]
59
+ # @return [Hash] hash with consumer specific info for details of error
60
+ def extract_consumer_info(consumer)
61
+ {
62
+ topic: consumer.topic.name,
63
+ consumer_group: consumer.topic.consumer_group.id,
64
+ partition: consumer.messages.metadata.partition,
65
+ first_offset: consumer.messages.first.offset,
66
+ last_offset: consumer.messages.last.offset,
67
+ comitted_offset: consumer.coordinator.seek_offset - 1,
68
+ consumer: consumer.class.to_s
69
+ }
70
+ end
71
+
72
+ # Extracts the basic error info
73
+ #
74
+ # @param error [StandardError] error that occurred
75
+ # @return [Array<String, String, String>] array with error name, message and backtrace
76
+ def extract_error_info(error)
77
+ error_message = error.message.to_s
78
+ error_message.force_encoding('utf-8')
79
+ error_message.scrub!
80
+
81
+ backtrace = (error.backtrace || [])
82
+
83
+ app_root = "#{::Karafka.root}/"
84
+
85
+ gem_home = if ENV.key?('GEM_HOME')
86
+ ENV['GEM_HOME']
87
+ else
88
+ File.expand_path(File.join(Karafka.gem_root.to_s, '../'))
89
+ end
90
+
91
+ gem_home = "#{gem_home}/"
92
+
93
+ backtrace.map! { |line| line.gsub(app_root, '') }
94
+ backtrace.map! { |line| line.gsub(gem_home, '') }
95
+
96
+ [
97
+ error.class.name,
98
+ error_message,
99
+ backtrace.join("\n")
100
+ ]
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Tracking
6
+ module Consumers
7
+ module Listeners
8
+ # Tracks pausing and un-pausing of topics partitions for both user requested and
9
+ # automatic events.
10
+ class Pausing < Base
11
+ # Indicate pause
12
+ #
13
+ # @param event [Karafka::Core::Monitoring::Event]
14
+ def on_client_pause(event)
15
+ track do |sampler|
16
+ sampler.pauses << pause_id(event)
17
+ end
18
+ end
19
+
20
+ # Indicate pause ended
21
+ #
22
+ # @param event [Karafka::Core::Monitoring::Event]
23
+ def on_client_resume(event)
24
+ track do |sampler|
25
+ sampler.pauses.delete pause_id(event)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ # @param event [Karafka::Core::Monitoring::Event]
32
+ # @return [String] pause id built from consumer group and topic details
33
+ def pause_id(event)
34
+ topic = event[:topic]
35
+ partition = event[:partition]
36
+ consumer_group_id = event[:subscription_group].consumer_group.id
37
+
38
+ [consumer_group_id, topic, partition].join('-')
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Tracking
6
+ module Consumers
7
+ module Listeners
8
+ # Listener that is used to collect metrics related to work processing
9
+ class Processing < Base
10
+ # Collect time metrics about worker work execution time
11
+ #
12
+ # @param event [Karafka::Core::Monitoring::Event]
13
+ def on_worker_processed(event)
14
+ track do |sampler|
15
+ sampler.times[:total] << event[:time]
16
+ end
17
+ end
18
+
19
+ # Counts work execution and processing states in consumer instances
20
+ #
21
+ # @param event [Karafka::Core::Monitoring::Event]
22
+ def on_consumer_consume(event)
23
+ consumer = event.payload[:caller]
24
+ messages_count = consumer.messages.count
25
+ jid = job_id(consumer, 'consume')
26
+ job_details = job_details(consumer, 'consume')
27
+
28
+ track do |sampler|
29
+ # We count batches and messages prior to the execution, so they are tracked even
30
+ # if error occurs, etc.
31
+ sampler.counters[:batches] += 1
32
+ sampler.counters[:messages] += messages_count
33
+ sampler.jobs[jid] = job_details
34
+ end
35
+ end
36
+
37
+ # Removes failed job from active jobs
38
+ #
39
+ # @param event [Karafka::Core::Monitoring::Event]
40
+ def on_error_occurred(event)
41
+ track do |sampler|
42
+ type = case event[:type]
43
+ when 'consumer.consume.error'
44
+ 'consume'
45
+ when 'consumer.revoked.error'
46
+ 'revoked'
47
+ when 'consumer.shutdown.error'
48
+ 'shutdown'
49
+ end
50
+
51
+ sampler.jobs.delete(
52
+ job_id(event[:caller], type)
53
+ )
54
+ end
55
+ end
56
+
57
+ # Collect info about consumption event that occurred and its metrics
58
+ # Removes the job from running jobs
59
+ #
60
+ # @param event [Karafka::Core::Monitoring::Event]
61
+ def on_consumer_consumed(event)
62
+ consumer = event.payload[:caller]
63
+ topic = consumer.topic
64
+ consumer_group_id = topic.consumer_group.id
65
+ messages_count = consumer.messages.count
66
+ time = event[:time]
67
+ jid = job_id(consumer, 'consume')
68
+
69
+ track do |sampler|
70
+ sampler.jobs.delete(jid)
71
+ sampler.times[consumer_group_id] << [topic.name, time, messages_count]
72
+ end
73
+ end
74
+
75
+ # Stores this job details
76
+ #
77
+ # @param event [Karafka::Core::Monitoring::Event]
78
+ def on_consumer_revoke(event)
79
+ consumer = event.payload[:caller]
80
+ jid = job_id(consumer, 'revoked')
81
+ job_details = job_details(consumer, 'revoked')
82
+
83
+ track do |sampler|
84
+ sampler.jobs[jid] = job_details
85
+ end
86
+ end
87
+
88
+ # Removes the job from running jobs
89
+ #
90
+ # @param event [Karafka::Core::Monitoring::Event]
91
+ def on_consumer_revoked(event)
92
+ consumer = event.payload[:caller]
93
+ jid = job_id(consumer, 'revoked')
94
+
95
+ track do |sampler|
96
+ sampler.jobs.delete(jid)
97
+ end
98
+ end
99
+
100
+ # Stores this job details
101
+ #
102
+ # @param event [Karafka::Core::Monitoring::Event]
103
+ def on_consumer_shutting_down(event)
104
+ consumer = event.payload[:caller]
105
+ jid = job_id(consumer, 'shutdown')
106
+ job_details = job_details(consumer, 'shutdown')
107
+
108
+ track do |sampler|
109
+ sampler.jobs[jid] = job_details
110
+ end
111
+ end
112
+
113
+ # Removes the job from running jobs
114
+ #
115
+ # @param event [Karafka::Core::Monitoring::Event]
116
+ def on_consumer_shutdown(event)
117
+ consumer = event.payload[:caller]
118
+ jid = job_id(consumer, 'shutdown')
119
+
120
+ track do |sampler|
121
+ sampler.jobs.delete(jid)
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ # Generates a job id that we can use to track jobs in an unique way
128
+ #
129
+ # @param consumer [::Karafka::BaseConsumer] consumer instance
130
+ # @param type [String] job type
131
+ def job_id(consumer, type)
132
+ "#{consumer.id}-#{type}"
133
+ end
134
+
135
+ # Gets consumer details for job tracking
136
+ #
137
+ # @param consumer [::Karafka::BaseConsumer] consumer instance
138
+ # @param type [String] job type
139
+ def job_details(consumer, type)
140
+ {
141
+ started_at: float_now,
142
+ topic: consumer.topic.name,
143
+ partition: consumer.messages.metadata.partition,
144
+ first_offset: consumer.messages.first.offset,
145
+ last_offset: consumer.messages.last.offset,
146
+ comitted_offset: consumer.coordinator.seek_offset - 1,
147
+ consumer: consumer.class.to_s,
148
+ consumer_group: consumer.topic.consumer_group.id,
149
+ type: type
150
+ }
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Tracking
6
+ module Consumers
7
+ module Listeners
8
+ # Listener used to collect metrics published by librdkafka
9
+ class Statistics < Base
10
+ # Collect Kafka metrics
11
+ #
12
+ # @param event [Karafka::Core::Monitoring::Event]
13
+ def on_statistics_emitted(event)
14
+ statistics = event[:statistics]
15
+ topics = statistics.fetch('topics')
16
+ cgrp = statistics.fetch('cgrp')
17
+ consumer_group_id = event[:consumer_group_id]
18
+
19
+ track do |sampler|
20
+ cg_details = extract_consumer_group_details(consumer_group_id, cgrp)
21
+ sampler.consumer_groups[consumer_group_id] = cg_details
22
+
23
+ topics.each do |topic_name, topic_values|
24
+ partitions = topic_values.fetch('partitions')
25
+
26
+ partitions.each do |partition_name, partition_statistics|
27
+ partition_id = partition_name.to_i
28
+
29
+ next unless partition_reportable?(partition_id, partition_statistics)
30
+
31
+ metrics = extract_partition_metrics(partition_statistics)
32
+
33
+ next if metrics.empty?
34
+
35
+ topics_details = cg_details[:topics]
36
+
37
+ topic_details = topics_details[topic_name] ||= {
38
+ name: topic_name,
39
+ partitions: {}
40
+ }
41
+
42
+ topic_details[:partitions][partition_id] = metrics.merge(
43
+ id: partition_id,
44
+ poll_state: poll_state(consumer_group_id, topic_name, partition_id)
45
+ )
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ # Extracts basic consumer group related details
54
+ # @param consumer_group_id [String]
55
+ # @param consumer_group_statistics [Hash]
56
+ # @return [Hash] consumer group relevant details
57
+ def extract_consumer_group_details(consumer_group_id, consumer_group_statistics)
58
+ {
59
+ id: consumer_group_id,
60
+ state: consumer_group_statistics.slice(
61
+ 'state',
62
+ 'join_state',
63
+ 'stateage',
64
+ 'rebalance_age',
65
+ 'rebalance_cnt',
66
+ 'rebalance_reason'
67
+ ),
68
+ topics: {}
69
+ }
70
+ end
71
+
72
+ # @param partition_id [Integer]
73
+ # @param partition_statistics [Hash]
74
+ # @return [Boolean] is this partition relevant to the current process, hence should we
75
+ # report about it in the context of the process.
76
+ def partition_reportable?(partition_id, partition_statistics)
77
+ return false if partition_id == -1
78
+
79
+ # Skip until lag info is available
80
+ return false if partition_statistics['consumer_lag'] == -1
81
+
82
+ # Collect information only about what we are subscribed to and what we fetch or
83
+ # work in any way. Stopped means, we no longer work with it
84
+ return false if partition_statistics['fetch_state'] == 'stopped'
85
+
86
+ true
87
+ end
88
+
89
+ # Extracts and formats partition relevant metrics
90
+ #
91
+ # @param partition_statistics [Hash]
92
+ # @return [Hash] extracted partition metrics
93
+ def extract_partition_metrics(partition_statistics)
94
+ metrics = partition_statistics.slice(
95
+ 'consumer_lag_stored',
96
+ 'consumer_lag_stored_d',
97
+ 'committed_offset',
98
+ 'stored_offset',
99
+ 'fetch_state'
100
+ )
101
+
102
+ # Rename as we do not need `consumer_` prefix
103
+ metrics.transform_keys! { |key| key.gsub('consumer_', '') }
104
+ metrics.transform_keys!(&:to_sym)
105
+
106
+ metrics
107
+ end
108
+
109
+ # @param consumer_group_id [String]
110
+ # @param topic_name [String]
111
+ # @param partition_id [Integer]
112
+ # @return [String] poll state / is partition paused or not
113
+ def poll_state(consumer_group_id, topic_name, partition_id)
114
+ pause_id = [consumer_group_id, topic_name, partition_id].join('-')
115
+
116
+ sampler.pauses.include?(pause_id) ? 'paused' : 'active'
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Tracking
6
+ module Consumers
7
+ module Listeners
8
+ # Listener that triggers reporting on Karafka process status changes
9
+ # Whenever the whole process status changes, we do not want to wait and want to report
10
+ # as fast as possible, hence `report!`. This improves the user experience and since
11
+ # status changes do not happen that often, we can handle few extra reports dispatches.
12
+ class Status < Base
13
+ # @param _event [Karafka::Core::Monitoring::Event]
14
+ #
15
+ # @note We do not use `#report!` here because this kicks in for each listener loop and
16
+ # those run the same time.
17
+ def on_connection_listener_before_fetch_loop(_event)
18
+ report
19
+ end
20
+
21
+ # Indicate as fast as possible that we've started moving to the quiet mode
22
+ #
23
+ # @param _event [Karafka::Core::Monitoring::Event]
24
+ def on_app_quieting(_event)
25
+ report!
26
+ end
27
+
28
+ # Indicate as fast as possible that we've reached the quiet mode
29
+ #
30
+ # @param _event [Karafka::Core::Monitoring::Event]
31
+ def on_app_quiet(_event)
32
+ report!
33
+ end
34
+
35
+ # Instrument on the fact that we're stopping
36
+ #
37
+ # @param _event [Karafka::Core::Monitoring::Event]
38
+ def on_app_stopping(_event)
39
+ # Make sure this is sent before shutdown
40
+ report!
41
+ end
42
+
43
+ # Instrument on the fact that Karafka has stopped
44
+ #
45
+ # We do this actually before the process ends but we need to do this so the UI does not
46
+ # have this "handling" stopping process.
47
+ #
48
+ # @param _event [Karafka::Core::Monitoring::Event]
49
+ def on_app_stopped(_event)
50
+ # Make sure this is sent before shutdown
51
+ report!
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end