karafka 2.0.33 → 2.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +13 -1
- data/Gemfile.lock +7 -6
- data/config/locales/errors.yml +4 -0
- data/lib/karafka/cli/topics.rb +143 -0
- data/lib/karafka/embedded.rb +4 -1
- data/lib/karafka/helpers/colorize.rb +6 -0
- data/lib/karafka/instrumentation/vendors/datadog/listener.rb +4 -247
- data/lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb +259 -0
- data/lib/karafka/routing/features/structurable/config.rb +18 -0
- data/lib/karafka/routing/features/structurable/contract.rb +30 -0
- data/lib/karafka/routing/features/structurable/topic.rb +44 -0
- data/lib/karafka/routing/features/structurable.rb +14 -0
- data/lib/karafka/templates/karafka.rb.erb +5 -0
- data/lib/karafka/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +8 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e2d2197fb0fe7eae8db43cc45ffbdcdc6da83d60731ae63b82dc4a24d852736e
|
4
|
+
data.tar.gz: 76b4b3f8ffb915b96a16f1eb83503283e5da46039fb952af383322773c4be551
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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.
|
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.
|
65
|
-
karafka-core (>= 2.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.
|
82
|
+
2.4.7
|
data/config/locales/errors.yml
CHANGED
@@ -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
|
data/lib/karafka/embedded.rb
CHANGED
@@ -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
|
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
|
-
#
|
10
|
-
|
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
|
data/lib/karafka/version.rb
CHANGED
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.
|
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-
|
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
|