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,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Lepus
|
|
8
|
+
module Web
|
|
9
|
+
# HTTP client for RabbitMQ Management API.
|
|
10
|
+
# Fetches queue and connection statistics.
|
|
11
|
+
class ManagementAPI
|
|
12
|
+
DEFAULT_PORT = 15672
|
|
13
|
+
|
|
14
|
+
attr_reader :base_url, :vhost
|
|
15
|
+
|
|
16
|
+
def initialize(base_url: nil, vhost: "/")
|
|
17
|
+
@base_url = base_url || derive_management_url
|
|
18
|
+
@vhost = vhost
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Fetch all queues for the configured vhost
|
|
22
|
+
# @return [Array<Hash>] array of queue data
|
|
23
|
+
def queues
|
|
24
|
+
data = get("/api/queues/#{encode_vhost}")
|
|
25
|
+
return [] unless data.is_a?(Array)
|
|
26
|
+
|
|
27
|
+
data.map { |q| normalize_queue(q) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Fetch all exchanges for the configured vhost
|
|
31
|
+
# @return [Array<Hash>] array of exchange data
|
|
32
|
+
def exchanges
|
|
33
|
+
data = get("/api/exchanges/#{encode_vhost}")
|
|
34
|
+
return [] unless data.is_a?(Array)
|
|
35
|
+
|
|
36
|
+
data
|
|
37
|
+
.reject { |e| e["name"].to_s.empty? || e["name"].to_s.start_with?("amq.") }
|
|
38
|
+
.map { |e| normalize_exchange(e) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Fetch all connections
|
|
42
|
+
# @return [Array<Hash>] array of connection data
|
|
43
|
+
def connections
|
|
44
|
+
data = get("/api/connections")
|
|
45
|
+
return [] unless data.is_a?(Array)
|
|
46
|
+
|
|
47
|
+
data.map { |c| normalize_connection(c) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Fetch a specific queue
|
|
51
|
+
# @param name [String] queue name
|
|
52
|
+
# @return [Hash, nil] queue data or nil if not found
|
|
53
|
+
def queue(name)
|
|
54
|
+
data = get("/api/queues/#{encode_vhost}/#{encode_name(name)}")
|
|
55
|
+
normalize_queue(data) if data
|
|
56
|
+
rescue NotFoundError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class Error < StandardError; end
|
|
61
|
+
|
|
62
|
+
class ConnectionError < Error; end
|
|
63
|
+
|
|
64
|
+
class AuthenticationError < Error; end
|
|
65
|
+
|
|
66
|
+
class NotFoundError < Error; end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def parse_rabbitmq_uri
|
|
71
|
+
URI.parse(Lepus.config.rabbitmq_url)
|
|
72
|
+
rescue
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def derive_management_url
|
|
77
|
+
uri = parse_rabbitmq_uri
|
|
78
|
+
return "http://localhost:#{DEFAULT_PORT}" unless uri
|
|
79
|
+
|
|
80
|
+
"http://#{uri.host}:#{DEFAULT_PORT}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def credentials
|
|
84
|
+
uri = parse_rabbitmq_uri
|
|
85
|
+
return unless uri&.user
|
|
86
|
+
|
|
87
|
+
[uri.user, uri.password]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def encode_vhost
|
|
91
|
+
URI.encode_www_form_component(@vhost)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def encode_name(name)
|
|
95
|
+
URI.encode_www_form_component(name)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def get(path)
|
|
99
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
100
|
+
|
|
101
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
102
|
+
http.use_ssl = (uri.scheme == "https")
|
|
103
|
+
http.open_timeout = 5
|
|
104
|
+
http.read_timeout = 10
|
|
105
|
+
|
|
106
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
107
|
+
if (creds = credentials)
|
|
108
|
+
request.basic_auth(*creds)
|
|
109
|
+
end
|
|
110
|
+
request["Accept"] = "application/json"
|
|
111
|
+
|
|
112
|
+
response = http.request(request)
|
|
113
|
+
|
|
114
|
+
case response.code.to_i
|
|
115
|
+
when 200
|
|
116
|
+
JSON.parse(response.body)
|
|
117
|
+
when 401
|
|
118
|
+
raise AuthenticationError, "Authentication failed for RabbitMQ Management API"
|
|
119
|
+
when 404
|
|
120
|
+
raise NotFoundError, "Resource not found: #{path}"
|
|
121
|
+
else
|
|
122
|
+
raise Error, "RabbitMQ Management API error: #{response.code} - #{response.body}"
|
|
123
|
+
end
|
|
124
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, Net::OpenTimeout, Net::ReadTimeout => e
|
|
125
|
+
raise ConnectionError, "Failed to connect to RabbitMQ Management API: #{e.message}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def normalize_queue(q)
|
|
129
|
+
{
|
|
130
|
+
name: q["name"],
|
|
131
|
+
type: q["type"] || "classic",
|
|
132
|
+
messages: q["messages"] || 0,
|
|
133
|
+
messages_ready: q["messages_ready"] || 0,
|
|
134
|
+
messages_unacknowledged: q["messages_unacknowledged"] || 0,
|
|
135
|
+
consumers: q["consumers"] || 0,
|
|
136
|
+
memory: q["memory"] || 0,
|
|
137
|
+
message_stats: normalize_message_stats(q["message_stats"])
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def normalize_message_stats(stats)
|
|
142
|
+
return {} unless stats
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
publish: stats["publish"] || 0,
|
|
146
|
+
publish_rate: stats.dig("publish_details", "rate") || 0.0,
|
|
147
|
+
deliver_get: stats["deliver_get"] || 0,
|
|
148
|
+
deliver_get_rate: stats.dig("deliver_get_details", "rate") || 0.0,
|
|
149
|
+
ack: stats["ack"] || 0,
|
|
150
|
+
ack_rate: stats.dig("ack_details", "rate") || 0.0,
|
|
151
|
+
redeliver: stats["redeliver"] || 0,
|
|
152
|
+
redeliver_rate: stats.dig("redeliver_details", "rate") || 0.0
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def normalize_exchange(e)
|
|
157
|
+
{
|
|
158
|
+
name: e["name"],
|
|
159
|
+
type: e["type"],
|
|
160
|
+
durable: e["durable"],
|
|
161
|
+
auto_delete: e["auto_delete"],
|
|
162
|
+
message_stats: normalize_exchange_stats(e["message_stats"])
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def normalize_exchange_stats(stats)
|
|
167
|
+
return {} unless stats
|
|
168
|
+
|
|
169
|
+
{
|
|
170
|
+
publish_in: stats["publish_in"] || 0,
|
|
171
|
+
publish_in_rate: stats.dig("publish_in_details", "rate") || 0.0,
|
|
172
|
+
publish_out: stats["publish_out"] || 0,
|
|
173
|
+
publish_out_rate: stats.dig("publish_out_details", "rate") || 0.0
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def normalize_connection(c)
|
|
178
|
+
{
|
|
179
|
+
name: c["name"],
|
|
180
|
+
state: c["state"],
|
|
181
|
+
user: c["user"],
|
|
182
|
+
vhost: c["vhost"],
|
|
183
|
+
channels: c["channels"] || 0,
|
|
184
|
+
connected_at: c["connected_at"],
|
|
185
|
+
client_properties: {
|
|
186
|
+
connection_name: c.dig("client_properties", "connection_name")
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Web
|
|
5
|
+
class RespondWith
|
|
6
|
+
TEMPLATES = {
|
|
7
|
+
not_found: {
|
|
8
|
+
status: 404,
|
|
9
|
+
body: {error: "not_found"}
|
|
10
|
+
},
|
|
11
|
+
health: {
|
|
12
|
+
status: 200,
|
|
13
|
+
body: {status: "ok"}
|
|
14
|
+
},
|
|
15
|
+
ok: {
|
|
16
|
+
status: 200
|
|
17
|
+
}
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def self.json(template: nil, body: nil, status: nil, headers: {})
|
|
21
|
+
headers["content-type"] = "application/json"
|
|
22
|
+
body ||= TEMPLATES.dig(template, :body)
|
|
23
|
+
status ||= TEMPLATES.dig(template, :status) || 200
|
|
24
|
+
[status, headers, [MultiJson.dump(body)]]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/lepus/web.rb
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack"
|
|
4
|
+
require "multi_json"
|
|
5
|
+
require "pathname"
|
|
6
|
+
|
|
7
|
+
module Lepus
|
|
8
|
+
module Web
|
|
9
|
+
# Web-specific configuration extensions.
|
|
10
|
+
# Only activated when lepus/web is explicitly required.
|
|
11
|
+
module ConfigExtensions
|
|
12
|
+
attr_accessor :web_show_all_exchanges
|
|
13
|
+
|
|
14
|
+
def initialize(*)
|
|
15
|
+
super
|
|
16
|
+
@web_show_all_exchanges = false
|
|
17
|
+
# The FileBackend writes to a local path, which breaks when workers
|
|
18
|
+
# and the dashboard run in separate containers/hosts. Requiring
|
|
19
|
+
# `lepus/web` implies you want cross-process visibility, so default
|
|
20
|
+
# to the shared RabbitMQ-backed registry. Users can still override
|
|
21
|
+
# with `config.process_registry_backend = :file` in their initializer.
|
|
22
|
+
@process_registry_backend = :rabbitmq
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Web-specific consumer extensions.
|
|
27
|
+
# Tracks whether the last delivery resulted in an error (exception),
|
|
28
|
+
# allowing stats to distinguish explicit rejections from error rejections.
|
|
29
|
+
module ConsumerExtensions
|
|
30
|
+
def process_delivery(delivery_info, metadata, payload)
|
|
31
|
+
@_last_delivery_errored = false
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def last_delivery_errored?
|
|
36
|
+
@_last_delivery_errored == true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def on_delivery_error
|
|
42
|
+
@_last_delivery_errored = true
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Web-specific handler extensions.
|
|
48
|
+
# Adds per-consumer stats recording on message delivery outcomes.
|
|
49
|
+
module HandlerExtensions
|
|
50
|
+
attr_accessor :stats
|
|
51
|
+
|
|
52
|
+
def process_delivery(delivery_info, metadata, payload)
|
|
53
|
+
super.tap { |result| record_stats(result) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def record_stats(result)
|
|
59
|
+
return unless stats
|
|
60
|
+
|
|
61
|
+
case result
|
|
62
|
+
when :ack
|
|
63
|
+
stats.record_processed
|
|
64
|
+
when :reject, :nack, :requeue
|
|
65
|
+
if consumer.last_delivery_errored?
|
|
66
|
+
stats.record_errored
|
|
67
|
+
else
|
|
68
|
+
stats.record_rejected
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
rescue # rubocop:disable Lint/SuppressedException
|
|
72
|
+
# Never let stats recording interfere with message processing
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Web-specific worker extensions.
|
|
77
|
+
# Creates per-worker stats registry, collects metrics, and
|
|
78
|
+
# overrides heartbeat to include metrics data in heartbeat messages.
|
|
79
|
+
module WorkerExtensions
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def setup_consumers!
|
|
83
|
+
@stats_registry = Lepus::Consumers::StatsRegistry.new
|
|
84
|
+
super
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_handler(consumer_class, channel, queue, tag)
|
|
88
|
+
super.tap do |handler|
|
|
89
|
+
handler.stats = @stats_registry.for(consumer_class)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def heartbeat
|
|
94
|
+
process.heartbeat(metrics: metrics_data)
|
|
95
|
+
rescue Process::NotFoundError
|
|
96
|
+
self.process = nil
|
|
97
|
+
interrupt
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def metrics_data
|
|
101
|
+
{
|
|
102
|
+
rss_memory: safe_rss_memory,
|
|
103
|
+
connections: connection_pool_size,
|
|
104
|
+
consumers: @stats_registry&.all || []
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def safe_rss_memory
|
|
109
|
+
Processes::MEMORY_GRABBER.call(pid) * 1024 # Convert kB to bytes
|
|
110
|
+
rescue
|
|
111
|
+
0
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def connection_pool_size
|
|
115
|
+
@connection_pool&.size || 0
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Extend core classes with web-specific behavior when loaded
|
|
120
|
+
Lepus::Configuration.prepend(ConfigExtensions)
|
|
121
|
+
Lepus::Consumer.prepend(ConsumerExtensions)
|
|
122
|
+
Lepus::Consumers::Handler.prepend(HandlerExtensions)
|
|
123
|
+
Lepus::Consumers::Worker.prepend(WorkerExtensions)
|
|
124
|
+
|
|
125
|
+
# If `Lepus.config` was already memoized before `lepus/web` was required
|
|
126
|
+
# (the common case — initializers run before `routes.rb`), flip the flag
|
|
127
|
+
# now so the lazily-built `ProcessRegistry.backend` picks up the shared
|
|
128
|
+
# RabbitMQ-backed store. We only flip when it's still the out-of-the-box
|
|
129
|
+
# `:file` default to avoid stomping on an explicit choice.
|
|
130
|
+
#
|
|
131
|
+
# We deliberately do **not** reset an already-memoized backend: the
|
|
132
|
+
# supervisor boots the registry after `config/environment` loads, so no
|
|
133
|
+
# backend exists yet at this point during normal boot. Resetting would
|
|
134
|
+
# leave a fresh unstarted backend behind and the next `.add` would raise.
|
|
135
|
+
if Lepus.instance_variable_defined?(:@config)
|
|
136
|
+
existing = Lepus.instance_variable_get(:@config)
|
|
137
|
+
existing.process_registry_backend = :rabbitmq if existing && existing.process_registry_backend == :file
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
class << self
|
|
141
|
+
attr_accessor :aggregator
|
|
142
|
+
attr_accessor :management_api
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def self.assets_path
|
|
146
|
+
@assets_path ||= Pathname.new(File.expand_path("../../", __dir__)).join("web")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def self.start_aggregator
|
|
150
|
+
return if aggregator&.running?
|
|
151
|
+
|
|
152
|
+
self.aggregator = Aggregator.new
|
|
153
|
+
aggregator.start
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.stop_aggregator
|
|
157
|
+
aggregator&.stop
|
|
158
|
+
self.aggregator = nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def self.start_management_api
|
|
162
|
+
self.management_api = Lepus.config.build_management_api
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def self.stop_management_api
|
|
166
|
+
self.management_api = nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Start all web services (aggregator and management API)
|
|
170
|
+
def self.start
|
|
171
|
+
start_aggregator
|
|
172
|
+
start_management_api
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Stop all web services
|
|
176
|
+
def self.stop
|
|
177
|
+
stop_aggregator
|
|
178
|
+
stop_management_api
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def self.render_index(env)
|
|
182
|
+
base = base_path(env)
|
|
183
|
+
html = File.read(assets_path.join("index.html"))
|
|
184
|
+
html.gsub("__BASE_PATH__", base)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def self.base_path(env)
|
|
188
|
+
script_name = env["SCRIPT_NAME"].to_s
|
|
189
|
+
script_name = script_name.chomp("/")
|
|
190
|
+
"#{script_name}/"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def self.mime_for(path)
|
|
194
|
+
case File.extname(path)
|
|
195
|
+
when ".html" then "text/html"
|
|
196
|
+
when ".css" then "text/css"
|
|
197
|
+
when ".js" then "application/javascript"
|
|
198
|
+
when ".png" then "image/png"
|
|
199
|
+
when ".jpg", ".jpeg" then "image/jpeg"
|
|
200
|
+
when ".svg" then "image/svg+xml"
|
|
201
|
+
when ".woff", ".woff2" then "font/woff"
|
|
202
|
+
when ".ttf" then "font/ttf"
|
|
203
|
+
when ".eot" then "application/vnd.ms-fontobject"
|
|
204
|
+
else "application/octet-stream"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Make the Web module directly mountable as a Rack application.
|
|
209
|
+
#
|
|
210
|
+
# Mounting via `mount Lepus::Web => "/lepus"` in routes.rb does not call
|
|
211
|
+
# `Lepus::Web.start` — Rails just stashes a reference to this module and
|
|
212
|
+
# dispatches requests to `.call`. So we lazily start the aggregator and
|
|
213
|
+
# management API on first request and memoize the Rack app. This only
|
|
214
|
+
# runs in processes that actually serve HTTP for the dashboard; the
|
|
215
|
+
# supervisor loads `routes.rb` during boot but never dispatches requests,
|
|
216
|
+
# so it never pays this cost.
|
|
217
|
+
def self.call(env)
|
|
218
|
+
ensure_started
|
|
219
|
+
(@rack_app ||= App.build).call(env)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def self.ensure_started
|
|
223
|
+
return if @rack_app && aggregator&.running?
|
|
224
|
+
|
|
225
|
+
@boot_mutex ||= Mutex.new
|
|
226
|
+
@boot_mutex.synchronize do
|
|
227
|
+
start unless aggregator&.running?
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Require web sub-files (not managed by Zeitwerk)
|
|
234
|
+
require_relative "web/respond_with"
|
|
235
|
+
require_relative "web/management_api"
|
|
236
|
+
require_relative "web/aggregator"
|
|
237
|
+
require_relative "web/api"
|
|
238
|
+
require_relative "web/app"
|
data/lib/lepus.rb
CHANGED
|
@@ -15,13 +15,23 @@ require "zeitwerk"
|
|
|
15
15
|
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
|
16
16
|
loader.inflector.inflect "json" => "JSON"
|
|
17
17
|
loader.inflector.inflect "cli" => "CLI"
|
|
18
|
+
loader.inflector.inflect "api" => "API"
|
|
19
|
+
loader.inflector.inflect "management_api" => "ManagementAPI"
|
|
18
20
|
loader.collapse("#{__dir__}/lepus/rails.rb")
|
|
19
21
|
loader.collapse("#{__dir__}/lepus/rails/*")
|
|
20
22
|
loader.ignore("#{__dir__}/puma")
|
|
23
|
+
loader.ignore("#{__dir__}/lepus/testing.rb")
|
|
24
|
+
loader.ignore("#{__dir__}/lepus/unique.rb")
|
|
25
|
+
loader.ignore("#{__dir__}/lepus/testing/*")
|
|
21
26
|
loader.ignore("#{__dir__}/lepus/rails")
|
|
22
27
|
loader.ignore("#{__dir__}/lepus/rails.rb")
|
|
23
28
|
loader.ignore("#{__dir__}/lepus/cli.rb")
|
|
24
|
-
loader.ignore("#{__dir__}/lepus/middlewares")
|
|
29
|
+
loader.ignore("#{__dir__}/lepus/consumers/middlewares")
|
|
30
|
+
loader.ignore("#{__dir__}/lepus/producers/middlewares")
|
|
31
|
+
loader.ignore("#{__dir__}/lepus/web.rb")
|
|
32
|
+
loader.ignore("#{__dir__}/lepus/web")
|
|
33
|
+
loader.ignore("#{__dir__}/lepus/prometheus.rb")
|
|
34
|
+
loader.ignore("#{__dir__}/lepus/prometheus")
|
|
25
35
|
loader.log! if ENV["DEBUG"]
|
|
26
36
|
loader.setup
|
|
27
37
|
|
|
@@ -43,6 +53,9 @@ module Lepus
|
|
|
43
53
|
class InvalidConsumerConfigError < Error
|
|
44
54
|
end
|
|
45
55
|
|
|
56
|
+
class InvalidProducerConfigError < Error
|
|
57
|
+
end
|
|
58
|
+
|
|
46
59
|
class ShutdownError < Error
|
|
47
60
|
end
|
|
48
61
|
|
|
@@ -50,41 +63,39 @@ module Lepus
|
|
|
50
63
|
class MaxRecoveryAttemptsExhaustedError < ShutdownError
|
|
51
64
|
end
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def logger
|
|
56
|
-
@logger ||= DEFAULT_LOGGER
|
|
57
|
-
end
|
|
66
|
+
class << self
|
|
67
|
+
attr_writer :logger
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
def logger
|
|
70
|
+
@logger ||= DEFAULT_LOGGER
|
|
71
|
+
end
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
def instrument(channel, **options, &block)
|
|
74
|
+
if defined?(ActiveSupport::Notifications)
|
|
75
|
+
ActiveSupport::Notifications.instrument("#{channel}.lepus", **options, &block)
|
|
76
|
+
else
|
|
77
|
+
yield(options.dup)
|
|
78
|
+
end
|
|
68
79
|
end
|
|
69
|
-
end
|
|
70
80
|
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
def eager_load_consumers!
|
|
82
|
+
return false unless Lepus.config.consumers_directory.exist?
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
Dir[config.consumers_directory.join("**/*.rb")].map { |path| Pathname.new(path) }.each do |path|
|
|
85
|
+
next unless path.extname == ".rb"
|
|
76
86
|
|
|
77
|
-
|
|
87
|
+
require(path.expand_path.to_s)
|
|
88
|
+
end
|
|
89
|
+
true
|
|
78
90
|
end
|
|
79
|
-
true
|
|
80
|
-
end
|
|
81
91
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
def config
|
|
93
|
+
@config ||= Configuration.new
|
|
94
|
+
end
|
|
85
95
|
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
def configure
|
|
97
|
+
yield config
|
|
98
|
+
end
|
|
88
99
|
end
|
|
89
100
|
end
|
|
90
101
|
|
|
@@ -92,4 +103,4 @@ if defined?(::Rails)
|
|
|
92
103
|
require_relative "lepus/rails"
|
|
93
104
|
end
|
|
94
105
|
|
|
95
|
-
|
|
106
|
+
loader.eager_load
|