karafka-web 0.7.9 → 0.8.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (163) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/ci.yml +21 -6
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +66 -0
  6. data/Gemfile.lock +22 -22
  7. data/docker-compose.yml +3 -1
  8. data/karafka-web.gemspec +2 -2
  9. data/lib/karafka/web/config.rb +16 -3
  10. data/lib/karafka/web/contracts/config.rb +7 -2
  11. data/lib/karafka/web/errors.rb +12 -0
  12. data/lib/karafka/web/inflector.rb +33 -0
  13. data/lib/karafka/web/installer.rb +20 -11
  14. data/lib/karafka/web/management/actions/base.rb +36 -0
  15. data/lib/karafka/web/management/actions/clean_boot_file.rb +33 -0
  16. data/lib/karafka/web/management/actions/create_initial_states.rb +77 -0
  17. data/lib/karafka/web/management/actions/create_topics.rb +139 -0
  18. data/lib/karafka/web/management/actions/delete_topics.rb +30 -0
  19. data/lib/karafka/web/management/actions/enable.rb +117 -0
  20. data/lib/karafka/web/management/actions/extend_boot_file.rb +39 -0
  21. data/lib/karafka/web/management/actions/migrate_states_data.rb +18 -0
  22. data/lib/karafka/web/management/migrations/0_base.rb +58 -0
  23. data/lib/karafka/web/management/migrations/0_set_initial_consumers_metrics.rb +36 -0
  24. data/lib/karafka/web/management/migrations/0_set_initial_consumers_state.rb +43 -0
  25. data/lib/karafka/web/management/migrations/1699543515_fill_missing_received_and_sent_bytes_in_consumers_metrics.rb +26 -0
  26. data/lib/karafka/web/management/migrations/1699543515_fill_missing_received_and_sent_bytes_in_consumers_state.rb +23 -0
  27. data/lib/karafka/web/management/migrations/1700234522_introduce_waiting_in_consumers_metrics.rb +24 -0
  28. data/lib/karafka/web/management/migrations/1700234522_introduce_waiting_in_consumers_state.rb +20 -0
  29. data/lib/karafka/web/management/migrations/1700234522_remove_processing_from_consumers_metrics.rb +24 -0
  30. data/lib/karafka/web/management/migrations/1700234522_remove_processing_from_consumers_state.rb +20 -0
  31. data/lib/karafka/web/management/migrations/1704722380_split_listeners_into_active_and_paused_in_metrics.rb +36 -0
  32. data/lib/karafka/web/management/migrations/1704722380_split_listeners_into_active_and_paused_in_states.rb +32 -0
  33. data/lib/karafka/web/management/migrator.rb +117 -0
  34. data/lib/karafka/web/processing/consumer.rb +39 -38
  35. data/lib/karafka/web/processing/consumers/aggregators/metrics.rb +15 -7
  36. data/lib/karafka/web/processing/consumers/aggregators/state.rb +8 -3
  37. data/lib/karafka/web/processing/consumers/contracts/aggregated_stats.rb +5 -1
  38. data/lib/karafka/web/processing/publisher.rb +59 -0
  39. data/lib/karafka/web/tracking/consumers/contracts/job.rb +3 -2
  40. data/lib/karafka/web/tracking/consumers/contracts/partition.rb +1 -0
  41. data/lib/karafka/web/tracking/consumers/contracts/report.rb +6 -1
  42. data/lib/karafka/web/tracking/consumers/contracts/subscription_group.rb +10 -1
  43. data/lib/karafka/web/tracking/consumers/listeners/connections.rb +49 -0
  44. data/lib/karafka/web/tracking/consumers/listeners/pausing.rb +7 -4
  45. data/lib/karafka/web/tracking/consumers/listeners/processing.rb +78 -70
  46. data/lib/karafka/web/tracking/consumers/listeners/statistics.rb +40 -13
  47. data/lib/karafka/web/tracking/consumers/sampler.rb +82 -25
  48. data/lib/karafka/web/tracking/helpers/ttls/array.rb +72 -0
  49. data/lib/karafka/web/tracking/helpers/ttls/hash.rb +34 -0
  50. data/lib/karafka/web/tracking/helpers/ttls/stats.rb +49 -0
  51. data/lib/karafka/web/tracking/helpers/ttls/windows.rb +32 -0
  52. data/lib/karafka/web/tracking/reporter.rb +1 -0
  53. data/lib/karafka/web/ui/app.rb +22 -4
  54. data/lib/karafka/web/ui/base.rb +18 -2
  55. data/lib/karafka/web/ui/controllers/base.rb +34 -4
  56. data/lib/karafka/web/ui/controllers/become_pro.rb +1 -1
  57. data/lib/karafka/web/ui/controllers/cluster.rb +33 -9
  58. data/lib/karafka/web/ui/controllers/consumers.rb +8 -2
  59. data/lib/karafka/web/ui/controllers/dashboard.rb +2 -2
  60. data/lib/karafka/web/ui/controllers/errors.rb +2 -2
  61. data/lib/karafka/web/ui/controllers/jobs.rb +55 -5
  62. data/lib/karafka/web/ui/controllers/requests/params.rb +5 -0
  63. data/lib/karafka/web/ui/controllers/responses/deny.rb +15 -0
  64. data/lib/karafka/web/ui/controllers/responses/file.rb +23 -0
  65. data/lib/karafka/web/ui/controllers/responses/{data.rb → render.rb} +3 -3
  66. data/lib/karafka/web/ui/controllers/routing.rb +11 -2
  67. data/lib/karafka/web/ui/controllers/status.rb +1 -1
  68. data/lib/karafka/web/ui/helpers/application_helper.rb +70 -0
  69. data/lib/karafka/web/ui/lib/hash_proxy.rb +29 -14
  70. data/lib/karafka/web/ui/lib/sorter.rb +170 -0
  71. data/lib/karafka/web/ui/models/counters.rb +6 -0
  72. data/lib/karafka/web/ui/models/health.rb +23 -2
  73. data/lib/karafka/web/ui/models/jobs.rb +48 -0
  74. data/lib/karafka/web/ui/models/metrics/charts/aggregated.rb +33 -0
  75. data/lib/karafka/web/ui/models/metrics/charts/topics.rb +1 -10
  76. data/lib/karafka/web/ui/models/process.rb +2 -1
  77. data/lib/karafka/web/ui/models/status.rb +23 -7
  78. data/lib/karafka/web/ui/models/topic.rb +3 -1
  79. data/lib/karafka/web/ui/models/visibility_filter.rb +16 -0
  80. data/lib/karafka/web/ui/pro/app.rb +44 -6
  81. data/lib/karafka/web/ui/pro/controllers/cluster.rb +1 -0
  82. data/lib/karafka/web/ui/pro/controllers/consumers.rb +52 -6
  83. data/lib/karafka/web/ui/pro/controllers/dashboard.rb +1 -1
  84. data/lib/karafka/web/ui/pro/controllers/dlq.rb +1 -1
  85. data/lib/karafka/web/ui/pro/controllers/errors.rb +3 -3
  86. data/lib/karafka/web/ui/pro/controllers/explorer.rb +8 -8
  87. data/lib/karafka/web/ui/pro/controllers/health.rb +34 -2
  88. data/lib/karafka/web/ui/pro/controllers/jobs.rb +11 -0
  89. data/lib/karafka/web/ui/pro/controllers/messages.rb +42 -0
  90. data/lib/karafka/web/ui/pro/controllers/routing.rb +11 -2
  91. data/lib/karafka/web/ui/pro/views/consumers/_breadcrumbs.erb +8 -2
  92. data/lib/karafka/web/ui/pro/views/consumers/_consumer.erb +14 -8
  93. data/lib/karafka/web/ui/pro/views/consumers/_counters.erb +8 -6
  94. data/lib/karafka/web/ui/pro/views/consumers/consumer/_job.erb +4 -1
  95. data/lib/karafka/web/ui/pro/views/consumers/consumer/_no_jobs.erb +1 -1
  96. data/lib/karafka/web/ui/pro/views/consumers/consumer/_partition.erb +1 -3
  97. data/lib/karafka/web/ui/pro/views/consumers/consumer/_subscription_group.erb +28 -11
  98. data/lib/karafka/web/ui/pro/views/consumers/consumer/_tabs.erb +10 -3
  99. data/lib/karafka/web/ui/pro/views/consumers/index.erb +3 -3
  100. data/lib/karafka/web/ui/pro/views/consumers/pending_jobs.erb +43 -0
  101. data/lib/karafka/web/ui/pro/views/consumers/{jobs.erb → running_jobs.erb} +11 -10
  102. data/lib/karafka/web/ui/pro/views/dashboard/index.erb +7 -1
  103. data/lib/karafka/web/ui/pro/views/explorer/message/_message_actions.erb +18 -0
  104. data/lib/karafka/web/ui/pro/views/explorer/message/_metadata.erb +43 -0
  105. data/lib/karafka/web/ui/pro/views/explorer/message/_payload.erb +21 -0
  106. data/lib/karafka/web/ui/pro/views/explorer/message/_payload_actions.erb +19 -0
  107. data/lib/karafka/web/ui/pro/views/explorer/show.erb +9 -84
  108. data/lib/karafka/web/ui/pro/views/health/_breadcrumbs.erb +8 -0
  109. data/lib/karafka/web/ui/pro/views/health/_partition.erb +1 -3
  110. data/lib/karafka/web/ui/pro/views/health/_partition_offset.erb +4 -4
  111. data/lib/karafka/web/ui/pro/views/health/_partition_times.erb +32 -0
  112. data/lib/karafka/web/ui/pro/views/health/_tabs.erb +9 -0
  113. data/lib/karafka/web/ui/pro/views/health/changes.erb +66 -0
  114. data/lib/karafka/web/ui/pro/views/health/offsets.erb +14 -14
  115. data/lib/karafka/web/ui/pro/views/health/overview.erb +11 -11
  116. data/lib/karafka/web/ui/pro/views/jobs/_job.erb +1 -1
  117. data/lib/karafka/web/ui/pro/views/jobs/_no_jobs.erb +1 -1
  118. data/lib/karafka/web/ui/pro/views/jobs/pending.erb +39 -0
  119. data/lib/karafka/web/ui/pro/views/jobs/running.erb +39 -0
  120. data/lib/karafka/web/ui/pro/views/routing/_consumer_group.erb +2 -2
  121. data/lib/karafka/web/ui/pro/views/routing/_topic.erb +9 -0
  122. data/lib/karafka/web/ui/pro/views/routing/show.erb +12 -0
  123. data/lib/karafka/web/ui/pro/views/shared/_navigation.erb +1 -1
  124. data/lib/karafka/web/ui/public/javascripts/application.js +10 -0
  125. data/lib/karafka/web/ui/public/stylesheets/application.css +4 -0
  126. data/lib/karafka/web/ui/views/cluster/_breadcrumbs.erb +16 -0
  127. data/lib/karafka/web/ui/views/cluster/_tabs.erb +27 -0
  128. data/lib/karafka/web/ui/views/cluster/brokers.erb +27 -0
  129. data/lib/karafka/web/ui/views/cluster/topics.erb +35 -0
  130. data/lib/karafka/web/ui/views/consumers/_counters.erb +8 -6
  131. data/lib/karafka/web/ui/views/consumers/_summary.erb +2 -2
  132. data/lib/karafka/web/ui/views/consumers/index.erb +3 -3
  133. data/lib/karafka/web/ui/views/dashboard/_ranges_selector.erb +23 -7
  134. data/lib/karafka/web/ui/views/dashboard/index.erb +19 -8
  135. data/lib/karafka/web/ui/views/errors/show.erb +2 -23
  136. data/lib/karafka/web/ui/views/jobs/_breadcrumbs.erb +17 -1
  137. data/lib/karafka/web/ui/views/jobs/_job.erb +1 -1
  138. data/lib/karafka/web/ui/views/jobs/_no_jobs.erb +1 -1
  139. data/lib/karafka/web/ui/views/jobs/_tabs.erb +27 -0
  140. data/lib/karafka/web/ui/views/jobs/{index.erb → pending.erb} +9 -7
  141. data/lib/karafka/web/ui/{pro/views/jobs/index.erb → views/jobs/running.erb} +9 -11
  142. data/lib/karafka/web/ui/views/routing/_consumer_group.erb +14 -12
  143. data/lib/karafka/web/ui/views/shared/_navigation.erb +1 -1
  144. data/lib/karafka/web/ui/views/shared/_pagination.erb +1 -1
  145. data/lib/karafka/web/ui/views/shared/exceptions/not_allowed.erb +37 -0
  146. data/lib/karafka/web/ui/views/status/show.erb +17 -2
  147. data/lib/karafka/web/ui/views/status/warnings/_routing_topics_presence.erb +15 -0
  148. data/lib/karafka/web/version.rb +1 -1
  149. data/lib/karafka/web.rb +6 -2
  150. data.tar.gz.sig +0 -0
  151. metadata +61 -26
  152. metadata.gz.sig +0 -0
  153. data/lib/karafka/web/management/base.rb +0 -34
  154. data/lib/karafka/web/management/clean_boot_file.rb +0 -31
  155. data/lib/karafka/web/management/create_initial_states.rb +0 -101
  156. data/lib/karafka/web/management/create_topics.rb +0 -133
  157. data/lib/karafka/web/management/delete_topics.rb +0 -28
  158. data/lib/karafka/web/management/enable.rb +0 -102
  159. data/lib/karafka/web/management/extend_boot_file.rb +0 -37
  160. data/lib/karafka/web/tracking/ttl_array.rb +0 -59
  161. data/lib/karafka/web/tracking/ttl_hash.rb +0 -16
  162. data/lib/karafka/web/ui/pro/views/dashboard/_ranges_selector.erb +0 -39
  163. data/lib/karafka/web/ui/views/cluster/index.erb +0 -74
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Management
6
+ module Migrations
7
+ # Since we have introduced notion of pause listeners, we need to reflect this in the
8
+ # UI, so the scaling changes are visible
9
+ class SplitListenersIntoActiveAndPausedInStates < Base
10
+ self.versions_until = '1.2.2'
11
+ self.type = :consumers_state
12
+
13
+ # @param state [Hash]
14
+ def migrate(state)
15
+ listeners = if state[:stats].key?(:listeners)
16
+ state[:stats][:listeners].to_i
17
+ elsif state[:stats].key?(:listeners_count)
18
+ state[:stats][:listeners_count].to_i
19
+ else
20
+ 0
21
+ end
22
+
23
+ state[:stats][:listeners] = {
24
+ active: listeners,
25
+ standby: 0
26
+ }
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ # Namespace for all cross-context management operations that are needed to make sure everything
6
+ # operate as expected.
7
+ module Management
8
+ # Migrator used to run migrations on the states topics
9
+ # There are cases during upgrades, where extra fields may be added and other data, so in
10
+ # order not to deal with cases of some information missing, we can just migrate the data
11
+ # and ensure all the fields that we require after upgrade are present
12
+ #
13
+ # Migrations are similar to the once that are present in Ruby on Rails conceptually.
14
+ #
15
+ # We take our most recent state and we can alter it "in place". The altered result will be
16
+ # passed to the consecutive migrations and then republished back to Kafka. This allows us
17
+ # to manage Web UI aggregated data easily.
18
+ #
19
+ # @note We do not migrate the consumers reports for the following reasons:
20
+ # - if would be extremely hard to migrate them as they are being published and can be still
21
+ # published when the migrations are running
22
+ # - we would have to run migrations on each message
23
+ # - we already have a mechanism in the processing consumer that skips outdated records for
24
+ # rolling migrations
25
+ # - those records are short-lived and the expectation is for the user not to run old and
26
+ # new consumers together for an extensive period of time
27
+ #
28
+ # @note It will raise an error if we try to run migrations but the schemas we want to operate
29
+ # are newer. This will prevent us from damaging the data and ensures that we only move
30
+ # forward with the migrations. This can happen in case of a rolling upgrade, where old
31
+ # instance that is going to be terminated would get a temporary assignment with already
32
+ # migrated state.
33
+ class Migrator
34
+ # Include this so we can reference the schema versions easily
35
+ include Processing::Consumers::Aggregators
36
+
37
+ # Picks needed data from Kafka, alters it with migrations and puts the updated data
38
+ # back into Kafka. This ensures, that our Web UI topics that hold aggregated data are
39
+ # always aligned with the Web UI expectations
40
+ #
41
+ # @note To simplify things we always migrate and update all the topics data even if only
42
+ # part was migrated. That way we always ensure that all the elements are up to date
43
+ def call
44
+ ensure_migrable!
45
+ # If migrating returns `false` it means no migrations happened
46
+ migrate && publish
47
+ end
48
+
49
+ private
50
+
51
+ # Raise an exception if there would be an attempt to run migrations on a newer schema for
52
+ # any states we manage. We can only move forward, so attempt to migrate for example from
53
+ # 1.0.0 to 0.9.0 should be considered and error.
54
+ def ensure_migrable!
55
+ if consumers_state[:schema_version] > State::SCHEMA_VERSION
56
+ raise(
57
+ Errors::Management::IncompatibleSchemaError,
58
+ 'consumers state newer than supported'
59
+ )
60
+ end
61
+
62
+ if consumers_metrics[:schema_version] > Metrics::SCHEMA_VERSION
63
+ raise(
64
+ Errors::Management::IncompatibleSchemaError,
65
+ 'consumers metrics newer than supported'
66
+ )
67
+ end
68
+
69
+ true
70
+ end
71
+
72
+ # Applies migrations if needed and mutates the in-memory data
73
+ #
74
+ # @return [Boolean] were there any migrations applied
75
+ def migrate
76
+ any_migrations = false
77
+
78
+ Migrations::Base.sorted_descendants.each do |migration_class|
79
+ data = send(migration_class.type)
80
+
81
+ next unless migration_class.applicable?(data[:schema_version])
82
+
83
+ migration_class.new.migrate(data)
84
+
85
+ any_migrations = true
86
+ end
87
+
88
+ any_migrations
89
+ end
90
+
91
+ # Publishes all the states migrated records
92
+ def publish
93
+ consumers_state[:schema_version] = State::SCHEMA_VERSION
94
+ consumers_metrics[:schema_version] = Metrics::SCHEMA_VERSION
95
+
96
+ # Migrator may run in the context of the processing consumer prior to any states
97
+ # fetching related to processing. We use sync to make sure, that the following
98
+ # processing related states fetched fetch the new states
99
+ Processing::Publisher.publish!(
100
+ consumers_state,
101
+ consumers_metrics
102
+ )
103
+ end
104
+
105
+ # @return [Hash] current consumers states most recent state
106
+ def consumers_state
107
+ @consumers_state ||= Processing::Consumers::State.current!
108
+ end
109
+
110
+ # @return [Hash] current consumers metrics most recent state
111
+ def consumers_metrics
112
+ @consumers_metrics ||= Processing::Consumers::Metrics.current!
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -11,35 +11,19 @@ module Karafka
11
11
  class Consumer < Karafka::BaseConsumer
12
12
  include ::Karafka::Core::Helpers::Time
13
13
 
14
- # @param args [Object] all the arguments `Karafka::BaseConsumer` accepts by default
15
- def initialize(*args)
16
- super
17
-
18
- @flush_interval = ::Karafka::Web.config.processing.interval
19
-
20
- @schema_manager = Consumers::SchemaManager.new
21
- @state_aggregator = Consumers::Aggregators::State.new(@schema_manager)
22
- @state_contract = Consumers::Contracts::State.new
23
-
24
- @metrics_aggregator = Consumers::Aggregators::Metrics.new
25
- @metrics_contract = Consumers::Contracts::Metrics.new
26
-
27
- # We set this that way so we report with first batch and so we report as fast as possible
28
- @flushed_at = monotonic_now - @flush_interval
29
- @established = false
30
- end
31
-
32
14
  # Aggregates consumers state into a single current state representation
33
15
  def consume
16
+ bootstrap!
17
+
34
18
  consumers_messages = messages.select { |message| message.payload[:type] == 'consumer' }
35
19
 
36
20
  # If there is even one incompatible message, we need to stop
37
21
  consumers_messages.each do |message|
38
- case @schema_manager.call(message)
22
+ case @reports_schema_manager.call(message)
39
23
  when :current
40
24
  true
41
25
  when :newer
42
- @schema_manager.invalidate!
26
+ @reports_schema_manager.invalidate!
43
27
 
44
28
  dispatch
45
29
 
@@ -49,6 +33,9 @@ module Karafka
49
33
  # requests without significant or any impact on data quality but without having to
50
34
  # worry about backwards compatibility. Errors are tracked independently, so it should
51
35
  # not be a problem.
36
+ #
37
+ # In case user wants to do a rolling upgrade, the user docs state that this can happen
38
+ # and it is something user should be aware
52
39
  when :older
53
40
  next
54
41
  else
@@ -83,6 +70,35 @@ module Karafka
83
70
 
84
71
  private
85
72
 
73
+ # Prepares all the initial objects and ensures all the needed states are as expected
74
+ # @note We do not run it in the `#initialize` anymore as `#initialize` happens before
75
+ # the work starts so errors there are handled differently. We want this initial setup
76
+ # to operate and fail (if needed) during messages consumption phase
77
+ def bootstrap!
78
+ return if @bootstrapped
79
+
80
+ # Run the migrator on the assignment to make sure all our data is as expected
81
+ # While users may run the CLI command this is a fail-safe for zero downtime deployments
82
+ # It costs us two extra requests to Kafka topics as we migrate prior to fetching the
83
+ # states to the aggregators but this is done on purpose not to mix those two contexts.
84
+ Management::Migrator.new.call
85
+
86
+ @flush_interval = ::Karafka::Web.config.processing.interval
87
+
88
+ @reports_schema_manager = Consumers::SchemaManager.new
89
+ @state_aggregator = Consumers::Aggregators::State.new(@reports_schema_manager)
90
+ @state_contract = Consumers::Contracts::State.new
91
+
92
+ @metrics_aggregator = Consumers::Aggregators::Metrics.new
93
+ @metrics_contract = Consumers::Contracts::Metrics.new
94
+
95
+ # We set this that way so we report with first batch and so we report as fast as possible
96
+ @flushed_at = monotonic_now - @flush_interval
97
+ @established = false
98
+
99
+ @bootstrapped = true
100
+ end
101
+
86
102
  # Flushes the state of the Web-UI to the DB
87
103
  def dispatch
88
104
  return unless @established
@@ -114,24 +130,9 @@ module Karafka
114
130
  def flush
115
131
  @flushed_at = monotonic_now
116
132
 
117
- ::Karafka::Web.producer.produce_many_async(
118
- [
119
- {
120
- topic: Karafka::Web.config.topics.consumers.states,
121
- payload: Zlib::Deflate.deflate(@state.to_json),
122
- # This will ensure that the consumer states are compacted
123
- key: Karafka::Web.config.topics.consumers.states,
124
- partition: 0,
125
- headers: { 'zlib' => 'true' }
126
- },
127
- {
128
- topic: Karafka::Web.config.topics.consumers.metrics,
129
- payload: Zlib::Deflate.deflate(@metrics.to_json),
130
- key: Karafka::Web.config.topics.consumers.metrics,
131
- partition: 0,
132
- headers: { 'zlib' => 'true' }
133
- }
134
- ]
133
+ Publisher.publish(
134
+ @state,
135
+ @metrics
135
136
  )
136
137
  end
137
138
  end
@@ -10,9 +10,8 @@ module Karafka
10
10
  # values for charts and metrics
11
11
  class Metrics < Base
12
12
  # Current schema version
13
- # This can be used in the future for detecting incompatible changes and writing
14
- # migrations
15
- SCHEMA_VERSION = '1.0.0'
13
+ # This is used for detecting incompatible changes and writing migrations
14
+ SCHEMA_VERSION = '1.1.2'
16
15
 
17
16
  def initialize
18
17
  super
@@ -107,9 +106,18 @@ module Karafka
107
106
 
108
107
  # Last stable offsets freeze durations - we pick the max freeze to indicate
109
108
  # the longest open transaction that potentially may be hanging
110
- ls_offsets_fd = partitions_data
111
- .map { |p_details| p_details.fetch(:ls_offset_fd, 0) }
112
- .reject(&:negative?)
109
+ # We select only those partitions for which LSO != HO as in any other case this
110
+ # just means we've reached the end of data and ls may freeze because there is no
111
+ # more data flowing. Such cases should not be reported as ls offset freezes because
112
+ # there is no more data to be processed and can grow until more data is present
113
+ # this does not indicate "bad" freezing that we are interested in
114
+ ls_offsets_fds = partitions_data.map do |p_details|
115
+ next if p_details.fetch(:ls_offset, 0) == p_details.fetch(:hi_offset, 0)
116
+
117
+ ls_offset_fd = p_details.fetch(:ls_offset_fd, 0)
118
+
119
+ ls_offset_fd.negative? ? nil : ls_offset_fd
120
+ end
113
121
 
114
122
  cgs[group_name] ||= {}
115
123
  cgs[group_name][topic_name] = {
@@ -119,7 +127,7 @@ module Karafka
119
127
  # Take max last stable offset duration without any change. This can
120
128
  # indicate a hanging transaction, because the offset will not move forward
121
129
  # and will stay with a growing freeze duration when stuck
122
- ls_offset_fd: ls_offsets_fd.max || 0
130
+ ls_offset_fd: ls_offsets_fds.compact.max || 0
123
131
  }
124
132
  end
125
133
 
@@ -20,7 +20,7 @@ module Karafka
20
20
  # Current schema version
21
21
  # This can be used in the future for detecting incompatible changes and writing
22
22
  # migrations
23
- SCHEMA_VERSION = '1.1.0'
23
+ SCHEMA_VERSION = '1.2.2'
24
24
 
25
25
  # @param schema_manager [Karafka::Web::Processing::Consumers::SchemaManager] schema
26
26
  # manager that tracks the compatibility of schemas.
@@ -126,9 +126,11 @@ module Karafka
126
126
  stats[:workers] = 0
127
127
  stats[:processes] = 0
128
128
  stats[:rss] = 0
129
- stats[:listeners] = 0
129
+ stats[:listeners] = { active: 0, standby: 0 }
130
130
  stats[:lag] = 0
131
131
  stats[:lag_stored] = 0
132
+ stats[:bytes_received] = 0
133
+ stats[:bytes_sent] = 0
132
134
  utilization = 0
133
135
 
134
136
  @active_reports
@@ -149,7 +151,10 @@ module Karafka
149
151
  stats[:busy] += report_stats[:busy]
150
152
  stats[:enqueued] += report_stats[:enqueued]
151
153
  stats[:workers] += report_process[:workers] || 0
152
- stats[:listeners] += report_process[:listeners] || 0
154
+ stats[:bytes_received] += report_process[:bytes_received] || 0
155
+ stats[:bytes_sent] += report_process[:bytes_sent] || 0
156
+ stats[:listeners][:active] += report_process[:listeners][:active]
157
+ stats[:listeners][:standby] += report_process[:listeners][:standby]
153
158
  stats[:processes] += 1
154
159
  stats[:rss] += report_process[:memory_usage]
155
160
  stats[:lag] += lags.compact.reject(&:negative?).sum
@@ -20,10 +20,14 @@ module Karafka
20
20
  required(:workers) { |val| val.is_a?(Integer) && val >= 0 }
21
21
  required(:processes) { |val| val.is_a?(Integer) && val >= 0 }
22
22
  required(:rss) { |val| val.is_a?(Numeric) && val >= 0 }
23
- required(:listeners) { |val| val.is_a?(Integer) && val >= 0 }
24
23
  required(:utilization) { |val| val.is_a?(Numeric) && val >= 0 }
25
24
  required(:lag_stored) { |val| val.is_a?(Integer) }
26
25
  required(:lag) { |val| val.is_a?(Integer) }
26
+
27
+ nested(:listeners) do
28
+ required(:active) { |val| val.is_a?(Integer) && val >= 0 }
29
+ required(:standby) { |val| val.is_a?(Integer) && val >= 0 }
30
+ end
27
31
  end
28
32
  end
29
33
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Processing
6
+ # Object responsible for publishing states data back into Kafka so it can be used in the UI
7
+ class Publisher
8
+ class << self
9
+ # Publishes data back to Kafka in an async fashion
10
+ #
11
+ # @param consumers_state [Hash] consumers current state
12
+ # @param consumers_metrics [Hash] consumers current metrics
13
+ def publish(consumers_state, consumers_metrics)
14
+ ::Karafka::Web.producer.produce_many_async(
15
+ prepare_data(consumers_state, consumers_metrics)
16
+ )
17
+ end
18
+
19
+ # Publishes data back to Kafka in a sync fashion
20
+ #
21
+ # @param consumers_state [Hash] consumers current state
22
+ # @param consumers_metrics [Hash] consumers current metrics
23
+ def publish!(consumers_state, consumers_metrics)
24
+ ::Karafka::Web.producer.produce_many_sync(
25
+ prepare_data(consumers_state, consumers_metrics)
26
+ )
27
+ end
28
+
29
+ private
30
+
31
+ # Converts the states into format that we can dispatch to Kafka
32
+ #
33
+ # @param consumers_state [Hash] consumers current state
34
+ # @param consumers_metrics [Hash] consumers current metrics
35
+ # @return [Array<Hash>]
36
+ def prepare_data(consumers_state, consumers_metrics)
37
+ [
38
+ {
39
+ topic: Karafka::Web.config.topics.consumers.states,
40
+ payload: Zlib::Deflate.deflate(consumers_state.to_json),
41
+ # This will ensure that the consumer states are compacted
42
+ key: Karafka::Web.config.topics.consumers.states,
43
+ partition: 0,
44
+ headers: { 'zlib' => 'true' }
45
+ },
46
+ {
47
+ topic: Karafka::Web.config.topics.consumers.metrics,
48
+ payload: Zlib::Deflate.deflate(consumers_metrics.to_json),
49
+ key: Karafka::Web.config.topics.consumers.metrics,
50
+ partition: 0,
51
+ headers: { 'zlib' => 'true' }
52
+ }
53
+ ]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -11,18 +11,19 @@ module Karafka
11
11
 
12
12
  required(:consumer) { |val| val.is_a?(String) }
13
13
  required(:consumer_group) { |val| val.is_a?(String) }
14
- required(:started_at) { |val| val.is_a?(Float) && val >= 0 }
14
+ required(:updated_at) { |val| val.is_a?(Float) && val >= 0 }
15
15
  required(:topic) { |val| val.is_a?(String) }
16
16
  required(:partition) { |val| val.is_a?(Integer) && val >= 0 }
17
17
  required(:first_offset) { |val| val.is_a?(Integer) && (val >= 0 || val == -1001) }
18
18
  required(:last_offset) { |val| val.is_a?(Integer) && (val >= 0 || val == -1001) }
19
19
  required(:committed_offset) { |val| val.is_a?(Integer) }
20
20
  required(:messages) { |val| val.is_a?(Integer) && val >= 0 }
21
- required(:type) { |val| %w[consume revoked shutdown].include?(val) }
21
+ required(:type) { |val| %w[consume revoked shutdown tick].include?(val) }
22
22
  required(:tags) { |val| val.is_a?(Karafka::Core::Taggable::Tags) }
23
23
  # -1 can be here for workless flows
24
24
  required(:consumption_lag) { |val| val.is_a?(Integer) && (val >= 0 || val == -1) }
25
25
  required(:processing_lag) { |val| val.is_a?(Integer) && (val >= 0 || val == -1) }
26
+ required(:status) { |val| %w[running pending].include?(val) }
26
27
  end
27
28
  end
28
29
  end
@@ -20,6 +20,7 @@ module Karafka
20
20
  required(:stored_offset_fd) { |val| val.is_a?(Integer) && val >= 0 }
21
21
  required(:fetch_state) { |val| val.is_a?(String) && !val.empty? }
22
22
  required(:poll_state) { |val| val.is_a?(String) && !val.empty? }
23
+ required(:poll_state_ch) { |val| val.is_a?(Integer) && val >= 0 }
23
24
  required(:hi_offset) { |val| val.is_a?(Integer) }
24
25
  required(:hi_offset_fd) { |val| val.is_a?(Integer) && val >= 0 }
25
26
  required(:lo_offset) { |val| val.is_a?(Integer) }
@@ -27,10 +27,14 @@ module Karafka
27
27
  required(:memory_size) { |val| val.is_a?(Integer) && val >= 0 }
28
28
  required(:status) { |val| ::Karafka::Status::STATES.key?(val.to_s.to_sym) }
29
29
  required(:threads) { |val| val.is_a?(Integer) && val >= 0 }
30
- required(:listeners) { |val| val.is_a?(Integer) && val >= 0 }
31
30
  required(:workers) { |val| val.is_a?(Integer) && val.positive? }
32
31
  required(:tags) { |val| val.is_a?(Karafka::Core::Taggable::Tags) }
33
32
 
33
+ nested(:listeners) do
34
+ required(:active) { |val| val.is_a?(Integer) && val >= 0 }
35
+ required(:standby) { |val| val.is_a?(Integer) && val >= 0 }
36
+ end
37
+
34
38
  required(:cpu_usage) do |val|
35
39
  val.is_a?(Array) &&
36
40
  val.all? { |key| key.is_a?(Numeric) } &&
@@ -52,6 +56,7 @@ module Karafka
52
56
  nested(:stats) do
53
57
  required(:busy) { |val| val.is_a?(Integer) && val >= 0 }
54
58
  required(:enqueued) { |val| val.is_a?(Integer) && val >= 0 }
59
+ required(:waiting) { |val| val.is_a?(Integer) && val >= 0 }
55
60
  required(:utilization) { |val| val.is_a?(Numeric) && val >= 0 }
56
61
 
57
62
  nested(:total) do
@@ -12,7 +12,16 @@ module Karafka
12
12
 
13
13
  required(:id) { |val| val.is_a?(String) && !val.empty? }
14
14
  required(:topics) { |val| val.is_a?(Hash) }
15
- required(:state) { |val| val.is_a?(Hash) }
15
+
16
+ nested(:state) do
17
+ required(:state) { |val| val.is_a?(String) && !val.empty? }
18
+ required(:join_state) { |val| val.is_a?(String) && !val.empty? }
19
+ required(:stateage) { |val| val.is_a?(Integer) && val >= 0 }
20
+ required(:rebalance_age) { |val| val.is_a?(Integer) && val >= 0 }
21
+ required(:rebalance_cnt) { |val| val.is_a?(Integer) && val >= 0 }
22
+ required(:rebalance_reason) { |val| val.is_a?(String) && !val.empty? }
23
+ required(:poll_age) { |val| val.is_a?(Numeric) && val >= 0 }
24
+ end
16
25
 
17
26
  virtual do |data, errors|
18
27
  next unless errors.empty?
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Tracking
6
+ module Consumers
7
+ module Listeners
8
+ # Listener for listening on connections related events like polling, etc
9
+ class Connections < Base
10
+ # Set first poll time before we start fetching so we always have a poll time
11
+ # and we don't have to worry about it being always available
12
+ # @param event [Karafka::Core::Monitoring::Event]
13
+ def on_connection_listener_before_fetch_loop(event)
14
+ on_connection_listener_fetch_loop_received(event)
15
+ end
16
+
17
+ # When fetch loop is done it means this subscription group is no longer active and we
18
+ # should stop reporting. The listener was stopped.
19
+ #
20
+ # @param event [Karafka::Core::Monitoring::Event]
21
+ def on_connection_listener_after_fetch_loop(event)
22
+ subscription_group = event[:subscription_group]
23
+ sg_id = subscription_group.id
24
+ cg_id = subscription_group.consumer_group.id
25
+
26
+ track do |sampler|
27
+ sampler.consumer_groups[cg_id][:subscription_groups].delete(sg_id)
28
+ sampler.subscription_groups.delete(sg_id)
29
+ end
30
+ end
31
+
32
+ # Tracks the moment a poll happened on a given subscription group
33
+ #
34
+ # @param event [Karafka::Core::Monitoring::Event]
35
+ def on_connection_listener_fetch_loop_received(event)
36
+ sg_id = event[:subscription_group].id
37
+
38
+ track do |sampler|
39
+ sampler.subscription_groups[sg_id] = {
40
+ polled_at: monotonic_now
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -11,9 +11,12 @@ module Karafka
11
11
  # Indicate pause
12
12
  #
13
13
  # @param event [Karafka::Core::Monitoring::Event]
14
- def on_client_pause(event)
14
+ def on_consumer_consuming_pause(event)
15
15
  track do |sampler|
16
- sampler.pauses << pause_id(event)
16
+ sampler.pauses[pause_id(event)] = {
17
+ timeout: event[:timeout],
18
+ paused_till: monotonic_now + event[:timeout]
19
+ }
17
20
  end
18
21
  end
19
22
 
@@ -33,9 +36,9 @@ module Karafka
33
36
  def pause_id(event)
34
37
  topic = event[:topic]
35
38
  partition = event[:partition]
36
- consumer_group_id = event[:subscription_group].consumer_group.id
39
+ subscription_group_id = event[:subscription_group].id
37
40
 
38
- [consumer_group_id, topic, partition].join('-')
41
+ [subscription_group_id, topic, partition].join('-')
39
42
  end
40
43
  end
41
44
  end