lepus 0.0.1.rc2 → 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/.gitignore +0 -1
- data/Gemfile +5 -0
- data/Gemfile.lock +12 -1
- data/README.md +179 -0
- data/config.ru +14 -0
- 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 +1 -0
- data/gemfiles/Gemfile.rails-5.2.lock +59 -46
- data/gemfiles/Gemfile.rails-6.1 +1 -0
- data/gemfiles/Gemfile.rails-6.1.lock +72 -58
- data/gemfiles/Gemfile.rails-7.2.lock +8 -1
- data/gemfiles/Gemfile.rails-8.0.lock +8 -1
- data/lepus.gemspec +5 -1
- data/lib/lepus/cli.rb +24 -0
- data/lib/lepus/configuration.rb +42 -0
- data/lib/lepus/consumer.rb +12 -0
- data/lib/lepus/consumers/handler.rb +3 -1
- data/lib/lepus/consumers/stats.rb +70 -0
- data/lib/lepus/consumers/stats_registry.rb +29 -0
- data/lib/lepus/consumers/worker.rb +7 -6
- data/lib/lepus/process.rb +4 -4
- 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 +28 -67
- 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 +3 -1
- data/lib/lepus/supervisor.rb +9 -2
- 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 +5 -0
- 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 +103 -5
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
module Lepus
|
|
6
|
+
module Prometheus
|
|
7
|
+
# Hooks that run inside each Lepus process and forward metrics to the
|
|
8
|
+
# prometheus_exporter server via Lepus::Prometheus.emit.
|
|
9
|
+
module Instrumentation
|
|
10
|
+
# Tracks per-delivery outcomes and latency. Always emits a metric, even
|
|
11
|
+
# when the underlying consumer raises, so error rates are observable.
|
|
12
|
+
module HandlerExtensions
|
|
13
|
+
def process_delivery(delivery_info, metadata, payload)
|
|
14
|
+
start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
15
|
+
result = nil
|
|
16
|
+
error_class = nil
|
|
17
|
+
begin
|
|
18
|
+
result = super
|
|
19
|
+
rescue => e
|
|
20
|
+
error_class = e.class.name
|
|
21
|
+
raise
|
|
22
|
+
ensure
|
|
23
|
+
Lepus::Prometheus.emit(
|
|
24
|
+
:delivery,
|
|
25
|
+
consumer: @consumer_class.name,
|
|
26
|
+
queue: queue_name_for_metric,
|
|
27
|
+
result: error_class ? "error" : result.to_s,
|
|
28
|
+
error: error_class.to_s,
|
|
29
|
+
duration: ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def queue_name_for_metric
|
|
38
|
+
q = queue
|
|
39
|
+
q.respond_to?(:name) ? q.name : q.to_s
|
|
40
|
+
rescue
|
|
41
|
+
""
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Emits process-level gauges on each heartbeat tick. rss_memory is keyed
|
|
46
|
+
# on (kind, name) only to avoid leaking a new series per pid; pid and
|
|
47
|
+
# hostname stay available on the `lepus_process_info` info gauge.
|
|
48
|
+
module WorkerExtensions
|
|
49
|
+
def heartbeat
|
|
50
|
+
super
|
|
51
|
+
ensure
|
|
52
|
+
Lepus::Prometheus.emit(
|
|
53
|
+
:process,
|
|
54
|
+
kind: kind.to_s,
|
|
55
|
+
name: name,
|
|
56
|
+
rss_memory: safe_rss_memory_bytes
|
|
57
|
+
)
|
|
58
|
+
Lepus::Prometheus.emit(
|
|
59
|
+
:process_info,
|
|
60
|
+
kind: kind.to_s,
|
|
61
|
+
name: name,
|
|
62
|
+
pid: pid.to_s,
|
|
63
|
+
hostname: safe_hostname
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def safe_rss_memory_bytes
|
|
70
|
+
Lepus::Processes::MEMORY_GRABBER.call(pid) * 1024
|
|
71
|
+
rescue
|
|
72
|
+
0
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def safe_hostname
|
|
76
|
+
Socket.gethostname
|
|
77
|
+
rescue
|
|
78
|
+
""
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Periodic poller that turns RabbitMQ queue stats into gauge events.
|
|
83
|
+
# Runs in a single thread inside whichever process enabled it.
|
|
84
|
+
class QueuePoller
|
|
85
|
+
@thread = nil
|
|
86
|
+
@mutex = Mutex.new
|
|
87
|
+
|
|
88
|
+
class << self
|
|
89
|
+
def start(interval:, api:)
|
|
90
|
+
@mutex.synchronize do
|
|
91
|
+
stop_locked
|
|
92
|
+
@thread = Thread.new { run_loop(interval, api) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def stop
|
|
97
|
+
@mutex.synchronize { stop_locked }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def running?
|
|
101
|
+
@mutex.synchronize { !@thread.nil? && @thread.alive? }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def stop_locked
|
|
107
|
+
@thread&.kill
|
|
108
|
+
@thread = nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def run_loop(interval, api)
|
|
112
|
+
loop do
|
|
113
|
+
poll_once(api)
|
|
114
|
+
sleep interval
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def poll_once(api)
|
|
119
|
+
api.queues.each do |q|
|
|
120
|
+
Lepus::Prometheus.emit(
|
|
121
|
+
:queue,
|
|
122
|
+
name: q[:name],
|
|
123
|
+
messages: q[:messages].to_i,
|
|
124
|
+
messages_ready: q[:messages_ready].to_i,
|
|
125
|
+
messages_unacknowledged: q[:messages_unacknowledged].to_i,
|
|
126
|
+
consumers: q[:consumers].to_i,
|
|
127
|
+
memory: q[:memory].to_i
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
Lepus::Prometheus.emit(:queue_poll, timestamp: Time.now.to_f)
|
|
131
|
+
rescue => e
|
|
132
|
+
Lepus::Prometheus.emit(:queue_poll_error, error: e.class.name)
|
|
133
|
+
Lepus.logger.warn("[Lepus::Prometheus] queue poll failed: #{e.message}")
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class << self
|
|
139
|
+
def install!
|
|
140
|
+
return if @installed
|
|
141
|
+
|
|
142
|
+
Lepus::Consumers::Handler.prepend(HandlerExtensions)
|
|
143
|
+
Lepus::Consumers::Worker.prepend(WorkerExtensions)
|
|
144
|
+
subscribe_publish_events
|
|
145
|
+
@installed = true
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def subscribe_publish_events
|
|
151
|
+
return unless defined?(ActiveSupport::Notifications)
|
|
152
|
+
|
|
153
|
+
ActiveSupport::Notifications.subscribe(/\Apublish\.lepus\z/) do |*args|
|
|
154
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
155
|
+
Lepus::Prometheus.emit(
|
|
156
|
+
:publish,
|
|
157
|
+
exchange: event.payload[:exchange].to_s,
|
|
158
|
+
routing_key: event.payload[:routing_key].to_s,
|
|
159
|
+
duration: event.duration / 1000.0
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
Lepus::Prometheus::Instrumentation.install!
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prometheus_exporter"
|
|
4
|
+
require "prometheus_exporter/client"
|
|
5
|
+
|
|
6
|
+
require_relative "prometheus/instrumentation"
|
|
7
|
+
|
|
8
|
+
module Lepus
|
|
9
|
+
# Optional integration with the prometheus_exporter gem.
|
|
10
|
+
# Require "lepus/prometheus" in your Lepus process to start shipping
|
|
11
|
+
# metrics to a running prometheus_exporter server via the default client.
|
|
12
|
+
#
|
|
13
|
+
# On the prometheus_exporter server side, load the companion collector:
|
|
14
|
+
# prometheus_exporter -a lepus/prometheus/collector
|
|
15
|
+
module Prometheus
|
|
16
|
+
DEFAULT_QUEUE_POLL_INTERVAL = 30
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
attr_writer :client
|
|
20
|
+
|
|
21
|
+
def client
|
|
22
|
+
@client ||= ::PrometheusExporter::Client.default
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Emit an opaque metric payload to the exporter server.
|
|
26
|
+
# Silently swallows transport errors so instrumentation cannot
|
|
27
|
+
# break the caller; non-transport bugs surface as debug logs.
|
|
28
|
+
def emit(metric, **data)
|
|
29
|
+
client.send_json(type: "lepus", metric: metric.to_s, **data)
|
|
30
|
+
rescue => e
|
|
31
|
+
Lepus.logger.debug { "[Lepus::Prometheus] emit(#{metric}) failed: #{e.class}: #{e.message}" }
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Start polling the RabbitMQ management API for queue-level gauges.
|
|
36
|
+
# Safe to call once per process. Requires "lepus/web/management_api".
|
|
37
|
+
def watch_queues(interval: DEFAULT_QUEUE_POLL_INTERVAL, api: nil)
|
|
38
|
+
require "lepus/web/management_api"
|
|
39
|
+
api ||= Lepus::Web::ManagementAPI.new
|
|
40
|
+
Instrumentation::QueuePoller.start(interval: interval, api: api)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def stop_watching_queues
|
|
44
|
+
Instrumentation::QueuePoller.stop
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/lepus/publisher.rb
CHANGED
|
@@ -42,7 +42,9 @@ module Lepus
|
|
|
42
42
|
|
|
43
43
|
payload, opts = prepare_message(message, **options)
|
|
44
44
|
exchange = channel.exchange(exchange_name, exchange_options)
|
|
45
|
-
|
|
45
|
+
Lepus.instrument(:publish, exchange: exchange_name, routing_key: opts[:routing_key]) do
|
|
46
|
+
exchange.publish(payload, opts)
|
|
47
|
+
end
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
private
|
data/lib/lepus/supervisor.rb
CHANGED
|
@@ -32,6 +32,8 @@ module Lepus
|
|
|
32
32
|
@configured_processes = {}
|
|
33
33
|
|
|
34
34
|
super
|
|
35
|
+
|
|
36
|
+
@name ||= hostname
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
def start
|
|
@@ -81,8 +83,6 @@ module Lepus
|
|
|
81
83
|
end
|
|
82
84
|
|
|
83
85
|
def boot
|
|
84
|
-
ProcessRegistry.start
|
|
85
|
-
|
|
86
86
|
Lepus.instrument(:start_process, process: self) do
|
|
87
87
|
if require_file
|
|
88
88
|
Kernel.require(require_file)
|
|
@@ -96,6 +96,13 @@ module Lepus
|
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
# Start the registry *after* the host app is loaded. Mounting
|
|
100
|
+
# `Lepus::Web` in `routes.rb` (or any late `require "lepus/web"`) flips
|
|
101
|
+
# the default backend to :rabbitmq; starting the registry earlier
|
|
102
|
+
# would lock in the FileBackend and the later flip would point the
|
|
103
|
+
# registry at a backend that was never started.
|
|
104
|
+
ProcessRegistry.start
|
|
105
|
+
|
|
99
106
|
setup_consumers
|
|
100
107
|
check_bunny_connection
|
|
101
108
|
|
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
|