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,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "base64"
|
|
6
|
+
|
|
7
|
+
module Lepus
|
|
8
|
+
class ProcessRegistry
|
|
9
|
+
# File-based backend for process registry storage.
|
|
10
|
+
# Stores process data in a file using Marshal serialization.
|
|
11
|
+
# This is the default backend for apps not using the web dashboard.
|
|
12
|
+
class FileBackend
|
|
13
|
+
include Backend
|
|
14
|
+
|
|
15
|
+
attr_reader :path
|
|
16
|
+
|
|
17
|
+
def initialize(path: nil)
|
|
18
|
+
@path = path
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def start
|
|
22
|
+
@path ||= Pathname.new(Dir.tmpdir).join("lepus_process_registry.store")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def stop
|
|
26
|
+
path.delete if path&.exist?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add(process, metrics: {})
|
|
30
|
+
transaction do |data|
|
|
31
|
+
data[process.id] = process.to_h
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete(process)
|
|
36
|
+
transaction do |data|
|
|
37
|
+
data.delete(process.id)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def find(id)
|
|
42
|
+
raw = read.fetch(id) { raise(Lepus::Process::NotFoundError.new(id)) }
|
|
43
|
+
Lepus::Process.coerce(raw)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def exists?(id)
|
|
47
|
+
read.key?(id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def all
|
|
51
|
+
read.keys.map { |id| find(id) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def count
|
|
55
|
+
return 0 unless path
|
|
56
|
+
|
|
57
|
+
read.size
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def clear
|
|
61
|
+
return unless path
|
|
62
|
+
|
|
63
|
+
write({})
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def transaction
|
|
69
|
+
data = read
|
|
70
|
+
yield data
|
|
71
|
+
write(data)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def read
|
|
75
|
+
with_lock(File::LOCK_SH) do |f|
|
|
76
|
+
if f.size.zero?
|
|
77
|
+
{}
|
|
78
|
+
else
|
|
79
|
+
encoded = f.read
|
|
80
|
+
Marshal.load(Base64.strict_decode64(encoded))
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def write(data)
|
|
86
|
+
with_lock(File::LOCK_EX) do |f|
|
|
87
|
+
f.rewind
|
|
88
|
+
f.truncate(0)
|
|
89
|
+
encoded = Base64.strict_encode64(Marshal.dump(data))
|
|
90
|
+
f.write(encoded)
|
|
91
|
+
f.flush
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def with_lock(lock_type)
|
|
96
|
+
unless path
|
|
97
|
+
raise "ProcessRegistry not started. Call Lepus::ProcessRegistry.start first."
|
|
98
|
+
end
|
|
99
|
+
File.open(path, File::RDWR | File::CREAT | File::BINARY, 0o644) do |f|
|
|
100
|
+
f.flock(lock_type)
|
|
101
|
+
result = yield f
|
|
102
|
+
f.flock(File::LOCK_UN)
|
|
103
|
+
result
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Lepus
|
|
6
|
+
class ProcessRegistry
|
|
7
|
+
# Builds heartbeat messages for RabbitMQ publishing.
|
|
8
|
+
class MessageBuilder
|
|
9
|
+
VERSION = "1.0"
|
|
10
|
+
|
|
11
|
+
def initialize(process, metrics: {})
|
|
12
|
+
@process = process
|
|
13
|
+
@metrics = metrics
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_heartbeat
|
|
17
|
+
{
|
|
18
|
+
type: "heartbeat",
|
|
19
|
+
version: VERSION,
|
|
20
|
+
process: process_data,
|
|
21
|
+
metrics: metrics_data
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def build_deregister
|
|
26
|
+
{
|
|
27
|
+
type: "deregister",
|
|
28
|
+
version: VERSION,
|
|
29
|
+
process_id: @process.id,
|
|
30
|
+
timestamp: Time.now.iso8601(6)
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_json
|
|
35
|
+
JSON.generate(build_heartbeat)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def process_data
|
|
41
|
+
{
|
|
42
|
+
id: @process.id,
|
|
43
|
+
name: @process.name,
|
|
44
|
+
pid: @process.pid,
|
|
45
|
+
hostname: @process.hostname,
|
|
46
|
+
kind: @process.kind,
|
|
47
|
+
supervisor_id: @process.supervisor_id,
|
|
48
|
+
application: Lepus.config.application_name,
|
|
49
|
+
last_heartbeat_at: format_time(@process.last_heartbeat_at)
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def metrics_data
|
|
54
|
+
{
|
|
55
|
+
rss_memory: @metrics[:rss_memory] || safe_rss_memory,
|
|
56
|
+
connections: @metrics[:connections] || 0,
|
|
57
|
+
consumers: @metrics[:consumers] || []
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def safe_rss_memory
|
|
62
|
+
@process.rss_memory * 1024 # Convert kB to bytes (MEMORY_GRABBER returns kB)
|
|
63
|
+
rescue
|
|
64
|
+
0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def format_time(time)
|
|
68
|
+
time&.iso8601(6)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
6
|
+
module Lepus
|
|
7
|
+
class ProcessRegistry
|
|
8
|
+
# RabbitMQ-based backend for process registry.
|
|
9
|
+
# Publishes heartbeats to a fanout exchange for web dashboard aggregation.
|
|
10
|
+
# Also writes locally via FileBackend for local queries when aggregator is unavailable.
|
|
11
|
+
class RabbitmqBackend
|
|
12
|
+
include Backend
|
|
13
|
+
|
|
14
|
+
HEARTBEAT_EXCHANGE = "lepus.heartbeat"
|
|
15
|
+
|
|
16
|
+
attr_reader :fallback
|
|
17
|
+
|
|
18
|
+
def initialize(fallback: nil)
|
|
19
|
+
@fallback = fallback || FileBackend.new
|
|
20
|
+
@connection = nil
|
|
21
|
+
@channel = nil
|
|
22
|
+
@exchange = nil
|
|
23
|
+
@mutex = Mutex.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def start
|
|
27
|
+
@fallback.start
|
|
28
|
+
setup_channel_and_exchange
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stop
|
|
32
|
+
@fallback.stop
|
|
33
|
+
close_channel
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def add(process, metrics: {})
|
|
37
|
+
@fallback.add(process)
|
|
38
|
+
publish_heartbeat(process, metrics: metrics)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def delete(process)
|
|
42
|
+
@fallback.delete(process)
|
|
43
|
+
publish_deregister(process)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def find(id)
|
|
47
|
+
@fallback.find(id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def exists?(id)
|
|
51
|
+
@fallback.exists?(id)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def all
|
|
55
|
+
@fallback.all
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def count
|
|
59
|
+
@fallback.count
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def clear
|
|
63
|
+
@fallback.clear
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def path
|
|
67
|
+
@fallback.path
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def setup_channel_and_exchange
|
|
73
|
+
return unless rabbitmq_available?
|
|
74
|
+
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
return if @channel&.open?
|
|
77
|
+
|
|
78
|
+
@connection = Lepus.config.create_connection(suffix: "(registry)")
|
|
79
|
+
@channel = @connection.create_channel
|
|
80
|
+
@exchange = @channel.fanout(
|
|
81
|
+
HEARTBEAT_EXCHANGE,
|
|
82
|
+
durable: false,
|
|
83
|
+
auto_delete: false
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
rescue => e
|
|
87
|
+
Lepus.logger.warn("[ProcessRegistry] Failed to setup RabbitMQ channel: #{e.message}")
|
|
88
|
+
@connection = nil
|
|
89
|
+
@channel = nil
|
|
90
|
+
@exchange = nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Tear down the dedicated registry connection on supervisor shutdown.
|
|
94
|
+
# Bunny's graceful close waits up to 15s per channel for a broker
|
|
95
|
+
# `close-ok` continuation; during forked supervisor shutdown the broker
|
|
96
|
+
# sometimes never replies and SIGTERM handling blows past its 10s budget,
|
|
97
|
+
# timing out the integration specs. We bound the graceful attempt at 2s
|
|
98
|
+
# and fall back to closing the socket directly so the process can exit.
|
|
99
|
+
CLOSE_TIMEOUT = 2
|
|
100
|
+
|
|
101
|
+
def close_channel
|
|
102
|
+
@mutex.synchronize do
|
|
103
|
+
force_close_connection if @connection
|
|
104
|
+
end
|
|
105
|
+
ensure
|
|
106
|
+
@connection = nil
|
|
107
|
+
@channel = nil
|
|
108
|
+
@exchange = nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def force_close_connection
|
|
112
|
+
Timeout.timeout(CLOSE_TIMEOUT) { @connection.close(false) } if @connection.open?
|
|
113
|
+
rescue => e
|
|
114
|
+
Lepus.logger.warn("[ProcessRegistry] Failed to close RabbitMQ connection: #{e.message}")
|
|
115
|
+
begin
|
|
116
|
+
@connection.instance_variable_get(:@transport)&.close
|
|
117
|
+
rescue
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def publish_heartbeat(process, metrics: {})
|
|
123
|
+
return unless @exchange
|
|
124
|
+
|
|
125
|
+
message = MessageBuilder.new(process, metrics: metrics)
|
|
126
|
+
@exchange.publish(
|
|
127
|
+
message.to_json,
|
|
128
|
+
content_type: "application/json"
|
|
129
|
+
)
|
|
130
|
+
rescue => e
|
|
131
|
+
Lepus.logger.warn("[ProcessRegistry] Failed to publish heartbeat: #{e.message}")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def publish_deregister(process)
|
|
135
|
+
return unless @exchange
|
|
136
|
+
|
|
137
|
+
message = MessageBuilder.new(process).build_deregister
|
|
138
|
+
@exchange.publish(
|
|
139
|
+
JSON.generate(message),
|
|
140
|
+
content_type: "application/json"
|
|
141
|
+
)
|
|
142
|
+
rescue => e
|
|
143
|
+
Lepus.logger.warn("[ProcessRegistry] Failed to publish deregister: #{e.message}")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def rabbitmq_available?
|
|
147
|
+
Lepus.config.rabbitmq_url.present?
|
|
148
|
+
rescue
|
|
149
|
+
true
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "pathname"
|
|
4
|
-
require "tmpdir"
|
|
5
|
-
require "base64"
|
|
6
|
-
|
|
7
3
|
module Lepus
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# I'll refactor this class later when we have a better idea of the requirements.
|
|
4
|
+
# Process registry that delegates to a configurable backend.
|
|
5
|
+
# Default backend is FileBackend for local file-based storage.
|
|
6
|
+
# Use RabbitmqBackend to share process data across apps via web dashboard.
|
|
12
7
|
class ProcessRegistry
|
|
13
8
|
class << self
|
|
14
|
-
|
|
9
|
+
def backend
|
|
10
|
+
@backend ||= Lepus.config.build_process_registry_backend
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_writer :backend
|
|
14
|
+
|
|
15
|
+
def reset_backend!
|
|
16
|
+
@backend = nil
|
|
17
|
+
end
|
|
15
18
|
|
|
16
19
|
def start
|
|
17
|
-
|
|
20
|
+
backend.start
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def stop
|
|
21
|
-
|
|
24
|
+
backend.stop
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
def reset!
|
|
@@ -27,82 +30,40 @@ module Lepus
|
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
def add(process)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
backend.add(process)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def update(process, metrics: {})
|
|
37
|
+
backend.update(process, metrics: metrics)
|
|
33
38
|
end
|
|
34
|
-
alias_method :update, :add
|
|
35
39
|
|
|
36
40
|
def delete(process)
|
|
37
|
-
|
|
38
|
-
data.delete(process.id)
|
|
39
|
-
end
|
|
41
|
+
backend.delete(process)
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def find(id)
|
|
43
|
-
|
|
44
|
-
Lepus::Process.coerce(raw)
|
|
45
|
+
backend.find(id)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
def exists?(id)
|
|
48
|
-
|
|
49
|
+
backend.exists?(id)
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
def all
|
|
52
|
-
|
|
53
|
+
backend.all
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
def count
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
read.size
|
|
57
|
+
backend.count
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
def clear
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
write({})
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
private
|
|
68
|
-
|
|
69
|
-
def transaction
|
|
70
|
-
data = read
|
|
71
|
-
yield data
|
|
72
|
-
write(data)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def read
|
|
76
|
-
with_lock(File::LOCK_SH) do |f|
|
|
77
|
-
if f.size.zero?
|
|
78
|
-
{}
|
|
79
|
-
else
|
|
80
|
-
encoded = f.read
|
|
81
|
-
Marshal.load(Base64.strict_decode64(encoded))
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def write(data)
|
|
87
|
-
with_lock(File::LOCK_EX) do |f|
|
|
88
|
-
f.rewind
|
|
89
|
-
f.truncate(0)
|
|
90
|
-
encoded = Base64.strict_encode64(Marshal.dump(data))
|
|
91
|
-
f.write(encoded)
|
|
92
|
-
f.flush
|
|
93
|
-
end
|
|
61
|
+
backend.clear
|
|
94
62
|
end
|
|
95
63
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
end
|
|
100
|
-
File.open(path, File::RDWR | File::CREAT | File::BINARY, 0o644) do |f|
|
|
101
|
-
f.flock(lock_type)
|
|
102
|
-
result = yield f
|
|
103
|
-
f.flock(File::LOCK_UN)
|
|
104
|
-
result
|
|
105
|
-
end
|
|
64
|
+
# For backward compatibility with tests that check @path
|
|
65
|
+
def path
|
|
66
|
+
backend.respond_to?(:path) ? backend.path : nil
|
|
106
67
|
end
|
|
107
68
|
end
|
|
108
69
|
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
|