bug_bunny 4.7.0 → 4.8.1
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/.agents/skills/documentation-writer/SKILL.md +45 -0
- data/.agents/skills/gem-release/SKILL.md +114 -0
- data/.agents/skills/quality-code/SKILL.md +51 -0
- data/.agents/skills/rabbitmq-expert/SKILL.md +1555 -0
- data/.agents/skills/sentry/SKILL.md +135 -0
- data/.agents/skills/sentry/references/api-endpoints.md +147 -0
- data/.agents/skills/sentry/scripts/sentry.rb +194 -0
- data/.agents/skills/skill-builder/SKILL.md +232 -0
- data/.agents/skills/skill-manager/SKILL.md +172 -0
- data/.agents/skills/skill-manager/scripts/sync.rb +310 -0
- data/.agents/skills/yard/SKILL.md +311 -0
- data/.agents/skills/yard/references/tipos.md +144 -0
- data/CHANGELOG.md +21 -0
- data/CLAUDE.md +29 -220
- data/lib/bug_bunny/client.rb +4 -3
- data/lib/bug_bunny/controller.rb +10 -14
- data/lib/bug_bunny/exception.rb +1 -1
- data/lib/bug_bunny/middleware/raise_error.rb +3 -3
- data/lib/bug_bunny/observability.rb +5 -5
- data/lib/bug_bunny/producer.rb +11 -13
- data/lib/bug_bunny/railtie.rb +8 -7
- data/lib/bug_bunny/request.rb +3 -11
- data/lib/bug_bunny/resource.rb +51 -21
- data/lib/bug_bunny/routing/route_set.rb +32 -21
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +3 -2
- data/lib/generators/bug_bunny/install/install_generator.rb +45 -5
- data/lib/tasks/bug_bunny.rake +50 -0
- data/skill/SKILL.md +230 -0
- data/skill/references/client-middleware.md +144 -0
- data/skill/references/consumer.md +104 -0
- data/skill/references/controller.md +105 -0
- data/skill/references/errores.md +97 -0
- data/skill/references/resource.md +116 -0
- data/skill/references/routing.md +82 -0
- data/skill/references/testing.md +138 -0
- data/skills-lock.json +10 -0
- data/skills.lock +24 -0
- data/skills.yml +19 -0
- metadata +27 -17
- data/.claude/commands/pr.md +0 -42
- data/.claude/commands/release.md +0 -41
- data/.claude/commands/rubocop.md +0 -22
- data/.claude/commands/test.md +0 -28
- data/.claude/commands/yard.md +0 -46
- data/docs/concepts.md +0 -140
- data/docs/howto/controller.md +0 -194
- data/docs/howto/middleware_client.md +0 -119
- data/docs/howto/middleware_consumer.md +0 -127
- data/docs/howto/rails.md +0 -214
- data/docs/howto/resource.md +0 -200
- data/docs/howto/routing.md +0 -133
- data/docs/howto/testing.md +0 -259
- data/docs/howto/tracing.md +0 -119
- data/mejoras.md +0 -33
data/docs/concepts.md
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
# Concepts
|
|
2
|
-
|
|
3
|
-
## What is BugBunny?
|
|
4
|
-
|
|
5
|
-
BugBunny is a Ruby gem that implements a RESTful routing layer over AMQP (RabbitMQ). It lets microservices communicate through RabbitMQ using familiar HTTP-like patterns: verbs (GET, POST, PUT, DELETE), controllers, declarative routes, synchronous RPC, and fire-and-forget publishing.
|
|
6
|
-
|
|
7
|
-
**Problem it solves:** Direct HTTP coupling between microservices creates a tight dependency graph — if Service B is down, Service A fails immediately. RabbitMQ as a message bus decouples availability, but raw AMQP APIs are low-level and verbose. BugBunny gives you the ergonomics of a web framework on top of the reliability of a message broker.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## AMQP in 5 Minutes
|
|
12
|
-
|
|
13
|
-
AMQP (Advanced Message Queuing Protocol) is the protocol RabbitMQ implements. The key concepts:
|
|
14
|
-
|
|
15
|
-
**Exchange** — Receives messages from producers and routes them to queues based on rules. Types:
|
|
16
|
-
- `direct` — Routes to queues whose binding key exactly matches the routing key.
|
|
17
|
-
- `topic` — Routes using wildcard patterns (`orders.*`, `#.error`).
|
|
18
|
-
- `fanout` — Broadcasts to all bound queues regardless of routing key.
|
|
19
|
-
|
|
20
|
-
**Queue** — Stores messages until a consumer picks them up. Durable queues survive broker restarts.
|
|
21
|
-
|
|
22
|
-
**Routing Key** — A string the producer attaches to the message. The exchange uses it to decide which queues receive the message.
|
|
23
|
-
|
|
24
|
-
**Binding** — A link between an exchange and a queue, optionally with a routing key pattern.
|
|
25
|
-
|
|
26
|
-
In BugBunny, the **path** of the message (e.g., `nodes/123`) travels inside the AMQP `type` header. The routing key determines *which service* receives the message; the path determines *which controller and action* handles it inside that service.
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
## Architecture
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
Service A (Producer)
|
|
34
|
-
BugBunny::Resource.find(id)
|
|
35
|
-
BugBunny::Client#request(path)
|
|
36
|
-
|
|
|
37
|
-
v
|
|
38
|
-
BugBunny::Producer
|
|
39
|
-
| publishes to exchange with:
|
|
40
|
-
| routing_key: 'nodes'
|
|
41
|
-
| type header: 'nodes/123'
|
|
42
|
-
| reply_to: 'amq.rabbitmq.reply-to' (RPC only)
|
|
43
|
-
| correlation_id: 'abc-123' (RPC only)
|
|
44
|
-
v
|
|
45
|
-
[ RabbitMQ Exchange ]
|
|
46
|
-
|
|
|
47
|
-
v
|
|
48
|
-
[ RabbitMQ Queue ]
|
|
49
|
-
|
|
|
50
|
-
v
|
|
51
|
-
Service B (Consumer)
|
|
52
|
-
BugBunny::Consumer (subscribe loop)
|
|
53
|
-
|
|
|
54
|
-
v
|
|
55
|
-
ConsumerMiddleware::Stack
|
|
56
|
-
(tracing, auth, etc.)
|
|
57
|
-
|
|
|
58
|
-
v
|
|
59
|
-
Router (BugBunny.routes)
|
|
60
|
-
matches 'GET nodes/123' → NodesController#show
|
|
61
|
-
|
|
|
62
|
-
v
|
|
63
|
-
NodesController#show
|
|
64
|
-
render status: :ok, json: node
|
|
65
|
-
|
|
|
66
|
-
v (RPC only)
|
|
67
|
-
reply → amq.rabbitmq.reply-to
|
|
68
|
-
|
|
|
69
|
-
v
|
|
70
|
-
Service A (unblocked)
|
|
71
|
-
future.value → { body: {...}, headers: {...} }
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
---
|
|
75
|
-
|
|
76
|
-
## RPC vs Fire-and-Forget
|
|
77
|
-
|
|
78
|
-
BugBunny supports two communication patterns. Choosing the right one matters for system design.
|
|
79
|
-
|
|
80
|
-
### Synchronous RPC (`:rpc`)
|
|
81
|
-
|
|
82
|
-
The producer blocks until the consumer replies. Uses RabbitMQ's `amq.rabbitmq.reply-to` pseudo-queue — no temporary queues are created.
|
|
83
|
-
|
|
84
|
-
```ruby
|
|
85
|
-
response = client.request('users/42', method: :get)
|
|
86
|
-
# blocks here until the consumer sends back a reply (or timeout)
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
**Use when:** Service A needs the result to continue. Example: fetching user data before building a response, validating inventory before placing an order.
|
|
90
|
-
|
|
91
|
-
**Timeout:** Configurable via `config.rpc_timeout`. Raises `BugBunny::RequestTimeout` if the consumer does not reply in time.
|
|
92
|
-
|
|
93
|
-
**Cost:** Ties up a thread in Service A for the duration of the call.
|
|
94
|
-
|
|
95
|
-
### Fire-and-Forget (`:publish`)
|
|
96
|
-
|
|
97
|
-
The producer publishes and continues immediately. No reply is expected or waited for.
|
|
98
|
-
|
|
99
|
-
```ruby
|
|
100
|
-
client.publish('events', body: { type: 'order.placed', order_id: 99 })
|
|
101
|
-
# returns immediately with { 'status' => 202 }
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
**Use when:** Service A does not need a result. Example: emitting audit events, triggering background jobs, sending notifications.
|
|
105
|
-
|
|
106
|
-
**Cost:** None — but you have no confirmation that the message was processed successfully.
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
## Key Components
|
|
111
|
-
|
|
112
|
-
| Class | Role |
|
|
113
|
-
|---|---|
|
|
114
|
-
| `BugBunny::Configuration` | Global settings. Validates required fields on `BugBunny.configure`. |
|
|
115
|
-
| `BugBunny::Session` | Wraps a Bunny channel. Declares exchanges and queues. Thread-safe with double-checked locking. |
|
|
116
|
-
| `BugBunny::Producer` | Publishes messages. Implements RPC with `Concurrent::IVar`. |
|
|
117
|
-
| `BugBunny::Client` | High-level publisher API. Manages a connection pool and middleware stack. |
|
|
118
|
-
| `BugBunny::Consumer` | Subscribe loop. Routes messages to controllers via `BugBunny.routes`. |
|
|
119
|
-
| `BugBunny::ConsumerMiddleware::Stack` | Pipeline of middlewares executed before `process_message`. |
|
|
120
|
-
| `BugBunny::Controller` | Base class for message handlers. Supports `before_action`, `after_action`, `around_action`, `rescue_from`. |
|
|
121
|
-
| `BugBunny::Resource` | ActiveRecord-like ORM over AMQP. Provides `find`, `where`, `create`, `save`, `destroy`. |
|
|
122
|
-
| `BugBunny::Routing::RouteSet` | Stores and matches declared routes. |
|
|
123
|
-
| `BugBunny::Observability` | Mixin for structured logging. `safe_log` never raises. |
|
|
124
|
-
|
|
125
|
-
---
|
|
126
|
-
|
|
127
|
-
## Connection Pool
|
|
128
|
-
|
|
129
|
-
BugBunny uses the `connection_pool` gem to share connections safely across threads (Puma workers, Sidekiq threads).
|
|
130
|
-
|
|
131
|
-
```
|
|
132
|
-
ConnectionPool
|
|
133
|
-
slot 0: Bunny::Session → BugBunny::Session → BugBunny::Producer
|
|
134
|
-
slot 1: Bunny::Session → BugBunny::Session → BugBunny::Producer
|
|
135
|
-
slot N: ...
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Each pool slot caches its `Session` and `Producer` for the lifetime of the slot. This avoids re-creating AMQP channels (expensive) and prevents the double `basic_consume` error that would occur if a new Producer were created on a reused channel.
|
|
139
|
-
|
|
140
|
-
Thread safety is guaranteed by `ConnectionPool` itself: each slot is used by one thread at a time, so no additional mutex is needed at the Session or Producer level.
|
data/docs/howto/controller.md
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
# Controllers
|
|
2
|
-
|
|
3
|
-
Controllers receive routed messages and produce responses. They follow the same lifecycle as ActionController in Rails.
|
|
4
|
-
|
|
5
|
-
## Defining a Controller
|
|
6
|
-
|
|
7
|
-
```ruby
|
|
8
|
-
module BugBunny
|
|
9
|
-
module Controllers
|
|
10
|
-
class NodesController < BugBunny::Controller
|
|
11
|
-
def index
|
|
12
|
-
nodes = Node.all
|
|
13
|
-
render status: :ok, json: nodes.map(&:as_json)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def show
|
|
17
|
-
node = Node.find(params[:id])
|
|
18
|
-
render status: :ok, json: node.as_json
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def create
|
|
22
|
-
node = Node.new(node_params)
|
|
23
|
-
if node.save
|
|
24
|
-
render status: :created, json: node.as_json
|
|
25
|
-
else
|
|
26
|
-
render status: :unprocessable_entity, json: { errors: node.errors }
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
The controller namespace defaults to `BugBunny::Controllers`. Override it globally with `config.controller_namespace`, or per route group with `namespace` blocks in the DSL.
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
## params
|
|
39
|
-
|
|
40
|
-
`params` merges path parameters, query string parameters, and body parameters into a single `HashWithIndifferentAccess`:
|
|
41
|
-
|
|
42
|
-
```ruby
|
|
43
|
-
# Message for PUT nodes/42?verbose=true with body { "node": { "status": "active" } }
|
|
44
|
-
params[:id] # => "42" (from path)
|
|
45
|
-
params[:verbose] # => "true" (from query string)
|
|
46
|
-
params[:node][:status] # => "active" (from JSON body)
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Use strong-parameters style to extract what you need:
|
|
50
|
-
|
|
51
|
-
```ruby
|
|
52
|
-
def node_params
|
|
53
|
-
params.require(:node).permit(:name, :status)
|
|
54
|
-
end
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
## before_action
|
|
60
|
-
|
|
61
|
-
Runs before the action. Rendering inside a `before_action` halts the chain — the action and `after_action` callbacks do not run.
|
|
62
|
-
|
|
63
|
-
```ruby
|
|
64
|
-
class ApplicationController < BugBunny::Controller
|
|
65
|
-
before_action :authenticate!
|
|
66
|
-
before_action :set_node, only: [:show, :update, :destroy, :drain]
|
|
67
|
-
|
|
68
|
-
private
|
|
69
|
-
|
|
70
|
-
def authenticate!
|
|
71
|
-
token = request_headers['X-Service-Token']
|
|
72
|
-
render status: :unauthorized, json: { error: 'Unauthorized' } unless valid_token?(token)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def set_node
|
|
76
|
-
@node = Node.find(params[:id])
|
|
77
|
-
render status: :not_found, json: { error: 'Not found' } unless @node
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## after_action
|
|
85
|
-
|
|
86
|
-
Runs after the action completes successfully. Skipped if a `before_action` halted the chain or if the action raised an exception — same behavior as Rails.
|
|
87
|
-
|
|
88
|
-
```ruby
|
|
89
|
-
class NodesController < ApplicationController
|
|
90
|
-
after_action :emit_audit_event, only: [:create, :update, :destroy]
|
|
91
|
-
|
|
92
|
-
private
|
|
93
|
-
|
|
94
|
-
def emit_audit_event
|
|
95
|
-
AuditLog.record(action: action_name, node_id: params[:id], actor: current_service)
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## around_action
|
|
103
|
-
|
|
104
|
-
Wraps the action. Must call `yield` to execute the inner chain.
|
|
105
|
-
|
|
106
|
-
```ruby
|
|
107
|
-
class NodesController < ApplicationController
|
|
108
|
-
around_action :with_distributed_lock, only: [:drain]
|
|
109
|
-
|
|
110
|
-
private
|
|
111
|
-
|
|
112
|
-
def with_distributed_lock
|
|
113
|
-
DistributedLock.acquire("node:#{params[:id]}") { yield }
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## rescue_from
|
|
121
|
-
|
|
122
|
-
Catches exceptions raised during the action and maps them to responses. Handlers are inherited and can be defined in a base `ApplicationController`.
|
|
123
|
-
|
|
124
|
-
```ruby
|
|
125
|
-
class ApplicationController < BugBunny::Controller
|
|
126
|
-
rescue_from ActiveRecord::RecordNotFound do |e|
|
|
127
|
-
render status: :not_found, json: { error: e.message }
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
rescue_from ActiveRecord::RecordInvalid do |e|
|
|
131
|
-
render status: :unprocessable_entity, json: { errors: e.record.errors.full_messages }
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
rescue_from StandardError do |e|
|
|
135
|
-
logger.error("Unhandled error: #{e.class} — #{e.message}")
|
|
136
|
-
render status: :internal_server_error, json: { error: 'Internal server error' }
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
|
-
## render
|
|
144
|
-
|
|
145
|
-
```ruby
|
|
146
|
-
render status: :ok, json: resource.as_json
|
|
147
|
-
render status: :created, json: { id: record.id }
|
|
148
|
-
render status: :no_content, json: nil
|
|
149
|
-
render status: :unprocessable_entity, json: { errors: object.errors }
|
|
150
|
-
render status: :not_found, json: { error: 'Not found' }
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Accepts any Rack status symbol (`:ok`, `:created`, `:not_found`, etc.) or an integer status code.
|
|
154
|
-
|
|
155
|
-
### Adding response headers
|
|
156
|
-
|
|
157
|
-
```ruby
|
|
158
|
-
# Per-response headers (merged, non-destructive)
|
|
159
|
-
render status: :ok, json: data, headers: { 'X-Request-Id' => request_id }
|
|
160
|
-
|
|
161
|
-
# Headers set for the lifetime of the action (accessible via response_headers)
|
|
162
|
-
def show
|
|
163
|
-
response_headers['X-Cache'] = 'HIT'
|
|
164
|
-
render status: :ok, json: Node.find(params[:id]).as_json
|
|
165
|
-
end
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
---
|
|
169
|
-
|
|
170
|
-
## Accessing AMQP headers
|
|
171
|
-
|
|
172
|
-
```ruby
|
|
173
|
-
def create
|
|
174
|
-
trace_id = request_headers['X-Trace-Id']
|
|
175
|
-
# ...
|
|
176
|
-
end
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
`request_headers` returns the raw AMQP headers hash from `properties.headers`.
|
|
180
|
-
|
|
181
|
-
---
|
|
182
|
-
|
|
183
|
-
## log_tags
|
|
184
|
-
|
|
185
|
-
Injects contextual tags into all log lines produced within the action's execution:
|
|
186
|
-
|
|
187
|
-
```ruby
|
|
188
|
-
class NodesController < ApplicationController
|
|
189
|
-
log_tag { params[:id] }
|
|
190
|
-
log_tag { current_tenant }
|
|
191
|
-
end
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Works with Rails' `ActiveSupport::TaggedLogging`.
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
# Client Middleware
|
|
2
|
-
|
|
3
|
-
The client middleware stack wraps outgoing requests in an onion (Faraday-style). Each middleware can inspect and modify the request before it reaches the producer, and inspect the response on the way back.
|
|
4
|
-
|
|
5
|
-
## Built-in Middlewares
|
|
6
|
-
|
|
7
|
-
### `BugBunny::Middleware::RaiseError`
|
|
8
|
-
|
|
9
|
-
Maps HTTP-like status codes in the response to Ruby exceptions:
|
|
10
|
-
|
|
11
|
-
| Status | Exception |
|
|
12
|
-
|--------|-----------|
|
|
13
|
-
| 400 | `BugBunny::BadRequest` |
|
|
14
|
-
| 401 | `BugBunny::Unauthorized` |
|
|
15
|
-
| 403 | `BugBunny::Forbidden` |
|
|
16
|
-
| 404 | `BugBunny::NotFound` |
|
|
17
|
-
| 409 | `BugBunny::Conflict` |
|
|
18
|
-
| 422 | `BugBunny::UnprocessableEntity` |
|
|
19
|
-
| 4xx | `BugBunny::ClientError` |
|
|
20
|
-
| 500 | `BugBunny::InternalServerError` |
|
|
21
|
-
| 5xx | `BugBunny::ServerError` |
|
|
22
|
-
| Timeout| `BugBunny::RequestTimeout` |
|
|
23
|
-
|
|
24
|
-
### `BugBunny::Middleware::JsonResponse`
|
|
25
|
-
|
|
26
|
-
Parses the response body from JSON and returns a `HashWithIndifferentAccess`. Without this middleware, the response body is a raw String.
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
## Using Middlewares with Client
|
|
31
|
-
|
|
32
|
-
```ruby
|
|
33
|
-
pool = ConnectionPool.new(size: 5, timeout: 5) { BugBunny.create_connection }
|
|
34
|
-
client = BugBunny::Client.new(pool: pool) do |stack|
|
|
35
|
-
stack.use BugBunny::Middleware::RaiseError
|
|
36
|
-
stack.use BugBunny::Middleware::JsonResponse
|
|
37
|
-
stack.use MyLoggingMiddleware
|
|
38
|
-
end
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Middlewares execute in the order they are registered (FIFO). `RaiseError` and `JsonResponse` should be registered in that order so that `RaiseError` sees the parsed body.
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## Writing a Custom Middleware
|
|
46
|
-
|
|
47
|
-
Inherit from `BugBunny::Middleware::Base` and implement `call`:
|
|
48
|
-
|
|
49
|
-
```ruby
|
|
50
|
-
class RequestLoggingMiddleware < BugBunny::Middleware::Base
|
|
51
|
-
def call(request)
|
|
52
|
-
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
53
|
-
response = app.call(request)
|
|
54
|
-
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
55
|
-
|
|
56
|
-
Rails.logger.info("amqp_request path=#{request.path} method=#{request.method} duration_s=#{duration.round(4)}")
|
|
57
|
-
response
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
`app.call(request)` invokes the next middleware in the stack (or the producer at the end of the chain). The return value is the response hash `{ 'status' => Integer, 'body' => ..., 'headers' => Hash }`.
|
|
63
|
-
|
|
64
|
-
### Modifying the request
|
|
65
|
-
|
|
66
|
-
```ruby
|
|
67
|
-
class InjectTraceHeaderMiddleware < BugBunny::Middleware::Base
|
|
68
|
-
def call(request)
|
|
69
|
-
request.headers['X-Trace-Id'] = MyTracer.current_trace_id
|
|
70
|
-
app.call(request)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### Modifying the response
|
|
76
|
-
|
|
77
|
-
```ruby
|
|
78
|
-
class ResponseCachingMiddleware < BugBunny::Middleware::Base
|
|
79
|
-
def call(request)
|
|
80
|
-
cached = Cache.get(request.path)
|
|
81
|
-
return cached if cached
|
|
82
|
-
|
|
83
|
-
response = app.call(request)
|
|
84
|
-
Cache.set(request.path, response, ttl: 30) if response['status'] == 200
|
|
85
|
-
response
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## Middlewares on Resource
|
|
93
|
-
|
|
94
|
-
`BugBunny::Resource` uses `RaiseError` and `JsonResponse` by default. Add custom middlewares via the class-level DSL:
|
|
95
|
-
|
|
96
|
-
```ruby
|
|
97
|
-
class RemoteNode < BugBunny::Resource
|
|
98
|
-
client_middleware do |stack|
|
|
99
|
-
stack.use RequestLoggingMiddleware
|
|
100
|
-
stack.use InjectTraceHeaderMiddleware
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Custom middlewares are injected after the core ones, so `RaiseError` and `JsonResponse` always run first.
|
|
106
|
-
|
|
107
|
-
Middlewares defined in a parent class are inherited:
|
|
108
|
-
|
|
109
|
-
```ruby
|
|
110
|
-
class ApplicationResource < BugBunny::Resource
|
|
111
|
-
client_middleware do |stack|
|
|
112
|
-
stack.use InjectTraceHeaderMiddleware
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
class RemoteNode < ApplicationResource
|
|
117
|
-
# inherits InjectTraceHeaderMiddleware
|
|
118
|
-
end
|
|
119
|
-
```
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
# Consumer Middleware
|
|
2
|
-
|
|
3
|
-
The consumer middleware stack runs before every message reaches the router. It is the right place for cross-cutting concerns: distributed tracing, authentication, audit logging, rate limiting.
|
|
4
|
-
|
|
5
|
-
## Execution Order
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
RabbitMQ message arrives
|
|
9
|
-
|
|
|
10
|
-
v
|
|
11
|
-
ConsumerMiddleware::Stack
|
|
12
|
-
Middleware A (first registered)
|
|
13
|
-
Middleware B
|
|
14
|
-
Middleware C
|
|
15
|
-
process_message (router → controller → action)
|
|
16
|
-
Middleware C (post-processing)
|
|
17
|
-
Middleware B (post-processing)
|
|
18
|
-
Middleware A (post-processing)
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Middlewares execute FIFO. If no middlewares are registered, the overhead is zero.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## Writing a Middleware
|
|
26
|
-
|
|
27
|
-
Inherit from `BugBunny::ConsumerMiddleware::Base` and implement `call`:
|
|
28
|
-
|
|
29
|
-
```ruby
|
|
30
|
-
class TracingMiddleware < BugBunny::ConsumerMiddleware::Base
|
|
31
|
-
def call(delivery_info, properties, body)
|
|
32
|
-
trace_id = properties.headers&.dig('X-Trace-Id') || SecureRandom.uuid
|
|
33
|
-
MyTracer.with_trace(trace_id) do
|
|
34
|
-
@app.call(delivery_info, properties, body)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
`@app.call(delivery_info, properties, body)` passes control to the next middleware. Always call it (unless you intentionally want to halt processing).
|
|
41
|
-
|
|
42
|
-
### Available data
|
|
43
|
-
|
|
44
|
-
| Argument | Type | Contents |
|
|
45
|
-
|---|---|---|
|
|
46
|
-
| `delivery_info` | `Bunny::DeliveryInfo` | `routing_key`, `exchange`, `delivery_tag`, `redelivered` |
|
|
47
|
-
| `properties` | `Bunny::MessageProperties` | `headers` (custom AMQP headers), `correlation_id`, `reply_to`, `content_type` |
|
|
48
|
-
| `body` | `String` | Raw message payload (typically JSON) |
|
|
49
|
-
|
|
50
|
-
### Post-processing
|
|
51
|
-
|
|
52
|
-
Code after `@app.call` runs once the message has been fully processed (controller action completed):
|
|
53
|
-
|
|
54
|
-
```ruby
|
|
55
|
-
class AuditMiddleware < BugBunny::ConsumerMiddleware::Base
|
|
56
|
-
def call(delivery_info, properties, body)
|
|
57
|
-
@app.call(delivery_info, properties, body)
|
|
58
|
-
rescue => e
|
|
59
|
-
AuditLog.record_failure(routing_key: delivery_info.routing_key, error: e.class.name)
|
|
60
|
-
raise
|
|
61
|
-
ensure
|
|
62
|
-
AuditLog.record_received(routing_key: delivery_info.routing_key)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## Registering Middlewares
|
|
70
|
-
|
|
71
|
-
```ruby
|
|
72
|
-
# After BugBunny.configure
|
|
73
|
-
BugBunny.consumer_middlewares.use TracingMiddleware
|
|
74
|
-
BugBunny.consumer_middlewares.use AuditMiddleware
|
|
75
|
-
BugBunny.consumer_middlewares.use AuthenticationMiddleware
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Registrations are thread-safe. You can register middlewares at any point before the Consumer starts subscribing.
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## Auto-registration from External Gems
|
|
83
|
-
|
|
84
|
-
Integration gems can register themselves transparently when required, without the user modifying the `configure` block:
|
|
85
|
-
|
|
86
|
-
```ruby
|
|
87
|
-
# lib/my_tracing_gem/bug_bunny.rb
|
|
88
|
-
require 'my_tracing_gem/bug_bunny/consumer_middleware'
|
|
89
|
-
BugBunny.consumer_middlewares.use MyTracingGem::BugBunny::ConsumerMiddleware
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
The user only needs:
|
|
93
|
-
|
|
94
|
-
```ruby
|
|
95
|
-
require 'my_tracing_gem/bug_bunny'
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## Halting Message Processing
|
|
101
|
-
|
|
102
|
-
To reject a message without routing it (e.g., authentication failure):
|
|
103
|
-
|
|
104
|
-
```ruby
|
|
105
|
-
class AuthMiddleware < BugBunny::ConsumerMiddleware::Base
|
|
106
|
-
def call(delivery_info, properties, body)
|
|
107
|
-
token = properties.headers&.dig('X-Service-Token')
|
|
108
|
-
unless TokenValidator.valid?(token)
|
|
109
|
-
# Do not call @app — message is effectively dropped from this consumer's perspective
|
|
110
|
-
# The channel will nack/reject it based on the consumer's error handling
|
|
111
|
-
return
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
@app.call(delivery_info, properties, body)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Note: halting in a middleware skips all routing and controller logic, but the message is still acknowledged at the AMQP level (the Consumer's normal ack happens in `process_message`, which was never reached). If you need to nack/reject, interact with `delivery_info.delivery_tag` directly — but this is an advanced use case.
|
|
120
|
-
|
|
121
|
-
---
|
|
122
|
-
|
|
123
|
-
## Inspecting the Stack
|
|
124
|
-
|
|
125
|
-
```ruby
|
|
126
|
-
BugBunny.consumer_middlewares.empty? # => false if any middleware registered
|
|
127
|
-
```
|