bug_bunny 4.8.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/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 +8 -0
- data/CLAUDE.md +28 -231
- data/lib/bug_bunny/version.rb +1 -1
- 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 +24 -0
- data/skills.yml +19 -0
- metadata +24 -28
- data/.claude/commands/gem-ai-setup.md +0 -174
- data/.claude/commands/pr.md +0 -53
- data/.claude/commands/release.md +0 -52
- data/.claude/commands/rubocop.md +0 -22
- data/.claude/commands/service-ai-setup.md +0 -168
- data/.claude/commands/test.md +0 -28
- data/.claude/commands/yard.md +0 -46
- data/docs/_index.md +0 -50
- data/docs/ai/_index.md +0 -56
- data/docs/ai/antipatterns.md +0 -166
- data/docs/ai/api.md +0 -251
- data/docs/ai/architecture.md +0 -92
- data/docs/ai/errors.md +0 -158
- data/docs/ai/faq_external.md +0 -133
- data/docs/ai/faq_internal.md +0 -86
- data/docs/ai/glossary.md +0 -45
- 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/docs/howto/testing.md
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
# Testing
|
|
2
|
-
|
|
3
|
-
BugBunny applications can be tested at two levels: **unit tests** (with Bunny doubles) and **integration tests** (with a full mocked AMQP stack). No real RabbitMQ server is required.
|
|
4
|
-
|
|
5
|
-
## Setup
|
|
6
|
-
|
|
7
|
-
```ruby
|
|
8
|
-
# spec/spec_helper.rb
|
|
9
|
-
require 'bug_bunny'
|
|
10
|
-
require 'rspec'
|
|
11
|
-
|
|
12
|
-
Dir[File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f }
|
|
13
|
-
|
|
14
|
-
RSpec.configure do |config|
|
|
15
|
-
config.before(:each) do
|
|
16
|
-
# Reset global state between tests
|
|
17
|
-
BugBunny.instance_variable_set(:@consumer_middlewares, nil)
|
|
18
|
-
BugBunny.instance_variable_set(:@routes, nil)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## Bunny Doubles
|
|
26
|
-
|
|
27
|
-
Create lightweight doubles for Bunny objects so tests never touch the network:
|
|
28
|
-
|
|
29
|
-
```ruby
|
|
30
|
-
# spec/support/bunny_mocks.rb
|
|
31
|
-
|
|
32
|
-
def build_bunny_channel(opts = {})
|
|
33
|
-
channel = instance_double(Bunny::Channel)
|
|
34
|
-
allow(channel).to receive(:open?).and_return(true)
|
|
35
|
-
allow(channel).to receive(:prefetch)
|
|
36
|
-
allow(channel).to receive(:confirm_select)
|
|
37
|
-
allow(channel).to receive(:ack)
|
|
38
|
-
allow(channel).to receive(:reject)
|
|
39
|
-
allow(channel).to receive(:close)
|
|
40
|
-
allow(channel).to receive(:default_exchange).and_return(build_bunny_exchange)
|
|
41
|
-
allow(channel).to receive(:topic).and_return(build_bunny_exchange)
|
|
42
|
-
allow(channel).to receive(:direct).and_return(build_bunny_exchange)
|
|
43
|
-
channel
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def build_bunny_connection(opts = {})
|
|
47
|
-
conn = instance_double(Bunny::Session)
|
|
48
|
-
allow(conn).to receive(:open?).and_return(true)
|
|
49
|
-
allow(conn).to receive(:start)
|
|
50
|
-
allow(conn).to receive(:create_channel).and_return(build_bunny_channel)
|
|
51
|
-
conn
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def build_bunny_exchange
|
|
55
|
-
exchange = instance_double(Bunny::Exchange)
|
|
56
|
-
allow(exchange).to receive(:publish)
|
|
57
|
-
exchange
|
|
58
|
-
end
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## Unit Testing Controllers
|
|
64
|
-
|
|
65
|
-
Test controller actions directly via `Controller.call`, bypassing AMQP entirely:
|
|
66
|
-
|
|
67
|
-
```ruby
|
|
68
|
-
RSpec.describe NodesController do
|
|
69
|
-
let(:node) { Node.new(id: '42', name: 'web-01', status: 'active') }
|
|
70
|
-
|
|
71
|
-
describe '#show' do
|
|
72
|
-
it 'returns the node as JSON' do
|
|
73
|
-
allow(Node).to receive(:find).with('42').and_return(node)
|
|
74
|
-
|
|
75
|
-
response = NodesController.call(
|
|
76
|
-
headers: { type: 'nodes/42', 'x-http-method' => 'GET' },
|
|
77
|
-
body: ''
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
expect(response[:status]).to eq(200)
|
|
81
|
-
expect(response[:body][:name]).to eq('web-01')
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
describe '#show with missing node' do
|
|
86
|
-
it 'returns 404' do
|
|
87
|
-
allow(Node).to receive(:find).with('99').and_return(nil)
|
|
88
|
-
|
|
89
|
-
response = NodesController.call(
|
|
90
|
-
headers: { type: 'nodes/99', 'x-http-method' => 'GET' },
|
|
91
|
-
body: ''
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
expect(response[:status]).to eq(404)
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## Unit Testing before_action / after_action
|
|
103
|
-
|
|
104
|
-
```ruby
|
|
105
|
-
RSpec.describe NodesController do
|
|
106
|
-
describe 'before_action :authenticate!' do
|
|
107
|
-
it 'returns 401 when token is missing' do
|
|
108
|
-
response = NodesController.call(
|
|
109
|
-
headers: { type: 'nodes', 'x-http-method' => 'GET' },
|
|
110
|
-
body: ''
|
|
111
|
-
# no X-Service-Token header
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
expect(response[:status]).to eq(401)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
describe 'after_action :emit_audit_event' do
|
|
119
|
-
it 'emits an audit event after create' do
|
|
120
|
-
expect(AuditLog).to receive(:record).with(hash_including(action: 'create'))
|
|
121
|
-
|
|
122
|
-
NodesController.call(
|
|
123
|
-
headers: { type: 'nodes', 'x-http-method' => 'POST', 'X-Service-Token' => 'valid' },
|
|
124
|
-
body: '{"node":{"name":"web-01","status":"pending"}}'
|
|
125
|
-
)
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
---
|
|
132
|
-
|
|
133
|
-
## Unit Testing Consumer Middleware
|
|
134
|
-
|
|
135
|
-
```ruby
|
|
136
|
-
RSpec.describe TracingMiddleware do
|
|
137
|
-
subject(:middleware) { described_class.new(terminal) }
|
|
138
|
-
|
|
139
|
-
let(:terminal) { ->(di, props, body) { :processed } }
|
|
140
|
-
let(:delivery_info) { instance_double(Bunny::DeliveryInfo, routing_key: 'nodes') }
|
|
141
|
-
let(:properties) do
|
|
142
|
-
instance_double(Bunny::MessageProperties,
|
|
143
|
-
headers: { 'X-Trace-Id' => 'trace-123' },
|
|
144
|
-
correlation_id: 'corr-456'
|
|
145
|
-
)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
it 'extracts the trace header and sets context' do
|
|
149
|
-
expect(MyTracer).to receive(:with_trace).with('trace-123').and_yield
|
|
150
|
-
|
|
151
|
-
middleware.call(delivery_info, properties, '{}')
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
it 'generates a new trace id when header is absent' do
|
|
155
|
-
allow(properties).to receive(:headers).and_return({})
|
|
156
|
-
expect(MyTracer).to receive(:with_trace).with(be_a(String)).and_yield
|
|
157
|
-
|
|
158
|
-
middleware.call(delivery_info, properties, '{}')
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
## Integration Testing with a Mock Consumer
|
|
166
|
-
|
|
167
|
-
For tests that exercise the full routing + controller stack, use an in-process integration helper:
|
|
168
|
-
|
|
169
|
-
```ruby
|
|
170
|
-
# spec/support/integration_helper.rb
|
|
171
|
-
|
|
172
|
-
module IntegrationHelper
|
|
173
|
-
def process_message(method:, path:, body: '', headers: {})
|
|
174
|
-
delivery_info = instance_double(Bunny::DeliveryInfo,
|
|
175
|
-
delivery_tag: 1,
|
|
176
|
-
routing_key: 'test'
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
properties = instance_double(Bunny::MessageProperties,
|
|
180
|
-
type: path,
|
|
181
|
-
reply_to: nil,
|
|
182
|
-
correlation_id: SecureRandom.uuid,
|
|
183
|
-
headers: { 'x-http-method' => method.to_s.upcase }.merge(headers)
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
consumer = BugBunny::Consumer.new
|
|
187
|
-
# Allow channel operations on the mock session
|
|
188
|
-
allow(consumer.session.channel).to receive(:ack)
|
|
189
|
-
allow(consumer.session.channel).to receive(:reject)
|
|
190
|
-
allow(consumer.session.channel).to receive(:default_exchange).and_return(build_bunny_exchange)
|
|
191
|
-
|
|
192
|
-
consumer.send(:process_message, delivery_info, properties, body.to_json)
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
RSpec.configure do |config|
|
|
197
|
-
config.include IntegrationHelper, type: :integration
|
|
198
|
-
end
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
```ruby
|
|
202
|
-
# spec/integration/nodes_spec.rb, type: :integration
|
|
203
|
-
RSpec.describe 'Nodes API', type: :integration do
|
|
204
|
-
before do
|
|
205
|
-
BugBunny.routes.draw { resources :nodes }
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
it 'routes GET nodes/:id to NodesController#show' do
|
|
209
|
-
allow(Node).to receive(:find).with('42').and_return(Node.new(id: 42, name: 'web-01'))
|
|
210
|
-
|
|
211
|
-
process_message(method: :get, path: 'nodes/42')
|
|
212
|
-
|
|
213
|
-
expect(Node).to have_received(:find).with('42')
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
it 'returns 404 for unregistered routes' do
|
|
217
|
-
response = process_message(method: :get, path: 'unknown/path')
|
|
218
|
-
# response is captured via the consumer's handle_fatal_error path
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
## Unit Testing Configuration Validation
|
|
226
|
-
|
|
227
|
-
```ruby
|
|
228
|
-
RSpec.describe BugBunny::Configuration do
|
|
229
|
-
describe '#validate!' do
|
|
230
|
-
it 'raises ConfigurationError when host is blank' do
|
|
231
|
-
config = described_class.new
|
|
232
|
-
config.host = ''
|
|
233
|
-
|
|
234
|
-
expect { config.validate! }.to raise_error(BugBunny::ConfigurationError, /host is required/)
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
it 'raises ConfigurationError when port is out of range' do
|
|
238
|
-
config = described_class.new
|
|
239
|
-
config.host = 'localhost'
|
|
240
|
-
config.port = 99_999
|
|
241
|
-
|
|
242
|
-
expect { config.validate! }.to raise_error(BugBunny::ConfigurationError, /port must be in/)
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
---
|
|
249
|
-
|
|
250
|
-
## Running Tests
|
|
251
|
-
|
|
252
|
-
```bash
|
|
253
|
-
source /opt/homebrew/opt/chruby/share/chruby/chruby.sh && chruby ruby-3.3.8
|
|
254
|
-
|
|
255
|
-
bundle exec rspec # all tests
|
|
256
|
-
bundle exec rspec spec/unit/ # unit tests only
|
|
257
|
-
bundle exec rspec spec/integration/ # integration tests only
|
|
258
|
-
bundle exec rspec spec/unit/consumer_spec.rb # single file
|
|
259
|
-
```
|
data/docs/howto/tracing.md
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
# Distributed Tracing
|
|
2
|
-
|
|
3
|
-
BugBunny propagates trace context through the full RPC cycle, from the producer to the consumer and back. The mechanism is tracer-agnostic: BugBunny provides hooks and you supply the tracer-specific logic.
|
|
4
|
-
|
|
5
|
-
## What BugBunny Propagates by Default
|
|
6
|
-
|
|
7
|
-
The `correlation_id` AMQP property travels automatically from producer to consumer on every request. The Consumer wraps the entire execution in a `logger.tagged(correlation_id)` block when the logger supports tagged logging (Rails' `ActiveSupport::TaggedLogging`).
|
|
8
|
-
|
|
9
|
-
This alone connects producer and consumer log lines without any configuration.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Full Bidirectional Propagation
|
|
14
|
-
|
|
15
|
-
For distributed tracing systems (OpenTelemetry, AWS X-Ray, Datadog APM, etc.) you need to:
|
|
16
|
-
|
|
17
|
-
1. **Inject** the current trace header into outgoing requests (producer side).
|
|
18
|
-
2. **Extract** the trace header from incoming messages (consumer side) — done via Consumer Middleware.
|
|
19
|
-
3. **Re-inject** the updated trace header into the RPC reply (consumer side).
|
|
20
|
-
4. **Hydrate** the trace context from the reply headers back in the calling thread (producer side).
|
|
21
|
-
|
|
22
|
-
Steps 3 and 4 handle the case where the consumer creates a child span — the parent needs to know about it.
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## Configuration Hooks
|
|
27
|
-
|
|
28
|
-
### `rpc_reply_headers`
|
|
29
|
-
|
|
30
|
-
A `Proc` called in the consumer thread just before sending the RPC reply. Its return value is merged into the reply AMQP headers.
|
|
31
|
-
|
|
32
|
-
```ruby
|
|
33
|
-
BugBunny.configure do |config|
|
|
34
|
-
config.rpc_reply_headers = -> {
|
|
35
|
-
{ 'X-Trace-Header' => MyTracer.outgoing_header }
|
|
36
|
-
}
|
|
37
|
-
end
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Zero overhead when not set.
|
|
41
|
-
|
|
42
|
-
### `on_rpc_reply`
|
|
43
|
-
|
|
44
|
-
A `Proc` called in the producer thread after the RPC reply arrives, with the reply headers as argument. Use it to hydrate the trace context in the calling thread.
|
|
45
|
-
|
|
46
|
-
```ruby
|
|
47
|
-
BugBunny.configure do |config|
|
|
48
|
-
config.on_rpc_reply = ->(headers) {
|
|
49
|
-
MyTracer.hydrate(headers['X-Trace-Header'])
|
|
50
|
-
}
|
|
51
|
-
end
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## Full Example (tracer-agnostic)
|
|
57
|
-
|
|
58
|
-
```ruby
|
|
59
|
-
# config/initializers/bug_bunny.rb
|
|
60
|
-
|
|
61
|
-
BugBunny.configure do |config|
|
|
62
|
-
# ... connection config ...
|
|
63
|
-
|
|
64
|
-
# Step 3: inject updated trace header into RPC reply
|
|
65
|
-
config.rpc_reply_headers = -> {
|
|
66
|
-
{ 'X-Trace-Header' => MyTracer.generate_outgoing_header }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
# Step 4: hydrate trace context in the producer thread after reply
|
|
70
|
-
config.on_rpc_reply = ->(headers) {
|
|
71
|
-
MyTracer.hydrate_from_header(headers['X-Trace-Header'])
|
|
72
|
-
}
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Step 1: inject trace header into outgoing requests (client middleware)
|
|
76
|
-
class TraceInjectionMiddleware < BugBunny::Middleware::Base
|
|
77
|
-
def call(request)
|
|
78
|
-
request.headers['X-Trace-Header'] = MyTracer.generate_outgoing_header
|
|
79
|
-
app.call(request)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Step 2: extract trace header from incoming messages (consumer middleware)
|
|
84
|
-
class TraceExtractionMiddleware < BugBunny::ConsumerMiddleware::Base
|
|
85
|
-
def call(delivery_info, properties, body)
|
|
86
|
-
incoming_header = properties.headers&.dig('X-Trace-Header')
|
|
87
|
-
MyTracer.with_trace_from_header(incoming_header) do
|
|
88
|
-
@app.call(delivery_info, properties, body)
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Register both
|
|
94
|
-
BugBunny::Resource.client_middleware { |s| s.use TraceInjectionMiddleware }
|
|
95
|
-
BugBunny.consumer_middlewares.use TraceExtractionMiddleware
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## Fire-and-Forget Tracing
|
|
101
|
-
|
|
102
|
-
For `:publish` (fire-and-forget) calls, there is no reply, so `on_rpc_reply` and `rpc_reply_headers` do not apply. Use only the client middleware to inject the outgoing trace header:
|
|
103
|
-
|
|
104
|
-
```ruby
|
|
105
|
-
class TraceInjectionMiddleware < BugBunny::Middleware::Base
|
|
106
|
-
def call(request)
|
|
107
|
-
request.headers['X-Trace-Header'] = MyTracer.generate_outgoing_header
|
|
108
|
-
app.call(request)
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
The consumer middleware extracts it on the other side regardless of whether the call was RPC or fire-and-forget.
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## correlation_id
|
|
118
|
-
|
|
119
|
-
BugBunny sets `correlation_id` automatically on every RPC request (used internally to match replies). It is also forwarded as a log tag. If your tracer uses a separate header (e.g., `traceparent`), use the `X-Trace-Header` pattern above. If you want to use `correlation_id` as the trace ID, read it from `properties.correlation_id` in your consumer middleware.
|