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,240 @@
1
+ # Middleware
2
+
3
+ Lepus middlewares form a chain around each message — both for producers (around each publish) and consumers (around each delivery). Every middleware gets the message and the next app in the chain:
4
+
5
+ ```ruby
6
+ class MyMiddleware < Lepus::Middleware
7
+ def initialize(option: nil)
8
+ @option = option
9
+ end
10
+
11
+ def call(message, app)
12
+ # pre-processing
13
+ new_message = message.mutate(payload: transform(message.payload))
14
+ # pass down the chain (and let the consumer/publisher actually run)
15
+ result = app.call(new_message)
16
+ # post-processing
17
+ result
18
+ end
19
+
20
+ private
21
+
22
+ def transform(payload) = payload
23
+ end
24
+ ```
25
+
26
+ ## Registering
27
+
28
+ ### Per consumer / per producer
29
+
30
+ ```ruby
31
+ class OrdersConsumer < Lepus::Consumer
32
+ use :json, symbolize_keys: true # built-in by symbol
33
+ use :max_retry, retries: 5
34
+ use MyMiddleware, option: 'value' # your own class
35
+ end
36
+ ```
37
+
38
+ ### Globally
39
+
40
+ ```ruby
41
+ Lepus.configure do |config|
42
+ config.consumer_middlewares do |chain|
43
+ chain.use :json, symbolize_keys: true
44
+ chain.use :exception_logger
45
+ end
46
+
47
+ config.producer_middlewares do |chain|
48
+ chain.use :json
49
+ chain.use :correlation_id
50
+ chain.use :instrumentation
51
+ end
52
+ end
53
+ ```
54
+
55
+ Global middlewares run **before** per-class middlewares.
56
+
57
+ ## Built-in consumer middlewares
58
+
59
+ ### `:json`
60
+
61
+ Parse the payload from JSON.
62
+
63
+ ```ruby
64
+ use :json, symbolize_keys: true, on_error: proc { :reject }
65
+ ```
66
+
67
+ Options:
68
+
69
+ - `symbolize_keys: true` — hash keys become symbols.
70
+ - `on_error:` — a callable that takes the exception; its return becomes the disposition (default: `:reject`).
71
+
72
+ ### `:max_retry`
73
+
74
+ Track retry count via RabbitMQ `x-death` headers and route to an error queue after N attempts.
75
+
76
+ ```ruby
77
+ use :max_retry, retries: 5, error_queue: 'orders.error'
78
+ ```
79
+
80
+ Requires the consumer's `configure` to declare a `retry_queue:` so requeued messages loop through a delay queue.
81
+
82
+ ### `:exception_logger`
83
+
84
+ Catch exceptions from downstream middlewares/perform, log with backtrace, then re-raise (so higher-level error handling still sees the exception).
85
+
86
+ ```ruby
87
+ use :exception_logger
88
+ ```
89
+
90
+ ### `:honeybadger`
91
+
92
+ Notify Honeybadger on exceptions. Requires the `honeybadger` gem.
93
+
94
+ ```ruby
95
+ use :honeybadger
96
+ ```
97
+
98
+ ### `:unique`
99
+
100
+ Dedupe messages by `correlation_id`. Requires a storage backend (your code wires Redis or similar).
101
+
102
+ ```ruby
103
+ use :unique, store: MyRedisStore
104
+ ```
105
+
106
+ ## Built-in producer middlewares
107
+
108
+ ### `:json`
109
+
110
+ Serialize a hash payload as JSON; set `content_type` to `application/json`.
111
+
112
+ ```ruby
113
+ use :json
114
+ ```
115
+
116
+ ### `:correlation_id`
117
+
118
+ Auto-generate a UUID correlation id if one isn't already set on the message.
119
+
120
+ ```ruby
121
+ use :correlation_id
122
+ ```
123
+
124
+ ### `:header`
125
+
126
+ Add headers to every publish.
127
+
128
+ ```ruby
129
+ use :header, 'X-Service', 'orders-api'
130
+ use :header, 'X-Request-Id', -> { Current.request_id }
131
+ ```
132
+
133
+ The value can be a static value or a callable.
134
+
135
+ ### `:instrumentation`
136
+
137
+ Emit `ActiveSupport::Notifications` events before and after each publish. Event name: `publish.lepus`.
138
+
139
+ ```ruby
140
+ use :instrumentation
141
+ ```
142
+
143
+ Subscribe:
144
+
145
+ ```ruby
146
+ ActiveSupport::Notifications.subscribe('publish.lepus') do |name, start, finish, id, payload|
147
+ puts "published to #{payload[:exchange]} in #{(finish - start) * 1000}ms"
148
+ end
149
+ ```
150
+
151
+ ### `:unique`
152
+
153
+ Drop duplicate publishes. Requires a storage backend.
154
+
155
+ ```ruby
156
+ use :unique, store: MyRedisStore
157
+ ```
158
+
159
+ ## Writing your own
160
+
161
+ ### Consumer middleware
162
+
163
+ ```ruby
164
+ class LogLevelMiddleware < Lepus::Middleware
165
+ def initialize(level: :info)
166
+ @level = level
167
+ end
168
+
169
+ def call(message, app)
170
+ Lepus.logger.public_send(@level, "Processing: #{message.payload.inspect}")
171
+ app.call(message)
172
+ end
173
+ end
174
+
175
+ class OrdersConsumer < Lepus::Consumer
176
+ use LogLevelMiddleware, level: :debug
177
+ end
178
+ ```
179
+
180
+ ### Producer middleware
181
+
182
+ Same interface. Typical use: decorate the message with metadata before it's published.
183
+
184
+ ```ruby
185
+ class TenantScopingMiddleware < Lepus::Middleware
186
+ def call(message, app)
187
+ with_tenant = message.mutate(metadata: message.metadata.merge(headers: message.metadata[:headers].merge('X-Tenant-Id' => Current.tenant_id)))
188
+ app.call(with_tenant)
189
+ end
190
+ end
191
+ ```
192
+
193
+ ### Mutating a message
194
+
195
+ `message.mutate(**kwargs)` returns a new message with updated fields — payload, metadata, delivery_info. Don't mutate in place; build a new one and pass it down.
196
+
197
+ ```ruby
198
+ app.call(message.mutate(payload: transformed_payload))
199
+ ```
200
+
201
+ ## Order of execution
202
+
203
+ For a consumer:
204
+
205
+ ```
206
+ incoming delivery
207
+ → global consumer middlewares (in config order)
208
+ → per-consumer middlewares (in class-file order)
209
+ → perform(message)
210
+ → each middleware unwinds (post-processing)
211
+ → disposition returned
212
+ ```
213
+
214
+ For a producer:
215
+
216
+ ```
217
+ publish(payload)
218
+ → per-producer middlewares
219
+ → global producer middlewares
220
+ → Bunny publish
221
+ → unwind
222
+ ```
223
+
224
+ ## Conditional middleware
225
+
226
+ Middleware can decide not to call the next app — effectively short-circuiting:
227
+
228
+ ```ruby
229
+ class SkipDuringMaintenanceMiddleware < Lepus::Middleware
230
+ def call(message, app)
231
+ if Feature.maintenance_mode?
232
+ Lepus.logger.warn('skipping publish during maintenance')
233
+ return # do NOT call app.call(message)
234
+ end
235
+ app.call(message)
236
+ end
237
+ end
238
+ ```
239
+
240
+ For consumers, returning early means the consumer's `perform` never runs. The return value of `call` becomes the disposition.
data/docs/producers.md ADDED
@@ -0,0 +1,173 @@
1
+ # Producers
2
+
3
+ A producer publishes messages to an exchange. It's a subclass of `Lepus::Producer` with a `configure` call and whatever convenience methods you want on top.
4
+
5
+ ## Minimal example
6
+
7
+ ```ruby
8
+ class OrdersProducer < Lepus::Producer
9
+ configure(
10
+ exchange: { name: 'orders', type: :topic, durable: true },
11
+ publish: { persistent: true }
12
+ )
13
+
14
+ use :json
15
+ use :correlation_id
16
+ end
17
+ ```
18
+
19
+ Publish from anywhere in your app:
20
+
21
+ ```ruby
22
+ OrdersProducer.publish(
23
+ { order_id: 42, total: 99.99 },
24
+ routing_key: 'order.created'
25
+ )
26
+ ```
27
+
28
+ ## `configure` DSL
29
+
30
+ ```ruby
31
+ configure(
32
+ exchange: { name: 'orders', type: :topic, durable: true },
33
+ publish: { persistent: true, mandatory: false }
34
+ )
35
+ ```
36
+
37
+ | Key | Purpose |
38
+ |-----|---------|
39
+ | `exchange` | String (name only) or hash (`name`, `type`, `durable`, `auto_delete`). Auto-declared on first publish. |
40
+ | `publish` | Default publish options merged into every `publish` call: `persistent`, `mandatory`, `content_type`, `expiration`, etc. |
41
+
42
+ ## Publishing
43
+
44
+ ```ruby
45
+ OrdersProducer.publish(payload, routing_key: 'order.created')
46
+ OrdersProducer.publish(payload, routing_key: 'order.paid', headers: { tenant_id: 123 })
47
+ OrdersProducer.publish(payload, routing_key: 'order.created', expiration: 60_000)
48
+ ```
49
+
50
+ All Bunny publish options are supported. Frequently useful:
51
+
52
+ | Option | Purpose |
53
+ |--------|---------|
54
+ | `routing_key` | Routing key. |
55
+ | `persistent` | Persist the message across RabbitMQ restarts. |
56
+ | `headers` | Custom headers. |
57
+ | `correlation_id` | RPC pattern correlation. Auto-set by `:correlation_id` middleware. |
58
+ | `content_type` | MIME type. Auto-set to `application/json` by `:json` middleware. |
59
+ | `expiration` | Per-message TTL in milliseconds. |
60
+ | `mandatory` | Return the message if it can't be routed. |
61
+
62
+ ## Payload types
63
+
64
+ - A Hash — pair with `:json` middleware to serialize.
65
+ - A String — sent as-is.
66
+ - Anything else — pair with a middleware that turns it into bytes.
67
+
68
+ ## Middleware
69
+
70
+ ```ruby
71
+ class OrdersProducer < Lepus::Producer
72
+ use :json
73
+ use :correlation_id
74
+ use :instrumentation
75
+ use :header, 'X-Service', 'orders-api'
76
+ end
77
+ ```
78
+
79
+ Built-in producer middlewares:
80
+
81
+ | Name | Purpose |
82
+ |------|---------|
83
+ | `:json` | Serialize a hash into JSON; sets `content_type`. |
84
+ | `:correlation_id` | Auto-generate a UUID `correlation_id` if absent. |
85
+ | `:header` | Add a static or dynamic header: `use :header, 'X-Foo', 'bar'`. |
86
+ | `:instrumentation` | Emit `ActiveSupport::Notifications` events around each publish. |
87
+ | `:unique` | Reject duplicate publishes (by correlation id — needs external storage). |
88
+
89
+ Write your own — see [middleware.md](middleware.md).
90
+
91
+ ## Convenience class methods
92
+
93
+ Wrap `publish` with domain-specific signatures:
94
+
95
+ ```ruby
96
+ class OrdersProducer < Lepus::Producer
97
+ configure(exchange: { name: 'orders', type: :topic, durable: true })
98
+ use :json
99
+ use :correlation_id
100
+
101
+ def self.order_created(order)
102
+ publish(
103
+ { order_id: order.id, total: order.total, created_at: order.created_at },
104
+ routing_key: 'order.created'
105
+ )
106
+ end
107
+
108
+ def self.order_shipped(order)
109
+ publish(
110
+ { order_id: order.id, shipped_at: order.shipped_at },
111
+ routing_key: 'order.shipped'
112
+ )
113
+ end
114
+ end
115
+
116
+ # In your code:
117
+ OrdersProducer.order_created(order)
118
+ ```
119
+
120
+ ## Connection pooling
121
+
122
+ Producers share a single connection pool across the process:
123
+
124
+ ```ruby
125
+ Lepus.configure do |config|
126
+ config.producer do |p|
127
+ p.pool_size = 5
128
+ p.pool_timeout = 5.0
129
+ end
130
+ end
131
+ ```
132
+
133
+ ## Enable / disable publishing
134
+
135
+ Useful when you want to disable side effects in tests or a specific environment.
136
+
137
+ ```ruby
138
+ # Globally
139
+ Lepus::Producers.disable!
140
+ Lepus::Producers.enabled?(OrdersProducer) # => false
141
+
142
+ # By producer class
143
+ Lepus::Producers.disable!(OrdersProducer)
144
+
145
+ # By exchange name
146
+ Lepus::Producers.disable!('orders')
147
+
148
+ # Block-scoped
149
+ Lepus::Producers.without_publishing do
150
+ OrdersProducer.publish(...) # no-op
151
+ end
152
+
153
+ Lepus::Producers.with_publishing do
154
+ OrdersProducer.publish(...) # forced through even if disabled
155
+ end
156
+ ```
157
+
158
+ ## Error handling
159
+
160
+ A publish can fail for many reasons — connection down, channel closed, etc. By default, Bunny retries with exponential backoff within `recovery_attempts`. For anything beyond that, you catch and handle it:
161
+
162
+ ```ruby
163
+ begin
164
+ OrdersProducer.order_created(order)
165
+ rescue Bunny::Exception => e
166
+ Rails.logger.error("publish failed: #{e.message}")
167
+ # queue for later retry, fall back to a DB outbox, etc.
168
+ end
169
+ ```
170
+
171
+ ## Testing
172
+
173
+ See [testing.md](testing.md) — there's a `Lepus::Testing.producer_messages(ProducerClass)` helper that captures publishes in-memory.
@@ -0,0 +1,112 @@
1
+ # Prometheus metrics
2
+
3
+ Lepus ships an optional integration with [`prometheus_exporter`](https://github.com/discourse/prometheus_exporter). Consumer and producer processes forward metric payloads over TCP to a collector server; Prometheus scrapes the collector's `/metrics` endpoint.
4
+
5
+ ## Requirements
6
+
7
+ - `prometheus_exporter` in your Gemfile.
8
+ - A collector server reachable from every process that calls `require "lepus/prometheus"`. Typically this is the Lepus supervisor process itself, listening on `localhost:9394` (so forked workers can reach it without networking changes).
9
+
10
+ ## Enabling instrumentation
11
+
12
+ In each process that runs Lepus (supervisor + workers), load:
13
+
14
+ ```ruby
15
+ require "lepus/prometheus"
16
+ ```
17
+
18
+ This prepends instrumentation onto `Lepus::Consumers::Handler` and `Lepus::Consumers::Worker` and subscribes to the `publish.lepus` `ActiveSupport::Notifications` event. Requiring must happen before the supervisor forks workers so every child inherits the instrumentation.
19
+
20
+ ## Starting the collector server
21
+
22
+ Run the collector inside the supervisor so forked workers can send metrics to `localhost:9394`:
23
+
24
+ ```ruby
25
+ # config/initializers/lepus.rb
26
+ require "lepus/web"
27
+
28
+ if Rails.application.config.prometheus_enabled
29
+ require "lepus/prometheus"
30
+ require "lepus/prometheus/collector"
31
+ end
32
+
33
+ Lepus.configure do |config|
34
+ # …
35
+ end
36
+
37
+ if Rails.application.config.prometheus_enabled
38
+ Lepus::Supervisor.on_start do
39
+ require "prometheus_exporter/server"
40
+
41
+ collector = PrometheusExporter::Server::Collector.new
42
+ collector.register_collector(Lepus::Prometheus::Collector.new)
43
+
44
+ server = PrometheusExporter::Server::WebServer.new(
45
+ port: 9394,
46
+ bind: "0.0.0.0",
47
+ collector: collector
48
+ )
49
+ server.start
50
+ end
51
+ end
52
+ ```
53
+
54
+ Point Prometheus at `<lepus-host>:9394` in your scrape config.
55
+
56
+ ## Polling RabbitMQ queue stats
57
+
58
+ Queue depth is not published by the workers — it's pulled from the RabbitMQ Management API by a poller thread. Start it from `on_start`:
59
+
60
+ ```ruby
61
+ Lepus::Supervisor.on_start do
62
+ Lepus::Prometheus.watch_queues(interval: 30)
63
+ end
64
+ ```
65
+
66
+ The poller emits a `lepus_queue_poll_last_success_timestamp_seconds` gauge after each successful API round-trip and a `lepus_queue_poll_errors_total` counter on failure — alert on stale timestamps to catch a silently broken poller.
67
+
68
+ ## Metrics
69
+
70
+ | Metric | Type | Labels |
71
+ | --- | --- | --- |
72
+ | `lepus_messages_processed_total` | counter | `consumer`, `queue`, `result` (`ack`/`reject`/`requeue`/`nack`/`error`), `error` (exception class, empty on success) |
73
+ | `lepus_delivery_duration_seconds` | histogram | `consumer`, `queue` |
74
+ | `lepus_messages_published_total` | counter | `exchange`, `routing_key` |
75
+ | `lepus_publish_duration_seconds` | histogram | `exchange`, `routing_key` |
76
+ | `lepus_process_rss_memory_bytes` | gauge | `kind`, `name` |
77
+ | `lepus_process_info` | gauge (always `1`) | `kind`, `name`, `pid`, `hostname` |
78
+ | `lepus_queue_messages` | gauge | `name` |
79
+ | `lepus_queue_messages_ready` | gauge | `name` |
80
+ | `lepus_queue_messages_unacknowledged` | gauge | `name` |
81
+ | `lepus_queue_consumers` | gauge | `name` |
82
+ | `lepus_queue_memory_bytes` | gauge | `name` |
83
+ | `lepus_queue_poll_last_success_timestamp_seconds` | gauge | — |
84
+ | `lepus_queue_poll_errors_total` | counter | `error` |
85
+
86
+ `result="error"` is recorded for every delivery that raises out of the consumer, alongside the exception class — queryable as `rate(lepus_messages_processed_total{result="error"}[5m])`.
87
+
88
+ ## Configuration
89
+
90
+ ```ruby
91
+ Lepus.configure do |config|
92
+ # Histogram buckets (in seconds) used for delivery and publish latency.
93
+ config.prometheus_buckets = [0.01, 0.05, 0.1, 0.5, 1, 5]
94
+ end
95
+ ```
96
+
97
+ To send metrics to a collector on a different host:
98
+
99
+ ```ruby
100
+ require "prometheus_exporter/client"
101
+ Lepus::Prometheus.client = PrometheusExporter::Client.new(host: "collector.internal", port: 9394)
102
+ ```
103
+
104
+ ## Standalone collector mode
105
+
106
+ You can also run `prometheus_exporter` as a separate process and load just the collector:
107
+
108
+ ```sh
109
+ prometheus_exporter -a lepus/prometheus/collector
110
+ ```
111
+
112
+ `lib/lepus/prometheus/collector.rb` deliberately does not require the rest of the gem, so this works without a full Lepus boot.
data/docs/rails.md ADDED
@@ -0,0 +1,161 @@
1
+ # Rails Integration
2
+
3
+ When Rails is loaded, Lepus's Railtie wires up sensible defaults. No initializer is strictly required.
4
+
5
+ ## What the Railtie does
6
+
7
+ - Sets `config.logger = Rails.logger` (unless you've already set one).
8
+ - Sets `config.app_executor = Rails.application.executor` — every consumer `perform` runs inside the Rails executor, which means:
9
+ - Autoloading works (Zeitwerk is active).
10
+ - Query cache is cleared after each message.
11
+ - Connection cleanup runs after each message.
12
+ - Reloading works in development.
13
+ - Subscribes the log subscriber for friendly production log lines.
14
+ - Integrates with `Rails.error` — unhandled exceptions are reported there.
15
+
16
+ ## Overriding
17
+
18
+ ```ruby
19
+ # config/initializers/lepus.rb
20
+ Lepus.configure do |config|
21
+ config.rabbitmq_url = ENV.fetch('RABBITMQ_URL')
22
+ config.connection_name = 'my-service'
23
+
24
+ # Override the defaults:
25
+ config.logger = MyLogger.new
26
+ config.app_executor = nil # disable executor wrapping entirely
27
+
28
+ config.consumers_directory = 'app/consumers'
29
+
30
+ config.worker(:default) do |w|
31
+ w.pool_size = 5
32
+ w.before_fork { ActiveRecord::Base.connection_handler.clear_all_connections! }
33
+ w.after_fork { ActiveRecord::Base.establish_connection }
34
+ end
35
+ end
36
+ ```
37
+
38
+ ## Defining consumers and producers
39
+
40
+ Put them in `app/consumers/` and `app/producers/`. With `config.consumers_directory = 'app/consumers'`, `lepus start` with no arguments auto-loads all of them.
41
+
42
+ ```ruby
43
+ # app/consumers/orders_consumer.rb
44
+ class OrdersConsumer < Lepus::Consumer
45
+ configure(
46
+ queue: 'orders',
47
+ exchange: { name: 'orders', type: :topic, durable: true },
48
+ routing_key: 'order.*'
49
+ )
50
+ use :json, symbolize_keys: true
51
+
52
+ def perform(message)
53
+ Order.create!(message.payload)
54
+ :ack
55
+ end
56
+ end
57
+ ```
58
+
59
+ ```ruby
60
+ # app/producers/orders_producer.rb
61
+ class OrdersProducer < Lepus::Producer
62
+ configure(exchange: { name: 'orders', type: :topic, durable: true })
63
+ use :json
64
+ use :correlation_id
65
+ end
66
+ ```
67
+
68
+ ## Running alongside a Rails web app
69
+
70
+ ```bash
71
+ # Terminal 1 — web
72
+ bin/rails server
73
+
74
+ # Terminal 2 — consumers
75
+ bundle exec lepus start --require_file config/environment.rb
76
+ ```
77
+
78
+ Or use Foreman:
79
+
80
+ ```
81
+ # Procfile
82
+ web: bin/rails server
83
+ worker: bundle exec lepus start --require_file config/environment.rb
84
+ ```
85
+
86
+ ## The Puma plugin
87
+
88
+ For apps that want consumers running inside the same Puma process (development convenience, or small-scale production where you don't want another service):
89
+
90
+ ```ruby
91
+ # config/puma.rb
92
+ plugin :lepus
93
+ ```
94
+
95
+ The plugin forks consumer workers as Puma's cluster workers would, tying their lifecycle to Puma's.
96
+
97
+ **Caution:** running consumers inside Puma means process restarts on deploy take longer (waiting for in-flight messages) and you can't scale consumers independently of web requests. For non-trivial workloads, run them as a separate service.
98
+
99
+ ## Mounting the web dashboard
100
+
101
+ ```ruby
102
+ # config/routes.rb
103
+ require 'lepus/web'
104
+
105
+ authenticate :user, ->(u) { u.admin? } do
106
+ mount Lepus::Web::App, at: '/lepus'
107
+ end
108
+ ```
109
+
110
+ See [web.md](web.md).
111
+
112
+ ## Testing
113
+
114
+ ```ruby
115
+ # spec/rails_helper.rb (or spec/spec_helper.rb)
116
+ require 'lepus/testing'
117
+
118
+ RSpec.configure do |config|
119
+ config.before(:each) { Lepus::Testing.enable!; Lepus::Testing.reset! }
120
+ end
121
+ ```
122
+
123
+ See [testing.md](testing.md).
124
+
125
+ ## Active Record gotchas
126
+
127
+ Since `app_executor` is set to `Rails.application.executor`, Active Record connection management is handled per message — no manual `clear_active_connections!` needed.
128
+
129
+ For worker subprocesses, the `before_fork` / `after_fork` hooks close and reopen connections cleanly:
130
+
131
+ ```ruby
132
+ Lepus.configure do |config|
133
+ config.worker(:default) do |w|
134
+ w.before_fork { ActiveRecord::Base.connection_handler.clear_all_connections! }
135
+ w.after_fork { ActiveRecord::Base.establish_connection }
136
+ end
137
+ end
138
+ ```
139
+
140
+ Without these hooks, all children inherit the same database socket and things go badly fast.
141
+
142
+ ## Exception reporting
143
+
144
+ Lepus's Rails integration reports unhandled exceptions via `Rails.error`:
145
+
146
+ ```ruby
147
+ # config/application.rb or an initializer
148
+ Rails.error.subscribe do |exception, handled:, severity:, context:, source:|
149
+ Honeybadger.notify(exception, context: context) if source == 'lepus'
150
+ end
151
+ ```
152
+
153
+ Or use the `:honeybadger` middleware directly on your consumers — see [middleware.md](middleware.md).
154
+
155
+ ## Zeitwerk
156
+
157
+ Consumer and producer classes are autoloaded by Zeitwerk like any other Rails class. `app/consumers/orders_consumer.rb` → `OrdersConsumer`, namespaced with the usual folder-to-module rules.
158
+
159
+ ## Generators
160
+
161
+ None at the moment. Create consumer and producer files by hand (or your editor's snippet of choice).