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.
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Karafka
5
+ VERSION = "1.0.0"
6
+ end
7
+ 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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "julewire/karafka"