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,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "testing/exchange"
|
|
4
|
+
require_relative "testing/rspec_matchers"
|
|
5
|
+
require_relative "testing/message_builder"
|
|
6
|
+
|
|
7
|
+
module Lepus
|
|
8
|
+
module Testing
|
|
9
|
+
class << self
|
|
10
|
+
# Enable fake publishing mode and consumer error re-raising
|
|
11
|
+
def enable!
|
|
12
|
+
fake_publisher!
|
|
13
|
+
consumer_raise_errors!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Disable fake publishing mode and consumer error re-raising
|
|
17
|
+
def disable!
|
|
18
|
+
fake_publisher_disable!
|
|
19
|
+
consumer_capture_errors!
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Enable fake publishing mode for testing
|
|
23
|
+
# When enabled, messages are stored in fake exchanges instead of being sent to RabbitMQ
|
|
24
|
+
def fake_publisher!
|
|
25
|
+
@fake_publisher_enabled = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Check if fake publishing is enabled
|
|
29
|
+
def fake_publisher_enabled?
|
|
30
|
+
@fake_publisher_enabled == true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Disable fake publishing mode for testing
|
|
34
|
+
def fake_publisher_disable!
|
|
35
|
+
@fake_publisher_enabled = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# When enabled, consumer exceptions are re-raised instead of being converted to :reject
|
|
39
|
+
# This is useful in specs, so RSpec can capture failures instead of false positives
|
|
40
|
+
def consumer_raise_errors!
|
|
41
|
+
@consumer_raise_errors_enabled = true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Disable consumer error re-raising (default behavior converts to :reject)
|
|
45
|
+
def consumer_capture_errors!
|
|
46
|
+
@consumer_raise_errors_enabled = false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns true if consumer errors should be re-raised in tests
|
|
50
|
+
def consumer_raise_errors?
|
|
51
|
+
@consumer_raise_errors_enabled == true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Clear all messages from all fake exchanges
|
|
55
|
+
def clear_all_messages!
|
|
56
|
+
Exchange.clear_all_messages!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get all fake exchanges
|
|
60
|
+
def exchanges
|
|
61
|
+
Exchange.all
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get a specific fake exchange by name
|
|
65
|
+
def exchange(name)
|
|
66
|
+
Exchange[name]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get messages for a specific producer class
|
|
70
|
+
def producer_messages(producer_class)
|
|
71
|
+
return [] unless producer_class.respond_to?(:definition)
|
|
72
|
+
|
|
73
|
+
begin
|
|
74
|
+
exchange_name = producer_class.definition.exchange_name
|
|
75
|
+
Exchange[exchange_name].messages
|
|
76
|
+
rescue
|
|
77
|
+
# If there's an error getting the exchange name, return empty array
|
|
78
|
+
[]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Test a consumer with a message
|
|
83
|
+
# @param consumer_class [Class] The consumer class to test
|
|
84
|
+
# @param message_or_payload [Lepus::Message, Hash, String] The message to process
|
|
85
|
+
# @return [Symbol] The result of the consumer's perform method (:ack, :reject, :requeue, :nack)
|
|
86
|
+
def consumer_perform(consumer_class, message_or_payload)
|
|
87
|
+
message = build_message(message_or_payload)
|
|
88
|
+
consumer = consumer_class.new
|
|
89
|
+
consumer.process_delivery(message.delivery_info, message.metadata, message.payload)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Create a message builder for custom scenarios
|
|
93
|
+
# @return [MessageBuilder] A new message builder instance
|
|
94
|
+
def message_builder
|
|
95
|
+
MessageBuilder.new
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# Build a message from various input types
|
|
101
|
+
def build_message(message_or_payload)
|
|
102
|
+
case message_or_payload
|
|
103
|
+
when Lepus::Message
|
|
104
|
+
message_or_payload
|
|
105
|
+
when Hash
|
|
106
|
+
if message_or_payload.key?(:payload)
|
|
107
|
+
# Hash with payload and other options
|
|
108
|
+
payload = message_or_payload.delete(:payload)
|
|
109
|
+
MessageBuilder.new
|
|
110
|
+
.with_payload(payload)
|
|
111
|
+
.tap { |builder| apply_options(builder, message_or_payload) }
|
|
112
|
+
.build
|
|
113
|
+
else
|
|
114
|
+
# Hash as payload - create message with Hash payload
|
|
115
|
+
MessageBuilder.new
|
|
116
|
+
.with_payload(message_or_payload)
|
|
117
|
+
.with_content_type("application/json")
|
|
118
|
+
.build
|
|
119
|
+
end
|
|
120
|
+
when String
|
|
121
|
+
MessageBuilder.new
|
|
122
|
+
.with_payload(message_or_payload)
|
|
123
|
+
.with_content_type("text/plain")
|
|
124
|
+
.build
|
|
125
|
+
else
|
|
126
|
+
raise ArgumentError, "Invalid message type: #{message_or_payload.class}"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Apply options to a message builder
|
|
131
|
+
def apply_options(builder, options)
|
|
132
|
+
options.each do |key, value|
|
|
133
|
+
method_name = "with_#{key}"
|
|
134
|
+
if builder.respond_to?(method_name)
|
|
135
|
+
builder.send(method_name, value)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Override Publisher methods when testing module is loaded
|
|
141
|
+
def setup_publisher_overrides!
|
|
142
|
+
return if @overrides_setup
|
|
143
|
+
|
|
144
|
+
# Add messages method to Producer class
|
|
145
|
+
Lepus::Producer.class_eval do
|
|
146
|
+
# Get messages published by this producer (only available in testing mode)
|
|
147
|
+
# @return [Array<Hash>] Array of published messages
|
|
148
|
+
def self.messages
|
|
149
|
+
Lepus::Testing.producer_messages(self)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Override Publisher#publish
|
|
154
|
+
Lepus::Publisher.class_eval do
|
|
155
|
+
alias_method :original_publish, :publish
|
|
156
|
+
|
|
157
|
+
def publish(message, **options)
|
|
158
|
+
return unless Lepus::Producers.exchange_enabled?(exchange_name)
|
|
159
|
+
|
|
160
|
+
if Lepus::Testing.fake_publisher_enabled?
|
|
161
|
+
return fake_publish(message, **options)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
original_publish(message, **options)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Override Publisher#channel_publish
|
|
168
|
+
alias_method :original_channel_publish, :channel_publish
|
|
169
|
+
|
|
170
|
+
def channel_publish(channel, message, **options)
|
|
171
|
+
raise ArgumentError, "channel is required" unless channel
|
|
172
|
+
return unless Lepus::Producers.exchange_enabled?(exchange_name)
|
|
173
|
+
|
|
174
|
+
if Lepus::Testing.fake_publisher_enabled?
|
|
175
|
+
return fake_publish(message, **options)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
original_channel_publish(channel, message, **options)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Add fake_publish method
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def fake_publish(message, **options)
|
|
185
|
+
opts = Lepus::Publisher::DEFAULT_PUBLISH_OPTIONS.merge(options)
|
|
186
|
+
|
|
187
|
+
fake_message = {
|
|
188
|
+
exchange: exchange_name,
|
|
189
|
+
payload: message, # Store the original message, not the JSON string
|
|
190
|
+
routing_key: opts[:routing_key],
|
|
191
|
+
headers: opts[:headers],
|
|
192
|
+
persistent: opts[:persistent],
|
|
193
|
+
mandatory: opts[:mandatory],
|
|
194
|
+
immediate: opts[:immediate],
|
|
195
|
+
content_type: message.is_a?(String) ? "text/plain" : "application/json",
|
|
196
|
+
timestamp: Time.now
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
Lepus::Testing::Exchange[exchange_name].add_message(fake_message)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
@overrides_setup = true
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Automatically setup overrides when the testing module is required
|
|
210
|
+
Lepus::Testing.send(:setup_publisher_overrides!)
|
data/lib/lepus/unique.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "de_dupe"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
raise LoadError,
|
|
7
|
+
"The 'de-dupe' gem is required for Lepus unique middleware. " \
|
|
8
|
+
"Add `gem 'de-dupe'` to your Gemfile."
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
unless DeDupe.config.redis
|
|
12
|
+
raise Lepus::Error,
|
|
13
|
+
"DeDupe is not configured with Redis. " \
|
|
14
|
+
"Call DeDupe.configure { |c| c.redis = Redis.new } before requiring 'lepus/unique'."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
require_relative "producers/middlewares/unique"
|
|
18
|
+
require_relative "consumers/middlewares/unique"
|
data/lib/lepus/version.rb
CHANGED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "concurrent"
|
|
5
|
+
|
|
6
|
+
module Lepus
|
|
7
|
+
module Web
|
|
8
|
+
# Aggregates process heartbeats from RabbitMQ into in-memory state.
|
|
9
|
+
# Subscribes to the lepus.heartbeat fanout exchange and maintains
|
|
10
|
+
# a thread-safe cache of all processes across connected Lepus apps.
|
|
11
|
+
class Aggregator
|
|
12
|
+
HEARTBEAT_EXCHANGE = ProcessRegistry::RabbitmqBackend::HEARTBEAT_EXCHANGE
|
|
13
|
+
|
|
14
|
+
attr_reader :stale_threshold
|
|
15
|
+
|
|
16
|
+
def initialize(stale_threshold: nil)
|
|
17
|
+
@stale_threshold = stale_threshold || Lepus.config.process_alive_threshold
|
|
18
|
+
@processes = Concurrent::Map.new
|
|
19
|
+
@connection = nil
|
|
20
|
+
@channel = nil
|
|
21
|
+
@consumer = nil
|
|
22
|
+
@running = Concurrent::AtomicBoolean.new(false)
|
|
23
|
+
@pruning_task = nil
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def start
|
|
28
|
+
return if @running.true?
|
|
29
|
+
|
|
30
|
+
@mutex.synchronize do
|
|
31
|
+
return if @running.true?
|
|
32
|
+
|
|
33
|
+
@running.make_true
|
|
34
|
+
setup_subscription
|
|
35
|
+
start_pruning_task
|
|
36
|
+
end
|
|
37
|
+
rescue => e
|
|
38
|
+
Lepus.logger.error("[Web::Aggregator] Failed to start: #{e.message}")
|
|
39
|
+
@running.make_false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def stop
|
|
43
|
+
@mutex.synchronize do
|
|
44
|
+
@running.make_false
|
|
45
|
+
@pruning_task&.shutdown
|
|
46
|
+
@consumer&.cancel
|
|
47
|
+
@channel&.close if @channel&.open?
|
|
48
|
+
@connection&.close if @connection&.open?
|
|
49
|
+
end
|
|
50
|
+
rescue => e
|
|
51
|
+
Lepus.logger.warn("[Web::Aggregator] Error during shutdown: #{e.message}")
|
|
52
|
+
ensure
|
|
53
|
+
@pruning_task = nil
|
|
54
|
+
@consumer = nil
|
|
55
|
+
@channel = nil
|
|
56
|
+
@connection = nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def running?
|
|
60
|
+
@running.true?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def all_processes
|
|
64
|
+
prune_stale_entries
|
|
65
|
+
@processes.values.map { |data| data[:process] }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def find(id)
|
|
69
|
+
@processes[id]&.dig(:process)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def count
|
|
73
|
+
@processes.size
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def clear
|
|
77
|
+
@processes.clear
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def setup_subscription
|
|
83
|
+
@connection = Lepus.config.create_connection(suffix: "(web-aggregator)")
|
|
84
|
+
@channel = @connection.create_channel
|
|
85
|
+
|
|
86
|
+
exchange = @channel.fanout(
|
|
87
|
+
HEARTBEAT_EXCHANGE,
|
|
88
|
+
durable: false,
|
|
89
|
+
auto_delete: false
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
queue = @channel.queue("", exclusive: true, auto_delete: true)
|
|
93
|
+
queue.bind(exchange)
|
|
94
|
+
|
|
95
|
+
@consumer = queue.subscribe do |_delivery_info, _metadata, payload|
|
|
96
|
+
handle_message(payload)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def handle_message(payload)
|
|
101
|
+
data = JSON.parse(payload, symbolize_names: true)
|
|
102
|
+
|
|
103
|
+
case data[:type]
|
|
104
|
+
when "heartbeat"
|
|
105
|
+
process_heartbeat(data)
|
|
106
|
+
when "deregister"
|
|
107
|
+
process_deregistration(data)
|
|
108
|
+
end
|
|
109
|
+
rescue => e
|
|
110
|
+
Lepus.logger.warn("[Web::Aggregator] Failed to handle message: #{e.message}")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def process_heartbeat(data)
|
|
114
|
+
process_data = data[:process]
|
|
115
|
+
return unless process_data && process_data[:id]
|
|
116
|
+
|
|
117
|
+
metrics = data[:metrics] || {}
|
|
118
|
+
|
|
119
|
+
flat_process = process_data.merge(
|
|
120
|
+
rss_memory: metrics[:rss_memory] || 0,
|
|
121
|
+
connections: metrics[:connections] || 0,
|
|
122
|
+
consumers: metrics[:consumers] || []
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@processes[process_data[:id]] = {
|
|
126
|
+
process: flat_process,
|
|
127
|
+
received_at: Time.now
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def process_deregistration(data)
|
|
132
|
+
process_id = data[:process_id]
|
|
133
|
+
@processes.delete(process_id) if process_id
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def start_pruning_task
|
|
137
|
+
@pruning_task = Concurrent::TimerTask.new(
|
|
138
|
+
execution_interval: [@stale_threshold / 2, 30].min
|
|
139
|
+
) do
|
|
140
|
+
prune_stale_entries
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
@pruning_task.execute
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def prune_stale_entries
|
|
147
|
+
threshold = Time.now - @stale_threshold
|
|
148
|
+
@processes.each do |id, data|
|
|
149
|
+
@processes.delete(id) if data[:received_at] < threshold
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Web
|
|
5
|
+
class API
|
|
6
|
+
def initialize(aggregator: nil, management_api: nil)
|
|
7
|
+
@aggregator = aggregator
|
|
8
|
+
@management_api = management_api
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(env)
|
|
12
|
+
req = Rack::Request.new(env)
|
|
13
|
+
case req.path_info
|
|
14
|
+
when "/health"
|
|
15
|
+
Web::RespondWith.json(template: :health)
|
|
16
|
+
when "/processes"
|
|
17
|
+
processes_data
|
|
18
|
+
when "/queues"
|
|
19
|
+
queues_data
|
|
20
|
+
when "/connections"
|
|
21
|
+
connections_data
|
|
22
|
+
when "/exchanges"
|
|
23
|
+
exchanges_data
|
|
24
|
+
else
|
|
25
|
+
Web::RespondWith.json(template: :not_found)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def aggregator
|
|
32
|
+
@aggregator || Web.aggregator
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def management_api
|
|
36
|
+
@management_api || Web.management_api
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def processes_data
|
|
40
|
+
if aggregator&.running?
|
|
41
|
+
payload = aggregator.all_processes
|
|
42
|
+
Web::RespondWith.json(template: :ok, body: payload)
|
|
43
|
+
else
|
|
44
|
+
Web::RespondWith.json(template: :ok, body: [])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def queues_data
|
|
49
|
+
if management_api
|
|
50
|
+
raw_queues = management_api.queues
|
|
51
|
+
payload = annotate_queues_with_apps(raw_queues)
|
|
52
|
+
Web::RespondWith.json(template: :ok, body: payload)
|
|
53
|
+
else
|
|
54
|
+
Web::RespondWith.json(template: :ok, body: [])
|
|
55
|
+
end
|
|
56
|
+
rescue => e
|
|
57
|
+
Lepus.logger.warn("[Web::API] Failed to fetch queues: #{e.message}")
|
|
58
|
+
Web::RespondWith.json(template: :ok, body: [])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def connections_data
|
|
62
|
+
if management_api
|
|
63
|
+
payload = management_api.connections
|
|
64
|
+
Web::RespondWith.json(template: :ok, body: payload)
|
|
65
|
+
else
|
|
66
|
+
Web::RespondWith.json(template: :ok, body: [])
|
|
67
|
+
end
|
|
68
|
+
rescue => e
|
|
69
|
+
Lepus.logger.warn("[Web::API] Failed to fetch connections: #{e.message}")
|
|
70
|
+
Web::RespondWith.json(template: :ok, body: [])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def exchanges_data
|
|
74
|
+
if management_api
|
|
75
|
+
raw_exchanges = management_api.exchanges
|
|
76
|
+
payload = filter_exchanges(raw_exchanges)
|
|
77
|
+
Web::RespondWith.json(template: :ok, body: payload)
|
|
78
|
+
else
|
|
79
|
+
Web::RespondWith.json(template: :ok, body: [])
|
|
80
|
+
end
|
|
81
|
+
rescue => e
|
|
82
|
+
Lepus.logger.warn("[Web::API] Failed to fetch exchanges: #{e.message}")
|
|
83
|
+
Web::RespondWith.json(template: :ok, body: [])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def annotate_queues_with_apps(queues)
|
|
87
|
+
return queues unless aggregator&.running?
|
|
88
|
+
|
|
89
|
+
queue_app_map = build_queue_app_map
|
|
90
|
+
return queues if queue_app_map.empty?
|
|
91
|
+
|
|
92
|
+
queues.map do |queue|
|
|
93
|
+
app = queue_app_map[queue[:name]]
|
|
94
|
+
app ? queue.merge(application: app) : queue
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def filter_exchanges(exchanges)
|
|
99
|
+
return exchanges if Lepus.config.web_show_all_exchanges
|
|
100
|
+
return exchanges unless aggregator&.running?
|
|
101
|
+
|
|
102
|
+
known_exchanges = build_known_exchange_names
|
|
103
|
+
return exchanges if known_exchanges.empty?
|
|
104
|
+
|
|
105
|
+
exchanges.select { |e| known_exchanges.include?(e[:name]) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_queue_app_map
|
|
109
|
+
map = {}
|
|
110
|
+
aggregator.all_processes.each do |process|
|
|
111
|
+
app_name = process[:application]
|
|
112
|
+
next unless app_name
|
|
113
|
+
|
|
114
|
+
(process[:consumers] || []).each do |consumer|
|
|
115
|
+
map[consumer[:queue]] = app_name if consumer[:queue]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
map
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_known_exchange_names
|
|
122
|
+
names = Set.new
|
|
123
|
+
aggregator.all_processes.each do |process|
|
|
124
|
+
(process[:consumers] || []).each do |consumer|
|
|
125
|
+
names << consumer[:exchange] if consumer[:exchange]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
names
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Web
|
|
5
|
+
class App
|
|
6
|
+
def self.build
|
|
7
|
+
root = Web.assets_path
|
|
8
|
+
|
|
9
|
+
Rack::Builder.new do
|
|
10
|
+
use Rack::Static,
|
|
11
|
+
urls: ["/assets", "/sw.js"],
|
|
12
|
+
root: root.to_s
|
|
13
|
+
|
|
14
|
+
map "/api" do
|
|
15
|
+
run Lepus::Web::API.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
run lambda { |env|
|
|
19
|
+
req = Rack::Request.new(env)
|
|
20
|
+
path = req.path_info
|
|
21
|
+
|
|
22
|
+
if path == "/" || path == "/index.html"
|
|
23
|
+
[200, {"content-type" => "text/html"}, [Web.render_index(env)]]
|
|
24
|
+
else
|
|
25
|
+
file_path = root.join(path.sub(%r{^/}, ""))
|
|
26
|
+
if File.file?(file_path)
|
|
27
|
+
[200, {"content-type" => Web.mime_for(file_path)}, [File.binread(file_path)]]
|
|
28
|
+
else
|
|
29
|
+
[200, {"content-type" => "text/html"}, [Web.render_index(env)]]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|