lepus 0.0.1.beta2 → 0.1.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 +4 -4
- data/.github/workflows/linter.yml +21 -0
- data/.github/workflows/specs.yml +93 -13
- data/.gitignore +2 -0
- data/.rubocop.yml +10 -0
- data/.tool-versions +1 -1
- data/Gemfile +7 -0
- data/Gemfile.lock +36 -9
- data/Makefile +19 -0
- data/README.md +562 -7
- data/bin/setup +5 -2
- data/config.ru +14 -0
- data/docker-compose.yml +5 -3
- data/docs/README.md +80 -0
- data/docs/cli.md +108 -0
- data/docs/configuration.md +171 -0
- data/docs/consumers.md +168 -0
- data/docs/getting-started.md +136 -0
- data/docs/images/lepus-web.png +0 -0
- data/docs/middleware.md +240 -0
- data/docs/producers.md +173 -0
- data/docs/prometheus.md +112 -0
- data/docs/rails.md +161 -0
- data/docs/supervisor.md +112 -0
- data/docs/testing.md +141 -0
- data/docs/web.md +85 -0
- data/examples/grafana-dashboard.json +450 -0
- data/gemfiles/Gemfile.rails-5.2 +7 -0
- data/gemfiles/{rails52.gemfile.lock → Gemfile.rails-5.2.lock} +102 -69
- data/gemfiles/Gemfile.rails-6.1 +7 -0
- data/gemfiles/{rails61.gemfile.lock → Gemfile.rails-6.1.lock} +113 -79
- data/gemfiles/{rails52.gemfile → Gemfile.rails-7.2} +1 -1
- data/gemfiles/Gemfile.rails-7.2.lock +321 -0
- data/gemfiles/{rails61.gemfile → Gemfile.rails-8.0} +1 -1
- data/gemfiles/Gemfile.rails-8.0.lock +322 -0
- data/lepus.gemspec +7 -1
- data/lib/lepus/cli.rb +35 -4
- data/lib/lepus/configuration.rb +107 -0
- data/lib/lepus/connection_pool.rb +135 -0
- data/lib/lepus/consumer.rb +59 -41
- data/lib/lepus/consumers/config.rb +183 -0
- data/lib/lepus/consumers/handler.rb +56 -0
- data/lib/lepus/consumers/middleware_chain.rb +22 -0
- data/lib/lepus/consumers/middlewares/exception_logger.rb +27 -0
- data/lib/lepus/consumers/middlewares/honeybadger.rb +33 -0
- data/lib/lepus/consumers/middlewares/json.rb +37 -0
- data/lib/lepus/consumers/middlewares/max_retry.rb +83 -0
- data/lib/lepus/consumers/middlewares/unique.rb +65 -0
- data/lib/lepus/consumers/stats.rb +70 -0
- data/lib/lepus/consumers/stats_registry.rb +29 -0
- data/lib/lepus/consumers/worker.rb +141 -0
- data/lib/lepus/consumers/worker_factory.rb +124 -0
- data/lib/lepus/consumers.rb +6 -0
- data/lib/lepus/message/delivery_info.rb +72 -0
- data/lib/lepus/message/metadata.rb +99 -0
- data/lib/lepus/message.rb +88 -5
- data/lib/lepus/middleware_chain.rb +83 -0
- data/lib/lepus/primitive/hash.rb +29 -0
- data/lib/lepus/process.rb +24 -24
- data/lib/lepus/process_registry/backend.rb +49 -0
- data/lib/lepus/process_registry/file_backend.rb +108 -0
- data/lib/lepus/process_registry/message_builder.rb +72 -0
- data/lib/lepus/process_registry/rabbitmq_backend.rb +153 -0
- data/lib/lepus/process_registry.rb +56 -23
- data/lib/lepus/processes/base.rb +0 -5
- data/lib/lepus/processes/callbacks.rb +3 -0
- data/lib/lepus/processes/interruptible.rb +4 -8
- data/lib/lepus/processes/procline.rb +1 -1
- data/lib/lepus/processes/registrable.rb +1 -1
- data/lib/lepus/processes/runnable.rb +1 -1
- data/lib/lepus/processes.rb +15 -0
- data/lib/lepus/producer.rb +141 -30
- data/lib/lepus/producers/config.rb +46 -0
- data/lib/lepus/producers/definition.rb +48 -0
- data/lib/lepus/producers/hooks.rb +170 -0
- data/lib/lepus/producers/middleware_chain.rb +22 -0
- data/lib/lepus/producers/middlewares/correlation_id.rb +37 -0
- data/lib/lepus/producers/middlewares/header.rb +47 -0
- data/lib/lepus/producers/middlewares/instrumentation.rb +30 -0
- data/lib/lepus/producers/middlewares/json.rb +47 -0
- data/lib/lepus/producers/middlewares/unique.rb +67 -0
- data/lib/lepus/producers.rb +7 -0
- data/lib/lepus/prometheus/collector.rb +149 -0
- data/lib/lepus/prometheus/instrumentation.rb +168 -0
- data/lib/lepus/prometheus.rb +48 -0
- data/lib/lepus/publisher.rb +67 -0
- data/lib/lepus/supervisor/children_pipes.rb +25 -0
- data/lib/lepus/supervisor/lifecycle_hooks.rb +50 -0
- data/lib/lepus/supervisor/pidfiled.rb +1 -1
- data/lib/lepus/supervisor/registry_cleaner.rb +22 -0
- data/lib/lepus/supervisor.rb +129 -25
- data/lib/lepus/testing/exchange.rb +95 -0
- data/lib/lepus/testing/message_builder.rb +177 -0
- data/lib/lepus/testing/rspec_matchers.rb +258 -0
- data/lib/lepus/testing.rb +210 -0
- data/lib/lepus/unique.rb +18 -0
- data/lib/lepus/version.rb +1 -1
- data/lib/lepus/web/aggregator.rb +154 -0
- data/lib/lepus/web/api.rb +132 -0
- data/lib/lepus/web/app.rb +37 -0
- data/lib/lepus/web/management_api.rb +192 -0
- data/lib/lepus/web/respond_with.rb +28 -0
- data/lib/lepus/web.rb +238 -0
- data/lib/lepus.rb +39 -28
- data/test_offline.html +189 -0
- data/web/assets/css/styles.css +635 -0
- data/web/assets/js/app.js +6 -0
- data/web/assets/js/bootstrap.js +20 -0
- data/web/assets/js/controllers/connection_controller.js +44 -0
- data/web/assets/js/controllers/dashboard_controller.js +499 -0
- data/web/assets/js/controllers/queue_controller.js +17 -0
- data/web/assets/js/controllers/theme_controller.js +31 -0
- data/web/assets/js/offline-manager.js +233 -0
- data/web/assets/js/service-worker-manager.js +65 -0
- data/web/index.html +159 -0
- data/web/sw.js +144 -0
- metadata +177 -18
- data/lib/lepus/consumer_config.rb +0 -149
- data/lib/lepus/consumer_wrapper.rb +0 -46
- data/lib/lepus/lifecycle_hooks.rb +0 -49
- data/lib/lepus/middlewares/honeybadger.rb +0 -23
- data/lib/lepus/middlewares/json.rb +0 -35
- data/lib/lepus/middlewares/max_retry.rb +0 -57
- data/lib/lepus/processes/consumer.rb +0 -113
- data/lib/lepus/supervisor/config.rb +0 -45
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Producers
|
|
5
|
+
# Definition class for producer-specific settings
|
|
6
|
+
class Definition
|
|
7
|
+
attr_reader :publish_options
|
|
8
|
+
|
|
9
|
+
def initialize(options = {})
|
|
10
|
+
opts = Lepus::Primitive::Hash.new(options).deep_symbolize_keys
|
|
11
|
+
|
|
12
|
+
# Handle exchange configuration
|
|
13
|
+
exchange_config = opts.delete(:exchange) || {}
|
|
14
|
+
@exchange_options = Lepus::Publisher::DEFAULT_EXCHANGE_OPTIONS.merge(declaration_config(exchange_config))
|
|
15
|
+
|
|
16
|
+
# Handle default publish options
|
|
17
|
+
@publish_options = Lepus::Publisher::DEFAULT_PUBLISH_OPTIONS.merge(opts.delete(:publish) || {})
|
|
18
|
+
|
|
19
|
+
# Store any remaining options for future use
|
|
20
|
+
@options = opts
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def exchange_name
|
|
24
|
+
@exchange_options[:name] || raise(InvalidProducerConfigError, "Exchange name is required")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def exchange_options
|
|
28
|
+
@exchange_options.reject { |k, v| k == :name }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Normalizes a declaration config (for exchanges) into a configuration Hash.
|
|
34
|
+
#
|
|
35
|
+
# If the given `value` is a String, convert it to a Hash with the key `:name` and the value.
|
|
36
|
+
# If the given `value` is a Hash, leave it as is.
|
|
37
|
+
def declaration_config(value)
|
|
38
|
+
case value
|
|
39
|
+
when Hash then value
|
|
40
|
+
when String then {name: value}
|
|
41
|
+
when Symbol then {name: value.to_s}
|
|
42
|
+
when NilClass then {}
|
|
43
|
+
when TrueClass then {}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Producers
|
|
5
|
+
module Hooks
|
|
6
|
+
KEY = :lepus_producers
|
|
7
|
+
|
|
8
|
+
def self.reset!
|
|
9
|
+
Thread.current[KEY] = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Global enable publishing callbacks. If no producer/exchange is specified, all producers will be enabled.
|
|
13
|
+
# @param targets [Array<Lepus::Producer, String, Symbol>] Producer classes, exchange names, or both
|
|
14
|
+
# @return [void]
|
|
15
|
+
def enable!(*targets)
|
|
16
|
+
if targets.empty?
|
|
17
|
+
# Enable all producers
|
|
18
|
+
all_producers.each { |producer| repo[:producers][producer] = true }
|
|
19
|
+
else
|
|
20
|
+
targets.each do |target|
|
|
21
|
+
case target
|
|
22
|
+
when Class
|
|
23
|
+
ensure_producer_class(target)
|
|
24
|
+
repo[:producers][target] = true
|
|
25
|
+
when String, Symbol
|
|
26
|
+
exchange_name = target.to_s
|
|
27
|
+
repo[:exchanges][exchange_name] = true
|
|
28
|
+
else
|
|
29
|
+
raise ArgumentError, "Invalid producer or exchange name: #{target.inspect}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Global disable publishing callbacks. If no producer/exchange is specified, all producers will be disabled.
|
|
36
|
+
# @param targets [Array<Lepus::Producer, String, Symbol>] Producer classes, exchange names, or both
|
|
37
|
+
# @return [void]
|
|
38
|
+
def disable!(*targets)
|
|
39
|
+
if targets.empty?
|
|
40
|
+
# Disable all producers
|
|
41
|
+
all_producers.each { |producer| repo[:producers][producer] = false }
|
|
42
|
+
else
|
|
43
|
+
targets.each do |target|
|
|
44
|
+
case target
|
|
45
|
+
when Class
|
|
46
|
+
ensure_producer_class(target)
|
|
47
|
+
repo[:producers][target] = false
|
|
48
|
+
when String, Symbol
|
|
49
|
+
exchange_name = target.to_s
|
|
50
|
+
repo[:exchanges][exchange_name] = false
|
|
51
|
+
else
|
|
52
|
+
raise ArgumentError, "Invalid producer or exchange name: #{target.inspect}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if the given producer is enabled for publishing. If no producer is specified, all producers will be checked.
|
|
59
|
+
#
|
|
60
|
+
# @param producers [Array<Lepus::Producer>]
|
|
61
|
+
# @return [Boolean]
|
|
62
|
+
def disabled?(*producers)
|
|
63
|
+
if producers.empty?
|
|
64
|
+
all_producers.all? { |producer| !producer_enabled?(producer) }
|
|
65
|
+
else
|
|
66
|
+
producers.all? { |producer| !producer_enabled?(producer) }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Check if the given producer is enabled for publishing. If no producer is specified, all producers will be checked.
|
|
71
|
+
#
|
|
72
|
+
# @param producers [Array<Lepus::Producer>]
|
|
73
|
+
# @return [Boolean]
|
|
74
|
+
def enabled?(*producers)
|
|
75
|
+
if producers.empty?
|
|
76
|
+
all_producers.all? { |producer| producer_enabled?(producer) }
|
|
77
|
+
else
|
|
78
|
+
producers.all? { |producer| producer_enabled?(producer) }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check if the given exchange is enabled for publishing.
|
|
83
|
+
#
|
|
84
|
+
# @param exchange_name [String] The exchange name to check
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def exchange_enabled?(exchange_name)
|
|
87
|
+
# Check if exchange is explicitly configured
|
|
88
|
+
if repo[:exchanges].key?(exchange_name)
|
|
89
|
+
return repo[:exchanges][exchange_name]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Find all producers that use this exchange
|
|
93
|
+
matching_producers = all_producers.select do |producer|
|
|
94
|
+
producer.definition&.exchange_name == exchange_name
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# If no producers use this exchange, consider it enabled by default
|
|
98
|
+
return true if matching_producers.empty?
|
|
99
|
+
|
|
100
|
+
# Check if all matching producers are enabled
|
|
101
|
+
matching_producers.all? { |producer| producer_enabled?(producer) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Disable publishing callbacks execution for the block execution.
|
|
105
|
+
# Example:
|
|
106
|
+
# Lepus::Producers.without_publishing { User.create! }
|
|
107
|
+
# Lepus::Producers.without_publishing(UsersIndex, "exchange_name") { User.create! }
|
|
108
|
+
def without_publishing(*targets)
|
|
109
|
+
state_before_disable = deep_copy_repo
|
|
110
|
+
disable!(*targets)
|
|
111
|
+
|
|
112
|
+
yield
|
|
113
|
+
ensure
|
|
114
|
+
restore_repo(state_before_disable)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Enable the publishing callbacks execution for the block execution.
|
|
118
|
+
# Example:
|
|
119
|
+
# Lepus::Producers.with_publishing { User.create! }
|
|
120
|
+
# Lepus::Producers.with_publishing(UsersIndex, "exchange_name") { User.create! }
|
|
121
|
+
def with_publishing(*targets)
|
|
122
|
+
state_before_enable = deep_copy_repo
|
|
123
|
+
enable!(*targets)
|
|
124
|
+
|
|
125
|
+
yield
|
|
126
|
+
ensure
|
|
127
|
+
restore_repo(state_before_enable)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def all_producers
|
|
133
|
+
Lepus::Producer.descendants.reject(&:abstract_class?)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Check if a specific producer is enabled
|
|
137
|
+
def producer_enabled?(producer)
|
|
138
|
+
repo[:producers][producer]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def ensure_producer_class(value)
|
|
142
|
+
(value <= Lepus::Producer) ? value : raise(ArgumentError, "Invalid producer class: #{value.inspect}")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def deep_copy_repo
|
|
146
|
+
{
|
|
147
|
+
producers: repo[:producers].dup,
|
|
148
|
+
exchanges: repo[:exchanges].dup
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def restore_repo(saved_state)
|
|
153
|
+
Thread.current[KEY] = saved_state
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Data Structure:
|
|
157
|
+
#
|
|
158
|
+
# {
|
|
159
|
+
# producers: { <Lepus::Producer class> => <true|false>, ... },
|
|
160
|
+
# exchanges: { <String> => <true|false>, ... }
|
|
161
|
+
# }
|
|
162
|
+
def repo
|
|
163
|
+
Thread.current[KEY] ||= {
|
|
164
|
+
producers: all_producers.map { |k| [k, true] }.to_h,
|
|
165
|
+
exchanges: {}
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Producers
|
|
5
|
+
# Manages middleware registration and execution for producers.
|
|
6
|
+
# Middlewares can modify the message (payload, headers, routing_key, etc.)
|
|
7
|
+
# before it is published to RabbitMQ.
|
|
8
|
+
class MiddlewareChain < Lepus::MiddlewareChain
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def load_middleware(name, opts)
|
|
12
|
+
require_relative "middlewares/#{name}"
|
|
13
|
+
class_name = Primitive::String.new(name.to_s).classify
|
|
14
|
+
class_name = "JSON" if class_name == "Json"
|
|
15
|
+
klass = Lepus::Producers::Middlewares.const_get(class_name)
|
|
16
|
+
klass.new(**opts)
|
|
17
|
+
rescue LoadError, NameError => e
|
|
18
|
+
raise ArgumentError, "Producer middleware '#{name}' not found: #{e.message}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Producers
|
|
5
|
+
module Middlewares
|
|
6
|
+
# A middleware that auto-generates a correlation_id if missing.
|
|
7
|
+
class CorrelationId < Lepus::Middleware
|
|
8
|
+
# @param opts [Hash] The options for the middleware.
|
|
9
|
+
# @option opts [Proc, nil] :generator A custom generator proc.
|
|
10
|
+
# Defaults to SecureRandom.uuid.
|
|
11
|
+
def initialize(**opts)
|
|
12
|
+
super
|
|
13
|
+
@generator = opts.fetch(:generator, -> { SecureRandom.uuid })
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(message, app)
|
|
17
|
+
if message.metadata&.correlation_id.nil? || message.metadata.correlation_id.to_s.empty?
|
|
18
|
+
correlation_id = generator.respond_to?(:call) ? generator.call : generator.to_s
|
|
19
|
+
new_metadata = update_metadata(message.metadata, correlation_id: correlation_id)
|
|
20
|
+
message = message.mutate(metadata: new_metadata)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
app.call(message)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :generator
|
|
29
|
+
|
|
30
|
+
def update_metadata(metadata, **attrs)
|
|
31
|
+
current = metadata&.to_h || {}
|
|
32
|
+
Message::Metadata.new(**current.merge(attrs))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Producers
|
|
5
|
+
module Middlewares
|
|
6
|
+
# A middleware that adds default headers to messages.
|
|
7
|
+
# Headers can be static values or dynamic procs.
|
|
8
|
+
class Header < Lepus::Middleware
|
|
9
|
+
# @param opts [Hash] The options for the middleware.
|
|
10
|
+
# @option opts [Hash] :defaults ({}) Default headers to add.
|
|
11
|
+
# Values can be Procs that will be called with the message.
|
|
12
|
+
def initialize(**opts)
|
|
13
|
+
super
|
|
14
|
+
@defaults = opts.fetch(:defaults, {})
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(message, app)
|
|
18
|
+
new_headers = resolve_headers(message)
|
|
19
|
+
existing_headers = message.metadata&.headers || {}
|
|
20
|
+
merged_headers = new_headers.merge(existing_headers)
|
|
21
|
+
|
|
22
|
+
new_metadata = update_metadata(message.metadata, headers: merged_headers)
|
|
23
|
+
app.call(message.mutate(metadata: new_metadata))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :defaults
|
|
29
|
+
|
|
30
|
+
def resolve_headers(message)
|
|
31
|
+
defaults.each_with_object({}) do |(key, value), headers|
|
|
32
|
+
headers[key.to_s] = if value.respond_to?(:call)
|
|
33
|
+
(value.arity == 0) ? value.call : value.call(message)
|
|
34
|
+
else
|
|
35
|
+
value
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def update_metadata(metadata, **attrs)
|
|
41
|
+
current = metadata&.to_h || {}
|
|
42
|
+
Message::Metadata.new(**current.merge(attrs))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Producers
|
|
5
|
+
module Middlewares
|
|
6
|
+
# A middleware that emits instrumentation events via Lepus.instrument.
|
|
7
|
+
class Instrumentation < Lepus::Middleware
|
|
8
|
+
# @param opts [Hash] The options for the middleware.
|
|
9
|
+
# @option opts [String] :event_name ("publish") The event name suffix.
|
|
10
|
+
def initialize(**opts)
|
|
11
|
+
super
|
|
12
|
+
@event_name = opts.fetch(:event_name, "publish")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(message, app)
|
|
16
|
+
exchange = message.delivery_info&.exchange
|
|
17
|
+
routing_key = message.delivery_info&.routing_key
|
|
18
|
+
|
|
19
|
+
Lepus.instrument(event_name, exchange: exchange, routing_key: routing_key, message: message) do
|
|
20
|
+
app.call(message)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :event_name
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "multi_json"
|
|
4
|
+
|
|
5
|
+
module Lepus
|
|
6
|
+
module Producers
|
|
7
|
+
module Middlewares
|
|
8
|
+
# A middleware that serializes Hash payloads to JSON and sets the content_type.
|
|
9
|
+
class JSON < Lepus::Middleware
|
|
10
|
+
# @param opts [Hash] The options for the middleware.
|
|
11
|
+
# @option opts [Boolean] :only_hash (true) Only serialize Hash payloads.
|
|
12
|
+
def initialize(**opts)
|
|
13
|
+
super
|
|
14
|
+
@only_hash = opts.fetch(:only_hash, true)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(message, app)
|
|
18
|
+
payload = message.payload
|
|
19
|
+
|
|
20
|
+
if should_serialize?(payload)
|
|
21
|
+
serialized_payload = MultiJson.dump(payload)
|
|
22
|
+
new_metadata = update_metadata(message.metadata, content_type: "application/json")
|
|
23
|
+
message = message.mutate(payload: serialized_payload, metadata: new_metadata)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
app.call(message)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :only_hash
|
|
32
|
+
|
|
33
|
+
def should_serialize?(payload)
|
|
34
|
+
return false if payload.is_a?(String)
|
|
35
|
+
return payload.is_a?(Hash) if only_hash
|
|
36
|
+
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def update_metadata(metadata, **attrs)
|
|
41
|
+
current = metadata&.to_h || {}
|
|
42
|
+
Message::Metadata.new(**current.merge(attrs))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Producers
|
|
5
|
+
module Middlewares
|
|
6
|
+
# A middleware that prevents duplicate messages from being published
|
|
7
|
+
# using the de-dupe gem for Redis-based distributed locking.
|
|
8
|
+
#
|
|
9
|
+
# When a lock is acquired, the middleware adds x-dedupe-lock-key and
|
|
10
|
+
# x-dedupe-lock-id headers to the message so that a consumer middleware
|
|
11
|
+
# can release the lock after successful processing.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# class StoryCreatedProducer < Lepus::Producer
|
|
15
|
+
# configure(exchange: "story_created")
|
|
16
|
+
# use :unique, lock_key: "story", lock_id: ->(msg) { msg.payload[:story_id].to_s }
|
|
17
|
+
# end
|
|
18
|
+
class Unique < Lepus::Middleware
|
|
19
|
+
HEADER_LOCK_KEY = "x-dedupe-lock-key"
|
|
20
|
+
HEADER_LOCK_ID = "x-dedupe-lock-id"
|
|
21
|
+
HEADER_LOCK_TTL = "x-dedupe-lock-ttl"
|
|
22
|
+
|
|
23
|
+
# @param lock_key [String] Shared lock namespace (e.g., "story").
|
|
24
|
+
# @param lock_id [Proc] Callable that extracts a unique ID from the message.
|
|
25
|
+
# @param ttl [Integer, nil] Lock TTL in seconds. Defaults to DeDupe configuration.
|
|
26
|
+
def initialize(lock_key:, lock_id:, ttl: nil)
|
|
27
|
+
super()
|
|
28
|
+
@lock_key = lock_key
|
|
29
|
+
@lock_id = lock_id
|
|
30
|
+
@ttl = ttl
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call(message, app)
|
|
34
|
+
id = @lock_id.call(message)
|
|
35
|
+
return app.call(message) if id.nil?
|
|
36
|
+
|
|
37
|
+
lock_opts = {}
|
|
38
|
+
lock_opts[:ttl] = @ttl if @ttl
|
|
39
|
+
lock = DeDupe::Lock.new(lock_key: @lock_key, lock_id: id.to_s, **lock_opts)
|
|
40
|
+
return unless lock.acquire
|
|
41
|
+
|
|
42
|
+
message = add_dedupe_headers(message, id)
|
|
43
|
+
app.call(message)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def add_dedupe_headers(message, lock_id)
|
|
49
|
+
existing_headers = message.metadata&.headers || {}
|
|
50
|
+
new_headers = existing_headers.merge(
|
|
51
|
+
HEADER_LOCK_KEY => @lock_key,
|
|
52
|
+
HEADER_LOCK_ID => lock_id.to_s
|
|
53
|
+
)
|
|
54
|
+
new_headers[HEADER_LOCK_TTL] = @ttl if @ttl
|
|
55
|
+
|
|
56
|
+
new_metadata = update_metadata(message.metadata, headers: new_headers)
|
|
57
|
+
message.mutate(metadata: new_metadata)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def update_metadata(metadata, **attrs)
|
|
61
|
+
current = metadata&.to_h || {}
|
|
62
|
+
Message::Metadata.new(**current.merge(attrs))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file is intended to be loaded by the prometheus_exporter server:
|
|
4
|
+
#
|
|
5
|
+
# prometheus_exporter -a lepus/prometheus/collector
|
|
6
|
+
#
|
|
7
|
+
# It intentionally avoids requiring the rest of the Lepus gem so it can
|
|
8
|
+
# run standalone inside the exporter process. When Lepus is loaded in the
|
|
9
|
+
# same process, latency buckets fall back to Lepus.config.prometheus_buckets.
|
|
10
|
+
|
|
11
|
+
require "prometheus_exporter"
|
|
12
|
+
require "prometheus_exporter/server"
|
|
13
|
+
|
|
14
|
+
module Lepus
|
|
15
|
+
module Prometheus
|
|
16
|
+
class Collector < ::PrometheusExporter::Server::TypeCollector
|
|
17
|
+
DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10].freeze
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@metrics = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def type
|
|
24
|
+
"lepus"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def metrics
|
|
28
|
+
@metrics.values
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def collect(obj)
|
|
32
|
+
case obj["metric"]
|
|
33
|
+
when "delivery" then collect_delivery(obj)
|
|
34
|
+
when "publish" then collect_publish(obj)
|
|
35
|
+
when "process" then collect_process(obj)
|
|
36
|
+
when "process_info" then collect_process_info(obj)
|
|
37
|
+
when "queue" then collect_queue(obj)
|
|
38
|
+
when "queue_poll" then collect_queue_poll(obj)
|
|
39
|
+
when "queue_poll_error" then collect_queue_poll_error(obj)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def collect_delivery(obj)
|
|
46
|
+
labels = {
|
|
47
|
+
consumer: obj["consumer"],
|
|
48
|
+
queue: obj["queue"],
|
|
49
|
+
result: obj["result"],
|
|
50
|
+
error: obj["error"].to_s
|
|
51
|
+
}
|
|
52
|
+
counter(
|
|
53
|
+
"lepus_messages_processed_total",
|
|
54
|
+
"Total messages delivered to Lepus consumers, labeled by result and error class."
|
|
55
|
+
).observe(1, labels)
|
|
56
|
+
|
|
57
|
+
duration = obj["duration"].to_f
|
|
58
|
+
histogram(
|
|
59
|
+
"lepus_delivery_duration_seconds",
|
|
60
|
+
"Time spent processing a single Lepus message.",
|
|
61
|
+
buckets
|
|
62
|
+
).observe(duration, consumer: obj["consumer"], queue: obj["queue"])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def collect_publish(obj)
|
|
66
|
+
counter(
|
|
67
|
+
"lepus_messages_published_total",
|
|
68
|
+
"Total messages published through Lepus producers."
|
|
69
|
+
).observe(1, exchange: obj["exchange"], routing_key: obj["routing_key"])
|
|
70
|
+
|
|
71
|
+
duration = obj["duration"].to_f
|
|
72
|
+
histogram(
|
|
73
|
+
"lepus_publish_duration_seconds",
|
|
74
|
+
"Time spent publishing a single Lepus message.",
|
|
75
|
+
buckets
|
|
76
|
+
).observe(duration, exchange: obj["exchange"], routing_key: obj["routing_key"])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def collect_process(obj)
|
|
80
|
+
labels = {kind: obj["kind"], name: obj["name"]}
|
|
81
|
+
gauge(
|
|
82
|
+
"lepus_process_rss_memory_bytes",
|
|
83
|
+
"Resident-set memory of a Lepus process."
|
|
84
|
+
).observe(obj["rss_memory"].to_f, labels)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def collect_process_info(obj)
|
|
88
|
+
labels = {
|
|
89
|
+
kind: obj["kind"],
|
|
90
|
+
name: obj["name"],
|
|
91
|
+
pid: obj["pid"].to_s,
|
|
92
|
+
hostname: obj["hostname"].to_s
|
|
93
|
+
}
|
|
94
|
+
gauge(
|
|
95
|
+
"lepus_process_info",
|
|
96
|
+
"Info gauge for a Lepus process (always 1); use for joining pid/hostname labels."
|
|
97
|
+
).observe(1, labels)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def collect_queue(obj)
|
|
101
|
+
labels = {name: obj["name"]}
|
|
102
|
+
gauge("lepus_queue_messages", "Total messages in a RabbitMQ queue.")
|
|
103
|
+
.observe(obj["messages"].to_f, labels)
|
|
104
|
+
gauge("lepus_queue_messages_ready", "Messages ready for delivery in a RabbitMQ queue.")
|
|
105
|
+
.observe(obj["messages_ready"].to_f, labels)
|
|
106
|
+
gauge("lepus_queue_messages_unacknowledged", "Unacknowledged messages in a RabbitMQ queue.")
|
|
107
|
+
.observe(obj["messages_unacknowledged"].to_f, labels)
|
|
108
|
+
gauge("lepus_queue_consumers", "Number of consumers attached to a RabbitMQ queue.")
|
|
109
|
+
.observe(obj["consumers"].to_f, labels)
|
|
110
|
+
gauge("lepus_queue_memory_bytes", "Memory used by a RabbitMQ queue.")
|
|
111
|
+
.observe(obj["memory"].to_f, labels)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def collect_queue_poll(obj)
|
|
115
|
+
gauge(
|
|
116
|
+
"lepus_queue_poll_last_success_timestamp_seconds",
|
|
117
|
+
"Unix timestamp of the last successful RabbitMQ management API poll."
|
|
118
|
+
).observe(obj["timestamp"].to_f, {})
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def collect_queue_poll_error(obj)
|
|
122
|
+
counter(
|
|
123
|
+
"lepus_queue_poll_errors_total",
|
|
124
|
+
"Total errors encountered while polling the RabbitMQ management API, labeled by error class."
|
|
125
|
+
).observe(1, error: obj["error"].to_s)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def counter(name, help)
|
|
129
|
+
@metrics[name] ||= ::PrometheusExporter::Metric::Counter.new(name, help)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def gauge(name, help)
|
|
133
|
+
@metrics[name] ||= ::PrometheusExporter::Metric::Gauge.new(name, help)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def histogram(name, help, buckets)
|
|
137
|
+
@metrics[name] ||= ::PrometheusExporter::Metric::Histogram.new(name, help, buckets: buckets)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def buckets
|
|
141
|
+
if defined?(::Lepus) && ::Lepus.respond_to?(:config) && ::Lepus.config.respond_to?(:prometheus_buckets)
|
|
142
|
+
::Lepus.config.prometheus_buckets
|
|
143
|
+
else
|
|
144
|
+
DEFAULT_BUCKETS
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|