karafka 2.0.33 → 2.0.35

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 511cc39ce92e82706fcd6ac9c54b3416c7908cf5e9773ee15b972219046778ca
4
- data.tar.gz: 737059dc4f9de33577f4883590f607da1ee4ee330ff9f5c18bd42dfe261c567d
3
+ metadata.gz: e2d2197fb0fe7eae8db43cc45ffbdcdc6da83d60731ae63b82dc4a24d852736e
4
+ data.tar.gz: 76b4b3f8ffb915b96a16f1eb83503283e5da46039fb952af383322773c4be551
5
5
  SHA512:
6
- metadata.gz: d91e5de5bddd7b9ff2e423c986eda7f8e795528dc5ffb7a3694c82a2a096f8b3ee31679ac221bb0da4cd6efc7efa55ab195f081fbd2cf393df7d21549d950cd9
7
- data.tar.gz: dbea8026eb3385b6ee14a749b3bc0187f4e91e9e66b60f2e8ac74fcdd409ad238b8e6f7dd5e5d5fbb5f0cc36299795ed63b794f1dd4a2a2b53e26776a7523876
6
+ metadata.gz: 35d69a8fa9a26462f2167174d8700648909bbcf38363c5adef2f7ee42995b524c0d244bffe0bd76698f65df5475db78b38914f38d7eda239376c4031555ee885
7
+ data.tar.gz: 49a2256930ce83bc46b6471cf702b1ea2fe6f897a805f92e4dee7c3d9034a9625a44ecff0222b8396ed73858af5571cca4eaabca8d188a81312312c2225daa97
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Karafka framework changelog
2
2
 
3
+ ## 2.0.35 (2023-03-13)
4
+ - **[Feature]** Allow for defining topics config via the DSL and its automatic creation via CLI command.
5
+ - **[Feature]** Allow for full topics reset and topics repartitioning via the CLI.
6
+
7
+ ## 2.0.34 (2023-03-04)
8
+ - [Improvement] Attach an `embedded` tag to Karafka processes started using the embedded API.
9
+ - [Change] Renamed `Datadog::Listener` to `Datadog::MetricsListener` for consistency. (#1124)
10
+
11
+ ### Upgrade notes
12
+
13
+ 1. Replace `Datadog::Listener` references to `Datadog::MetricsListener`.
14
+
3
15
  ## 2.0.33 (2023-02-24)
4
16
  - **[Feature]** Support `perform_all_later` in ActiveJob adapter for Rails `7.1+`
5
17
  - **[Feature]** Introduce ability to assign and re-assign tags in consumer instances. This can be used for extra instrumentation that is context aware.
@@ -99,7 +111,7 @@ class KarafkaApp < Karafka::App
99
111
  - [Improvement] Expand `LoggerListener` with `client.resume` notification.
100
112
  - [Improvement] Replace random anonymous subscription groups ids with stable once.
101
113
  - [Improvement] Add `consumer.consume`, `consumer.revoke` and `consumer.shutting_down` notification events and move the revocation logic calling to strategies.
102
- - [Change] Rename job queue statistics `processing` key to `busy`. No changes needed because naming in the DataDog listener stays the same.
114
+ - [Change] Rename job queue statistics `processing` key to `busy`. No changes needed because naming in the DataDog listener stays the same.
103
115
  - [Fix] Fix proctitle listener state changes reporting on new states.
104
116
  - [Fix] Make sure all files descriptors are closed in the integration specs.
105
117
  - [Fix] Fix a case where empty subscription groups could leak into the execution flow.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- karafka (2.0.33)
4
+ karafka (2.0.35)
5
5
  karafka-core (>= 2.0.12, < 3.0.0)
6
6
  thor (>= 0.20)
7
7
  waterdrop (>= 2.4.10, < 3.0.0)
@@ -19,7 +19,7 @@ GEM
19
19
  minitest (>= 5.1)
20
20
  tzinfo (~> 2.0)
21
21
  byebug (11.1.3)
22
- concurrent-ruby (1.2.0)
22
+ concurrent-ruby (1.2.2)
23
23
  diff-lcs (1.5.0)
24
24
  docile (1.4.0)
25
25
  factory_bot (6.2.1)
@@ -37,7 +37,7 @@ GEM
37
37
  mini_portile2 (~> 2.6)
38
38
  rake (> 12)
39
39
  mini_portile2 (2.8.1)
40
- minitest (5.17.0)
40
+ minitest (5.18.0)
41
41
  rake (13.0.6)
42
42
  rspec (3.12.0)
43
43
  rspec-core (~> 3.12.0)
@@ -61,12 +61,13 @@ GEM
61
61
  thor (1.2.1)
62
62
  tzinfo (2.0.6)
63
63
  concurrent-ruby (~> 1.0)
64
- waterdrop (2.4.10)
65
- karafka-core (>= 2.0.9, < 3.0.0)
64
+ waterdrop (2.5.0)
65
+ karafka-core (>= 2.0.12, < 3.0.0)
66
66
  zeitwerk (~> 2.3)
67
67
  zeitwerk (2.6.7)
68
68
 
69
69
  PLATFORMS
70
+ arm64-darwin-21
70
71
  x86_64-linux
71
72
 
72
73
  DEPENDENCIES
@@ -78,4 +79,4 @@ DEPENDENCIES
78
79
  simplecov
79
80
 
80
81
  BUNDLED WITH
81
- 2.4.6
82
+ 2.4.7
@@ -45,6 +45,10 @@ en:
45
45
  dead_letter_queue.topic_format: 'needs to be a string with a Kafka accepted format'
46
46
  dead_letter_queue.active_format: needs to be either true or false
47
47
  active_format: needs to be either true or false
48
+ structurable.partitions_format: needs to be more or equal to 1
49
+ structurable.active_format: needs to be true
50
+ structurable.replication_factor_format: needs to be more or equal to 1
51
+ structurable.details_format: needs to be a hash with only symbol keys
48
52
  inconsistent_namespacing: |
49
53
  needs to be consistent namespacing style
50
54
  disable this validation by setting config.strict_topics_namespacing to false
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ class Cli < Thor
5
+ # CLI actions related to Kafka cluster topics management
6
+ class Topics < Base
7
+ include Helpers::Colorize
8
+
9
+ desc 'Allows for the topics management (create, delete, reset, repartition)'
10
+ # @param action [String] action we want to take
11
+ def call(action = 'missing')
12
+ case action
13
+ when 'create'
14
+ create
15
+ when 'delete'
16
+ delete
17
+ when 'reset'
18
+ reset
19
+ when 'repartition'
20
+ repartition
21
+ when 'migrate'
22
+ migrate
23
+ else
24
+ raise ::ArgumentError, "Invalid topics action: #{action}"
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # Creates topics based on the routing setup and configuration
31
+ def create
32
+ structurable_routing_topics.each do |topic|
33
+ name = topic.name
34
+
35
+ if existing_topics_names.include?(name)
36
+ puts "#{yellow('Skipping')} because topic #{name} already exists."
37
+ else
38
+ puts "Creating topic #{name}..."
39
+ Admin.create_topic(
40
+ name,
41
+ topic.structurable.partitions,
42
+ topic.structurable.replication_factor,
43
+ topic.structurable.details
44
+ )
45
+ puts "#{green('Created')} topic #{name}."
46
+ end
47
+ end
48
+ end
49
+
50
+ # Deletes routing based topics
51
+ def delete
52
+ structurable_routing_topics.each do |topic|
53
+ name = topic.name
54
+
55
+ if existing_topics_names.include?(name)
56
+ puts "Deleting topic #{name}..."
57
+ Admin.delete_topic(name)
58
+ puts "#{green('Deleted')} topic #{name}."
59
+ else
60
+ puts "#{yellow('Skipping')} because topic #{name} does not exist."
61
+ end
62
+ end
63
+ end
64
+
65
+ # Deletes routing based topics and re-creates them
66
+ def reset
67
+ delete
68
+
69
+ # We need to invalidate the metadata cache, otherwise we will think, that the topic
70
+ # already exists
71
+ @existing_topics = nil
72
+
73
+ create
74
+ end
75
+
76
+ # Creates missing topics and aligns the partitions count
77
+ def migrate
78
+ create
79
+
80
+ @existing_topics = nil
81
+
82
+ repartition
83
+ end
84
+
85
+ # Increases number of partitions on topics that have less partitions than defined
86
+ # Will **not** create topics if missing.
87
+ def repartition
88
+ existing_partitions = existing_topics.map do |topic|
89
+ [topic.fetch(:topic_name), topic.fetch(:partition_count)]
90
+ end.to_h
91
+
92
+ structurable_routing_topics.each do |topic|
93
+ name = topic.name
94
+
95
+ desired_count = topic.config.partitions
96
+ existing_count = existing_partitions.fetch(name, false)
97
+
98
+ if existing_count && existing_count < desired_count
99
+ puts "Increasing number of partitions to #{desired_count} on topic #{name}..."
100
+ Admin.create_partitions(name, desired_count)
101
+ change = desired_count - existing_count
102
+ puts "#{green('Created')} #{change} additional partitions on topic #{name}."
103
+ elsif existing_count
104
+ puts "#{yellow('Skipping')} because topic #{name} has #{existing_count} partitions."
105
+ else
106
+ puts "#{yellow('Skipping')} because topic #{name} does not exist."
107
+ end
108
+ end
109
+ end
110
+
111
+ # @return [Array<Karafka::Routing::Topic>] all available topics that can be managed
112
+ # @note If topic is defined in multiple consumer groups, first config will be used. This
113
+ # means, that this CLI will not work for simultaneous management of multiple clusters from
114
+ # a single CLI command execution flow.
115
+ def structurable_routing_topics
116
+ return @structurable_routing_topics if @structurable_routing_topics
117
+
118
+ collected_topics = {}
119
+
120
+ App.consumer_groups.each do |consumer_group|
121
+ consumer_group.topics.each do |topic|
122
+ # Skip topics that were explicitly disabled from management
123
+ next unless topic.structurable.active?
124
+
125
+ collected_topics[topic.name] ||= topic
126
+ end
127
+ end
128
+
129
+ @structurable_routing_topics = collected_topics.values
130
+ end
131
+
132
+ # @return [Array<Hash>] existing topics details
133
+ def existing_topics
134
+ @existing_topics ||= Admin.cluster_info.topics
135
+ end
136
+
137
+ # @return [Array<String>] names of already existing topics
138
+ def existing_topics_names
139
+ existing_topics.map { |topic| topic.fetch(:topic_name) }
140
+ end
141
+ end
142
+ end
143
+ end
@@ -7,7 +7,10 @@ module Karafka
7
7
  # Starts Karafka without supervision and without ownership of signals in a background thread
8
8
  # so it won't interrupt other things running
9
9
  def start
10
- Thread.new { Karafka::Server.start }
10
+ Thread.new do
11
+ Karafka::Process.tags.add(:execution_mode, 'embedded')
12
+ Karafka::Server.start
13
+ end
11
14
  end
12
15
 
13
16
  # Stops Karafka upon any event
@@ -15,6 +15,12 @@ module Karafka
15
15
  def red(string)
16
16
  "\033[0;31m#{string}\033[0m"
17
17
  end
18
+
19
+ # @param string [String] string we want to have in yellow
20
+ # @return [String] yellow string
21
+ def yellow(string)
22
+ "\033[1;33m#{string}\033[0m"
23
+ end
18
24
  end
19
25
  end
20
26
  end
@@ -1,258 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'metrics_listener'
4
+
3
5
  module Karafka
4
6
  module Instrumentation
5
7
  # Namespace for vendor specific instrumentation
6
8
  module Vendors
7
9
  # Datadog specific instrumentation
8
10
  module Datadog
9
- # Listener that can be used to subscribe to Karafka to receive stats via StatsD
10
- # and/or Datadog
11
- #
12
- # @note You need to setup the `dogstatsd-ruby` client and assign it
13
- class Listener
14
- include ::Karafka::Core::Configurable
15
- extend Forwardable
16
-
17
- def_delegators :config, :client, :rd_kafka_metrics, :namespace, :default_tags
18
-
19
- # Value object for storing a single rdkafka metric publishing details
20
- RdKafkaMetric = Struct.new(:type, :scope, :name, :key_location)
21
-
22
- # Namespace under which the DD metrics should be published
23
- setting :namespace, default: 'karafka'
24
-
25
- # Datadog client that we should use to publish the metrics
26
- setting :client
27
-
28
- # Default tags we want to publish (for example hostname)
29
- # Format as followed (example for hostname): `["host:#{Socket.gethostname}"]`
30
- setting :default_tags, default: []
31
-
32
- # All the rdkafka metrics we want to publish
33
- #
34
- # By default we publish quite a lot so this can be tuned
35
- # Note, that the once with `_d` come from Karafka, not rdkafka or Kafka
36
- setting :rd_kafka_metrics, default: [
37
- # Client metrics
38
- RdKafkaMetric.new(:count, :root, 'messages.consumed', 'rxmsgs_d'),
39
- RdKafkaMetric.new(:count, :root, 'messages.consumed.bytes', 'rxmsg_bytes'),
40
-
41
- # Broker metrics
42
- RdKafkaMetric.new(:count, :brokers, 'consume.attempts', 'txretries_d'),
43
- RdKafkaMetric.new(:count, :brokers, 'consume.errors', 'txerrs_d'),
44
- RdKafkaMetric.new(:count, :brokers, 'receive.errors', 'rxerrs_d'),
45
- RdKafkaMetric.new(:count, :brokers, 'connection.connects', 'connects_d'),
46
- RdKafkaMetric.new(:count, :brokers, 'connection.disconnects', 'disconnects_d'),
47
- RdKafkaMetric.new(:gauge, :brokers, 'network.latency.avg', %w[rtt avg]),
48
- RdKafkaMetric.new(:gauge, :brokers, 'network.latency.p95', %w[rtt p95]),
49
- RdKafkaMetric.new(:gauge, :brokers, 'network.latency.p99', %w[rtt p99]),
50
-
51
- # Topics metrics
52
- RdKafkaMetric.new(:gauge, :topics, 'consumer.lags', 'consumer_lag_stored'),
53
- RdKafkaMetric.new(:gauge, :topics, 'consumer.lags_delta', 'consumer_lag_stored_d')
54
- ].freeze
55
-
56
- configure
57
-
58
- # @param block [Proc] configuration block
59
- def initialize(&block)
60
- configure
61
- setup(&block) if block
62
- end
63
-
64
- # @param block [Proc] configuration block
65
- # @note We define this alias to be consistent with `WaterDrop#setup`
66
- def setup(&block)
67
- configure(&block)
68
- end
69
-
70
- # Hooks up to WaterDrop instrumentation for emitted statistics
71
- #
72
- # @param event [Karafka::Core::Monitoring::Event]
73
- def on_statistics_emitted(event)
74
- statistics = event[:statistics]
75
- consumer_group_id = event[:consumer_group_id]
76
-
77
- base_tags = default_tags + ["consumer_group:#{consumer_group_id}"]
78
-
79
- rd_kafka_metrics.each do |metric|
80
- report_metric(metric, statistics, base_tags)
81
- end
82
- end
83
-
84
- # Increases the errors count by 1
85
- #
86
- # @param event [Karafka::Core::Monitoring::Event]
87
- def on_error_occurred(event)
88
- extra_tags = ["type:#{event[:type]}"]
89
-
90
- if event.payload[:caller].respond_to?(:messages)
91
- extra_tags += consumer_tags(event.payload[:caller])
92
- end
93
-
94
- count('error_occurred', 1, tags: default_tags + extra_tags)
95
- end
96
-
97
- # Reports how many messages we've polled and how much time did we spend on it
98
- #
99
- # @param event [Karafka::Core::Monitoring::Event]
100
- def on_connection_listener_fetch_loop_received(event)
101
- time_taken = event[:time]
102
- messages_count = event[:messages_buffer].size
103
-
104
- consumer_group_id = event[:subscription_group].consumer_group_id
105
-
106
- extra_tags = ["consumer_group:#{consumer_group_id}"]
107
-
108
- histogram('listener.polling.time_taken', time_taken, tags: default_tags + extra_tags)
109
- histogram('listener.polling.messages', messages_count, tags: default_tags + extra_tags)
110
- end
111
-
112
- # Here we report majority of things related to processing as we have access to the
113
- # consumer
114
- # @param event [Karafka::Core::Monitoring::Event]
115
- def on_consumer_consumed(event)
116
- consumer = event.payload[:caller]
117
- messages = consumer.messages
118
- metadata = messages.metadata
119
-
120
- tags = default_tags + consumer_tags(consumer)
121
-
122
- count('consumer.messages', messages.count, tags: tags)
123
- count('consumer.batches', 1, tags: tags)
124
- gauge('consumer.offset', metadata.last_offset, tags: tags)
125
- histogram('consumer.consumed.time_taken', event[:time], tags: tags)
126
- histogram('consumer.batch_size', messages.count, tags: tags)
127
- histogram('consumer.processing_lag', metadata.processing_lag, tags: tags)
128
- histogram('consumer.consumption_lag', metadata.consumption_lag, tags: tags)
129
- end
130
-
131
- # @param event [Karafka::Core::Monitoring::Event]
132
- def on_consumer_revoked(event)
133
- tags = default_tags + consumer_tags(event.payload[:caller])
134
-
135
- count('consumer.revoked', 1, tags: tags)
136
- end
137
-
138
- # @param event [Karafka::Core::Monitoring::Event]
139
- def on_consumer_shutdown(event)
140
- tags = default_tags + consumer_tags(event.payload[:caller])
141
-
142
- count('consumer.shutdown', 1, tags: tags)
143
- end
144
-
145
- # Worker related metrics
146
- # @param event [Karafka::Core::Monitoring::Event]
147
- def on_worker_process(event)
148
- jq_stats = event[:jobs_queue].statistics
149
-
150
- gauge('worker.total_threads', Karafka::App.config.concurrency, tags: default_tags)
151
- histogram('worker.processing', jq_stats[:busy], tags: default_tags)
152
- histogram('worker.enqueued_jobs', jq_stats[:enqueued], tags: default_tags)
153
- end
154
-
155
- # We report this metric before and after processing for higher accuracy
156
- # Without this, the utilization would not be fully reflected
157
- # @param event [Karafka::Core::Monitoring::Event]
158
- def on_worker_processed(event)
159
- jq_stats = event[:jobs_queue].statistics
160
-
161
- histogram('worker.processing', jq_stats[:busy], tags: default_tags)
162
- end
163
-
164
- private
165
-
166
- %i[
167
- count
168
- gauge
169
- histogram
170
- increment
171
- decrement
172
- ].each do |metric_type|
173
- class_eval <<~METHODS, __FILE__, __LINE__ + 1
174
- def #{metric_type}(key, *args)
175
- client.#{metric_type}(
176
- namespaced_metric(key),
177
- *args
178
- )
179
- end
180
- METHODS
181
- end
182
-
183
- # Wraps metric name in listener's namespace
184
- # @param metric_name [String] RdKafkaMetric name
185
- # @return [String]
186
- def namespaced_metric(metric_name)
187
- "#{namespace}.#{metric_name}"
188
- end
189
-
190
- # Reports a given metric statistics to Datadog
191
- # @param metric [RdKafkaMetric] metric value object
192
- # @param statistics [Hash] hash with all the statistics emitted
193
- # @param base_tags [Array<String>] base tags we want to start with
194
- def report_metric(metric, statistics, base_tags)
195
- case metric.scope
196
- when :root
197
- public_send(
198
- metric.type,
199
- metric.name,
200
- statistics.fetch(*metric.key_location),
201
- tags: base_tags
202
- )
203
- when :brokers
204
- statistics.fetch('brokers').each_value do |broker_statistics|
205
- # Skip bootstrap nodes
206
- # Bootstrap nodes have nodeid -1, other nodes have positive
207
- # node ids
208
- next if broker_statistics['nodeid'] == -1
209
-
210
- public_send(
211
- metric.type,
212
- metric.name,
213
- broker_statistics.dig(*metric.key_location),
214
- tags: base_tags + ["broker:#{broker_statistics['nodename']}"]
215
- )
216
- end
217
- when :topics
218
- statistics.fetch('topics').each do |topic_name, topic_values|
219
- topic_values['partitions'].each do |partition_name, partition_statistics|
220
- next if partition_name == '-1'
221
- # Skip until lag info is available
222
- next if partition_statistics['consumer_lag'] == -1
223
-
224
- public_send(
225
- metric.type,
226
- metric.name,
227
- partition_statistics.dig(*metric.key_location),
228
- tags: base_tags + [
229
- "topic:#{topic_name}",
230
- "partition:#{partition_name}"
231
- ]
232
- )
233
- end
234
- end
235
- else
236
- raise ArgumentError, metric.scope
237
- end
238
- end
239
-
240
- # Builds basic per consumer tags for publication
241
- #
242
- # @param consumer [Karafka::BaseConsumer]
243
- # @return [Array<String>]
244
- def consumer_tags(consumer)
245
- messages = consumer.messages
246
- metadata = messages.metadata
247
- consumer_group_id = consumer.topic.consumer_group.id
248
-
249
- [
250
- "topic:#{metadata.topic}",
251
- "partition:#{metadata.partition}",
252
- "consumer_group:#{consumer_group_id}"
253
- ]
254
- end
255
- end
11
+ # Alias to keep backwards compatibility
12
+ Listener = MetricsListener
256
13
  end
257
14
  end
258
15
  end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Instrumentation
5
+ # Namespace for vendor specific instrumentation
6
+ module Vendors
7
+ # Datadog specific instrumentation
8
+ module Datadog
9
+ # Listener that can be used to subscribe to Karafka to receive stats via StatsD
10
+ # and/or Datadog
11
+ #
12
+ # @note You need to setup the `dogstatsd-ruby` client and assign it
13
+ class MetricsListener
14
+ include ::Karafka::Core::Configurable
15
+ extend Forwardable
16
+
17
+ def_delegators :config, :client, :rd_kafka_metrics, :namespace, :default_tags
18
+
19
+ # Value object for storing a single rdkafka metric publishing details
20
+ RdKafkaMetric = Struct.new(:type, :scope, :name, :key_location)
21
+
22
+ # Namespace under which the DD metrics should be published
23
+ setting :namespace, default: 'karafka'
24
+
25
+ # Datadog client that we should use to publish the metrics
26
+ setting :client
27
+
28
+ # Default tags we want to publish (for example hostname)
29
+ # Format as followed (example for hostname): `["host:#{Socket.gethostname}"]`
30
+ setting :default_tags, default: []
31
+
32
+ # All the rdkafka metrics we want to publish
33
+ #
34
+ # By default we publish quite a lot so this can be tuned
35
+ # Note, that the once with `_d` come from Karafka, not rdkafka or Kafka
36
+ setting :rd_kafka_metrics, default: [
37
+ # Client metrics
38
+ RdKafkaMetric.new(:count, :root, 'messages.consumed', 'rxmsgs_d'),
39
+ RdKafkaMetric.new(:count, :root, 'messages.consumed.bytes', 'rxmsg_bytes'),
40
+
41
+ # Broker metrics
42
+ RdKafkaMetric.new(:count, :brokers, 'consume.attempts', 'txretries_d'),
43
+ RdKafkaMetric.new(:count, :brokers, 'consume.errors', 'txerrs_d'),
44
+ RdKafkaMetric.new(:count, :brokers, 'receive.errors', 'rxerrs_d'),
45
+ RdKafkaMetric.new(:count, :brokers, 'connection.connects', 'connects_d'),
46
+ RdKafkaMetric.new(:count, :brokers, 'connection.disconnects', 'disconnects_d'),
47
+ RdKafkaMetric.new(:gauge, :brokers, 'network.latency.avg', %w[rtt avg]),
48
+ RdKafkaMetric.new(:gauge, :brokers, 'network.latency.p95', %w[rtt p95]),
49
+ RdKafkaMetric.new(:gauge, :brokers, 'network.latency.p99', %w[rtt p99]),
50
+
51
+ # Topics metrics
52
+ RdKafkaMetric.new(:gauge, :topics, 'consumer.lags', 'consumer_lag_stored'),
53
+ RdKafkaMetric.new(:gauge, :topics, 'consumer.lags_delta', 'consumer_lag_stored_d')
54
+ ].freeze
55
+
56
+ configure
57
+
58
+ # @param block [Proc] configuration block
59
+ def initialize(&block)
60
+ configure
61
+ setup(&block) if block
62
+ end
63
+
64
+ # @param block [Proc] configuration block
65
+ # @note We define this alias to be consistent with `WaterDrop#setup`
66
+ def setup(&block)
67
+ configure(&block)
68
+ end
69
+
70
+ # Hooks up to WaterDrop instrumentation for emitted statistics
71
+ #
72
+ # @param event [Karafka::Core::Monitoring::Event]
73
+ def on_statistics_emitted(event)
74
+ statistics = event[:statistics]
75
+ consumer_group_id = event[:consumer_group_id]
76
+
77
+ base_tags = default_tags + ["consumer_group:#{consumer_group_id}"]
78
+
79
+ rd_kafka_metrics.each do |metric|
80
+ report_metric(metric, statistics, base_tags)
81
+ end
82
+ end
83
+
84
+ # Increases the errors count by 1
85
+ #
86
+ # @param event [Karafka::Core::Monitoring::Event]
87
+ def on_error_occurred(event)
88
+ extra_tags = ["type:#{event[:type]}"]
89
+
90
+ if event.payload[:caller].respond_to?(:messages)
91
+ extra_tags += consumer_tags(event.payload[:caller])
92
+ end
93
+
94
+ count('error_occurred', 1, tags: default_tags + extra_tags)
95
+ end
96
+
97
+ # Reports how many messages we've polled and how much time did we spend on it
98
+ #
99
+ # @param event [Karafka::Core::Monitoring::Event]
100
+ def on_connection_listener_fetch_loop_received(event)
101
+ time_taken = event[:time]
102
+ messages_count = event[:messages_buffer].size
103
+
104
+ consumer_group_id = event[:subscription_group].consumer_group_id
105
+
106
+ extra_tags = ["consumer_group:#{consumer_group_id}"]
107
+
108
+ histogram('listener.polling.time_taken', time_taken, tags: default_tags + extra_tags)
109
+ histogram('listener.polling.messages', messages_count, tags: default_tags + extra_tags)
110
+ end
111
+
112
+ # Here we report majority of things related to processing as we have access to the
113
+ # consumer
114
+ # @param event [Karafka::Core::Monitoring::Event]
115
+ def on_consumer_consumed(event)
116
+ consumer = event.payload[:caller]
117
+ messages = consumer.messages
118
+ metadata = messages.metadata
119
+
120
+ tags = default_tags + consumer_tags(consumer)
121
+
122
+ count('consumer.messages', messages.count, tags: tags)
123
+ count('consumer.batches', 1, tags: tags)
124
+ gauge('consumer.offset', metadata.last_offset, tags: tags)
125
+ histogram('consumer.consumed.time_taken', event[:time], tags: tags)
126
+ histogram('consumer.batch_size', messages.count, tags: tags)
127
+ histogram('consumer.processing_lag', metadata.processing_lag, tags: tags)
128
+ histogram('consumer.consumption_lag', metadata.consumption_lag, tags: tags)
129
+ end
130
+
131
+ # @param event [Karafka::Core::Monitoring::Event]
132
+ def on_consumer_revoked(event)
133
+ tags = default_tags + consumer_tags(event.payload[:caller])
134
+
135
+ count('consumer.revoked', 1, tags: tags)
136
+ end
137
+
138
+ # @param event [Karafka::Core::Monitoring::Event]
139
+ def on_consumer_shutdown(event)
140
+ tags = default_tags + consumer_tags(event.payload[:caller])
141
+
142
+ count('consumer.shutdown', 1, tags: tags)
143
+ end
144
+
145
+ # Worker related metrics
146
+ # @param event [Karafka::Core::Monitoring::Event]
147
+ def on_worker_process(event)
148
+ jq_stats = event[:jobs_queue].statistics
149
+
150
+ gauge('worker.total_threads', Karafka::App.config.concurrency, tags: default_tags)
151
+ histogram('worker.processing', jq_stats[:busy], tags: default_tags)
152
+ histogram('worker.enqueued_jobs', jq_stats[:enqueued], tags: default_tags)
153
+ end
154
+
155
+ # We report this metric before and after processing for higher accuracy
156
+ # Without this, the utilization would not be fully reflected
157
+ # @param event [Karafka::Core::Monitoring::Event]
158
+ def on_worker_processed(event)
159
+ jq_stats = event[:jobs_queue].statistics
160
+
161
+ histogram('worker.processing', jq_stats[:busy], tags: default_tags)
162
+ end
163
+
164
+ private
165
+
166
+ %i[
167
+ count
168
+ gauge
169
+ histogram
170
+ increment
171
+ decrement
172
+ ].each do |metric_type|
173
+ class_eval <<~METHODS, __FILE__, __LINE__ + 1
174
+ def #{metric_type}(key, *args)
175
+ client.#{metric_type}(
176
+ namespaced_metric(key),
177
+ *args
178
+ )
179
+ end
180
+ METHODS
181
+ end
182
+
183
+ # Wraps metric name in listener's namespace
184
+ # @param metric_name [String] RdKafkaMetric name
185
+ # @return [String]
186
+ def namespaced_metric(metric_name)
187
+ "#{namespace}.#{metric_name}"
188
+ end
189
+
190
+ # Reports a given metric statistics to Datadog
191
+ # @param metric [RdKafkaMetric] metric value object
192
+ # @param statistics [Hash] hash with all the statistics emitted
193
+ # @param base_tags [Array<String>] base tags we want to start with
194
+ def report_metric(metric, statistics, base_tags)
195
+ case metric.scope
196
+ when :root
197
+ public_send(
198
+ metric.type,
199
+ metric.name,
200
+ statistics.fetch(*metric.key_location),
201
+ tags: base_tags
202
+ )
203
+ when :brokers
204
+ statistics.fetch('brokers').each_value do |broker_statistics|
205
+ # Skip bootstrap nodes
206
+ # Bootstrap nodes have nodeid -1, other nodes have positive
207
+ # node ids
208
+ next if broker_statistics['nodeid'] == -1
209
+
210
+ public_send(
211
+ metric.type,
212
+ metric.name,
213
+ broker_statistics.dig(*metric.key_location),
214
+ tags: base_tags + ["broker:#{broker_statistics['nodename']}"]
215
+ )
216
+ end
217
+ when :topics
218
+ statistics.fetch('topics').each do |topic_name, topic_values|
219
+ topic_values['partitions'].each do |partition_name, partition_statistics|
220
+ next if partition_name == '-1'
221
+ # Skip until lag info is available
222
+ next if partition_statistics['consumer_lag'] == -1
223
+
224
+ public_send(
225
+ metric.type,
226
+ metric.name,
227
+ partition_statistics.dig(*metric.key_location),
228
+ tags: base_tags + [
229
+ "topic:#{topic_name}",
230
+ "partition:#{partition_name}"
231
+ ]
232
+ )
233
+ end
234
+ end
235
+ else
236
+ raise ArgumentError, metric.scope
237
+ end
238
+ end
239
+
240
+ # Builds basic per consumer tags for publication
241
+ #
242
+ # @param consumer [Karafka::BaseConsumer]
243
+ # @return [Array<String>]
244
+ def consumer_tags(consumer)
245
+ messages = consumer.messages
246
+ metadata = messages.metadata
247
+ consumer_group_id = consumer.topic.consumer_group.id
248
+
249
+ [
250
+ "topic:#{metadata.topic}",
251
+ "partition:#{metadata.partition}",
252
+ "consumer_group:#{consumer_group_id}"
253
+ ]
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class Structurable < Base
7
+ # Config for structurable feature
8
+ Config = Struct.new(
9
+ :active,
10
+ :partitions,
11
+ :replication_factor,
12
+ :details,
13
+ keyword_init: true
14
+ ) { alias_method :active?, :active }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class Structurable < Base
7
+ # Basic validation of the Kafka expected config details
8
+ class Contract < Contracts::Base
9
+ configure do |config|
10
+ config.error_messages = YAML.safe_load(
11
+ File.read(
12
+ File.join(Karafka.gem_root, 'config', 'locales', 'errors.yml')
13
+ )
14
+ ).fetch('en').fetch('validations').fetch('topic')
15
+ end
16
+
17
+ nested :structurable do
18
+ required(:active) { |val| [true, false].include?(val) }
19
+ required(:partitions) { |val| val.is_a?(Integer) && val.positive? }
20
+ required(:replication_factor) { |val| val.is_a?(Integer) && val.positive? }
21
+ required(:details) do |val|
22
+ val.is_a?(Hash) &&
23
+ val.keys.all? { |key| key.is_a?(Symbol) }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class Structurable < Base
7
+ # Extension for managing Kafka topic configuration
8
+ module Topic
9
+ # @param active [Boolean] is the topic structure management feature active
10
+ # @param partitions [Integer]
11
+ # @param replication_factor [Integer]
12
+ # @param details [Hash] extra configuration for the topic
13
+ # @return [Config] defined structure
14
+ def config(active: true, partitions: 1, replication_factor: 1, **details)
15
+ @structurable ||= Config.new(
16
+ active: active,
17
+ partitions: partitions,
18
+ replication_factor: replication_factor,
19
+ details: details
20
+ )
21
+ end
22
+
23
+ # @return [Config] config details
24
+ def structurable
25
+ config
26
+ end
27
+
28
+ # @return [true] structurable is always active
29
+ def structurable?
30
+ structurable.active?
31
+ end
32
+
33
+ # @return [Hash] topic with all its native configuration options plus structurable
34
+ # settings
35
+ def to_h
36
+ super.merge(
37
+ structurable: structurable.to_h
38
+ ).freeze
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ # This feature allows to store per topic structure that can be later on used to bootstrap
7
+ # topics structure for test/development, etc. This allows to share the same set of settings
8
+ # for topics despite the environment. Pretty much similar to how the `structure.sql` or
9
+ # `schema.rb` operate for SQL dbs.
10
+ class Structurable < Base
11
+ end
12
+ end
13
+ end
14
+ end
@@ -53,6 +53,11 @@ class KarafkaApp < Karafka::App
53
53
  # active_job_topic :default
54
54
  <% end -%>
55
55
  topic :example do
56
+ # Uncomment this if you want Karafka to manage your topics configuration
57
+ # Managing topics configuration via routing will allow you to ensure config consistency
58
+ # across multiple environments
59
+ #
60
+ # config(partitions: 2, 'cleanup.policy': 'compact')
56
61
  consumer ExampleConsumer
57
62
  end
58
63
  end
@@ -3,5 +3,5 @@
3
3
  # Main module namespace
4
4
  module Karafka
5
5
  # Current Karafka version
6
- VERSION = '2.0.33'
6
+ VERSION = '2.0.35'
7
7
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: karafka
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.33
4
+ version: 2.0.35
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -35,7 +35,7 @@ cert_chain:
35
35
  Qf04B9ceLUaC4fPVEz10FyobjaFoY4i32xRto3XnrzeAgfEe4swLq8bQsR3w/EF3
36
36
  MGU0FeSV2Yj7Xc2x/7BzLK8xQn5l7Yy75iPF+KP3vVmDHnNl
37
37
  -----END CERTIFICATE-----
38
- date: 2023-02-24 00:00:00.000000000 Z
38
+ date: 2023-03-13 00:00:00.000000000 Z
39
39
  dependencies:
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: karafka-core
@@ -168,6 +168,7 @@ files:
168
168
  - lib/karafka/cli/info.rb
169
169
  - lib/karafka/cli/install.rb
170
170
  - lib/karafka/cli/server.rb
171
+ - lib/karafka/cli/topics.rb
171
172
  - lib/karafka/connection/client.rb
172
173
  - lib/karafka/connection/consumer_group_coordinator.rb
173
174
  - lib/karafka/connection/listener.rb
@@ -198,6 +199,7 @@ files:
198
199
  - lib/karafka/instrumentation/vendors/datadog/dashboard.json
199
200
  - lib/karafka/instrumentation/vendors/datadog/listener.rb
200
201
  - lib/karafka/instrumentation/vendors/datadog/logger_listener.rb
202
+ - lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb
201
203
  - lib/karafka/licenser.rb
202
204
  - lib/karafka/messages/batch_metadata.rb
203
205
  - lib/karafka/messages/builders/batch_metadata.rb
@@ -308,6 +310,10 @@ files:
308
310
  - lib/karafka/routing/features/manual_offset_management/config.rb
309
311
  - lib/karafka/routing/features/manual_offset_management/contract.rb
310
312
  - lib/karafka/routing/features/manual_offset_management/topic.rb
313
+ - lib/karafka/routing/features/structurable.rb
314
+ - lib/karafka/routing/features/structurable/config.rb
315
+ - lib/karafka/routing/features/structurable/contract.rb
316
+ - lib/karafka/routing/features/structurable/topic.rb
311
317
  - lib/karafka/routing/proxy.rb
312
318
  - lib/karafka/routing/router.rb
313
319
  - lib/karafka/routing/subscription_group.rb
metadata.gz.sig CHANGED
Binary file