bug_bunny 4.8.0 → 4.9.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/skills/documentation-writer/SKILL.md +45 -0
  3. data/.agents/skills/gem-release/SKILL.md +116 -0
  4. data/.agents/skills/quality-code/SKILL.md +51 -0
  5. data/.agents/skills/sentry/SKILL.md +135 -0
  6. data/.agents/skills/sentry/references/api-endpoints.md +147 -0
  7. data/.agents/skills/sentry/scripts/sentry.rb +194 -0
  8. data/.agents/skills/skill-builder/SKILL.md +293 -0
  9. data/.agents/skills/skill-manager/SKILL.md +225 -0
  10. data/.agents/skills/skill-manager/scripts/sync.rb +356 -0
  11. data/.agents/skills/yard/SKILL.md +311 -0
  12. data/.agents/skills/yard/references/tipos.md +144 -0
  13. data/CHANGELOG.md +14 -0
  14. data/CLAUDE.md +28 -225
  15. data/README.md +5 -3
  16. data/lib/bug_bunny/consumer.rb +21 -5
  17. data/lib/bug_bunny/otel.rb +47 -0
  18. data/lib/bug_bunny/producer.rb +13 -4
  19. data/lib/bug_bunny/request.rb +14 -2
  20. data/lib/bug_bunny/version.rb +1 -1
  21. data/lib/bug_bunny.rb +1 -0
  22. data/skill/SKILL.md +253 -0
  23. data/skill/references/client-middleware.md +161 -0
  24. data/skill/references/consumer.md +122 -0
  25. data/skill/references/controller.md +105 -0
  26. data/skill/references/errores.md +97 -0
  27. data/skill/references/resource.md +116 -0
  28. data/skill/references/routing.md +82 -0
  29. data/skill/references/testing.md +138 -0
  30. data/skills.lock +30 -0
  31. data/skills.yml +40 -0
  32. data/spec/integration/consumer_middleware_spec.rb +23 -2
  33. data/spec/unit/consumer_spec.rb +138 -6
  34. data/spec/unit/otel_spec.rb +54 -0
  35. data/spec/unit/producer_spec.rb +187 -0
  36. data/spec/unit/request_spec.rb +51 -0
  37. metadata +28 -29
  38. data/.agents/skills/rabbitmq-expert/SKILL.md +0 -1555
  39. data/.claude/commands/gem-ai-setup.md +0 -174
  40. data/.claude/commands/pr.md +0 -53
  41. data/.claude/commands/release.md +0 -52
  42. data/.claude/commands/rubocop.md +0 -22
  43. data/.claude/commands/service-ai-setup.md +0 -168
  44. data/.claude/commands/test.md +0 -28
  45. data/.claude/commands/yard.md +0 -46
  46. data/docs/_index.md +0 -50
  47. data/docs/ai/_index.md +0 -56
  48. data/docs/ai/antipatterns.md +0 -166
  49. data/docs/ai/api.md +0 -251
  50. data/docs/ai/architecture.md +0 -92
  51. data/docs/ai/errors.md +0 -158
  52. data/docs/ai/faq_external.md +0 -133
  53. data/docs/ai/faq_internal.md +0 -86
  54. data/docs/ai/glossary.md +0 -45
  55. data/docs/concepts.md +0 -140
  56. data/docs/howto/controller.md +0 -194
  57. data/docs/howto/middleware_client.md +0 -119
  58. data/docs/howto/middleware_consumer.md +0 -127
  59. data/docs/howto/rails.md +0 -214
  60. data/docs/howto/resource.md +0 -200
  61. data/docs/howto/routing.md +0 -133
  62. data/docs/howto/testing.md +0 -259
  63. data/docs/howto/tracing.md +0 -119
@@ -1,133 +0,0 @@
1
- # Routing
2
-
3
- Routes declare how incoming AMQP messages map to controllers and actions. They are evaluated on every message received by the Consumer.
4
-
5
- ## Drawing Routes
6
-
7
- Define routes in an initializer (or any file loaded at boot):
8
-
9
- ```ruby
10
- BugBunny.routes.draw do
11
- # routes here
12
- end
13
- ```
14
-
15
- Call `draw` only once. Multiple calls replace the previous route set.
16
-
17
- ---
18
-
19
- ## HTTP Verbs
20
-
21
- ```ruby
22
- BugBunny.routes.draw do
23
- get 'status', to: 'health#show'
24
- post 'events', to: 'events#create'
25
- put 'users/:id', to: 'users#update'
26
- delete 'users/:id', to: 'users#destroy'
27
- end
28
- ```
29
-
30
- The verb is set by the producer via the `x-http-method` AMQP header. `BugBunny::Resource` and `BugBunny::Client` set it automatically.
31
-
32
- Dynamic segments (`:id`) are extracted from the path and available in `params` inside the controller.
33
-
34
- ---
35
-
36
- ## Resources Macro
37
-
38
- `resources` generates the standard seven CRUD routes in one line:
39
-
40
- ```ruby
41
- resources :users
42
- ```
43
-
44
- Generates:
45
-
46
- | Verb | Path | Action |
47
- |--------|---------------|-----------|
48
- | GET | users | index |
49
- | POST | users | create |
50
- | GET | users/:id | show |
51
- | PUT | users/:id | update |
52
- | DELETE | users/:id | destroy |
53
-
54
- ### Filtering actions
55
-
56
- ```ruby
57
- resources :orders, only: [:index, :show, :create]
58
- resources :logs, except: [:update, :destroy]
59
- ```
60
-
61
- ---
62
-
63
- ## Member and Collection Routes
64
-
65
- ```ruby
66
- resources :nodes do
67
- member do
68
- put :drain # PUT nodes/:id/drain → NodesController#drain
69
- post :reboot # POST nodes/:id/reboot → NodesController#reboot
70
- end
71
-
72
- collection do
73
- post :rebalance # POST nodes/rebalance → NodesController#rebalance
74
- get :summary # GET nodes/summary → NodesController#summary
75
- end
76
- end
77
- ```
78
-
79
- Member routes receive `params[:id]` automatically. Collection routes do not.
80
-
81
- ---
82
-
83
- ## Namespace Blocks
84
-
85
- Group routes under a controller namespace. Namespaces stack: nested `namespace` blocks accumulate with `::`.
86
-
87
- ```ruby
88
- BugBunny.routes.draw do
89
- namespace :api do
90
- namespace :v1 do
91
- resources :metrics # → Api::V1::MetricsController
92
- resources :alerts # → Api::V1::AlertsController
93
- end
94
-
95
- resources :health # → Api::HealthController
96
- end
97
-
98
- resources :nodes # → BugBunny::Controllers::NodesController (global namespace)
99
- end
100
- ```
101
-
102
- The namespace in the route takes precedence over `config.controller_namespace`. Routes without a namespace block use the global controller namespace.
103
-
104
- ---
105
-
106
- ## Nested Resources
107
-
108
- ```ruby
109
- resources :clusters do
110
- resources :nodes do # → nodes/:id nested under clusters/:cluster_id
111
- member { put :drain }
112
- end
113
- end
114
- ```
115
-
116
- Nested resource routes inject all parent IDs into params. Example: `PUT clusters/c1/nodes/n2/drain` → `params[:cluster_id] = 'c1'`, `params[:id] = 'n2'`.
117
-
118
- ---
119
-
120
- ## Inspecting Routes
121
-
122
- ```ruby
123
- BugBunny.routes.recognize('GET', 'nodes/123')
124
- # => { controller: 'nodes', action: 'show', params: { 'id' => '123' }, namespace: nil }
125
-
126
- BugBunny.routes.recognize('POST', 'api/v1/metrics')
127
- # => { controller: 'metrics', action: 'create', params: {}, namespace: 'Api::V1' }
128
-
129
- BugBunny.routes.recognize('GET', 'unknown/path')
130
- # => nil
131
- ```
132
-
133
- Useful in tests to verify route definitions without sending real messages.
@@ -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
- ```
@@ -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.