waterdrop 2.3.2 → 2.4.1

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: 154b5f8a103fcfaa47c5d410583d1aa2c0b689b52cd284614b06e7f7a9460179
4
- data.tar.gz: c01ae313dbcf2830267438d3dd19f269a0a7bf29cad824af95f4313f6f04368e
3
+ metadata.gz: 70b84fa09ce807d6d72679917d6c9b98a4dd15b9eb50f0ae9c556acc6f084632
4
+ data.tar.gz: 8ccffe9e5331865dc93ab4c8b4b8ace2e0c3297f164c378b7919a14e307edf70
5
5
  SHA512:
6
- metadata.gz: 33e5ab153d45e265dd6cdb2b3ab9bb63bda7c8c4399097f1c5c130d4c493de635ee7fad957c83b97d6e5fdd6c4c049a0419ca5e0e063f8163478182cde6b92ed
7
- data.tar.gz: 3858e9ac8d2f860f888a5445ec8c59d98d35dce199816058da850bfaaf80308ccd40cff2eb5e872eb3ba111259c4c4d07d0a0219e0a641da0ad0c456d184030a
6
+ metadata.gz: 66f25845bada1a7de0f6bd100b7966f246604701f0394891bc959817f222b739a73fc674b49d288ebf3674c85e9872db37069771f347fad98eeade561373610c
7
+ data.tar.gz: 308eafaec616bf936517086de4bcee0efa03ed555ad91a061588b781d9214198fa67258a0d7044eae523fbba4da57fa447ddafdcf3169a234cdc4b0785151c6e
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # WaterDrop changelog
2
2
 
3
+ ## 2.4.1 (2022-08-01)
4
+ - Replace local statistics decorator with the one extracted to `karafka-core`.
5
+
6
+ ## 2.4.0 (2022-07-28)
7
+ - Small refactor of the DataDog/Statsd listener to align for future extraction to `karafka-common`.
8
+ - Replace `dry-monitor` with home-brew notification layer (API compatible) and allow for usage with `ActiveSupport::Notifications`.
9
+ - Remove all the common code into `karafka-core` and add it as a dependency.
10
+
11
+ ## 2.3.3 (2022-07-18)
12
+ - Replace `dry-validation` with home-brew validation layer and drop direct dependency on `dry-validation`.
13
+ - Remove indirect dependency on dry-configurable from DataDog listener (no changes required).
14
+
3
15
  ## 2.3.2 (2022-07-17)
4
16
  - Replace `dry-configurable` with home-brew config and drop direct dependency on `dry-configurable`.
5
17
 
data/Gemfile.lock CHANGED
@@ -1,17 +1,15 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- waterdrop (2.3.2)
5
- concurrent-ruby (>= 1.1)
6
- dry-monitor (~> 0.5)
7
- dry-validation (~> 1.7)
4
+ waterdrop (2.4.1)
5
+ karafka-core (>= 2.0.2, < 3.0.0)
8
6
  rdkafka (>= 0.10)
9
7
  zeitwerk (~> 2.3)
10
8
 
11
9
  GEM
12
10
  remote: https://rubygems.org/
13
11
  specs:
14
- activesupport (7.0.3)
12
+ activesupport (7.0.3.1)
15
13
  concurrent-ruby (~> 1.0, >= 1.0.2)
16
14
  i18n (>= 1.6, < 2)
17
15
  minitest (>= 5.1)
@@ -20,52 +18,15 @@ GEM
20
18
  concurrent-ruby (1.1.10)
21
19
  diff-lcs (1.5.0)
22
20
  docile (1.4.0)
23
- dry-configurable (0.15.0)
24
- concurrent-ruby (~> 1.0)
25
- dry-core (~> 0.6)
26
- dry-container (0.10.0)
27
- concurrent-ruby (~> 1.0)
28
- dry-core (0.8.0)
29
- concurrent-ruby (~> 1.0)
30
- dry-events (0.3.0)
31
- concurrent-ruby (~> 1.0)
32
- dry-core (~> 0.5, >= 0.5)
33
- dry-inflector (0.3.0)
34
- dry-initializer (3.1.1)
35
- dry-logic (1.2.0)
36
- concurrent-ruby (~> 1.0)
37
- dry-core (~> 0.5, >= 0.5)
38
- dry-monitor (0.6.1)
39
- dry-configurable (~> 0.13, >= 0.13.0)
40
- dry-core (~> 0.5, >= 0.5)
41
- dry-events (~> 0.2)
42
- zeitwerk (~> 2.5)
43
- dry-schema (1.9.3)
44
- concurrent-ruby (~> 1.0)
45
- dry-configurable (~> 0.13, >= 0.13.0)
46
- dry-core (~> 0.5, >= 0.5)
47
- dry-initializer (~> 3.0)
48
- dry-logic (~> 1.0)
49
- dry-types (~> 1.5)
50
- dry-types (1.5.1)
51
- concurrent-ruby (~> 1.0)
52
- dry-container (~> 0.3)
53
- dry-core (~> 0.5, >= 0.5)
54
- dry-inflector (~> 0.1, >= 0.1.2)
55
- dry-logic (~> 1.0, >= 1.0.2)
56
- dry-validation (1.8.1)
57
- concurrent-ruby (~> 1.0)
58
- dry-container (~> 0.7, >= 0.7.1)
59
- dry-core (~> 0.5, >= 0.5)
60
- dry-initializer (~> 3.0)
61
- dry-schema (~> 1.8, >= 1.8.0)
62
21
  factory_bot (6.2.1)
63
22
  activesupport (>= 5.0.0)
64
23
  ffi (1.15.5)
65
- i18n (1.10.0)
24
+ i18n (1.12.0)
66
25
  concurrent-ruby (~> 1.0)
26
+ karafka-core (2.0.2)
27
+ concurrent-ruby (>= 1.1)
67
28
  mini_portile2 (2.8.0)
68
- minitest (5.16.0)
29
+ minitest (5.16.2)
69
30
  rake (13.0.6)
70
31
  rdkafka (0.12.0)
71
32
  ffi (~> 1.15)
@@ -90,12 +51,11 @@ GEM
90
51
  simplecov_json_formatter (~> 0.1)
91
52
  simplecov-html (0.12.3)
92
53
  simplecov_json_formatter (0.1.4)
93
- tzinfo (2.0.4)
54
+ tzinfo (2.0.5)
94
55
  concurrent-ruby (~> 1.0)
95
56
  zeitwerk (2.6.0)
96
57
 
97
58
  PLATFORMS
98
- arm64-darwin-21
99
59
  x86_64-linux
100
60
 
101
61
  DEPENDENCIES
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Note**: Documentation presented here refers to WaterDrop `2.x`.
4
4
 
5
- WaterDrop `2.x` does **not** work with Karafka `1.*` and aims to either work as a standalone producer outside of Karafka `1.*` ecosystem or as a part of soon to be released Karafka `2.0.*`.
5
+ WaterDrop `2.x` works with Karafka `2.*` and aims to either work as a standalone producer or as a part of the Karafka `2.*`.
6
6
 
7
7
  Please refer to [this](https://github.com/karafka/waterdrop/tree/1.4) branch and its documentation for details about WaterDrop `1.*` usage.
8
8
 
@@ -10,17 +10,17 @@ Please refer to [this](https://github.com/karafka/waterdrop/tree/1.4) branch and
10
10
  [![Gem Version](https://badge.fury.io/rb/waterdrop.svg)](http://badge.fury.io/rb/waterdrop)
11
11
  [![Join the chat at https://slack.karafka.io](https://raw.githubusercontent.com/karafka/misc/master/slack.svg)](https://slack.karafka.io)
12
12
 
13
- Gem used to send messages to Kafka in an easy way with an extra validation layer. It is a part of the [Karafka](https://github.com/karafka/karafka) ecosystem.
13
+ A gem to send messages to Kafka easily with an extra validation layer. It is a part of the [Karafka](https://github.com/karafka/karafka) ecosystem.
14
14
 
15
15
  It:
16
16
 
17
- - Is thread safe
17
+ - Is thread-safe
18
18
  - Supports sync producing
19
19
  - Supports async producing
20
20
  - Supports buffering
21
21
  - Supports producing messages to multiple clusters
22
22
  - Supports multiple delivery policies
23
- - Works with Kafka 1.0+ and Ruby 2.6+
23
+ - Works with Kafka `1.0+` and Ruby `2.7+`
24
24
 
25
25
  ## Table of contents
26
26
 
@@ -30,6 +30,8 @@ It:
30
30
  * [Kafka configuration options](#kafka-configuration-options)
31
31
  - [Usage](#usage)
32
32
  * [Basic usage](#basic-usage)
33
+ * [Using WaterDrop across the application and with Ruby on Rails](#using-waterdrop-across-the-application-and-with-ruby-on-rails)
34
+ * [Using WaterDrop with a connection-pool](#using-waterdrop-with-a-connection-pool)
33
35
  * [Buffering](#buffering)
34
36
  + [Using WaterDrop to buffer messages based on the application logic](#using-waterdrop-to-buffer-messages-based-on-the-application-logic)
35
37
  + [Using WaterDrop with rdkafka buffers to achieve periodic auto-flushing](#using-waterdrop-with-rdkafka-buffers-to-achieve-periodic-auto-flushing)
@@ -42,6 +44,8 @@ It:
42
44
 
43
45
  ## Installation
44
46
 
47
+ **Note**: If you want to both produce and consume messages, please use [Karafka](https://github.com/karafka/karafka/). It integrates WaterDrop automatically.
48
+
45
49
  Add this to your Gemfile:
46
50
 
47
51
  ```ruby
@@ -56,7 +60,7 @@ bundle install
56
60
 
57
61
  ## Setup
58
62
 
59
- WaterDrop is a complex tool, that contains multiple configuration options. To keep everything organized, all the configuration options were divided into two groups:
63
+ WaterDrop is a complex tool that contains multiple configuration options. To keep everything organized, all the configuration options were divided into two groups:
60
64
 
61
65
  - WaterDrop options - options directly related to WaterDrop and its components
62
66
  - Kafka driver options - options related to `rdkafka`
@@ -103,8 +107,6 @@ You can create producers with different `kafka` settings. Documentation of the a
103
107
 
104
108
  ## Usage
105
109
 
106
- Please refer to the [documentation](https://www.rubydoc.info/gems/waterdrop) in case you're interested in the more advanced API.
107
-
108
110
  ### Basic usage
109
111
 
110
112
  To send Kafka messages, just create a producer and use it:
@@ -157,6 +159,41 @@ Here are all the things you can provide in the message hash:
157
159
 
158
160
  Keep in mind, that message you want to send should be either binary or stringified (to_s, to_json, etc).
159
161
 
162
+ ### Using WaterDrop across the application and with Ruby on Rails
163
+
164
+ If you plan to both produce and consume messages using Kafka, you should install and use [Karafka](https://github.com/karafka/karafka). It integrates automatically with Ruby on Rails applications and auto-configures WaterDrop producer to make it accessible via `Karafka#producer` method.
165
+
166
+ If you want to only produce messages from within your application, since WaterDrop is thread-safe you can create a single instance in an initializer like so:
167
+
168
+ ```ruby
169
+ KAFKA_PRODUCER = WaterDrop::Producer.new
170
+
171
+ KAFKA_PRODUCER.setup do |config|
172
+ config.kafka = { 'bootstrap.servers': 'localhost:9092' }
173
+ end
174
+
175
+ # And just dispatch messages
176
+ KAFKA_PRODUCER.produce_sync(topic: 'my-topic', payload: 'my message')
177
+ ```
178
+
179
+ ### Using WaterDrop with a connection-pool
180
+
181
+ While WaterDrop is thread-safe, there is no problem in using it with a connection pool inside high-intensity applications. The only thing worth keeping in mind, is that WaterDrop instances should be shutdown before the application is closed.
182
+
183
+ ```ruby
184
+ KAFKA_PRODUCERS_CP = ConnectionPool.new do
185
+ WaterDrop::Producer.new do |config|
186
+ config.kafka = { 'bootstrap.servers': 'localhost:9092' }
187
+ end
188
+ end
189
+
190
+ KAFKA_PRODUCERS_CP.with do |producer|
191
+ producer.produce_async(topic: 'my-topic', payload: 'my message')
192
+ end
193
+
194
+ KAFKA_PRODUCERS_CP.shutdown { |producer| producer.close }
195
+ ```
196
+
160
197
  ### Buffering
161
198
 
162
199
  WaterDrop producers support buffering messages in their internal buffers and on the `rdkafka` level via `queue.buffering.*` set of settings.
@@ -316,6 +353,8 @@ producer.monitor.subscribe(listener)
316
353
 
317
354
  You can also find [here](https://github.com/karafka/waterdrop/blob/master/lib/waterdrop/instrumentation/vendors/datadog/dashboard.json) a ready to import DataDog dashboard configuration file that you can use to monitor all of your producers.
318
355
 
356
+ ![Example WaterDrop DD dashboard](https://raw.githubusercontent.com/karafka/misc/master/printscreens/waterdrop_dd_dashboard_example.png)
357
+
319
358
  ### Error notifications
320
359
 
321
360
  WaterDrop allows you to listen to all errors that occur while producing messages and in its internal background threads. Things like reconnecting to Kafka upon network errors and others unrelated to publishing messages are all available under `error.occurred` notification key. You can subscribe to this event to ensure your setup is healthy and without any problems that would otherwise go unnoticed as long as messages are delivered.
data/config/errors.yml CHANGED
@@ -1,7 +1,30 @@
1
1
  en:
2
- dry_validation:
3
- errors:
4
- invalid_key_type: all keys need to be of type String
5
- invalid_value_type: all values need to be of type String
6
- max_payload_size: is more than `max_payload_size` config value
2
+ validations:
3
+ config:
4
+ missing: must be present
5
+ logger_format: must be present
6
+ deliver_format: must be boolean
7
+ id_format: must be a non-empty string
8
+ max_payload_size_format: must be an integer that is equal or bigger than 1
9
+ wait_timeout_format: must be a numeric that is bigger than 0
10
+ max_wait_timeout_format: must be an integer that is equal or bigger than 0
11
+ kafka_format: must be a hash with symbol based keys
7
12
  kafka_key_must_be_a_symbol: All keys under the kafka settings scope need to be symbols
13
+
14
+ message:
15
+ missing: must be present
16
+ partition_format: must be an integer greater or equal to -1
17
+ topic_format: 'does not match the topic allowed format'
18
+ partition_key_format: must be a non-empty string
19
+ timestamp_format: must be either time or integer
20
+ payload_format: must be string
21
+ headers_format: must be a hash
22
+ key_format: must be a non-empty string
23
+ payload_max_size: is more than `max_payload_size` config value
24
+ headers_invalid_key_type: all headers keys need to be of type String
25
+ headers_invalid_value_type: all headers values need to be of type String
26
+
27
+ test:
28
+ missing: must be present
29
+ nested.id_format: 'is invalid'
30
+ nested.id2_format: 'is invalid'
@@ -5,7 +5,7 @@
5
5
  module WaterDrop
6
6
  # Configuration object for setting up all options required by WaterDrop
7
7
  class Config
8
- include Configurable
8
+ include ::Karafka::Core::Configurable
9
9
 
10
10
  # Defaults for kafka settings, that will be overwritten only if not present already
11
11
  KAFKA_DEFAULTS = {
@@ -77,7 +77,7 @@ module WaterDrop
77
77
  # Propagates the kafka setting defaults unless they are already present
78
78
  # This makes it easier to set some values that users usually don't change but still allows them
79
79
  # to overwrite the whole hash if they want to
80
- # @param config [Dry::Configurable::Config] dry config of this producer
80
+ # @param config [WaterDrop::Configurable::Node] config of this producer
81
81
  def merge_kafka_defaults!(config)
82
82
  KAFKA_DEFAULTS.each do |key, value|
83
83
  next if config.kafka.key?(key)
@@ -3,27 +3,37 @@
3
3
  module WaterDrop
4
4
  module Contracts
5
5
  # Contract with validation rules for WaterDrop configuration details
6
- class Config < Base
7
- params do
8
- required(:id).filled(:str?)
9
- required(:logger).filled
10
- required(:deliver).filled(:bool?)
11
- required(:max_payload_size).filled(:int?, gteq?: 1)
12
- required(:max_wait_timeout).filled(:number?, gteq?: 0)
13
- required(:wait_timeout).filled(:number?, gt?: 0)
14
- required(:kafka).filled(:hash?)
6
+ class Config < ::Karafka::Core::Contractable::Contract
7
+ configure do |config|
8
+ config.error_messages = YAML.safe_load(
9
+ File.read(
10
+ File.join(WaterDrop.gem_root, 'config', 'errors.yml')
11
+ )
12
+ ).fetch('en').fetch('validations').fetch('config')
15
13
  end
16
14
 
15
+ required(:id) { |val| val.is_a?(String) && !val.empty? }
16
+ required(:logger) { |val| !val.nil? }
17
+ required(:deliver) { |val| [true, false].include?(val) }
18
+ required(:max_payload_size) { |val| val.is_a?(Integer) && val >= 1 }
19
+ required(:max_wait_timeout) { |val| val.is_a?(Numeric) && val >= 0 }
20
+ required(:wait_timeout) { |val| val.is_a?(Numeric) && val.positive? }
21
+ required(:kafka) { |val| val.is_a?(Hash) && !val.empty? }
22
+
17
23
  # rdkafka allows both symbols and strings as keys for config but then casts them to strings
18
24
  # This can be confusing, so we expect all keys to be symbolized
19
- rule(:kafka) do
20
- next unless value.is_a?(Hash)
25
+ virtual do |config, errors|
26
+ next true unless errors.empty?
27
+
28
+ errors = []
21
29
 
22
- value.each_key do |key|
23
- next if key.is_a?(Symbol)
30
+ config
31
+ .fetch(:kafka)
32
+ .keys
33
+ .reject { |key| key.is_a?(Symbol) }
34
+ .each { |key| errors << [[:kafka, key], :kafka_key_must_be_a_symbol] }
24
35
 
25
- key(:"kafka.#{key}").failure(:kafka_key_must_be_a_symbol)
26
- end
36
+ errors
27
37
  end
28
38
  end
29
39
  end
@@ -4,36 +4,56 @@ module WaterDrop
4
4
  module Contracts
5
5
  # Contract with validation rules for validating that all the message options that
6
6
  # we provide to producer ale valid and usable
7
- class Message < Base
7
+ class Message < ::Karafka::Core::Contractable::Contract
8
+ configure do |config|
9
+ config.error_messages = YAML.safe_load(
10
+ File.read(
11
+ File.join(WaterDrop.gem_root, 'config', 'errors.yml')
12
+ )
13
+ ).fetch('en').fetch('validations').fetch('message')
14
+ end
15
+
8
16
  # Regex to check that topic has a valid format
9
17
  TOPIC_REGEXP = /\A(\w|-|\.)+\z/
10
18
 
11
- # Checks, that the given value is a string
12
- STRING_ASSERTION = ->(value) { value.is_a?(String) }.to_proc
19
+ private_constant :TOPIC_REGEXP
13
20
 
14
- private_constant :TOPIC_REGEXP, :STRING_ASSERTION
21
+ attr_reader :max_payload_size
15
22
 
16
- option :max_payload_size
17
-
18
- params do
19
- required(:topic).filled(:str?, format?: TOPIC_REGEXP)
20
- required(:payload).filled(:str?)
21
- optional(:key).maybe(:str?, :filled?)
22
- optional(:partition).filled(:int?, gteq?: -1)
23
- optional(:partition_key).maybe(:str?, :filled?)
24
- optional(:timestamp).maybe { time? | int? }
25
- optional(:headers).maybe(:hash?)
23
+ # @param max_payload_size [Integer] max payload size
24
+ def initialize(max_payload_size:)
25
+ super()
26
+ @max_payload_size = max_payload_size
26
27
  end
27
28
 
28
- rule(:headers) do
29
- next unless value.is_a?(Hash)
29
+ required(:topic) { |val| val.is_a?(String) && TOPIC_REGEXP.match?(val) }
30
+ required(:payload) { |val| val.is_a?(String) }
31
+ optional(:key) { |val| val.nil? || (val.is_a?(String) && !val.empty?) }
32
+ optional(:partition) { |val| val.is_a?(Integer) && val >= -1 }
33
+ optional(:partition_key) { |val| val.nil? || (val.is_a?(String) && !val.empty?) }
34
+ optional(:timestamp) { |val| val.nil? || (val.is_a?(Time) || val.is_a?(Integer)) }
35
+ optional(:headers) { |val| val.nil? || val.is_a?(Hash) }
36
+
37
+ virtual do |config, errors|
38
+ next true unless errors.empty?
39
+ next true unless config.key?(:headers)
40
+ next true if config[:headers].nil?
30
41
 
31
- key.failure(:invalid_key_type) unless value.keys.all?(&STRING_ASSERTION)
32
- key.failure(:invalid_value_type) unless value.values.all?(&STRING_ASSERTION)
42
+ errors = []
43
+
44
+ config.fetch(:headers).each do |key, value|
45
+ errors << [%i[headers], :invalid_key_type] unless key.is_a?(String)
46
+ errors << [%i[headers], :invalid_value_type] unless value.is_a?(String)
47
+ end
48
+
49
+ errors
33
50
  end
34
51
 
35
- rule(:payload) do
36
- key.failure(:max_payload_size) if value.bytesize > max_payload_size
52
+ virtual do |config, errors, validator|
53
+ next true unless errors.empty?
54
+ next true if config[:payload].bytesize <= validator.max_payload_size
55
+
56
+ [[%i[payload], :max_size]]
37
57
  end
38
58
  end
39
59
  end
@@ -17,7 +17,7 @@ module WaterDrop
17
17
  @producer_id = producer_id
18
18
  @client_name = client_name
19
19
  @monitor = monitor
20
- @statistics_decorator = StatisticsDecorator.new
20
+ @statistics_decorator = ::Karafka::Core::Monitoring::StatisticsDecorator.new
21
21
  end
22
22
 
23
23
  # Emits decorated statistics to the monitor
@@ -2,41 +2,18 @@
2
2
 
3
3
  module WaterDrop
4
4
  module Instrumentation
5
- # Monitor is used to hookup external monitoring services to monitor how WaterDrop works
6
- # Since it is a pub-sub based on dry-monitor, you can use as many subscribers/loggers at the
7
- # same time, which means that you might have for example file logging and NewRelic at the same
8
- # time
9
- # @note This class acts as a singleton because we are only permitted to have single monitor
10
- # per running process (just as logger)
11
- class Monitor < Dry::Monitor::Notifications
12
- # List of events that we support in the system and to which a monitor client can hook up
13
- # @note The non-error once support timestamp benchmarking
14
- EVENTS = %w[
15
- producer.closed
16
-
17
- message.produced_async
18
- message.produced_sync
19
- message.acknowledged
20
- message.buffered
21
-
22
- messages.produced_async
23
- messages.produced_sync
24
- messages.buffered
25
-
26
- buffer.flushed_async
27
- buffer.flushed_sync
28
-
29
- statistics.emitted
30
-
31
- error.occurred
32
- ].freeze
33
-
34
- private_constant :EVENTS
35
-
36
- # @return [WaterDrop::Instrumentation::Monitor] monitor instance for system instrumentation
37
- def initialize
38
- super(:waterdrop)
39
- EVENTS.each(&method(:register_event))
5
+ # WaterDrop instrumentation monitor that we use to publish events
6
+ # By default uses our internal notifications bus but can be used with
7
+ # `ActiveSupport::Notifications` as well
8
+ class Monitor < ::Karafka::Core::Monitoring::Monitor
9
+ # @param notifications_bus [Object] either our internal notifications bus or
10
+ # `ActiveSupport::Notifications`
11
+ # @param namespace [String, nil] namespace for events or nil if no namespace
12
+ def initialize(
13
+ notifications_bus = WaterDrop::Instrumentation::Notifications.new,
14
+ namespace = nil
15
+ )
16
+ super(notifications_bus, namespace)
40
17
  end
41
18
  end
42
19
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WaterDrop
4
+ module Instrumentation
5
+ # Instrumented is used to hookup external monitoring services to monitor how WaterDrop works
6
+ class Notifications < ::Karafka::Core::Monitoring::Notifications
7
+ # List of events that we support in the system and to which a monitor client can hook up
8
+ # @note The non-error once support timestamp benchmarking
9
+ EVENTS = %w[
10
+ producer.closed
11
+
12
+ message.produced_async
13
+ message.produced_sync
14
+ message.acknowledged
15
+ message.buffered
16
+
17
+ messages.produced_async
18
+ messages.produced_sync
19
+ messages.buffered
20
+
21
+ buffer.flushed_async
22
+ buffer.flushed_sync
23
+
24
+ statistics.emitted
25
+
26
+ error.occurred
27
+ ].freeze
28
+
29
+ private_constant :EVENTS
30
+
31
+ # @return [WaterDrop::Instrumentation::Monitor] monitor instance for system instrumentation
32
+ def initialize
33
+ super
34
+ EVENTS.each { |event| register_event(event) }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -11,26 +11,29 @@ module WaterDrop
11
11
  #
12
12
  # @note You need to setup the `dogstatsd-ruby` client and assign it
13
13
  class Listener
14
- include Dry::Configurable
14
+ include ::Karafka::Core::Configurable
15
+ extend Forwardable
16
+
17
+ def_delegators :config, :client, :rd_kafka_metrics, :namespace, :default_tags
15
18
 
16
19
  # Value object for storing a single rdkafka metric publishing details
17
20
  RdKafkaMetric = Struct.new(:type, :scope, :name, :key_location)
18
21
 
19
22
  # Namespace under which the DD metrics should be published
20
- setting :namespace, default: 'waterdrop', reader: true
23
+ setting :namespace, default: 'waterdrop'
21
24
 
22
25
  # Datadog client that we should use to publish the metrics
23
- setting :client, reader: true
26
+ setting :client
24
27
 
25
28
  # Default tags we want to publish (for example hostname)
26
29
  # Format as followed (example for hostname): `["host:#{Socket.gethostname}"]`
27
- setting :default_tags, default: [], reader: true
30
+ setting :default_tags, default: []
28
31
 
29
32
  # All the rdkafka metrics we want to publish
30
33
  #
31
34
  # By default we publish quite a lot so this can be tuned
32
35
  # Note, that the once with `_d` come from WaterDrop, not rdkafka or Kafka
33
- setting :rd_kafka_metrics, reader: true, default: [
36
+ setting :rd_kafka_metrics, default: [
34
37
  # Client metrics
35
38
  RdKafkaMetric.new(:count, :root, 'calls', 'tx_d'),
36
39
  RdKafkaMetric.new(:histogram, :root, 'queue.size', 'msg_cnt_d'),
@@ -47,8 +50,11 @@ module WaterDrop
47
50
  RdKafkaMetric.new(:gauge, :brokers, 'network.latency.p99', %w[rtt p99])
48
51
  ].freeze
49
52
 
53
+ configure
54
+
50
55
  # @param block [Proc] configuration block
51
56
  def initialize(&block)
57
+ configure
52
58
  setup(&block) if block
53
59
  end
54
60
 
@@ -60,7 +66,7 @@ module WaterDrop
60
66
 
61
67
  # Hooks up to WaterDrop instrumentation for emitted statistics
62
68
  #
63
- # @param event [Dry::Events::Event]
69
+ # @param event [WaterDrop::Monitor::Event]
64
70
  def on_statistics_emitted(event)
65
71
  statistics = event[:statistics]
66
72
 
@@ -71,22 +77,15 @@ module WaterDrop
71
77
 
72
78
  # Increases the errors count by 1
73
79
  #
74
- # @param _event [Dry::Events::Event]
80
+ # @param _event [WaterDrop::Monitor::Event]
75
81
  def on_error_occurred(_event)
76
- client.count(
77
- namespaced_metric('error_occurred'),
78
- 1,
79
- tags: default_tags
80
- )
82
+ count('error_occurred', 1, tags: default_tags)
81
83
  end
82
84
 
83
85
  # Increases acknowledged messages counter
84
- # @param _event [Dry::Events::Event]
86
+ # @param _event [WaterDrop::Monitor::Event]
85
87
  def on_message_acknowledged(_event)
86
- client.increment(
87
- namespaced_metric('acknowledged'),
88
- tags: default_tags
89
- )
88
+ increment('acknowledged', tags: default_tags)
90
89
  end
91
90
 
92
91
  %i[
@@ -94,12 +93,12 @@ module WaterDrop
94
93
  produced_async
95
94
  ].each do |event_scope|
96
95
  class_eval <<~METHODS, __FILE__, __LINE__ + 1
97
- # @param event [Dry::Events::Event]
96
+ # @param event [WaterDrop::Monitor::Event]
98
97
  def on_message_#{event_scope}(event)
99
98
  report_message(event[:message][:topic], :#{event_scope})
100
99
  end
101
100
 
102
- # @param event [Dry::Events::Event]
101
+ # @param event [WaterDrop::Monitor::Event]
103
102
  def on_messages_#{event_scope}(event)
104
103
  event[:messages].each do |message|
105
104
  report_message(message[:topic], :#{event_scope})
@@ -114,10 +113,10 @@ module WaterDrop
114
113
  messages_buffered
115
114
  ].each do |event_scope|
116
115
  class_eval <<~METHODS, __FILE__, __LINE__ + 1
117
- # @param event [Dry::Events::Event]
116
+ # @param event [WaterDrop::Monitor::Event]
118
117
  def on_#{event_scope}(event)
119
- client.histogram(
120
- namespaced_metric('buffer.size'),
118
+ histogram(
119
+ 'buffer.size',
121
120
  event[:buffer].size,
122
121
  tags: default_tags
123
122
  )
@@ -132,7 +131,7 @@ module WaterDrop
132
131
  flushed_async
133
132
  ].each do |event_scope|
134
133
  class_eval <<~METHODS, __FILE__, __LINE__ + 1
135
- # @param event [Dry::Events::Event]
134
+ # @param event [WaterDrop::Monitor::Event]
136
135
  def on_buffer_#{event_scope}(event)
137
136
  event[:messages].each do |message|
138
137
  report_message(message[:topic], :#{event_scope})
@@ -143,14 +142,28 @@ module WaterDrop
143
142
 
144
143
  private
145
144
 
145
+ %i[
146
+ count
147
+ gauge
148
+ histogram
149
+ increment
150
+ decrement
151
+ ].each do |metric_type|
152
+ class_eval <<~METHODS, __FILE__, __LINE__ + 1
153
+ def #{metric_type}(key, *args)
154
+ client.#{metric_type}(
155
+ namespaced_metric(key),
156
+ *args
157
+ )
158
+ end
159
+ METHODS
160
+ end
161
+
146
162
  # Report that a message has been produced to a topic.
147
163
  # @param topic [String] Kafka topic
148
164
  # @param method_name [Symbol] method from which this message operation comes
149
165
  def report_message(topic, method_name)
150
- client.increment(
151
- namespaced_metric(method_name),
152
- tags: default_tags + ["topic:#{topic}"]
153
- )
166
+ increment(method_name, tags: default_tags + ["topic:#{topic}"])
154
167
  end
155
168
 
156
169
  # Wraps metric name in listener's namespace
@@ -166,9 +179,9 @@ module WaterDrop
166
179
  def report_metric(metric, statistics)
167
180
  case metric.scope
168
181
  when :root
169
- client.public_send(
182
+ public_send(
170
183
  metric.type,
171
- namespaced_metric(metric.name),
184
+ metric.name,
172
185
  statistics.fetch(*metric.key_location),
173
186
  tags: default_tags
174
187
  )
@@ -179,9 +192,9 @@ module WaterDrop
179
192
  # node ids
180
193
  next if broker_statistics['nodeid'] == -1
181
194
 
182
- client.public_send(
195
+ public_send(
183
196
  metric.type,
184
- namespaced_metric(metric.name),
197
+ metric.name,
185
198
  broker_statistics.dig(*metric.key_location),
186
199
  tags: default_tags + ["broker:#{broker_statistics['nodename']}"]
187
200
  )
@@ -3,5 +3,5 @@
3
3
  # WaterDrop library
4
4
  module WaterDrop
5
5
  # Current WaterDrop version
6
- VERSION = '2.3.2'
6
+ VERSION = '2.4.1'
7
7
  end
data/lib/waterdrop.rb CHANGED
@@ -3,9 +3,8 @@
3
3
  # External components
4
4
  # delegate should be removed because we don't need it, we just add it because of ruby-kafka
5
5
  %w[
6
- concurrent/array
7
- dry/monitor/notifications
8
- dry-validation
6
+ karafka-core
7
+ forwardable
9
8
  rdkafka
10
9
  json
11
10
  zeitwerk
data/waterdrop.gemspec CHANGED
@@ -16,9 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.description = spec.summary
17
17
  spec.license = 'MIT'
18
18
 
19
- spec.add_dependency 'concurrent-ruby', '>= 1.1'
20
- spec.add_dependency 'dry-monitor', '~> 0.5'
21
- spec.add_dependency 'dry-validation', '~> 1.7'
19
+ spec.add_dependency 'karafka-core', '>= 2.0.2', '< 3.0.0'
22
20
  spec.add_dependency 'rdkafka', '>= 0.10'
23
21
  spec.add_dependency 'zeitwerk', '~> 2.3'
24
22
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: waterdrop
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.2
4
+ version: 2.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -34,50 +34,28 @@ cert_chain:
34
34
  R2P11bWoCtr70BsccVrN8jEhzwXngMyI2gVt750Y+dbTu1KgRqZKp/ECe7ZzPzXj
35
35
  pIy9vHxTANKYVyI4qj8OrFdEM5BQNu8oQpL0iQ==
36
36
  -----END CERTIFICATE-----
37
- date: 2022-07-17 00:00:00.000000000 Z
37
+ date: 2022-08-01 00:00:00.000000000 Z
38
38
  dependencies:
39
39
  - !ruby/object:Gem::Dependency
40
- name: concurrent-ruby
40
+ name: karafka-core
41
41
  requirement: !ruby/object:Gem::Requirement
42
42
  requirements:
43
43
  - - ">="
44
44
  - !ruby/object:Gem::Version
45
- version: '1.1'
46
- type: :runtime
47
- prerelease: false
48
- version_requirements: !ruby/object:Gem::Requirement
49
- requirements:
50
- - - ">="
45
+ version: 2.0.2
46
+ - - "<"
51
47
  - !ruby/object:Gem::Version
52
- version: '1.1'
53
- - !ruby/object:Gem::Dependency
54
- name: dry-monitor
55
- requirement: !ruby/object:Gem::Requirement
56
- requirements:
57
- - - "~>"
58
- - !ruby/object:Gem::Version
59
- version: '0.5'
48
+ version: 3.0.0
60
49
  type: :runtime
61
50
  prerelease: false
62
51
  version_requirements: !ruby/object:Gem::Requirement
63
52
  requirements:
64
- - - "~>"
65
- - !ruby/object:Gem::Version
66
- version: '0.5'
67
- - !ruby/object:Gem::Dependency
68
- name: dry-validation
69
- requirement: !ruby/object:Gem::Requirement
70
- requirements:
71
- - - "~>"
53
+ - - ">="
72
54
  - !ruby/object:Gem::Version
73
- version: '1.7'
74
- type: :runtime
75
- prerelease: false
76
- version_requirements: !ruby/object:Gem::Requirement
77
- requirements:
78
- - - "~>"
55
+ version: 2.0.2
56
+ - - "<"
79
57
  - !ruby/object:Gem::Version
80
- version: '1.7'
58
+ version: 3.0.0
81
59
  - !ruby/object:Gem::Dependency
82
60
  name: rdkafka
83
61
  requirement: !ruby/object:Gem::Requirement
@@ -130,11 +108,7 @@ files:
130
108
  - docker-compose.yml
131
109
  - lib/waterdrop.rb
132
110
  - lib/waterdrop/config.rb
133
- - lib/waterdrop/configurable.rb
134
- - lib/waterdrop/configurable/leaf.rb
135
- - lib/waterdrop/configurable/node.rb
136
111
  - lib/waterdrop/contracts.rb
137
- - lib/waterdrop/contracts/base.rb
138
112
  - lib/waterdrop/contracts/config.rb
139
113
  - lib/waterdrop/contracts/message.rb
140
114
  - lib/waterdrop/errors.rb
@@ -142,10 +116,10 @@ files:
142
116
  - lib/waterdrop/instrumentation/callbacks/delivery.rb
143
117
  - lib/waterdrop/instrumentation/callbacks/error.rb
144
118
  - lib/waterdrop/instrumentation/callbacks/statistics.rb
145
- - lib/waterdrop/instrumentation/callbacks/statistics_decorator.rb
146
119
  - lib/waterdrop/instrumentation/callbacks_manager.rb
147
120
  - lib/waterdrop/instrumentation/logger_listener.rb
148
121
  - lib/waterdrop/instrumentation/monitor.rb
122
+ - lib/waterdrop/instrumentation/notifications.rb
149
123
  - lib/waterdrop/instrumentation/vendors/datadog/dashboard.json
150
124
  - lib/waterdrop/instrumentation/vendors/datadog/listener.rb
151
125
  - lib/waterdrop/patches/rdkafka/bindings.rb
metadata.gz.sig CHANGED
@@ -1 +1,4 @@
1
- -���mE�ΕQQiPq�ǶKP k�]�N|�<�b^@�{�@�i�[��B��7މ_˘d�v;G�8%��Htic�D�U+
1
+ A��ˣ�v=�o�*>�.C{YsbA,��aB��?#Ϻ��8\����wC�Ƈ��]%�S��Om�ȏY�u�vRm�l�a/����L��ӥ�J�����&����Ŷ{��a�&&Be�>b����}�}H�iݞL��h�����IE��
2
+ %�Z#�i��ޭ��P��%���A��%D"��۩u0lJ)��Yͦ��
3
+ }"�"�9�� B�.��?��)�)yLY3{q��7�x��-�E��fE�xmg�c��b�7Ԗ&��kt�Dx�7f����AY�X�\R����#L�_�
4
+ �*8/I���ȖskMo��?�_���A��\�V��&*>���ؼ�ң����_=�ry*�:�D�CD��\�������[0���I �6
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module WaterDrop
4
- module Configurable
5
- # Single end config value representation
6
- Leaf = Struct.new(:name, :default, :constructor)
7
- end
8
- end
@@ -1,100 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module WaterDrop
4
- module Configurable
5
- # Single non-leaf node
6
- # This is a core component for the configurable settings
7
- #
8
- # The idea here is simple: we collect settings (leafs) and children (nodes) information and we
9
- # only compile/initialize the values prior to user running the `#configure` API. This API needs
10
- # to run prior to using the result stuff even if there is nothing to configure
11
- class Node
12
- attr_reader :name, :nestings
13
-
14
- # We need to be able to redefine children for deep copy
15
- attr_accessor :children
16
-
17
- # @param name [Symbol] node name
18
- # @param nestings [Proc] block for nested settings
19
- def initialize(name, nestings = ->(_) {})
20
- @name = name
21
- @children = []
22
- @nestings = nestings
23
- instance_eval(&nestings)
24
- end
25
-
26
- # Allows for a single leaf or nested node definition
27
- #
28
- # @param name [Symbol] setting or nested node name
29
- # @param default [Object] default value
30
- # @param constructor [#call, nil] callable or nil
31
- # @param block [Proc] block for nested settings
32
- def setting(name, default: nil, constructor: nil, &block)
33
- @children << if block
34
- Node.new(name, block)
35
- else
36
- Leaf.new(name, default, constructor)
37
- end
38
- end
39
-
40
- # Allows for the configuration and setup of the settings
41
- #
42
- # Compile settings, allow for overrides via yielding
43
- # @return [Node] returns self after configuration
44
- def configure
45
- compile
46
- yield(self) if block_given?
47
- self
48
- end
49
-
50
- # @return [Hash] frozen config hash representation
51
- def to_h
52
- config = {}
53
-
54
- @children.each do |value|
55
- config[value.name] = if value.is_a?(Leaf)
56
- public_send(value.name)
57
- else
58
- value.to_h
59
- end
60
- end
61
-
62
- config.freeze
63
- end
64
-
65
- # Deep copies all the children nodes to allow us for templates building on a class level and
66
- # non-side-effect usage on an instance/inherited.
67
- # @return [Node] duplicated node
68
- def deep_dup
69
- dupped = Node.new(name, nestings)
70
-
71
- dupped.children += children.map do |value|
72
- value.is_a?(Leaf) ? value.dup : value.deep_dup
73
- end
74
-
75
- dupped
76
- end
77
-
78
- # Converts the settings definitions into end children
79
- # @note It runs once, after things are compiled, they will not be recompiled again
80
- def compile
81
- @children.each do |value|
82
- # Do not redefine something that was already set during compilation
83
- # This will allow us to reconfigure things and skip override with defaults
84
- next if respond_to?(value.name)
85
-
86
- singleton_class.attr_accessor value.name
87
-
88
- initialized = if value.is_a?(Leaf)
89
- value.constructor ? value.constructor.call(value.default) : value.default
90
- else
91
- value.compile
92
- value
93
- end
94
-
95
- public_send("#{value.name}=", initialized)
96
- end
97
- end
98
- end
99
- end
100
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module WaterDrop
4
- # A simple dry-configuration API compatible module for defining settings with defaults and a
5
- # constructor.
6
- module Configurable
7
- # A simple settings layer that works similar to dry-configurable
8
- # It allows us to define settings on a class and per instance level with templating on a class
9
- # level. It handles inheritance and allows for nested settings.
10
- #
11
- # @note The core settings template needs to be defined on a class level
12
- class << self
13
- # Sets up all the class methods and inits the core root node.
14
- # Useful when only per class settings are needed as does not include instance methods
15
- # @param base [Class] class that we extend
16
- def extended(base)
17
- base.extend ClassMethods
18
- end
19
-
20
- # Sets up all the class and instance methods and inits the core root node
21
- #
22
- # @param base [Class] class to which we want to add configuration
23
- #
24
- # Needs to be used when per instance configuration is needed
25
- def included(base)
26
- base.include InstanceMethods
27
- base.extend self
28
- end
29
- end
30
-
31
- # Instance related methods
32
- module InstanceMethods
33
- # @return [Node] config root node
34
- def config
35
- @config ||= self.class.config.deep_dup
36
- end
37
-
38
- # Allows for a per instance configuration (if needed)
39
- # @param block [Proc] block for configuration
40
- def configure(&block)
41
- config.configure(&block)
42
- end
43
- end
44
-
45
- # Class related methods
46
- module ClassMethods
47
- # @return [Node] root node for the settings
48
- def config
49
- return @config if @config
50
-
51
- # This will handle inheritance
52
- @config = if superclass.respond_to?(:config)
53
- superclass.config.deep_dup
54
- else
55
- Node.new(:root)
56
- end
57
- end
58
-
59
- # Allows for a per class configuration (if needed)
60
- # @param block [Proc] block for configuration
61
- def configure(&block)
62
- config.configure(&block)
63
- end
64
-
65
- # Pipes the settings setup to the config root node
66
- def setting(...)
67
- config.setting(...)
68
- end
69
- end
70
- end
71
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module WaterDrop
4
- module Contracts
5
- # Base for all the contracts in WaterDrop
6
- class Base < Dry::Validation::Contract
7
- config.messages.load_paths << File.join(WaterDrop.gem_root, 'config', 'errors.yml')
8
-
9
- # @param data [Hash] data for validation
10
- # @param error_class [Class] error class that should be used when validation fails
11
- # @return [Boolean] true
12
- # @raise [StandardError] any error provided in the error_class that inherits from the
13
- # standard error
14
- def validate!(data, error_class)
15
- result = call(data)
16
-
17
- return true if result.success?
18
-
19
- raise error_class, result.errors.to_h
20
- end
21
- end
22
- end
23
- end
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module WaterDrop
4
- module Instrumentation
5
- module Callbacks
6
- # Many of the librdkafka statistics are absolute values instead of a gauge.
7
- # This means, that for example number of messages sent is an absolute growing value
8
- # instead of being a value of messages sent from the last statistics report.
9
- # This decorator calculates the diff against previously emited stats, so we get also
10
- # the diff together with the original values
11
- class StatisticsDecorator
12
- def initialize
13
- @previous = {}.freeze
14
- end
15
-
16
- # @param emited_stats [Hash] original emited statistics
17
- # @return [Hash] emited statistics extended with the diff data
18
- # @note We modify the emited statistics, instead of creating new. Since we don't expose
19
- # any API to get raw data, users can just assume that the result of this decoration is
20
- # the proper raw stats that they can use
21
- def call(emited_stats)
22
- diff(
23
- @previous,
24
- emited_stats
25
- )
26
-
27
- @previous = emited_stats
28
-
29
- emited_stats.freeze
30
- end
31
-
32
- private
33
-
34
- # Calculates the diff of the provided values and modifies in place the emited statistics
35
- #
36
- # @param previous [Object] previous value from the given scope in which
37
- # we are
38
- # @param current [Object] current scope from emitted statistics
39
- # @return [Object] the diff if the values were numerics or the current scope
40
- def diff(previous, current)
41
- if current.is_a?(Hash)
42
- # @note We cannot use #each_key as we modify the content of the current scope
43
- # in place (in case it's a hash)
44
- current.keys.each do |key|
45
- append(
46
- current,
47
- key,
48
- diff((previous || {})[key], (current || {})[key])
49
- )
50
- end
51
- end
52
-
53
- # Diff can be computed only for numerics
54
- return current unless current.is_a?(Numeric)
55
- # If there was no previous value, delta is always zero
56
- return 0 unless previous
57
- # Should never happen but just in case, a type changed in between stats
58
- return current unless previous.is_a?(Numeric)
59
-
60
- current - previous
61
- end
62
-
63
- # Appends the result of the diff to a given key as long as the result is numeric
64
- #
65
- # @param current [Hash] current scope
66
- # @param key [Symbol] key based on which we were diffing
67
- # @param result [Object] diff result
68
- def append(current, key, result)
69
- return unless result.is_a?(Numeric)
70
- return if current.frozen?
71
-
72
- current["#{key}_d"] = result
73
- end
74
- end
75
- end
76
- end
77
- end