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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/Gemfile +5 -0
  4. data/Gemfile.lock +12 -1
  5. data/README.md +179 -0
  6. data/config.ru +14 -0
  7. data/docs/README.md +80 -0
  8. data/docs/cli.md +108 -0
  9. data/docs/configuration.md +171 -0
  10. data/docs/consumers.md +168 -0
  11. data/docs/getting-started.md +136 -0
  12. data/docs/images/lepus-web.png +0 -0
  13. data/docs/middleware.md +240 -0
  14. data/docs/producers.md +173 -0
  15. data/docs/prometheus.md +112 -0
  16. data/docs/rails.md +161 -0
  17. data/docs/supervisor.md +112 -0
  18. data/docs/testing.md +141 -0
  19. data/docs/web.md +85 -0
  20. data/examples/grafana-dashboard.json +450 -0
  21. data/gemfiles/Gemfile.rails-5.2 +1 -0
  22. data/gemfiles/Gemfile.rails-5.2.lock +59 -46
  23. data/gemfiles/Gemfile.rails-6.1 +1 -0
  24. data/gemfiles/Gemfile.rails-6.1.lock +72 -58
  25. data/gemfiles/Gemfile.rails-7.2.lock +8 -1
  26. data/gemfiles/Gemfile.rails-8.0.lock +8 -1
  27. data/lepus.gemspec +5 -1
  28. data/lib/lepus/cli.rb +24 -0
  29. data/lib/lepus/configuration.rb +42 -0
  30. data/lib/lepus/consumer.rb +12 -0
  31. data/lib/lepus/consumers/handler.rb +3 -1
  32. data/lib/lepus/consumers/stats.rb +70 -0
  33. data/lib/lepus/consumers/stats_registry.rb +29 -0
  34. data/lib/lepus/consumers/worker.rb +7 -6
  35. data/lib/lepus/process.rb +4 -4
  36. data/lib/lepus/process_registry/backend.rb +49 -0
  37. data/lib/lepus/process_registry/file_backend.rb +108 -0
  38. data/lib/lepus/process_registry/message_builder.rb +72 -0
  39. data/lib/lepus/process_registry/rabbitmq_backend.rb +153 -0
  40. data/lib/lepus/process_registry.rb +28 -67
  41. data/lib/lepus/prometheus/collector.rb +149 -0
  42. data/lib/lepus/prometheus/instrumentation.rb +168 -0
  43. data/lib/lepus/prometheus.rb +48 -0
  44. data/lib/lepus/publisher.rb +3 -1
  45. data/lib/lepus/supervisor.rb +9 -2
  46. data/lib/lepus/version.rb +1 -1
  47. data/lib/lepus/web/aggregator.rb +154 -0
  48. data/lib/lepus/web/api.rb +132 -0
  49. data/lib/lepus/web/app.rb +37 -0
  50. data/lib/lepus/web/management_api.rb +192 -0
  51. data/lib/lepus/web/respond_with.rb +28 -0
  52. data/lib/lepus/web.rb +238 -0
  53. data/lib/lepus.rb +5 -0
  54. data/test_offline.html +189 -0
  55. data/web/assets/css/styles.css +635 -0
  56. data/web/assets/js/app.js +6 -0
  57. data/web/assets/js/bootstrap.js +20 -0
  58. data/web/assets/js/controllers/connection_controller.js +44 -0
  59. data/web/assets/js/controllers/dashboard_controller.js +499 -0
  60. data/web/assets/js/controllers/queue_controller.js +17 -0
  61. data/web/assets/js/controllers/theme_controller.js +31 -0
  62. data/web/assets/js/offline-manager.js +233 -0
  63. data/web/assets/js/service-worker-manager.js +65 -0
  64. data/web/index.html +159 -0
  65. data/web/sw.js +144 -0
  66. metadata +103 -5
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- lepus (0.0.1.rc2)
4
+ lepus (0.1.0)
5
+ base64
5
6
  bunny
6
7
  concurrent-ruby
7
8
  multi_json
@@ -70,54 +71,57 @@ GEM
70
71
  minitest (>= 5.1)
71
72
  tzinfo (~> 2.0)
72
73
  zeitwerk (~> 2.3)
73
- addressable (2.8.7)
74
- public_suffix (>= 2.0.2, < 7.0)
75
- amq-protocol (2.5.1)
74
+ addressable (2.9.0)
75
+ public_suffix (>= 2.0.2, < 8.0)
76
+ amq-protocol (2.7.0)
76
77
  ast (2.4.3)
77
78
  base64 (0.3.0)
78
- bigdecimal (3.2.3)
79
+ bigdecimal (4.1.1)
79
80
  builder (3.3.0)
80
- bunny (2.24.0)
81
- amq-protocol (~> 2.3)
81
+ bunny (3.1.0)
82
+ amq-protocol (~> 2.7)
83
+ logger (~> 1, >= 1.7)
82
84
  sorted_set (~> 1, >= 1.0.2)
83
85
  coderay (1.1.3)
84
- concurrent-ruby (1.3.5)
86
+ concurrent-ruby (1.3.6)
85
87
  connection_pool (2.5.5)
86
- crack (1.0.0)
88
+ crack (1.0.1)
87
89
  bigdecimal
88
90
  rexml
89
91
  crass (1.0.6)
90
- date (3.4.1)
92
+ date (3.5.1)
91
93
  de-dupe (0.0.2)
92
94
  redis
93
95
  zeitwerk
94
96
  diff-lcs (1.6.2)
95
97
  docile (1.4.1)
96
- dotenv (2.8.1)
98
+ dotenv (3.2.0)
97
99
  erubi (1.13.1)
98
- globalid (1.2.1)
100
+ globalid (1.3.0)
99
101
  activesupport (>= 6.1)
100
- hashdiff (1.2.0)
101
- i18n (1.14.7)
102
+ hashdiff (1.2.1)
103
+ i18n (1.14.8)
102
104
  concurrent-ruby (~> 1.0)
103
- json (2.13.2)
105
+ io-console (0.8.2)
106
+ json (2.19.3)
104
107
  language_server-protocol (3.17.0.5)
105
108
  lint_roller (1.1.0)
106
109
  logger (1.7.0)
107
- loofah (2.24.1)
110
+ loofah (2.25.1)
108
111
  crass (~> 1.0.2)
109
112
  nokogiri (>= 1.12.0)
110
- mail (2.8.1)
113
+ mail (2.9.0)
114
+ logger
111
115
  mini_mime (>= 0.1.1)
112
116
  net-imap
113
117
  net-pop
114
118
  net-smtp
115
- marcel (1.0.4)
119
+ marcel (1.1.0)
116
120
  method_source (1.1.0)
117
121
  mini_mime (1.1.5)
118
- minitest (5.25.5)
122
+ minitest (5.26.1)
119
123
  multi_json (1.19.1)
120
- net-imap (0.4.22)
124
+ net-imap (0.4.23)
121
125
  date
122
126
  net-protocol
123
127
  net-pop (0.1.2)
@@ -126,20 +130,23 @@ GEM
126
130
  timeout
127
131
  net-smtp (0.5.1)
128
132
  net-protocol
129
- nio4r (2.7.4)
130
- nokogiri (1.15.7-x86_64-linux)
133
+ nio4r (2.7.5)
134
+ nokogiri (1.17.2-x86_64-linux)
131
135
  racc (~> 1.4)
132
- parallel (1.27.0)
133
- parser (3.3.9.0)
136
+ parallel (1.28.0)
137
+ parser (3.3.11.1)
134
138
  ast (~> 2.4.1)
135
139
  racc
136
- prism (1.4.0)
137
- pry (0.15.2)
140
+ prism (1.9.0)
141
+ prometheus_exporter (2.2.0)
142
+ webrick
143
+ pry (0.16.0)
138
144
  coderay (~> 1.1)
139
145
  method_source (~> 1.0)
140
- public_suffix (5.1.1)
146
+ reline (>= 0.6.0)
147
+ public_suffix (6.0.2)
141
148
  racc (1.8.1)
142
- rack (2.2.17)
149
+ rack (2.2.23)
143
150
  rack-test (2.2.0)
144
151
  rack (>= 1.3)
145
152
  rails (6.1.7.10)
@@ -161,8 +168,8 @@ GEM
161
168
  activesupport (>= 5.0.0)
162
169
  minitest
163
170
  nokogiri (>= 1.6)
164
- rails-html-sanitizer (1.6.2)
165
- loofah (~> 2.21)
171
+ rails-html-sanitizer (1.7.0)
172
+ loofah (~> 2.25)
166
173
  nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
167
174
  railties (6.1.7.10)
168
175
  actionpack (= 6.1.7.10)
@@ -171,28 +178,30 @@ GEM
171
178
  rake (>= 12.2)
172
179
  thor (~> 1.0)
173
180
  rainbow (3.1.1)
174
- rake (13.3.0)
181
+ rake (13.4.2)
175
182
  rbtree (0.4.6)
176
183
  redis (5.4.1)
177
184
  redis-client (>= 0.22.0)
178
- redis-client (0.27.0)
185
+ redis-client (0.28.0)
179
186
  connection_pool
180
- regexp_parser (2.11.2)
181
- rexml (3.4.2)
182
- rspec (3.13.1)
187
+ regexp_parser (2.12.0)
188
+ reline (0.6.3)
189
+ io-console (~> 0.5)
190
+ rexml (3.4.4)
191
+ rspec (3.13.2)
183
192
  rspec-core (~> 3.13.0)
184
193
  rspec-expectations (~> 3.13.0)
185
194
  rspec-mocks (~> 3.13.0)
186
- rspec-core (3.13.5)
195
+ rspec-core (3.13.6)
187
196
  rspec-support (~> 3.13.0)
188
197
  rspec-expectations (3.13.5)
189
198
  diff-lcs (>= 1.2.0, < 2.0)
190
199
  rspec-support (~> 3.13.0)
191
- rspec-mocks (3.13.5)
200
+ rspec-mocks (3.13.8)
192
201
  diff-lcs (>= 1.2.0, < 2.0)
193
202
  rspec-support (~> 3.13.0)
194
- rspec-support (3.13.5)
195
- rubocop (1.80.2)
203
+ rspec-support (3.13.7)
204
+ rubocop (1.84.2)
196
205
  json (~> 2.3)
197
206
  language_server-protocol (~> 3.17.0.2)
198
207
  lint_roller (~> 1.1.0)
@@ -200,18 +209,19 @@ GEM
200
209
  parser (>= 3.3.0.2)
201
210
  rainbow (>= 2.2.2, < 4.0)
202
211
  regexp_parser (>= 2.9.3, < 3.0)
203
- rubocop-ast (>= 1.46.0, < 2.0)
212
+ rubocop-ast (>= 1.49.0, < 2.0)
204
213
  ruby-progressbar (~> 1.7)
205
214
  unicode-display_width (>= 2.4.0, < 4.0)
206
- rubocop-ast (1.46.0)
215
+ rubocop-ast (1.49.1)
207
216
  parser (>= 3.3.7.2)
208
- prism (~> 1.4)
209
- rubocop-performance (1.21.1)
210
- rubocop (>= 1.48.1, < 2.0)
211
- rubocop-ast (>= 1.31.1, < 2.0)
212
- rubocop-rspec (3.7.0)
217
+ prism (~> 1.7)
218
+ rubocop-performance (1.26.1)
219
+ lint_roller (~> 1.1)
220
+ rubocop (>= 1.75.0, < 2.0)
221
+ rubocop-ast (>= 1.47.1, < 2.0)
222
+ rubocop-rspec (3.9.0)
213
223
  lint_roller (~> 1.1)
214
- rubocop (~> 1.72, >= 1.72.1)
224
+ rubocop (~> 1.81)
215
225
  ruby-progressbar (1.13.0)
216
226
  simplecov (0.22.0)
217
227
  docile (~> 1.1)
@@ -229,29 +239,30 @@ GEM
229
239
  actionpack (>= 6.1)
230
240
  activesupport (>= 6.1)
231
241
  sprockets (>= 3.0.0)
232
- standard (1.35.0.1)
242
+ standard (1.54.0)
233
243
  language_server-protocol (~> 3.17.0.2)
234
244
  lint_roller (~> 1.0)
235
- rubocop (~> 1.62)
245
+ rubocop (~> 1.84.0)
236
246
  standard-custom (~> 1.0.0)
237
- standard-performance (~> 1.3)
247
+ standard-performance (~> 1.8)
238
248
  standard-custom (1.0.2)
239
249
  lint_roller (~> 1.0)
240
250
  rubocop (~> 1.50)
241
- standard-performance (1.4.0)
251
+ standard-performance (1.9.0)
242
252
  lint_roller (~> 1.1)
243
- rubocop-performance (~> 1.21.0)
244
- thor (1.4.0)
245
- timeout (0.4.3)
253
+ rubocop-performance (~> 1.26.0)
254
+ thor (1.5.0)
255
+ timeout (0.6.1)
246
256
  tzinfo (2.0.6)
247
257
  concurrent-ruby (~> 1.0)
248
- unicode-display_width (3.1.5)
249
- unicode-emoji (~> 4.0, >= 4.0.4)
250
- unicode-emoji (4.0.4)
251
- webmock (3.25.1)
258
+ unicode-display_width (3.2.0)
259
+ unicode-emoji (~> 4.1)
260
+ unicode-emoji (4.2.0)
261
+ webmock (3.26.2)
252
262
  addressable (>= 2.8.0)
253
263
  crack (>= 0.3.2)
254
264
  hashdiff (>= 0.4.0, < 2.0.0)
265
+ webrick (1.9.2)
255
266
  websocket-driver (0.8.0)
256
267
  base64
257
268
  websocket-extensions (>= 0.1.0)
@@ -266,7 +277,10 @@ DEPENDENCIES
266
277
  de-dupe
267
278
  dotenv
268
279
  lepus!
280
+ prometheus_exporter (< 2.3)
269
281
  pry
282
+ rack (>= 2.2)
283
+ rack-test
270
284
  rails (~> 6.1, >= 6.1.7.10)
271
285
  rspec
272
286
  rubocop
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- lepus (0.0.1.rc2)
4
+ lepus (0.1.0)
5
+ base64
5
6
  bunny
6
7
  concurrent-ruby
7
8
  multi_json
@@ -157,6 +158,8 @@ GEM
157
158
  prettyprint
158
159
  prettyprint (0.2.0)
159
160
  prism (1.4.0)
161
+ prometheus_exporter (2.3.1)
162
+ webrick
160
163
  pry (0.15.2)
161
164
  coderay (~> 1.1)
162
165
  method_source (~> 1.0)
@@ -287,6 +290,7 @@ GEM
287
290
  addressable (>= 2.8.0)
288
291
  crack (>= 0.3.2)
289
292
  hashdiff (>= 0.4.0, < 2.0.0)
293
+ webrick (1.9.2)
290
294
  websocket-driver (0.8.0)
291
295
  base64
292
296
  websocket-extensions (>= 0.1.0)
@@ -300,7 +304,10 @@ DEPENDENCIES
300
304
  de-dupe
301
305
  dotenv
302
306
  lepus!
307
+ prometheus_exporter
303
308
  pry
309
+ rack (>= 2.2)
310
+ rack-test
304
311
  rails (~> 7.2, >= 7.2.2.2)
305
312
  rspec
306
313
  rubocop
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- lepus (0.0.1.rc2)
4
+ lepus (0.1.0)
5
+ base64
5
6
  bunny
6
7
  concurrent-ruby
7
8
  multi_json
@@ -157,6 +158,8 @@ GEM
157
158
  prettyprint
158
159
  prettyprint (0.2.0)
159
160
  prism (1.4.0)
161
+ prometheus_exporter (2.3.1)
162
+ webrick
160
163
  pry (0.15.2)
161
164
  coderay (~> 1.1)
162
165
  method_source (~> 1.0)
@@ -288,6 +291,7 @@ GEM
288
291
  addressable (>= 2.8.0)
289
292
  crack (>= 0.3.2)
290
293
  hashdiff (>= 0.4.0, < 2.0.0)
294
+ webrick (1.9.2)
291
295
  websocket-driver (0.8.0)
292
296
  base64
293
297
  websocket-extensions (>= 0.1.0)
@@ -301,7 +305,10 @@ DEPENDENCIES
301
305
  de-dupe
302
306
  dotenv
303
307
  lepus!
308
+ prometheus_exporter
304
309
  pry
310
+ rack (>= 2.2)
311
+ rack-test
305
312
  rails (~> 8.0, >= 8.0.2.1)
306
313
  rspec
307
314
  rubocop
data/lepus.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  RabbitMQ consumers/producer for ruby applications
13
13
  SUMMARY
14
14
  spec.description = <<~DESCRIPTION
15
- RabbitMQ consumers/producer for ruby applicationsd
15
+ RabbitMQ consumers/producer for ruby applications
16
16
  DESCRIPTION
17
17
 
18
18
  spec.homepage = "https://github.com/marcosgz/lepus"
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
36
36
  spec.executables = spec.files.grep(%r{^exec/}) { |f| File.basename(f) }
37
37
  spec.require_paths = ["lib"]
38
38
 
39
+ spec.add_dependency "base64", ">= 0.0.0"
39
40
  spec.add_dependency "bunny", ">= 0.0.0"
40
41
  spec.add_dependency "thor", ">= 0.0.0"
41
42
  spec.add_dependency "zeitwerk", ">= 0.0.0"
@@ -44,6 +45,8 @@ Gem::Specification.new do |spec|
44
45
 
45
46
  spec.add_development_dependency "dotenv"
46
47
  spec.add_development_dependency "pry"
48
+ spec.add_development_dependency "rack", ">= 2.2"
49
+ spec.add_development_dependency "rack-test"
47
50
  spec.add_development_dependency "rspec"
48
51
  spec.add_development_dependency "rubocop"
49
52
  spec.add_development_dependency "rubocop-performance"
@@ -52,4 +55,5 @@ Gem::Specification.new do |spec|
52
55
  spec.add_development_dependency "webmock"
53
56
  spec.add_development_dependency "simplecov"
54
57
  spec.add_development_dependency "de-dupe"
58
+ spec.add_development_dependency "prometheus_exporter"
55
59
  end
data/lib/lepus/cli.rb CHANGED
@@ -30,5 +30,29 @@ module Lepus
30
30
 
31
31
  Lepus::Supervisor.start(**opts)
32
32
  end
33
+
34
+ desc "web", "Run Lepus Web dashboard"
35
+ method_option :port, type: :numeric, aliases: "-p", default: 9292, desc: "Port to listen on"
36
+ method_option :host, type: :string, aliases: "-o", default: "0.0.0.0", desc: "Host to bind"
37
+ def web
38
+ port = (options[:port] || 9292).to_i
39
+ host = options[:host] || "0.0.0.0"
40
+
41
+ puts "Starting Lepus Web dashboard on http://#{host}:#{port}"
42
+ puts "Press Ctrl+C to stop"
43
+
44
+ if system("which rackup > /dev/null 2>&1")
45
+
46
+ exec "rackup -p #{port} -o #{host} #{__dir__}/../../config.ru"
47
+ else
48
+ puts <<~MSG
49
+ Rack is not installed. Please install it using the following command:
50
+
51
+ gem install rack
52
+
53
+ Then run the web dashboard again.
54
+ MSG
55
+ end
56
+ end
33
57
  end
34
58
  end
@@ -40,6 +40,27 @@ module Lepus
40
40
  # @return [Integer] the threshold in seconds to consider a process alive. Default is 5 minutes.
41
41
  attr_accessor :process_alive_threshold
42
42
 
43
+ # @return [Symbol] the process registry backend to use (:file or :rabbitmq). Default is :file.
44
+ attr_accessor :process_registry_backend
45
+
46
+ # @return [String, nil] the application name shown in the web dashboard.
47
+ # Falls back to {#connection_name} when not explicitly set so that hosts
48
+ # that only configure `connection_name` still group correctly in the UI.
49
+ attr_writer :application_name
50
+
51
+ def application_name
52
+ @application_name || connection_name
53
+ end
54
+
55
+ # @return [String, nil] the RabbitMQ Management API URL.
56
+ attr_accessor :management_api_url
57
+
58
+ # @return [Array<Numeric>] histogram buckets (in seconds) used by the
59
+ # prometheus_exporter collector for delivery and publish latency.
60
+ attr_accessor :prometheus_buckets
61
+
62
+ DEFAULT_PROMETHEUS_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10].freeze
63
+
43
64
  def initialize
44
65
  @connection_name = "Lepus (#{Lepus::VERSION})"
45
66
  @rabbitmq_url = ENV.fetch("RABBITMQ_URL", DEFAULT_RABBITMQ_URL) || DEFAULT_RABBITMQ_URL
@@ -49,6 +70,27 @@ module Lepus
49
70
  @consumers_directory = DEFAULT_CONSUMERS_DIRECTORY
50
71
  @process_heartbeat_interval = 60
51
72
  @process_alive_threshold = 5 * 60
73
+ @process_registry_backend = :file
74
+ @application_name = nil
75
+ @management_api_url = nil
76
+ @prometheus_buckets = DEFAULT_PROMETHEUS_BUCKETS
77
+ end
78
+
79
+ # Builds the process registry backend based on configuration.
80
+ # @return [Lepus::ProcessRegistry::Backend] the configured backend
81
+ def build_process_registry_backend
82
+ case process_registry_backend
83
+ when :rabbitmq
84
+ ProcessRegistry::RabbitmqBackend.new
85
+ else
86
+ ProcessRegistry::FileBackend.new
87
+ end
88
+ end
89
+
90
+ # Builds the Management API client based on configuration.
91
+ # @return [Lepus::Web::ManagementAPI] the management API client
92
+ def build_management_api
93
+ Web::ManagementAPI.new(base_url: management_api_url)
52
94
  end
53
95
 
54
96
  # @param suffix [String] the suffix to add to the connection name
@@ -103,6 +103,7 @@ module Lepus
103
103
  rescue Lepus::InvalidConsumerReturnError
104
104
  raise
105
105
  rescue Exception # rubocop:disable Lint/RescueException
106
+ on_delivery_error
106
107
  # In testing mode, re-raise exceptions if consumer_raise_errors? is enabled
107
108
  if defined?(Lepus::Testing) && Lepus::Testing.consumer_raise_errors?
108
109
  raise
@@ -111,6 +112,12 @@ module Lepus
111
112
  reject!
112
113
  end
113
114
 
115
+ # Returns whether the last delivery resulted in an error.
116
+ # Always false in core; overridden by Lepus::Web when loaded.
117
+ def last_delivery_errored?
118
+ false
119
+ end
120
+
114
121
  protected
115
122
 
116
123
  def logger
@@ -174,6 +181,11 @@ module Lepus
174
181
  end
175
182
  end
176
183
 
184
+ # Hook called when a delivery raises an exception.
185
+ # No-op in core; overridden by Lepus::Web to track error state.
186
+ def on_delivery_error
187
+ end
188
+
177
189
  def verify_result(result)
178
190
  return if %i[ack reject requeue nack].include?(result)
179
191
 
@@ -22,7 +22,9 @@ module Lepus
22
22
  def process_delivery(delivery_info, metadata, payload)
23
23
  consumer
24
24
  .process_delivery(delivery_info, metadata, payload)
25
- .tap { |result| process_result(result, delivery_info.delivery_tag) }
25
+ .tap do |result|
26
+ process_result(result, delivery_info.delivery_tag)
27
+ end
26
28
  end
27
29
 
28
30
  private
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module Lepus
6
+ module Consumers
7
+ # Thread-safe per-consumer-class statistics tracker.
8
+ # Uses atomic counters to track processed/rejected/errored message counts.
9
+ class Stats
10
+ attr_reader :consumer_class
11
+
12
+ def initialize(consumer_class)
13
+ @consumer_class = consumer_class
14
+ @processed = Concurrent::AtomicFixnum.new(0)
15
+ @rejected = Concurrent::AtomicFixnum.new(0)
16
+ @errored = Concurrent::AtomicFixnum.new(0)
17
+ end
18
+
19
+ def record_processed
20
+ @processed.increment
21
+ end
22
+
23
+ def record_rejected
24
+ @rejected.increment
25
+ end
26
+
27
+ def record_errored
28
+ @errored.increment
29
+ end
30
+
31
+ def processed
32
+ @processed.value
33
+ end
34
+
35
+ def rejected
36
+ @rejected.value
37
+ end
38
+
39
+ def errored
40
+ @errored.value
41
+ end
42
+
43
+ def to_h
44
+ config = consumer_class.config
45
+ {
46
+ class_name: consumer_class.name,
47
+ exchange: config.exchange_name,
48
+ queue: config.queue_name,
49
+ route: extract_route(config),
50
+ threads: config.worker_threads,
51
+ processed: @processed.value,
52
+ rejected: @rejected.value,
53
+ errored: @errored.value
54
+ }
55
+ end
56
+
57
+ private
58
+
59
+ def extract_route(config)
60
+ binds = config.binds_args
61
+ return nil if binds.empty?
62
+
63
+ keys = binds.filter_map { |b| b[:routing_key] }
64
+ return nil if keys.empty?
65
+
66
+ (keys.length == 1) ? keys.first : keys
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module Lepus
6
+ module Consumers
7
+ # Per-worker registry of consumer stats.
8
+ # Uses Concurrent::Map for thread-safe lazy initialization.
9
+ class StatsRegistry
10
+ def initialize
11
+ @stats = Concurrent::Map.new
12
+ end
13
+
14
+ def for(consumer_class)
15
+ @stats.compute_if_absent(consumer_class.name) do
16
+ Stats.new(consumer_class)
17
+ end
18
+ end
19
+
20
+ def all
21
+ @stats.values.map(&:to_h)
22
+ end
23
+
24
+ def connection_count
25
+ @stats.size
26
+ end
27
+ end
28
+ end
29
+ end
@@ -110,12 +110,7 @@ module Lepus
110
110
  main_queue.bind(exchange, **opts)
111
111
  end
112
112
 
113
- consumer_handler = Lepus::Consumers::Handler.new(
114
- consumer_class,
115
- channel,
116
- main_queue,
117
- "#{consumer_class.name}-#{n + 1}"
118
- )
113
+ consumer_handler = build_handler(consumer_class, channel, main_queue, "#{consumer_class.name}-#{n + 1}")
119
114
 
120
115
  consumer_handler.on_delivery do |delivery_info, metadata, payload|
121
116
  consumer_handler.process_delivery(delivery_info, metadata, payload)
@@ -126,6 +121,12 @@ module Lepus
126
121
  end
127
122
  end
128
123
 
124
+ # Factory method for creating consumer handlers.
125
+ # Overridden by Lepus::Web to attach stats tracking.
126
+ def build_handler(consumer_class, channel, queue, tag)
127
+ Lepus::Consumers::Handler.new(consumer_class, channel, queue, tag)
128
+ end
129
+
129
130
  def connection_pool
130
131
  return @connection_pool if defined?(@connection_pool)
131
132
 
data/lib/lepus/process.rb CHANGED
@@ -74,12 +74,12 @@ module Lepus
74
74
  Processes::MEMORY_GRABBER.call(pid)
75
75
  end
76
76
 
77
- def heartbeat
77
+ def heartbeat(metrics: {})
78
78
  now = Time.now
79
79
  Lepus.instrument :heartbeat_process, process: self, rss_memory: 0, last_heartbeat_at: now do |payload|
80
80
  ProcessRegistry.find(id) # ensure process is still registered
81
81
 
82
- update_attributes(last_heartbeat_at: now)
82
+ update_attributes(last_heartbeat_at: now, metrics: metrics)
83
83
  payload[:rss_memory] = rss_memory
84
84
  rescue Exception => error # rubocop:disable Lint/RescueException
85
85
  payload[:error] = error
@@ -87,9 +87,9 @@ module Lepus
87
87
  end
88
88
  end
89
89
 
90
- def update_attributes(new_attributes)
90
+ def update_attributes(metrics: {}, **new_attributes)
91
91
  @attributes = @attributes.merge(new_attributes)
92
- ProcessRegistry.update(self)
92
+ ProcessRegistry.update(self, metrics: metrics)
93
93
  end
94
94
 
95
95
  def destroy!
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ class ProcessRegistry
5
+ # Abstract backend interface for process registry storage.
6
+ # Implementations must provide all methods defined here.
7
+ module Backend
8
+ def start
9
+ raise NotImplementedError, "#{self.class}#start must be implemented"
10
+ end
11
+
12
+ def stop
13
+ raise NotImplementedError, "#{self.class}#stop must be implemented"
14
+ end
15
+
16
+ def add(process)
17
+ raise NotImplementedError, "#{self.class}#add must be implemented"
18
+ end
19
+
20
+ def update(process, metrics: {})
21
+ add(process, metrics: metrics)
22
+ end
23
+
24
+ def delete(process)
25
+ raise NotImplementedError, "#{self.class}#delete must be implemented"
26
+ end
27
+
28
+ def find(id)
29
+ raise NotImplementedError, "#{self.class}#find must be implemented"
30
+ end
31
+
32
+ def exists?(id)
33
+ raise NotImplementedError, "#{self.class}#exists? must be implemented"
34
+ end
35
+
36
+ def all
37
+ raise NotImplementedError, "#{self.class}#all must be implemented"
38
+ end
39
+
40
+ def count
41
+ raise NotImplementedError, "#{self.class}#count must be implemented"
42
+ end
43
+
44
+ def clear
45
+ raise NotImplementedError, "#{self.class}#clear must be implemented"
46
+ end
47
+ end
48
+ end
49
+ end