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.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/linter.yml +21 -0
  3. data/.github/workflows/specs.yml +93 -13
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +10 -0
  6. data/.tool-versions +1 -1
  7. data/Gemfile +7 -0
  8. data/Gemfile.lock +36 -9
  9. data/Makefile +19 -0
  10. data/README.md +562 -7
  11. data/bin/setup +5 -2
  12. data/config.ru +14 -0
  13. data/docker-compose.yml +5 -3
  14. data/docs/README.md +80 -0
  15. data/docs/cli.md +108 -0
  16. data/docs/configuration.md +171 -0
  17. data/docs/consumers.md +168 -0
  18. data/docs/getting-started.md +136 -0
  19. data/docs/images/lepus-web.png +0 -0
  20. data/docs/middleware.md +240 -0
  21. data/docs/producers.md +173 -0
  22. data/docs/prometheus.md +112 -0
  23. data/docs/rails.md +161 -0
  24. data/docs/supervisor.md +112 -0
  25. data/docs/testing.md +141 -0
  26. data/docs/web.md +85 -0
  27. data/examples/grafana-dashboard.json +450 -0
  28. data/gemfiles/Gemfile.rails-5.2 +7 -0
  29. data/gemfiles/{rails52.gemfile.lock → Gemfile.rails-5.2.lock} +102 -69
  30. data/gemfiles/Gemfile.rails-6.1 +7 -0
  31. data/gemfiles/{rails61.gemfile.lock → Gemfile.rails-6.1.lock} +113 -79
  32. data/gemfiles/{rails52.gemfile → Gemfile.rails-7.2} +1 -1
  33. data/gemfiles/Gemfile.rails-7.2.lock +321 -0
  34. data/gemfiles/{rails61.gemfile → Gemfile.rails-8.0} +1 -1
  35. data/gemfiles/Gemfile.rails-8.0.lock +322 -0
  36. data/lepus.gemspec +7 -1
  37. data/lib/lepus/cli.rb +35 -4
  38. data/lib/lepus/configuration.rb +107 -0
  39. data/lib/lepus/connection_pool.rb +135 -0
  40. data/lib/lepus/consumer.rb +59 -41
  41. data/lib/lepus/consumers/config.rb +183 -0
  42. data/lib/lepus/consumers/handler.rb +56 -0
  43. data/lib/lepus/consumers/middleware_chain.rb +22 -0
  44. data/lib/lepus/consumers/middlewares/exception_logger.rb +27 -0
  45. data/lib/lepus/consumers/middlewares/honeybadger.rb +33 -0
  46. data/lib/lepus/consumers/middlewares/json.rb +37 -0
  47. data/lib/lepus/consumers/middlewares/max_retry.rb +83 -0
  48. data/lib/lepus/consumers/middlewares/unique.rb +65 -0
  49. data/lib/lepus/consumers/stats.rb +70 -0
  50. data/lib/lepus/consumers/stats_registry.rb +29 -0
  51. data/lib/lepus/consumers/worker.rb +141 -0
  52. data/lib/lepus/consumers/worker_factory.rb +124 -0
  53. data/lib/lepus/consumers.rb +6 -0
  54. data/lib/lepus/message/delivery_info.rb +72 -0
  55. data/lib/lepus/message/metadata.rb +99 -0
  56. data/lib/lepus/message.rb +88 -5
  57. data/lib/lepus/middleware_chain.rb +83 -0
  58. data/lib/lepus/primitive/hash.rb +29 -0
  59. data/lib/lepus/process.rb +24 -24
  60. data/lib/lepus/process_registry/backend.rb +49 -0
  61. data/lib/lepus/process_registry/file_backend.rb +108 -0
  62. data/lib/lepus/process_registry/message_builder.rb +72 -0
  63. data/lib/lepus/process_registry/rabbitmq_backend.rb +153 -0
  64. data/lib/lepus/process_registry.rb +56 -23
  65. data/lib/lepus/processes/base.rb +0 -5
  66. data/lib/lepus/processes/callbacks.rb +3 -0
  67. data/lib/lepus/processes/interruptible.rb +4 -8
  68. data/lib/lepus/processes/procline.rb +1 -1
  69. data/lib/lepus/processes/registrable.rb +1 -1
  70. data/lib/lepus/processes/runnable.rb +1 -1
  71. data/lib/lepus/processes.rb +15 -0
  72. data/lib/lepus/producer.rb +141 -30
  73. data/lib/lepus/producers/config.rb +46 -0
  74. data/lib/lepus/producers/definition.rb +48 -0
  75. data/lib/lepus/producers/hooks.rb +170 -0
  76. data/lib/lepus/producers/middleware_chain.rb +22 -0
  77. data/lib/lepus/producers/middlewares/correlation_id.rb +37 -0
  78. data/lib/lepus/producers/middlewares/header.rb +47 -0
  79. data/lib/lepus/producers/middlewares/instrumentation.rb +30 -0
  80. data/lib/lepus/producers/middlewares/json.rb +47 -0
  81. data/lib/lepus/producers/middlewares/unique.rb +67 -0
  82. data/lib/lepus/producers.rb +7 -0
  83. data/lib/lepus/prometheus/collector.rb +149 -0
  84. data/lib/lepus/prometheus/instrumentation.rb +168 -0
  85. data/lib/lepus/prometheus.rb +48 -0
  86. data/lib/lepus/publisher.rb +67 -0
  87. data/lib/lepus/supervisor/children_pipes.rb +25 -0
  88. data/lib/lepus/supervisor/lifecycle_hooks.rb +50 -0
  89. data/lib/lepus/supervisor/pidfiled.rb +1 -1
  90. data/lib/lepus/supervisor/registry_cleaner.rb +22 -0
  91. data/lib/lepus/supervisor.rb +129 -25
  92. data/lib/lepus/testing/exchange.rb +95 -0
  93. data/lib/lepus/testing/message_builder.rb +177 -0
  94. data/lib/lepus/testing/rspec_matchers.rb +258 -0
  95. data/lib/lepus/testing.rb +210 -0
  96. data/lib/lepus/unique.rb +18 -0
  97. data/lib/lepus/version.rb +1 -1
  98. data/lib/lepus/web/aggregator.rb +154 -0
  99. data/lib/lepus/web/api.rb +132 -0
  100. data/lib/lepus/web/app.rb +37 -0
  101. data/lib/lepus/web/management_api.rb +192 -0
  102. data/lib/lepus/web/respond_with.rb +28 -0
  103. data/lib/lepus/web.rb +238 -0
  104. data/lib/lepus.rb +39 -28
  105. data/test_offline.html +189 -0
  106. data/web/assets/css/styles.css +635 -0
  107. data/web/assets/js/app.js +6 -0
  108. data/web/assets/js/bootstrap.js +20 -0
  109. data/web/assets/js/controllers/connection_controller.js +44 -0
  110. data/web/assets/js/controllers/dashboard_controller.js +499 -0
  111. data/web/assets/js/controllers/queue_controller.js +17 -0
  112. data/web/assets/js/controllers/theme_controller.js +31 -0
  113. data/web/assets/js/offline-manager.js +233 -0
  114. data/web/assets/js/service-worker-manager.js +65 -0
  115. data/web/index.html +159 -0
  116. data/web/sw.js +144 -0
  117. metadata +177 -18
  118. data/lib/lepus/consumer_config.rb +0 -149
  119. data/lib/lepus/consumer_wrapper.rb +0 -46
  120. data/lib/lepus/lifecycle_hooks.rb +0 -49
  121. data/lib/lepus/middlewares/honeybadger.rb +0 -23
  122. data/lib/lepus/middlewares/json.rb +0 -35
  123. data/lib/lepus/middlewares/max_retry.rb +0 -57
  124. data/lib/lepus/processes/consumer.rb +0 -113
  125. 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
- extend self
54
-
55
- def logger
56
- @logger ||= DEFAULT_LOGGER
57
- end
66
+ class << self
67
+ attr_writer :logger
58
68
 
59
- def logger=(logger)
60
- @logger = logger
61
- end
69
+ def logger
70
+ @logger ||= DEFAULT_LOGGER
71
+ end
62
72
 
63
- def instrument(channel, **options, &block)
64
- if defined?(ActiveSupport::Notifications)
65
- ActiveSupport::Notifications.instrument("#{channel}.lepus", **options, &block)
66
- else
67
- yield(options.dup)
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
- def eager_load_consumers!
72
- return false unless Lepus.config.consumers_directory.exist?
81
+ def eager_load_consumers!
82
+ return false unless Lepus.config.consumers_directory.exist?
73
83
 
74
- Dir[config.consumers_directory.join("**/*.rb")].map { |path| Pathname.new(path) }.each do |path|
75
- next unless path.extname == ".rb"
84
+ Dir[config.consumers_directory.join("**/*.rb")].map { |path| Pathname.new(path) }.each do |path|
85
+ next unless path.extname == ".rb"
76
86
 
77
- require(path.expand_path.to_s)
87
+ require(path.expand_path.to_s)
88
+ end
89
+ true
78
90
  end
79
- true
80
- end
81
91
 
82
- def self.config
83
- @config ||= Configuration.new
84
- end
92
+ def config
93
+ @config ||= Configuration.new
94
+ end
85
95
 
86
- def self.configure
87
- yield config
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
- # loader.eager_load
106
+ loader.eager_load