julewire-karafka 1.0.0
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 +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +58 -0
- data/docs/advanced-configuration.md +8 -0
- data/docs/configuration.md +49 -0
- data/docs/consumer-events.md +33 -0
- data/docs/message-context.md +54 -0
- data/docs/silencing.md +35 -0
- data/docs/waterdrop.md +24 -0
- data/julewire-karafka.gemspec +41 -0
- data/lib/julewire/karafka/configuration.rb +52 -0
- data/lib/julewire/karafka/event_payload.rb +91 -0
- data/lib/julewire/karafka/event_severity.rb +108 -0
- data/lib/julewire/karafka/fork_hooks.rb +46 -0
- data/lib/julewire/karafka/installer.rb +31 -0
- data/lib/julewire/karafka/message_context.rb +42 -0
- data/lib/julewire/karafka/message_execution.rb +54 -0
- data/lib/julewire/karafka/messaging_attributes.rb +56 -0
- data/lib/julewire/karafka/monitor_listener.rb +82 -0
- data/lib/julewire/karafka/monitor_subscription.rb +166 -0
- data/lib/julewire/karafka/payload_reader.rb +74 -0
- data/lib/julewire/karafka/version.rb +7 -0
- data/lib/julewire/karafka/waterdrop_installer.rb +60 -0
- data/lib/julewire/karafka/waterdrop_middleware.rb +41 -0
- data/lib/julewire/karafka.rb +45 -0
- data/lib/julewire-karafka.rb +3 -0
- metadata +128 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Karafka
|
|
5
|
+
module MessageContext
|
|
6
|
+
class << self
|
|
7
|
+
def call(message, configuration:, fields: nil, &)
|
|
8
|
+
raise ArgumentError, "block required" unless block_given?
|
|
9
|
+
|
|
10
|
+
fields ||= PayloadReader.message_payload(message)
|
|
11
|
+
carrier = carrier_for(fields, configuration)
|
|
12
|
+
|
|
13
|
+
Julewire::Core::Propagation::Carrier.restore(carrier, key: configuration.carrier_key) do
|
|
14
|
+
Julewire::Core::Integration::Facade.with_neutral(message_neutral(fields)) do
|
|
15
|
+
Julewire::Core::Integration::Facade.with_attributes(message_attributes(fields), &)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def carrier_for(fields, configuration)
|
|
23
|
+
return {} unless configuration.propagation?
|
|
24
|
+
|
|
25
|
+
headers = fields[:headers].is_a?(Hash) ? fields[:headers] : {}
|
|
26
|
+
filter = configuration.carrier_filter
|
|
27
|
+
return headers unless filter
|
|
28
|
+
|
|
29
|
+
filtered = filter.call(headers, message: fields)
|
|
30
|
+
filtered.is_a?(Hash) ? filtered : {}
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
IntegrationHealth.record_failure(e, action: :carrier_filter, component: :message_context)
|
|
33
|
+
{}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def message_attributes(fields) = { karafka: fields }
|
|
37
|
+
|
|
38
|
+
def message_neutral(fields) = MessagingAttributes.message(fields)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Karafka
|
|
5
|
+
module MessageExecution
|
|
6
|
+
DEFAULT_TYPE = :karafka_message
|
|
7
|
+
DEFAULT_SUMMARY_EVENT = "message.completed"
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def call(message, configuration: Configuration.new, **options, &)
|
|
11
|
+
raise ArgumentError, "block required" unless block_given?
|
|
12
|
+
|
|
13
|
+
fields = PayloadReader.message_payload(message)
|
|
14
|
+
execution_fields = execution_fields(options)
|
|
15
|
+
type = execution_fields.delete(:type) || DEFAULT_TYPE
|
|
16
|
+
id = execution_fields.delete(:id) || execution_id(fields)
|
|
17
|
+
emit_summary = execution_fields.delete(:emit_summary) { true }
|
|
18
|
+
summary_event = execution_fields.delete(:summary_event) || DEFAULT_SUMMARY_EVENT
|
|
19
|
+
summary_severity = execution_fields.delete(:summary_severity)
|
|
20
|
+
summary_source = execution_fields.delete(:summary_source) || configuration.source
|
|
21
|
+
MessageContext.call(message, configuration: configuration, fields: fields) do
|
|
22
|
+
Julewire::Core::Integration::Facade.with_execution(
|
|
23
|
+
type: type,
|
|
24
|
+
id: id,
|
|
25
|
+
emit_summary: emit_summary,
|
|
26
|
+
fields: execution_fields,
|
|
27
|
+
summary_event: summary_event,
|
|
28
|
+
summary_severity: summary_severity,
|
|
29
|
+
summary_source: summary_source,
|
|
30
|
+
&
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def execution_fields(options)
|
|
38
|
+
values = Core::Integration::Values::Shape
|
|
39
|
+
fields = values.hash_or_empty(options)
|
|
40
|
+
fields.empty? ? {} : fields
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def execution_id(fields)
|
|
44
|
+
topic = fields[:topic]
|
|
45
|
+
partition = fields[:partition]
|
|
46
|
+
offset = fields[:offset]
|
|
47
|
+
return if topic.nil? || partition.nil? || offset.nil?
|
|
48
|
+
|
|
49
|
+
"#{topic}:#{partition}:#{offset}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Karafka
|
|
5
|
+
module MessagingAttributes
|
|
6
|
+
class << self
|
|
7
|
+
def message(fields)
|
|
8
|
+
Core::Fields::AttributeKeys.fields(
|
|
9
|
+
Core::Fields::AttributeKeys::MESSAGING_SYSTEM => "kafka",
|
|
10
|
+
Core::Fields::AttributeKeys::MESSAGING_OPERATION_NAME => "process",
|
|
11
|
+
Core::Fields::AttributeKeys::MESSAGING_OPERATION_TYPE => "process",
|
|
12
|
+
Core::Fields::AttributeKeys::MESSAGING_DESTINATION_NAME => fields[:topic],
|
|
13
|
+
Core::Fields::AttributeKeys::MESSAGING_DESTINATION_PARTITION_ID => string_value(fields[:partition]),
|
|
14
|
+
Core::Fields::AttributeKeys::MESSAGING_KAFKA_OFFSET => string_value(fields[:offset]),
|
|
15
|
+
Core::Fields::AttributeKeys::MESSAGING_KAFKA_MESSAGE_KEY => string_value(fields[:key])
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def monitor(name, payload, role:)
|
|
20
|
+
Core::Fields::AttributeKeys.fields(
|
|
21
|
+
Core::Fields::AttributeKeys::MESSAGING_SYSTEM => "kafka",
|
|
22
|
+
Core::Fields::AttributeKeys::MESSAGING_OPERATION_NAME => name.to_s,
|
|
23
|
+
Core::Fields::AttributeKeys::MESSAGING_OPERATION_TYPE => operation_type(name, role: role),
|
|
24
|
+
Core::Fields::AttributeKeys::MESSAGING_DESTINATION_NAME => payload[:topic] || payload.dig(:message, :topic),
|
|
25
|
+
Core::Fields::AttributeKeys::MESSAGING_DESTINATION_PARTITION_ID => string_value(
|
|
26
|
+
payload[:partition] || payload.dig(:message, :partition)
|
|
27
|
+
),
|
|
28
|
+
Core::Fields::AttributeKeys::MESSAGING_BATCH_MESSAGE_COUNT => message_count(payload),
|
|
29
|
+
Core::Fields::AttributeKeys::MESSAGING_CONSUMER_GROUP_NAME => payload[:consumer_group],
|
|
30
|
+
Core::Fields::AttributeKeys::MESSAGING_KAFKA_OFFSET => string_value(first_offset(payload))
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def message_count(payload)
|
|
37
|
+
payload[:messages_count] || payload.dig(:messages, :count)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def first_offset(payload)
|
|
41
|
+
payload[:first_offset] || payload.dig(:message, :offset)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def operation_type(name, role:)
|
|
45
|
+
return "send" if role == :producer
|
|
46
|
+
|
|
47
|
+
"receive" if name.to_s.include?("consume")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def string_value(value)
|
|
51
|
+
value&.to_s
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Karafka
|
|
5
|
+
class MonitorListener
|
|
6
|
+
Profile = Data.define(
|
|
7
|
+
:component,
|
|
8
|
+
:event_prefix,
|
|
9
|
+
:logger_name,
|
|
10
|
+
:messaging_role,
|
|
11
|
+
:config_method,
|
|
12
|
+
:important_events,
|
|
13
|
+
:severity
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
CONSUMER_PROFILE = Profile.new(
|
|
17
|
+
component: :listener,
|
|
18
|
+
event_prefix: "karafka",
|
|
19
|
+
logger_name: "Karafka.monitor",
|
|
20
|
+
messaging_role: :consumer,
|
|
21
|
+
config_method: :consumer_event_names,
|
|
22
|
+
important_events: Configuration::IMPORTANT_CONSUMER_EVENT_NAMES,
|
|
23
|
+
severity: ->(name, event, payload) { EventSeverity.consumer(name, event: event, payload: payload) }
|
|
24
|
+
).freeze
|
|
25
|
+
PRODUCER_PROFILE = Profile.new(
|
|
26
|
+
component: :waterdrop_listener,
|
|
27
|
+
event_prefix: "waterdrop",
|
|
28
|
+
logger_name: "WaterDrop.monitor",
|
|
29
|
+
messaging_role: :producer,
|
|
30
|
+
config_method: :producer_event_names,
|
|
31
|
+
important_events: Configuration::IMPORTANT_PRODUCER_EVENT_NAMES,
|
|
32
|
+
severity: ->(name, _event, payload) { EventSeverity.producer(name, payload) }
|
|
33
|
+
).freeze
|
|
34
|
+
private_constant :Profile, :CONSUMER_PROFILE, :PRODUCER_PROFILE
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
def consumer(configuration = Configuration.new)
|
|
38
|
+
new(configuration, profile: CONSUMER_PROFILE)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def producer(configuration = Configuration.new)
|
|
42
|
+
new(configuration, profile: PRODUCER_PROFILE)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize(configuration = Configuration.new, profile:)
|
|
47
|
+
@configuration = configuration
|
|
48
|
+
@profile = profile
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
attr_writer :configuration
|
|
52
|
+
|
|
53
|
+
def emit(name, event)
|
|
54
|
+
IntegrationHealth.with_failure_health(
|
|
55
|
+
action: :emit,
|
|
56
|
+
component: @profile.component,
|
|
57
|
+
event: name
|
|
58
|
+
) do
|
|
59
|
+
payload = EventPayload.call(name, event)
|
|
60
|
+
Core::Integration::Facade.emit(
|
|
61
|
+
severity: @profile.severity.call(name, event, payload),
|
|
62
|
+
event: "#{@profile.event_prefix}.#{name.tr(".", "_")}",
|
|
63
|
+
logger: @profile.logger_name,
|
|
64
|
+
source: @configuration.source,
|
|
65
|
+
error: EventPayload.error(event),
|
|
66
|
+
neutral: messaging_attributes(name, payload),
|
|
67
|
+
attributes: event_attributes(name, payload)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def event_attributes(_name, payload)
|
|
74
|
+
Core.deep_compact_empty(@profile.event_prefix.to_sym => payload)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def messaging_attributes(name, payload)
|
|
78
|
+
MessagingAttributes.monitor(name, payload, role: @profile.messaging_role)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Karafka
|
|
5
|
+
module MonitorSubscription
|
|
6
|
+
PROFILE_CONSTANTS = {
|
|
7
|
+
consumer: :CONSUMER_PROFILE,
|
|
8
|
+
producer: :PRODUCER_PROFILE
|
|
9
|
+
}.freeze
|
|
10
|
+
private_constant :PROFILE_CONSTANTS
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def install!(monitor, profile:, configuration: Configuration.new)
|
|
14
|
+
profile = monitor_listener_profile(profile)
|
|
15
|
+
state = subscription_state(monitor, profile)
|
|
16
|
+
listener = listener_for(state, configuration, profile)
|
|
17
|
+
subscriptions = subscriptions_for(state)
|
|
18
|
+
desired_events = event_names(monitor, configuration, profile)
|
|
19
|
+
|
|
20
|
+
unsubscribe_removed_events(monitor, subscriptions, desired_events, profile)
|
|
21
|
+
|
|
22
|
+
desired_events.each do |event_name|
|
|
23
|
+
next if subscriptions.key?(event_name)
|
|
24
|
+
|
|
25
|
+
callback = ->(event) { listener.emit(event_name, event) }
|
|
26
|
+
subscriptions[event_name] = callback if subscribe_event(monitor, event_name, profile, &callback)
|
|
27
|
+
end
|
|
28
|
+
store_subscription_state(monitor, listener: listener, subscriptions: subscriptions, profile: profile)
|
|
29
|
+
listener
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def monitor_listener_profile(profile)
|
|
35
|
+
constant_name = PROFILE_CONSTANTS.fetch(profile) { return profile }
|
|
36
|
+
MonitorListener.const_get(constant_name, false)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def listener_for(state, configuration, profile)
|
|
40
|
+
listener = state && state[:listener]
|
|
41
|
+
if listener
|
|
42
|
+
listener.configuration = configuration
|
|
43
|
+
listener
|
|
44
|
+
else
|
|
45
|
+
MonitorListener.new(configuration, profile: profile)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def subscriptions_for(state)
|
|
50
|
+
return {} unless state
|
|
51
|
+
|
|
52
|
+
state[:subscriptions].is_a?(Hash) ? state[:subscriptions].dup : {}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def subscription_state(monitor, profile)
|
|
56
|
+
subscription_state_store(profile).fetch(monitor)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def store_subscription_state(monitor, listener:, subscriptions:, profile:)
|
|
60
|
+
subscription_state_store(profile).store(
|
|
61
|
+
monitor,
|
|
62
|
+
{ listener: listener, subscriptions: subscriptions.freeze }.freeze
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def install_marker(profile)
|
|
67
|
+
:"@julewire_karafka_#{profile.component}_state"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def subscription_state_store(profile)
|
|
71
|
+
Core::Integration::IvarState.new(install_marker(profile))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def event_names(monitor, configuration, profile)
|
|
75
|
+
configured = configuration.public_send(profile.config_method)
|
|
76
|
+
return profile.important_events if configured == :important
|
|
77
|
+
|
|
78
|
+
if all_events?(configured)
|
|
79
|
+
available_events = available_events_for(monitor)
|
|
80
|
+
return available_events unless available_events.empty?
|
|
81
|
+
|
|
82
|
+
return profile.important_events
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
Array(configured)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def available_events_for(monitor)
|
|
89
|
+
available = direct_available_events(monitor)
|
|
90
|
+
return available unless available.empty?
|
|
91
|
+
|
|
92
|
+
available = notification_bus_available_events(monitor)
|
|
93
|
+
return available unless available.empty?
|
|
94
|
+
|
|
95
|
+
listener_event_names(monitor)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def direct_available_events(monitor)
|
|
99
|
+
return [] unless monitor.respond_to?(:available_events)
|
|
100
|
+
|
|
101
|
+
Array(monitor.available_events)
|
|
102
|
+
rescue StandardError
|
|
103
|
+
[]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def notification_bus_available_events(monitor)
|
|
107
|
+
return [] unless monitor.respond_to?(:notifications_bus)
|
|
108
|
+
|
|
109
|
+
bus = monitor.notifications_bus
|
|
110
|
+
return [] unless bus.respond_to?(:available_events)
|
|
111
|
+
|
|
112
|
+
Array(bus.available_events)
|
|
113
|
+
rescue StandardError
|
|
114
|
+
[]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def listener_event_names(monitor)
|
|
118
|
+
return [] unless monitor.respond_to?(:listeners)
|
|
119
|
+
|
|
120
|
+
listeners = monitor.listeners
|
|
121
|
+
listeners.is_a?(Hash) ? listeners.keys : []
|
|
122
|
+
rescue StandardError
|
|
123
|
+
[]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def all_events?(configured)
|
|
127
|
+
%i[all available].include?(configured)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def unsubscribe_removed_events(monitor, subscriptions, desired_events, profile)
|
|
131
|
+
return unless monitor.respond_to?(:unsubscribe)
|
|
132
|
+
|
|
133
|
+
desired = desired_events.to_h { [it, true] }
|
|
134
|
+
subscriptions.each_key.to_a.each do |event_name|
|
|
135
|
+
next if desired.key?(event_name)
|
|
136
|
+
|
|
137
|
+
callback = subscriptions.delete(event_name)
|
|
138
|
+
unsubscribe_event(monitor, event_name, callback, profile)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def unsubscribe_event(monitor, event_name, callback, profile)
|
|
143
|
+
IntegrationHealth.with_failure_health(
|
|
144
|
+
action: :unsubscribe,
|
|
145
|
+
component: profile.component,
|
|
146
|
+
event: event_name
|
|
147
|
+
) do
|
|
148
|
+
monitor.unsubscribe(callback || event_name)
|
|
149
|
+
true
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def subscribe_event(monitor, event_name, profile, &)
|
|
154
|
+
return false unless monitor.respond_to?(:subscribe)
|
|
155
|
+
|
|
156
|
+
IntegrationHealth.with_failure_health(action: :subscribe, component: profile.component, event: event_name) do
|
|
157
|
+
monitor.subscribe(event_name, &)
|
|
158
|
+
true
|
|
159
|
+
end || false
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private_constant :MonitorSubscription
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Karafka
|
|
5
|
+
module PayloadReader
|
|
6
|
+
class << self
|
|
7
|
+
def consumer_payload(payload)
|
|
8
|
+
consumer = value(payload, :caller)
|
|
9
|
+
message_set = value(consumer, :messages)
|
|
10
|
+
metadata = value(message_set, :metadata)
|
|
11
|
+
messages = messages_for(consumer)
|
|
12
|
+
first = messages.first
|
|
13
|
+
last = messages.last
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
consumer_class: consumer&.class&.name,
|
|
17
|
+
consumer_id: value(consumer, :id),
|
|
18
|
+
consumer_group: nested_value(consumer, :topic, :consumer_group, :id),
|
|
19
|
+
subscription_group: nested_value(consumer, :topic, :subscription_group, :id),
|
|
20
|
+
topic: consumer_topic(consumer, metadata, first),
|
|
21
|
+
partition: consumer_partition(consumer, metadata, first),
|
|
22
|
+
messages_count: count(messages, metadata),
|
|
23
|
+
first_offset: value(metadata, :first_offset) || value(first, :offset),
|
|
24
|
+
last_offset: value(metadata, :last_offset) || value(last, :offset),
|
|
25
|
+
processing_lag_ms: value(metadata, :processing_lag),
|
|
26
|
+
consumption_lag_ms: value(metadata, :consumption_lag)
|
|
27
|
+
}.compact
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def message_payload(message)
|
|
31
|
+
{
|
|
32
|
+
topic: value(message, :topic),
|
|
33
|
+
partition: value(message, :partition),
|
|
34
|
+
offset: value(message, :offset),
|
|
35
|
+
key: value(message, :key),
|
|
36
|
+
headers: headers(message)
|
|
37
|
+
}.compact
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def messages_for(consumer)
|
|
41
|
+
messages = value(consumer, :messages)
|
|
42
|
+
return Array(messages) if messages
|
|
43
|
+
|
|
44
|
+
message = value(consumer, :message)
|
|
45
|
+
message ? [message] : []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def headers(message)
|
|
49
|
+
value(message, :headers) || {}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def count(messages, metadata)
|
|
53
|
+
value(metadata, :size) || messages.size.nonzero?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def consumer_topic(consumer, metadata, first)
|
|
57
|
+
nested_value(consumer, :topic, :name) || value(metadata, :topic) || value(first, :topic)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def consumer_partition(consumer, metadata, first)
|
|
61
|
+
value(consumer, :partition) || value(metadata, :partition) || value(first, :partition)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def nested_value(object, *method_names)
|
|
65
|
+
Core::Integration::Values::Read.nested_value(object, *method_names)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def value(object, method_name)
|
|
69
|
+
Core::Integration::Values::Read.value(object, method_name)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Karafka
|
|
5
|
+
module WaterdropInstaller
|
|
6
|
+
MIDDLEWARE_INSTALL = Core::Integration::IvarState.new(:@julewire_karafka_waterdrop_middleware)
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def install!(producer, configuration: Configuration.new)
|
|
10
|
+
return false unless configuration.enabled?
|
|
11
|
+
|
|
12
|
+
install_or_update_middleware(producer, configuration) if middleware_needed?(producer, configuration)
|
|
13
|
+
install_listener(producer, configuration) if configuration.producer_events?
|
|
14
|
+
producer
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def middleware_needed?(producer, configuration)
|
|
20
|
+
configuration.propagation? || installed_middleware(producer)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def install_or_update_middleware(producer, configuration)
|
|
24
|
+
install_middleware(producer, configuration)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def install_middleware(producer, configuration)
|
|
28
|
+
existing = MIDDLEWARE_INSTALL.fetch(producer)
|
|
29
|
+
if existing
|
|
30
|
+
existing.configuration = configuration
|
|
31
|
+
return existing
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
middleware = producer.middleware if producer.respond_to?(:middleware)
|
|
35
|
+
return unless middleware.respond_to?(:prepend)
|
|
36
|
+
|
|
37
|
+
installed = WaterdropMiddleware.new(configuration: configuration)
|
|
38
|
+
middleware.prepend(installed)
|
|
39
|
+
MIDDLEWARE_INSTALL.store(producer, installed)
|
|
40
|
+
installed
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
IntegrationHealth.record_failure(e, action: :install, component: :waterdrop_installer)
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def install_listener(producer, configuration)
|
|
47
|
+
monitor = producer.monitor if producer.respond_to?(:monitor)
|
|
48
|
+
MonitorSubscription.install!(monitor, configuration: configuration, profile: :producer) if monitor
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
IntegrationHealth.record_failure(e, action: :install, component: :waterdrop_installer)
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def installed_middleware(producer)
|
|
55
|
+
MIDDLEWARE_INSTALL.fetch(producer)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Karafka
|
|
5
|
+
class WaterdropMiddleware
|
|
6
|
+
def initialize(configuration: Configuration.new)
|
|
7
|
+
@configuration = configuration
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_writer :configuration
|
|
11
|
+
|
|
12
|
+
def call(message)
|
|
13
|
+
inject_carrier(message) if @configuration.propagation?
|
|
14
|
+
message
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def inject_carrier(message)
|
|
20
|
+
IntegrationHealth.with_failure_health(action: :carrier_inject, component: :waterdrop_middleware) do
|
|
21
|
+
headers = headers_for(message)
|
|
22
|
+
Julewire::Core::Propagation::Carrier.inject(
|
|
23
|
+
headers,
|
|
24
|
+
key: @configuration.carrier_key,
|
|
25
|
+
max_bytes: @configuration.carrier_max_bytes
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def headers_for(message)
|
|
31
|
+
if message.respond_to?(:headers)
|
|
32
|
+
message.headers ||= {}
|
|
33
|
+
elsif message.is_a?(Hash)
|
|
34
|
+
message[:headers] ||= {}
|
|
35
|
+
else
|
|
36
|
+
{}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zeitwerk"
|
|
4
|
+
require "julewire/core"
|
|
5
|
+
|
|
6
|
+
module Julewire
|
|
7
|
+
module Karafka
|
|
8
|
+
class Error < Julewire::Error; end
|
|
9
|
+
IntegrationHealth = Core::Integration::Health.scoped(:karafka)
|
|
10
|
+
|
|
11
|
+
extend Core::Integration::Configurable
|
|
12
|
+
|
|
13
|
+
configurable_with { Configuration }
|
|
14
|
+
|
|
15
|
+
InstallResult = Data.define(:consumer, :producer)
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def install!(app: nil, monitor: nil, producer: nil, consumer: true, configuration: config)
|
|
19
|
+
return false unless configuration.enabled?
|
|
20
|
+
|
|
21
|
+
consumer_result = Installer.install!(app: app, monitor: monitor, configuration: configuration) if consumer
|
|
22
|
+
producer_result = WaterdropInstaller.install!(producer, configuration: configuration) if producer
|
|
23
|
+
|
|
24
|
+
return InstallResult.new(consumer_result, producer_result) if consumer && producer
|
|
25
|
+
|
|
26
|
+
producer ? producer_result : consumer_result
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def inject!(message, configuration: config)
|
|
30
|
+
WaterdropMiddleware.new(configuration: configuration).call(message)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def with_message(message, configuration: config, &)
|
|
34
|
+
MessageContext.call(message, configuration: configuration, &)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def with_message_execution(message, configuration: config, **execution_options, &)
|
|
38
|
+
MessageExecution.call(message, configuration: configuration, **execution_options, &)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
loader = Zeitwerk::Loader.for_gem_extension(self)
|
|
44
|
+
loader.setup
|
|
45
|
+
end
|