delivery_boy 2.0.0.alpha.1 → 2.0.0.alpha.2
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
- data/.github/CODEOWNERS +4 -0
- data/CHANGELOG +47 -1
- data/Gemfile +2 -0
- data/README.md +93 -45
- data/delivery_boy.gemspec +0 -1
- data/lib/delivery_boy/config.rb +47 -10
- data/lib/delivery_boy/datadog.rb +192 -0
- data/lib/delivery_boy/instance.rb +261 -42
- data/lib/delivery_boy/instrumenter.rb +26 -0
- data/lib/delivery_boy/railtie.rb +10 -5
- data/lib/delivery_boy/version.rb +1 -1
- data/lib/delivery_boy.rb +22 -12
- metadata +4 -2
- data/.github/workflows/codeql.yaml +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4dac7da9e416e7c55878674f91006d43b73811e27b1aa05897604d48c6e3162
|
|
4
|
+
data.tar.gz: b5536e1e66d7c8882fe6452b552f50f2fa4e37846870bea0c6b6f5ceef399028
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8390928820e66c27e3ec762344df31d0e4101831e0f1cca21b2289939fbc23cad4f554b17314eb7f92d48e7f3bd1b6d90ebfceac3e3b41065dbabacf3f2aa0b6
|
|
7
|
+
data.tar.gz: 2bf809365bd9f693cf1dd03f61e6cb37c5c43682087ecc013cd0626d558a83bdc04b4dd4b065dc6a9e0170479dc192f2b26f56cc3501ce77aa820b351dddb817
|
data/.github/CODEOWNERS
ADDED
data/CHANGELOG
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## Unreleased
|
|
3
|
+
## Unreleased (v2.0.0)
|
|
4
|
+
|
|
5
|
+
### Migration to librdkafka
|
|
6
|
+
|
|
7
|
+
* Migrated from ruby-kafka to rdkafka (librdkafka) for improved performance and stability
|
|
8
|
+
|
|
9
|
+
### New Features
|
|
10
|
+
|
|
11
|
+
* PLAIN and SCRAM-SHA-256/512 SASL authentication now fully functional with librdkafka
|
|
12
|
+
* Added `sasl_username` and `sasl_password` as consolidated config options (work for both PLAIN and SCRAM)
|
|
13
|
+
* Added automatic `security.protocol` detection (PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL)
|
|
14
|
+
* Backward compatible SASL configuration - existing mechanism-specific options (sasl_plain_username, sasl_scram_username) continue to work
|
|
15
|
+
* OAUTHBEARER authentication via OIDC configuration (librdkafka handles token refresh automatically)
|
|
16
|
+
|
|
17
|
+
### Breaking Changes & Known Limitations
|
|
18
|
+
|
|
19
|
+
* **AWS MSK IAM authentication is no longer supported** (librdkafka limitation)
|
|
20
|
+
- Alternative 1: Use AWS MSK SCRAM-SHA-512 authentication (recommended)
|
|
21
|
+
- Alternative 2: Use mTLS (mutual TLS) with client certificates
|
|
22
|
+
* **OAUTHBEARER authentication changed from callback-based to OIDC configuration**
|
|
23
|
+
- `sasl_oauth_token_provider` is no longer supported
|
|
24
|
+
- Use new OIDC config options instead: `sasl_oauthbearer_method`, `sasl_oauthbearer_client_id`, `sasl_oauthbearer_client_secret`, `sasl_oauthbearer_token_endpoint_url`
|
|
25
|
+
- librdkafka handles token refresh automatically (no custom code needed)
|
|
26
|
+
* `sasl_plain_authzid` is not supported by librdkafka and will be ignored with a warning
|
|
27
|
+
* `compression_threshold` is no longer supported - librdkafka applies compression automatically to all batches when `compression_codec` is set
|
|
28
|
+
|
|
29
|
+
### Config options no longer used
|
|
30
|
+
|
|
31
|
+
The following config options are silently ignored (no error, but no effect):
|
|
32
|
+
* `max_queue_size` - rdkafka manages its own internal queue
|
|
33
|
+
* `ssl_ca_certs_from_system` - no librdkafka equivalent
|
|
34
|
+
* `ssl_verify_hostname` - not mapped to librdkafka
|
|
35
|
+
* `sasl_scram_mechanism` - use `sasl_mechanism` instead
|
|
36
|
+
* `log_level` - rdkafka has its own logging configuration
|
|
37
|
+
|
|
38
|
+
### Datadog Integration
|
|
39
|
+
|
|
40
|
+
* Datadog metrics continue to work with the same metric names as before
|
|
41
|
+
* Now uses `DeliveryBoy::Datadog` instead of `Kafka::Datadog`
|
|
42
|
+
* Instrumentation is built on top of ActiveSupport::Notifications
|
|
43
|
+
* Some metrics are no longer available due to rdkafka's different architecture:
|
|
44
|
+
- `producer.buffer.fill_ratio` / `producer.buffer.fill_percentage`
|
|
45
|
+
- `producer.deliver.attempts`
|
|
46
|
+
- `producer.ack.delay`
|
|
47
|
+
- `async_producer.queue.fill_ratio`
|
|
48
|
+
|
|
49
|
+
### Other Changes
|
|
4
50
|
|
|
5
51
|
* `compression_codec` in the `DeliveryBoy::Config` is now coerces its value into
|
|
6
52
|
a symbol if the value is present.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -172,7 +172,7 @@ The codec used to compress messages. Must be either `snappy` or `gzip`.
|
|
|
172
172
|
|
|
173
173
|
##### `compression_threshold`
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
**Deprecated:** This setting has no effect with librdkafka. Compression is applied automatically to all batches when `compression_codec` is set.
|
|
176
176
|
|
|
177
177
|
#### Network
|
|
178
178
|
|
|
@@ -226,88 +226,136 @@ The password required to read the ssl_client_cert_key. Must be used in combinati
|
|
|
226
226
|
|
|
227
227
|
#### SASL Authentication and authorization
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
DeliveryBoy supports SASL authentication with multiple mechanisms.
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
##### GSSAPI (Kerberos)
|
|
232
232
|
|
|
233
|
-
|
|
233
|
+
###### `sasl_gssapi_principal`
|
|
234
234
|
|
|
235
235
|
The GSSAPI principal.
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
###### `sasl_gssapi_keytab`
|
|
238
238
|
|
|
239
239
|
Optional GSSAPI keytab.
|
|
240
240
|
|
|
241
|
-
|
|
241
|
+
**Example:**
|
|
242
242
|
|
|
243
|
-
|
|
243
|
+
```ruby
|
|
244
|
+
DeliveryBoy.configure do |config|
|
|
245
|
+
config.sasl_mechanism = "GSSAPI"
|
|
246
|
+
config.sasl_gssapi_principal = "kafka/hostname@REALM"
|
|
247
|
+
config.sasl_gssapi_keytab = "/path/to/keytab"
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
##### PLAIN Authentication
|
|
252
|
+
|
|
253
|
+
###### `sasl_plain_username`
|
|
254
|
+
|
|
255
|
+
The username used to authenticate (legacy option).
|
|
244
256
|
|
|
245
|
-
|
|
257
|
+
###### `sasl_plain_password`
|
|
246
258
|
|
|
247
|
-
The
|
|
259
|
+
The password used to authenticate (legacy option).
|
|
248
260
|
|
|
249
|
-
|
|
261
|
+
###### `sasl_username`
|
|
250
262
|
|
|
251
|
-
The
|
|
263
|
+
The username used to authenticate (new consolidated option, works for both PLAIN and SCRAM).
|
|
252
264
|
|
|
253
|
-
|
|
265
|
+
###### `sasl_password`
|
|
254
266
|
|
|
255
|
-
|
|
256
|
-
|
|
267
|
+
The password used to authenticate (new consolidated option, works for both PLAIN and SCRAM).
|
|
268
|
+
|
|
269
|
+
**Example:**
|
|
257
270
|
|
|
258
271
|
```ruby
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
272
|
+
DeliveryBoy.configure do |config|
|
|
273
|
+
config.sasl_mechanism = "PLAIN"
|
|
274
|
+
# You can use either the new consolidated options:
|
|
275
|
+
config.sasl_username = "your-username"
|
|
276
|
+
config.sasl_password = "your-password"
|
|
277
|
+
# Or the legacy mechanism-specific options:
|
|
278
|
+
# config.sasl_plain_username = "your-username"
|
|
279
|
+
# config.sasl_plain_password = "your-password"
|
|
263
280
|
end
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
##### SCRAM Authentication
|
|
284
|
+
|
|
285
|
+
Supports SCRAM-SHA-256 and SCRAM-SHA-512 mechanisms.
|
|
286
|
+
|
|
287
|
+
###### `sasl_scram_username`
|
|
264
288
|
|
|
289
|
+
The username used to authenticate (legacy option).
|
|
290
|
+
|
|
291
|
+
###### `sasl_scram_password`
|
|
292
|
+
|
|
293
|
+
The password used to authenticate (legacy option).
|
|
294
|
+
|
|
295
|
+
**Example:**
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
265
298
|
DeliveryBoy.configure do |config|
|
|
266
|
-
config.
|
|
267
|
-
|
|
299
|
+
config.sasl_mechanism = "SCRAM-SHA-256" # or "SCRAM-SHA-512"
|
|
300
|
+
# You can use either the new consolidated options:
|
|
301
|
+
config.sasl_username = "your-username"
|
|
302
|
+
config.sasl_password = "your-password"
|
|
303
|
+
# Or the legacy mechanism-specific options:
|
|
304
|
+
# config.sasl_scram_username = "your-username"
|
|
305
|
+
# config.sasl_scram_password = "your-password"
|
|
268
306
|
end
|
|
269
307
|
```
|
|
270
308
|
|
|
271
|
-
|
|
309
|
+
##### OAUTHBEARER (OIDC)
|
|
310
|
+
|
|
311
|
+
OAUTHBEARER authentication is supported via OIDC configuration. librdkafka handles token acquisition and refresh automatically.
|
|
312
|
+
|
|
313
|
+
###### `sasl_oauthbearer_method`
|
|
272
314
|
|
|
273
|
-
|
|
315
|
+
Set to `"oidc"` to enable OIDC-based OAUTHBEARER authentication.
|
|
274
316
|
|
|
275
|
-
|
|
317
|
+
###### `sasl_oauthbearer_client_id`
|
|
276
318
|
|
|
277
|
-
|
|
319
|
+
The OAuth client ID for your application.
|
|
278
320
|
|
|
279
|
-
|
|
321
|
+
###### `sasl_oauthbearer_client_secret`
|
|
280
322
|
|
|
281
|
-
|
|
323
|
+
The OAuth client secret for your application.
|
|
282
324
|
|
|
283
|
-
|
|
325
|
+
###### `sasl_oauthbearer_token_endpoint_url`
|
|
284
326
|
|
|
285
|
-
|
|
327
|
+
The URL of the OAuth token endpoint (e.g., `https://auth.example.com/oauth/token`).
|
|
286
328
|
|
|
287
|
-
|
|
329
|
+
###### `sasl_oauthbearer_scope` (optional)
|
|
288
330
|
|
|
289
|
-
|
|
331
|
+
OAuth scope to request.
|
|
290
332
|
|
|
291
|
-
|
|
333
|
+
###### `sasl_oauthbearer_extensions` (optional)
|
|
334
|
+
|
|
335
|
+
Additional SASL extensions as comma-separated key=value pairs.
|
|
292
336
|
|
|
293
337
|
```ruby
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
DeliveryBoy.configure do |c|
|
|
303
|
-
c.sasl_aws_msk_iam_access_key_id = role.credentials.access_key_id
|
|
304
|
-
c.sasl_aws_msk_iam_secret_key_id = role.credentials.secret_access_key
|
|
305
|
-
c.sasl_aws_msk_iam_session_token = role.credentials.session_token
|
|
306
|
-
c.sasl_aws_msk_iam_aws_region = ENV["AWS_REGION"]
|
|
307
|
-
c.ssl_ca_certs_from_system = true
|
|
338
|
+
DeliveryBoy.configure do |config|
|
|
339
|
+
config.sasl_mechanism = "OAUTHBEARER"
|
|
340
|
+
config.sasl_oauthbearer_method = "oidc"
|
|
341
|
+
config.sasl_oauthbearer_client_id = "your-client-id"
|
|
342
|
+
config.sasl_oauthbearer_client_secret = "your-client-secret"
|
|
343
|
+
config.sasl_oauthbearer_token_endpoint_url = "https://auth.example.com/oauth/token"
|
|
344
|
+
# Optional:
|
|
345
|
+
# config.sasl_oauthbearer_scope = "kafka"
|
|
308
346
|
end
|
|
309
347
|
```
|
|
310
348
|
|
|
349
|
+
**Note:** The legacy `sasl_oauth_token_provider` callback is no longer supported. Use OIDC configuration instead.
|
|
350
|
+
|
|
351
|
+
#### AWS MSK IAM Authentication
|
|
352
|
+
|
|
353
|
+
**Note:** AWS MSK IAM authentication is not supported by librdkafka.
|
|
354
|
+
|
|
355
|
+
**Recommended alternatives:**
|
|
356
|
+
1. **Use AWS MSK SCRAM authentication** - Create SCRAM credentials in AWS Secrets Manager and use SCRAM-SHA-512
|
|
357
|
+
2. **Use mTLS** - Configure mutual TLS with client certificates
|
|
358
|
+
|
|
311
359
|
### Testing
|
|
312
360
|
|
|
313
361
|
DeliveryBoy provides a test mode out of the box. When this mode is enabled, messages will be stored in memory rather than being sent to Kafka. If you use RSpec, enabling test mode is as easy as adding this to your spec helper:
|
data/delivery_boy.gemspec
CHANGED
data/lib/delivery_boy/config.rb
CHANGED
|
@@ -16,10 +16,6 @@ module DeliveryBoy
|
|
|
16
16
|
transactional_timeout * 1000
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def isolation_level
|
|
20
|
-
transactional ? "read_uncommitted" : "read_committed"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
19
|
def max_buffer_kbytesize
|
|
24
20
|
max_buffer_bytesize / 1024
|
|
25
21
|
end
|
|
@@ -28,7 +24,36 @@ module DeliveryBoy
|
|
|
28
24
|
delivery_interval * 1000
|
|
29
25
|
end
|
|
30
26
|
|
|
31
|
-
def
|
|
27
|
+
def ack_timeout_ms
|
|
28
|
+
ack_timeout * 1000
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def retry_backoff_ms
|
|
32
|
+
retry_backoff * 1000
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def sasl_enabled?
|
|
36
|
+
return false unless sasl_mechanism && !sasl_mechanism.empty?
|
|
37
|
+
sasl_mechanism.upcase != "GSSAPI"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def validate_aws_msk_iam!
|
|
41
|
+
if sasl_aws_msk_iam_access_key_id || sasl_aws_msk_iam_secret_key_id || sasl_aws_msk_iam_aws_region
|
|
42
|
+
raise ConfigError, <<~ERROR
|
|
43
|
+
AWS MSK IAM authentication is not supported by librdkafka.
|
|
44
|
+
|
|
45
|
+
Alternatives:
|
|
46
|
+
1. Use AWS MSK SCRAM-SHA-512 authentication (recommended)
|
|
47
|
+
- Create SCRAM credentials in AWS Secrets Manager
|
|
48
|
+
- Set sasl_mechanism = "SCRAM-SHA-512"
|
|
49
|
+
- Set sasl_username and sasl_password
|
|
50
|
+
|
|
51
|
+
2. Use mTLS (mutual TLS) with client certificates
|
|
52
|
+
- Configure ssl_client_cert and ssl_client_cert_key
|
|
53
|
+
|
|
54
|
+
See migration guide: https://github.com/zendesk/delivery_boy/blob/master/MIGRATION.md#aws-msk-iam
|
|
55
|
+
ERROR
|
|
56
|
+
end
|
|
32
57
|
end
|
|
33
58
|
|
|
34
59
|
# Basic
|
|
@@ -36,10 +61,10 @@ module DeliveryBoy
|
|
|
36
61
|
string :client_id, default: "delivery_boy"
|
|
37
62
|
string :log_level, default: nil
|
|
38
63
|
|
|
39
|
-
# Buffering
|
|
64
|
+
# Buffering (defaults match librdkafka defaults to avoid queue overflow at high throughput)
|
|
40
65
|
integer :max_buffer_bytesize, default: 10_000_000
|
|
41
|
-
integer :max_buffer_size, default:
|
|
42
|
-
integer :max_queue_size, default:
|
|
66
|
+
integer :max_buffer_size, default: 100_000
|
|
67
|
+
integer :max_queue_size, default: 100_000
|
|
43
68
|
|
|
44
69
|
# Network timeouts
|
|
45
70
|
integer :connect_timeout, default: 10
|
|
@@ -71,7 +96,7 @@ module DeliveryBoy
|
|
|
71
96
|
boolean :ssl_verify_hostname, default: true
|
|
72
97
|
|
|
73
98
|
# Supported: GSSAPI, PLAIN, SCRAM-SHA-256, SCRAM-SHA-512, OAUTHBEARER
|
|
74
|
-
string :sasl_mechanism, default:
|
|
99
|
+
string :sasl_mechanism, default: nil
|
|
75
100
|
# SASL authentication
|
|
76
101
|
string :sasl_gssapi_principal
|
|
77
102
|
string :sasl_gssapi_keytab
|
|
@@ -83,9 +108,21 @@ module DeliveryBoy
|
|
|
83
108
|
string :sasl_scram_mechanism
|
|
84
109
|
boolean :sasl_over_ssl, default: true
|
|
85
110
|
|
|
86
|
-
# SASL
|
|
111
|
+
# New consolidated SASL options (librdkafka-aligned)
|
|
112
|
+
string :sasl_username, default: nil
|
|
113
|
+
string :sasl_password, default: nil
|
|
114
|
+
|
|
115
|
+
# SASL OAUTHBEARER (legacy - callback-based, not supported with librdkafka)
|
|
87
116
|
attr_accessor :sasl_oauth_token_provider
|
|
88
117
|
|
|
118
|
+
# SASL OAUTHBEARER OIDC (librdkafka native support)
|
|
119
|
+
string :sasl_oauthbearer_method, default: nil # "oidc" for OIDC-based auth
|
|
120
|
+
string :sasl_oauthbearer_client_id, default: nil
|
|
121
|
+
string :sasl_oauthbearer_client_secret, default: nil
|
|
122
|
+
string :sasl_oauthbearer_token_endpoint_url, default: nil
|
|
123
|
+
string :sasl_oauthbearer_scope, default: nil
|
|
124
|
+
string :sasl_oauthbearer_extensions, default: nil
|
|
125
|
+
|
|
89
126
|
# AWS IAM authentication
|
|
90
127
|
string :sasl_aws_msk_iam_access_key_id
|
|
91
128
|
string :sasl_aws_msk_iam_secret_key_id
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "datadog/statsd"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
warn "In order to report Kafka client metrics to Datadog you need to install the `dogstatsd-ruby` gem."
|
|
7
|
+
raise
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require "active_support/subscriber"
|
|
11
|
+
|
|
12
|
+
module DeliveryBoy
|
|
13
|
+
# Reports operational metrics to a Datadog agent using the Statsd protocol.
|
|
14
|
+
#
|
|
15
|
+
# require "delivery_boy/datadog"
|
|
16
|
+
#
|
|
17
|
+
# # Default is "ruby_kafka" (kept for backward compatibility).
|
|
18
|
+
# DeliveryBoy::Datadog.namespace = "custom-namespace"
|
|
19
|
+
#
|
|
20
|
+
# # Default is "127.0.0.1".
|
|
21
|
+
# DeliveryBoy::Datadog.host = "statsd.something.com"
|
|
22
|
+
#
|
|
23
|
+
# # Default is 8125.
|
|
24
|
+
# DeliveryBoy::Datadog.port = 1234
|
|
25
|
+
#
|
|
26
|
+
module Datadog
|
|
27
|
+
STATSD_NAMESPACE = "ruby_kafka"
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
attr_reader :host, :port, :socket_path
|
|
31
|
+
|
|
32
|
+
def configure
|
|
33
|
+
yield self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def statsd
|
|
37
|
+
@statsd ||= if socket_path
|
|
38
|
+
::Datadog::Statsd.new(socket_path: socket_path, namespace: namespace, tags: tags)
|
|
39
|
+
else
|
|
40
|
+
::Datadog::Statsd.new(host, port, namespace: namespace, tags: tags)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def statsd=(statsd)
|
|
45
|
+
clear
|
|
46
|
+
@statsd = statsd
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def host=(host)
|
|
50
|
+
@host = host
|
|
51
|
+
clear
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def port=(port)
|
|
55
|
+
@port = port
|
|
56
|
+
clear
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def socket_path=(socket_path)
|
|
60
|
+
@socket_path = socket_path
|
|
61
|
+
clear
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def namespace
|
|
65
|
+
@namespace ||= STATSD_NAMESPACE
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def namespace=(namespace)
|
|
69
|
+
@namespace = namespace
|
|
70
|
+
clear
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def tags
|
|
74
|
+
@tags ||= []
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def tags=(tags)
|
|
78
|
+
@tags = tags
|
|
79
|
+
clear
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def close
|
|
83
|
+
@statsd&.close
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def clear
|
|
89
|
+
close
|
|
90
|
+
@statsd = nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class StatsdSubscriber < ActiveSupport::Subscriber
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
%w[increment histogram count timing gauge].each do |type|
|
|
98
|
+
define_method(type) do |*args, **kwargs|
|
|
99
|
+
emit(type, *args, **kwargs)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def emit(type, *args, tags: {})
|
|
104
|
+
tags = tags.map { |k, v| "#{k}:#{v}" }.to_a
|
|
105
|
+
DeliveryBoy::Datadog.statsd.send(type, *args, tags: tags)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class ProducerSubscriber < StatsdSubscriber
|
|
110
|
+
def produce_message(event)
|
|
111
|
+
client = event.payload.fetch(:client_id)
|
|
112
|
+
topic = event.payload.fetch(:topic)
|
|
113
|
+
message_size = event.payload.fetch(:message_size)
|
|
114
|
+
buffer_size = event.payload.fetch(:buffer_size)
|
|
115
|
+
|
|
116
|
+
tags = {client: client, topic: topic}
|
|
117
|
+
|
|
118
|
+
if event.payload.key?(:exception)
|
|
119
|
+
increment("producer.produce.errors", tags: tags)
|
|
120
|
+
else
|
|
121
|
+
increment("producer.produce.messages", tags: tags)
|
|
122
|
+
histogram("producer.produce.message_size", message_size, tags: tags)
|
|
123
|
+
count("producer.produce.message_size.sum", message_size, tags: tags)
|
|
124
|
+
histogram("producer.buffer.size", buffer_size, tags: tags)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def deliver_messages(event)
|
|
129
|
+
client = event.payload.fetch(:client_id)
|
|
130
|
+
message_count = event.payload.fetch(:delivered_message_count)
|
|
131
|
+
|
|
132
|
+
tags = {client: client}
|
|
133
|
+
|
|
134
|
+
increment("producer.deliver.errors", tags: tags) if event.payload.key?(:exception)
|
|
135
|
+
timing("producer.deliver.latency", event.duration, tags: tags)
|
|
136
|
+
count("producer.deliver.messages", message_count, tags: tags)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def deliver(event)
|
|
140
|
+
client = event.payload.fetch(:client_id)
|
|
141
|
+
topic = event.payload.fetch(:topic)
|
|
142
|
+
message_size = event.payload.fetch(:message_size)
|
|
143
|
+
|
|
144
|
+
tags = {client: client, topic: topic}
|
|
145
|
+
|
|
146
|
+
if event.payload.key?(:exception)
|
|
147
|
+
increment("producer.deliver.errors", tags: tags)
|
|
148
|
+
else
|
|
149
|
+
increment("producer.produce.messages", tags: tags)
|
|
150
|
+
histogram("producer.produce.message_size", message_size, tags: tags)
|
|
151
|
+
count("producer.produce.message_size.sum", message_size, tags: tags)
|
|
152
|
+
timing("producer.deliver.latency", event.duration, tags: tags)
|
|
153
|
+
count("producer.deliver.messages", 1, tags: tags)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def deliver_async(event)
|
|
158
|
+
client = event.payload.fetch(:client_id)
|
|
159
|
+
topic = event.payload.fetch(:topic)
|
|
160
|
+
message_size = event.payload.fetch(:message_size)
|
|
161
|
+
queue_size = event.payload.fetch(:queue_size, 0)
|
|
162
|
+
|
|
163
|
+
tags = {client: client, topic: topic}
|
|
164
|
+
|
|
165
|
+
if event.payload.key?(:exception)
|
|
166
|
+
increment("async_producer.produce.errors", tags: tags)
|
|
167
|
+
else
|
|
168
|
+
increment("producer.produce.messages", tags: tags)
|
|
169
|
+
histogram("producer.produce.message_size", message_size, tags: tags)
|
|
170
|
+
count("producer.produce.message_size.sum", message_size, tags: tags)
|
|
171
|
+
histogram("async_producer.queue.size", queue_size, tags: tags)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def ack_message(event)
|
|
176
|
+
tags = {
|
|
177
|
+
client: event.payload.fetch(:client_id),
|
|
178
|
+
topic: event.payload.fetch(:topic)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
increment("producer.ack.messages", tags: tags)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def delivery_error(event)
|
|
185
|
+
tags = {client: event.payload.fetch(:client_id)}
|
|
186
|
+
increment("producer.ack.errors", tags: tags)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
attach_to "delivery_boy"
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module DeliveryBoy
|
|
2
4
|
# This class implements the actual logic of DeliveryBoy. The DeliveryBoy module
|
|
3
5
|
# has a module-level singleton instance.
|
|
4
6
|
class Instance
|
|
5
|
-
def initialize(config, logger)
|
|
7
|
+
def initialize(config, logger, instrumenter: NullInstrumenter.new)
|
|
6
8
|
@config = config
|
|
7
9
|
@logger = logger
|
|
8
|
-
@
|
|
10
|
+
@instrumenter = instrumenter
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
def deliver(value, topic:, **options)
|
|
@@ -15,9 +17,19 @@ module DeliveryBoy
|
|
|
15
17
|
options_clone.delete(:create_time)
|
|
16
18
|
end
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
message_size = value.to_s.bytesize
|
|
21
|
+
|
|
22
|
+
instrumentation_payload = {
|
|
23
|
+
client_id: config.client_id,
|
|
24
|
+
topic: topic,
|
|
25
|
+
message_size: message_size
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@instrumenter.instrument("deliver", instrumentation_payload) do
|
|
29
|
+
sync_producer
|
|
30
|
+
.produce(payload: value, topic: topic, **options_clone)
|
|
31
|
+
.wait
|
|
32
|
+
end
|
|
21
33
|
end
|
|
22
34
|
|
|
23
35
|
def deliver_async!(value, topic:, **options)
|
|
@@ -27,8 +39,19 @@ module DeliveryBoy
|
|
|
27
39
|
options_clone.delete(:create_time)
|
|
28
40
|
end
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
message_size = value.to_s.bytesize
|
|
43
|
+
|
|
44
|
+
instrumentation_payload = {
|
|
45
|
+
client_id: config.client_id,
|
|
46
|
+
topic: topic,
|
|
47
|
+
message_size: message_size,
|
|
48
|
+
queue_size: async_producer_queue_size
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@instrumenter.instrument("deliver_async", instrumentation_payload) do
|
|
52
|
+
async_producer
|
|
53
|
+
.produce(payload: value, topic: topic, **options_clone)
|
|
54
|
+
end
|
|
32
55
|
end
|
|
33
56
|
|
|
34
57
|
def shutdown
|
|
@@ -37,17 +60,43 @@ module DeliveryBoy
|
|
|
37
60
|
end
|
|
38
61
|
|
|
39
62
|
def produce(value, topic:, **options)
|
|
40
|
-
|
|
41
|
-
|
|
63
|
+
options_clone = options.clone
|
|
64
|
+
if options[:create_time]
|
|
65
|
+
options_clone[:timestamp] = Time.at(options[:create_time])
|
|
66
|
+
options_clone.delete(:create_time)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
message_size = value.to_s.bytesize
|
|
70
|
+
|
|
71
|
+
instrumentation_payload = {
|
|
72
|
+
client_id: config.client_id,
|
|
73
|
+
topic: topic,
|
|
74
|
+
message_size: message_size,
|
|
75
|
+
buffer_size: handles.size
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@instrumenter.instrument("produce_message", instrumentation_payload) do
|
|
79
|
+
handle = sync_producer.produce(payload: value, topic: topic, **options_clone)
|
|
80
|
+
handles.push(handle)
|
|
81
|
+
end
|
|
42
82
|
end
|
|
43
83
|
|
|
44
84
|
def deliver_messages
|
|
45
|
-
handles.
|
|
46
|
-
|
|
85
|
+
message_count = handles.size
|
|
86
|
+
|
|
87
|
+
instrumentation_payload = {
|
|
88
|
+
client_id: config.client_id,
|
|
89
|
+
delivered_message_count: message_count
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@instrumenter.instrument("deliver_messages", instrumentation_payload) do
|
|
93
|
+
sync_producer.flush
|
|
94
|
+
handles.clear
|
|
95
|
+
end
|
|
47
96
|
end
|
|
48
97
|
|
|
49
98
|
def clear_buffer
|
|
50
|
-
handles.
|
|
99
|
+
handles.clear
|
|
51
100
|
end
|
|
52
101
|
|
|
53
102
|
def buffer_size
|
|
@@ -71,70 +120,240 @@ module DeliveryBoy
|
|
|
71
120
|
def async_producer
|
|
72
121
|
# The async producer doesn't have to be per-thread, since all deliveries are
|
|
73
122
|
# performed by a single background thread.
|
|
74
|
-
@async_producer ||=
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
123
|
+
@async_producer ||= begin
|
|
124
|
+
producer = Rdkafka::Config.new({
|
|
125
|
+
"bootstrap.servers": config.brokers.join(","),
|
|
126
|
+
"queue.buffering.backpressure.threshold": config.delivery_threshold,
|
|
127
|
+
"queue.buffering.max.ms": config.delivery_interval_ms
|
|
128
|
+
}.merge(producer_options)).producer
|
|
129
|
+
|
|
130
|
+
producer.delivery_callback = delivery_callback
|
|
131
|
+
producer
|
|
132
|
+
end
|
|
79
133
|
end
|
|
80
134
|
|
|
81
135
|
def async_producer?
|
|
82
136
|
!@async_producer.nil?
|
|
83
137
|
end
|
|
84
138
|
|
|
139
|
+
def async_producer_queue_size
|
|
140
|
+
return 0 unless async_producer?
|
|
141
|
+
# rdkafka doesn't expose queue size directly, return 0 as approximation
|
|
142
|
+
0
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def delivery_callback
|
|
146
|
+
instrumenter = @instrumenter
|
|
147
|
+
client_id = config.client_id
|
|
148
|
+
|
|
149
|
+
proc do |delivery_report|
|
|
150
|
+
if delivery_report.error
|
|
151
|
+
instrumenter.instrument("delivery_error", {
|
|
152
|
+
client_id: client_id,
|
|
153
|
+
error: delivery_report.error
|
|
154
|
+
})
|
|
155
|
+
else
|
|
156
|
+
instrumenter.instrument("ack_message", {
|
|
157
|
+
client_id: client_id,
|
|
158
|
+
topic: delivery_report.topic_name,
|
|
159
|
+
partition: delivery_report.partition,
|
|
160
|
+
offset: delivery_report.offset
|
|
161
|
+
})
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
85
166
|
def kafka
|
|
86
167
|
@kafka ||= Rdkafka::Config.new({
|
|
87
168
|
"bootstrap.servers": config.brokers.join(",")
|
|
88
169
|
}.merge(producer_options))
|
|
89
170
|
end
|
|
90
171
|
|
|
91
|
-
|
|
172
|
+
def sasl_options
|
|
173
|
+
return {} unless config.sasl_mechanism && !config.sasl_mechanism.empty?
|
|
174
|
+
|
|
175
|
+
config.validate_aws_msk_iam! if config.sasl_enabled?
|
|
176
|
+
|
|
177
|
+
options = {}
|
|
178
|
+
|
|
179
|
+
mechanism = config.sasl_mechanism.upcase
|
|
180
|
+
|
|
181
|
+
case mechanism
|
|
182
|
+
when "GSSAPI"
|
|
183
|
+
options.merge!(gssapi_options)
|
|
184
|
+
when "PLAIN"
|
|
185
|
+
options.merge!(plain_options)
|
|
186
|
+
when "SCRAM-SHA-256", "SCRAM-SHA-512"
|
|
187
|
+
options["sasl.mechanism"] = mechanism
|
|
188
|
+
options.merge!(scram_options)
|
|
189
|
+
when "OAUTHBEARER"
|
|
190
|
+
options.merge!(oauthbearer_options)
|
|
191
|
+
else
|
|
192
|
+
logger.warn "Unknown SASL mechanism: #{config.sasl_mechanism}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
options.compact
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def gssapi_options
|
|
199
|
+
{
|
|
200
|
+
"sasl.mechanism" => "GSSAPI",
|
|
201
|
+
"sasl.kerberos.principal" => config.sasl_gssapi_principal,
|
|
202
|
+
"sasl.kerberos.keytab" => config.sasl_gssapi_keytab
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def plain_options
|
|
207
|
+
username = config.sasl_username || config.sasl_plain_username
|
|
208
|
+
password = config.sasl_password || config.sasl_plain_password
|
|
209
|
+
|
|
210
|
+
if username.nil? || username.to_s.empty? || password.nil? || password.to_s.empty?
|
|
211
|
+
raise ConfigError, "PLAIN authentication requires sasl_username and sasl_password to be set"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Note: sasl_plain_authzid doesn't have a librdkafka equivalent
|
|
215
|
+
# Log warning if set, but don't fail
|
|
216
|
+
if config.sasl_plain_authzid && !config.sasl_plain_authzid.empty?
|
|
217
|
+
logger.warn "sasl_plain_authzid is not supported by librdkafka and will be ignored"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
{
|
|
221
|
+
"sasl.mechanism" => "PLAIN",
|
|
222
|
+
"sasl.username" => username,
|
|
223
|
+
"sasl.password" => password
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def scram_options
|
|
228
|
+
username = config.sasl_username || config.sasl_scram_username
|
|
229
|
+
password = config.sasl_password || config.sasl_scram_password
|
|
230
|
+
|
|
231
|
+
if username.nil? || username.to_s.empty? || password.nil? || password.to_s.empty?
|
|
232
|
+
raise ConfigError, "SCRAM authentication requires sasl_username and sasl_password to be set"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
{
|
|
236
|
+
"sasl.username" => username,
|
|
237
|
+
"sasl.password" => password
|
|
238
|
+
}
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def oauthbearer_options
|
|
242
|
+
# Check for legacy token provider (not supported)
|
|
243
|
+
if config.sasl_oauth_token_provider
|
|
244
|
+
raise ConfigError, <<~ERROR
|
|
245
|
+
sasl_oauth_token_provider is no longer supported with librdkafka.
|
|
246
|
+
|
|
247
|
+
Migration options:
|
|
248
|
+
1. Use OIDC configuration (recommended for OIDC providers like Auth0, Okta):
|
|
249
|
+
config.sasl_oauthbearer_method = "oidc"
|
|
250
|
+
config.sasl_oauthbearer_client_id = "your-client-id"
|
|
251
|
+
config.sasl_oauthbearer_client_secret = "your-client-secret"
|
|
252
|
+
config.sasl_oauthbearer_token_endpoint_url = "https://auth.example.com/oauth/token"
|
|
253
|
+
|
|
254
|
+
2. Use SCRAM-SHA-256/512 as an alternative authentication method.
|
|
255
|
+
|
|
256
|
+
See: https://github.com/zendesk/delivery_boy/blob/master/MIGRATION.md#oauthbearer
|
|
257
|
+
ERROR
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
if config.sasl_oauthbearer_method&.downcase == "oidc"
|
|
261
|
+
if config.sasl_oauthbearer_client_id.nil? || config.sasl_oauthbearer_client_id.empty?
|
|
262
|
+
raise ConfigError, "OAUTHBEARER OIDC requires sasl_oauthbearer_client_id to be set"
|
|
263
|
+
end
|
|
264
|
+
if config.sasl_oauthbearer_client_secret.nil? || config.sasl_oauthbearer_client_secret.empty?
|
|
265
|
+
raise ConfigError, "OAUTHBEARER OIDC requires sasl_oauthbearer_client_secret to be set"
|
|
266
|
+
end
|
|
267
|
+
if config.sasl_oauthbearer_token_endpoint_url.nil? || config.sasl_oauthbearer_token_endpoint_url.empty?
|
|
268
|
+
raise ConfigError, "OAUTHBEARER OIDC requires sasl_oauthbearer_token_endpoint_url to be set"
|
|
269
|
+
end
|
|
270
|
+
else
|
|
271
|
+
raise ConfigError, <<~ERROR
|
|
272
|
+
OAUTHBEARER requires OIDC configuration.
|
|
273
|
+
|
|
274
|
+
Set the following options:
|
|
275
|
+
config.sasl_oauthbearer_method = "oidc"
|
|
276
|
+
config.sasl_oauthbearer_client_id = "your-client-id"
|
|
277
|
+
config.sasl_oauthbearer_client_secret = "your-client-secret"
|
|
278
|
+
config.sasl_oauthbearer_token_endpoint_url = "https://auth.example.com/oauth/token"
|
|
279
|
+
ERROR
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
options = {
|
|
283
|
+
"sasl.mechanism" => "OAUTHBEARER",
|
|
284
|
+
"sasl.oauthbearer.method" => "oidc",
|
|
285
|
+
"sasl.oauthbearer.client.id" => config.sasl_oauthbearer_client_id,
|
|
286
|
+
"sasl.oauthbearer.client.secret" => config.sasl_oauthbearer_client_secret,
|
|
287
|
+
"sasl.oauthbearer.token.endpoint.url" => config.sasl_oauthbearer_token_endpoint_url
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
options["sasl.oauthbearer.scope"] = config.sasl_oauthbearer_scope if config.sasl_oauthbearer_scope
|
|
291
|
+
options["sasl.oauthbearer.extensions"] = config.sasl_oauthbearer_extensions if config.sasl_oauthbearer_extensions
|
|
292
|
+
|
|
293
|
+
options
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def security_protocol
|
|
297
|
+
has_ssl = config.ssl_ca_cert || config.ssl_ca_cert_file_path
|
|
298
|
+
has_sasl = config.sasl_enabled? || config.sasl_gssapi_principal
|
|
299
|
+
|
|
300
|
+
if config.sasl_over_ssl == false && has_ssl
|
|
301
|
+
raise ConfigError, <<~ERROR
|
|
302
|
+
sasl_over_ssl=false with SSL certificates configured is not supported by librdkafka.
|
|
303
|
+
|
|
304
|
+
librdkafka's security.protocol cannot express "SSL for verification but SASL over plaintext".
|
|
305
|
+
|
|
306
|
+
Options:
|
|
307
|
+
1. Remove SSL certificate configuration to use SASL_PLAINTEXT
|
|
308
|
+
2. Remove sasl_over_ssl=false to use SASL_SSL (recommended)
|
|
309
|
+
|
|
310
|
+
Note: sasl_over_ssl is deprecated and will be removed in a future version.
|
|
311
|
+
ERROR
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
if has_sasl && has_ssl
|
|
315
|
+
"SASL_SSL"
|
|
316
|
+
elsif has_sasl
|
|
317
|
+
"SASL_PLAINTEXT"
|
|
318
|
+
elsif has_ssl
|
|
319
|
+
"SSL"
|
|
320
|
+
else
|
|
321
|
+
"PLAINTEXT"
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
92
325
|
def producer_options
|
|
93
326
|
if config.transactional? && config.transactional_id.nil?
|
|
94
327
|
raise "transactional_id must be set"
|
|
95
328
|
end
|
|
96
329
|
|
|
97
330
|
{
|
|
331
|
+
"client.id": config.client_id,
|
|
98
332
|
"socket.connection.setup.timeout.ms": config.connection_timeout_ms,
|
|
99
333
|
"socket.timeout.ms": config.socket_timeout_ms,
|
|
100
334
|
"request.required.acks": config.required_acks,
|
|
101
|
-
"request.timeout.ms": config.
|
|
335
|
+
"request.timeout.ms": config.ack_timeout_ms,
|
|
102
336
|
"message.send.max.retries": config.max_retries,
|
|
103
|
-
"retry.backoff.ms": config.
|
|
337
|
+
"retry.backoff.ms": config.retry_backoff_ms,
|
|
104
338
|
"queue.buffering.max.messages": config.max_buffer_size,
|
|
105
|
-
"queue.buffering.max.kbytes": config.
|
|
339
|
+
"queue.buffering.max.kbytes": config.max_buffer_kbytesize,
|
|
106
340
|
"compression.codec": config.compression_codec, # values none, gzip, snappy, lz4, zstd
|
|
107
341
|
"enable.idempotence": config.idempotent,
|
|
108
342
|
"transactional.id": config.transactional_id,
|
|
109
343
|
"transaction.timeout.ms": config.transactional_timeout_ms,
|
|
110
|
-
|
|
111
|
-
# SSL options
|
|
344
|
+
"security.protocol": security_protocol,
|
|
112
345
|
"ssl.ca.pem": config.ssl_ca_cert,
|
|
113
346
|
"ssl.ca.location": config.ssl_ca_cert_file_path,
|
|
114
347
|
"ssl.certificate.pem": config.ssl_client_cert,
|
|
115
348
|
"ssl.key.pem": config.ssl_client_cert_key,
|
|
116
|
-
"ssl.key.password": config.ssl_client_cert_key_password
|
|
349
|
+
"ssl.key.password": config.ssl_client_cert_key_password
|
|
117
350
|
# ssl_ca_certs_from_system: config.ssl_ca_certs_from_system, # TODO: there is no corresponding librdkafka option. check what this does
|
|
118
351
|
# ssl_verify_hostname: config.ssl_verify_hostname, # check
|
|
119
|
-
|
|
120
|
-
"sasl.kerberos.keytab": config.sasl_gssapi_keytab
|
|
121
|
-
# sasl_plain_authzid: config.sasl_plain_authzid, # no corresponding librdkafka option, check
|
|
122
|
-
# 'sasl.username': config.sasl_plain_username,
|
|
123
|
-
# 'sasl.password': config.sasl_plain_password,
|
|
124
|
-
# 'sasl.username': config.sasl_scram_username,
|
|
125
|
-
# 'sasl.passord': config.sasl_scram_password,
|
|
126
|
-
# 'sasl.mechanism': config.sasl_scram_mechanism,
|
|
127
|
-
# sasl_over_ssl: config.sasl_over_ssl, # conditional value check again
|
|
128
|
-
# sasl_oauth_token_provider: config.sasl_oauth_token_provider, # cb code
|
|
129
|
-
# sasl_aws_msk_iam_access_key_id: config.sasl_aws_msk_iam_access_key_id, # not supported
|
|
130
|
-
# sasl_aws_msk_iam_secret_key_id: config.sasl_aws_msk_iam_secret_key_id, # not supported
|
|
131
|
-
# sasl_aws_msk_iam_session_token: config.sasl_aws_msk_iam_session_token, # not supported
|
|
132
|
-
# sasl_aws_msk_iam_aws_region: config.sasl_aws_msk_iam_aws_region # not supported
|
|
133
|
-
}
|
|
352
|
+
}.merge(sasl_options)
|
|
134
353
|
end
|
|
135
354
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
355
|
+
def handles
|
|
356
|
+
Thread.current[:delivery_boy_handles] ||= []
|
|
357
|
+
end
|
|
139
358
|
end
|
|
140
359
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DeliveryBoy
|
|
4
|
+
class Instrumenter
|
|
5
|
+
NAMESPACE = "delivery_boy"
|
|
6
|
+
|
|
7
|
+
def initialize(default_payload: {})
|
|
8
|
+
require "active_support/notifications"
|
|
9
|
+
@default_payload = default_payload
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def instrument(event_name, payload = {}, &block)
|
|
13
|
+
ActiveSupport::Notifications.instrument(
|
|
14
|
+
"#{event_name}.#{NAMESPACE}",
|
|
15
|
+
@default_payload.merge(payload),
|
|
16
|
+
&block
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class NullInstrumenter
|
|
22
|
+
def instrument(*, &block)
|
|
23
|
+
block&.call
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/delivery_boy/railtie.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module DeliveryBoy
|
|
2
4
|
class Railtie < Rails::Railtie
|
|
3
5
|
initializer "delivery_boy.load_config" do
|
|
@@ -12,12 +14,15 @@ module DeliveryBoy
|
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
if config.datadog_enabled
|
|
15
|
-
require "
|
|
17
|
+
require "delivery_boy/datadog"
|
|
18
|
+
|
|
19
|
+
DeliveryBoy::Datadog.host = config.datadog_host if config.datadog_host.present?
|
|
20
|
+
DeliveryBoy::Datadog.port = config.datadog_port if config.datadog_port.present?
|
|
21
|
+
DeliveryBoy::Datadog.namespace = config.datadog_namespace if config.datadog_namespace.present?
|
|
22
|
+
DeliveryBoy::Datadog.tags = config.datadog_tags if config.datadog_tags.present?
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Kafka::Datadog.namespace = config.datadog_namespace if config.datadog_namespace.present?
|
|
20
|
-
Kafka::Datadog.tags = config.datadog_tags if config.datadog_tags.present?
|
|
24
|
+
# Enable instrumentation
|
|
25
|
+
DeliveryBoy.instrumenter = DeliveryBoy::Instrumenter.new(default_payload: {})
|
|
21
26
|
end
|
|
22
27
|
end
|
|
23
28
|
end
|
data/lib/delivery_boy/version.rb
CHANGED
data/lib/delivery_boy.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "logger"
|
|
2
|
-
# require "kafka"
|
|
3
4
|
require "delivery_boy/version"
|
|
5
|
+
require "delivery_boy/instrumenter"
|
|
4
6
|
require "delivery_boy/instance"
|
|
5
7
|
require "delivery_boy/fake"
|
|
6
8
|
require "delivery_boy/config"
|
|
@@ -23,20 +25,19 @@ module DeliveryBoy
|
|
|
23
25
|
# @param partition_key [String, nil] a key used to deterministically assign
|
|
24
26
|
# a partition to the message.
|
|
25
27
|
# @return [nil]
|
|
26
|
-
# @raise [
|
|
27
|
-
# @raise [Kafka::DeliveryFailed] if delivery failed for some reason.
|
|
28
|
+
# @raise [Rdkafka::RdkafkaError] if delivery failed for some reason.
|
|
28
29
|
def deliver(value, topic:, **options)
|
|
29
30
|
instance.deliver(value, topic: topic, **options)
|
|
30
31
|
end
|
|
31
32
|
|
|
32
|
-
# Like {.deliver_async!}, but handles +
|
|
33
|
+
# Like {.deliver_async!}, but handles +Rdkafka::RdkafkaError+ errors
|
|
33
34
|
# by logging them and just going on with normal business.
|
|
34
35
|
#
|
|
35
36
|
# @return [nil]
|
|
36
37
|
def deliver_async(value, topic:, **options)
|
|
37
38
|
deliver_async!(value, topic: topic, **options)
|
|
38
|
-
rescue
|
|
39
|
-
logger.error "Message for `#{topic}` dropped due to
|
|
39
|
+
rescue Rdkafka::RdkafkaError => e
|
|
40
|
+
logger.error "Message for `#{topic}` dropped due to error: #{e.message}"
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
# Like {.deliver}, but returns immediately.
|
|
@@ -48,14 +49,14 @@ module DeliveryBoy
|
|
|
48
49
|
instance.deliver_async!(value, topic: topic, **options)
|
|
49
50
|
end
|
|
50
51
|
|
|
51
|
-
# Like {.produce!}, but handles +
|
|
52
|
+
# Like {.produce!}, but handles +Rdkafka::RdkafkaError+ errors
|
|
52
53
|
# by logging them and just going on with normal business.
|
|
53
54
|
#
|
|
54
55
|
# @return [nil]
|
|
55
56
|
def produce(value, topic:, **options)
|
|
56
57
|
produce!(value, topic: topic, **options)
|
|
57
|
-
rescue
|
|
58
|
-
logger.error "Message for `#{topic}` dropped due to
|
|
58
|
+
rescue Rdkafka::RdkafkaError => e
|
|
59
|
+
logger.error "Message for `#{topic}` dropped due to error: #{e.message}"
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
# Appends the given message to the producer buffer but does not send it until {.deliver_messages} is called.
|
|
@@ -68,7 +69,7 @@ module DeliveryBoy
|
|
|
68
69
|
# @param partition_key [String, nil] a key used to deterministically assign
|
|
69
70
|
# a partition to the message.
|
|
70
71
|
# @return [nil]
|
|
71
|
-
# @raise [
|
|
72
|
+
# @raise [Rdkafka::RdkafkaError] if the producer's buffer is full.
|
|
72
73
|
def produce!(value, topic:, **options)
|
|
73
74
|
instance.produce(value, topic: topic, **options)
|
|
74
75
|
end
|
|
@@ -76,7 +77,7 @@ module DeliveryBoy
|
|
|
76
77
|
# Delivers the items currently in the producer buffer.
|
|
77
78
|
#
|
|
78
79
|
# @return [nil]
|
|
79
|
-
# @raise [
|
|
80
|
+
# @raise [Rdkafka::RdkafkaError] if delivery failed for some reason.
|
|
80
81
|
def deliver_messages
|
|
81
82
|
instance.deliver_messages
|
|
82
83
|
end
|
|
@@ -114,6 +115,15 @@ module DeliveryBoy
|
|
|
114
115
|
|
|
115
116
|
attr_writer :logger
|
|
116
117
|
|
|
118
|
+
# The instrumenter used by DeliveryBoy for emitting metrics.
|
|
119
|
+
#
|
|
120
|
+
# @return [DeliveryBoy::Instrumenter, DeliveryBoy::NullInstrumenter]
|
|
121
|
+
def instrumenter
|
|
122
|
+
@instrumenter ||= NullInstrumenter.new
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
attr_writer :instrumenter
|
|
126
|
+
|
|
117
127
|
# The configuration used by DeliveryBoy.
|
|
118
128
|
#
|
|
119
129
|
# @return [DeliveryBoy::Config]
|
|
@@ -150,7 +160,7 @@ module DeliveryBoy
|
|
|
150
160
|
private
|
|
151
161
|
|
|
152
162
|
def instance
|
|
153
|
-
@instance ||= Instance.new(config, logger)
|
|
163
|
+
@instance ||= Instance.new(config, logger, instrumenter: instrumenter)
|
|
154
164
|
end
|
|
155
165
|
end
|
|
156
166
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: delivery_boy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.0.alpha.
|
|
4
|
+
version: 2.0.0.alpha.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Schierbeck
|
|
@@ -44,8 +44,8 @@ executables: []
|
|
|
44
44
|
extensions: []
|
|
45
45
|
extra_rdoc_files: []
|
|
46
46
|
files:
|
|
47
|
+
- ".github/CODEOWNERS"
|
|
47
48
|
- ".github/workflows/ci.yml"
|
|
48
|
-
- ".github/workflows/codeql.yaml"
|
|
49
49
|
- ".github/workflows/publish.yml"
|
|
50
50
|
- ".github/workflows/stale.yml"
|
|
51
51
|
- ".gitignore"
|
|
@@ -65,8 +65,10 @@ files:
|
|
|
65
65
|
- lib/delivery_boy.rb
|
|
66
66
|
- lib/delivery_boy/config.rb
|
|
67
67
|
- lib/delivery_boy/config_error.rb
|
|
68
|
+
- lib/delivery_boy/datadog.rb
|
|
68
69
|
- lib/delivery_boy/fake.rb
|
|
69
70
|
- lib/delivery_boy/instance.rb
|
|
71
|
+
- lib/delivery_boy/instrumenter.rb
|
|
70
72
|
- lib/delivery_boy/railtie.rb
|
|
71
73
|
- lib/delivery_boy/rspec.rb
|
|
72
74
|
- lib/delivery_boy/version.rb
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
name: "CodeQL public repository scanning"
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
schedule:
|
|
6
|
-
- cron: "0 0 * * *"
|
|
7
|
-
pull_request_target:
|
|
8
|
-
types: [opened, synchronize, reopened]
|
|
9
|
-
workflow_dispatch:
|
|
10
|
-
|
|
11
|
-
permissions:
|
|
12
|
-
contents: read
|
|
13
|
-
security-events: write
|
|
14
|
-
actions: read
|
|
15
|
-
packages: read
|
|
16
|
-
|
|
17
|
-
jobs:
|
|
18
|
-
trigger-codeql:
|
|
19
|
-
uses: zendesk/prodsec-code-scanning/.github/workflows/codeql_advanced_shared.yml@production
|