BugBunny
RESTful messaging over RabbitMQ for Ruby microservices.
BugBunny maps AMQP messages to controllers, routes, and models using the same patterns as Rails. Services communicate through RabbitMQ without HTTP coupling, with full support for synchronous RPC and fire-and-forget publishing.
Installation
gem 'bug_bunny'bundle install
rails generate bug_bunny:install # Rails onlyQuickstart
BugBunny connects two services through RabbitMQ. One service hosts the consumer (server side); the other uses a Resource or Client to call it (client side).
Service B — Consumer
# config/initializers/bug_bunny.rb
BugBunny.configure do |config|
config.host = ENV.fetch('RABBITMQ_HOST', 'localhost')
config.port = 5672
config.username = ENV.fetch('RABBITMQ_USER', 'guest')
config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
end
# config/initializers/bug_bunny_routes.rb
BugBunny.routes.draw do
resources :nodes
end
# app/controllers/bug_bunny/controllers/nodes_controller.rb
module BugBunny
module Controllers
class NodesController < BugBunny::Controller
def show
node = Node.find(params[:id])
render status: :ok, json: node.as_json
end
def index
render status: :ok, json: Node.all.map(&:as_json)
end
end
end
end
# Worker entrypoint (dedicated thread or process)
consumer = BugBunny::Consumer.new
consumer.subscribe(
queue_name: 'inventory_queue',
exchange_name: 'inventory',
routing_key: 'nodes'
)Service A — Producer
# config/initializers/bug_bunny.rb — same connection config as above
# Pool shared across threads (Puma / Sidekiq)
BUG_BUNNY_POOL = ConnectionPool.new(size: 5, timeout: 5) do
BugBunny.create_connection
end
class RemoteNode < BugBunny::Resource
self.exchange = 'inventory'
self.resource_name = 'nodes'
attribute :name, :string
attribute :status, :string
end
RemoteNode.connection_pool = BUG_BUNNY_POOL
# Use it like ActiveRecord
node = RemoteNode.find('node-123') # GET nodes/node-123 via RabbitMQ
node.status = 'active'
node.save # PUT nodes/node-123
RemoteNode.where(status: 'active') # GET nodes?status=active
RemoteNode.create(name: 'web-01', status: 'pending')Modes of Use
Resource ORM — ActiveRecord-like model for a remote service. Handles CRUD, validations, change tracking, and typed or dynamic attributes. Best when you own both sides of the communication.
Direct Client — BugBunny::Client for explicit RPC or fire-and-forget calls with full middleware control. Best when calling external services or when you need precise control over the request.
Consumer — Subscribe loop that routes incoming messages to controllers, with a middleware stack for cross-cutting concerns (tracing, auth, auditing).
Configuration
BugBunny.configure do |config|
# Connection — required
config.host = 'localhost'
config.port = 5672
config.username = 'guest'
config.password = 'guest'
config.vhost = '/'
# Resilience
config.max_reconnect_attempts = 10 # nil = infinite
config.max_reconnect_interval = 60 # seconds, ceiling for backoff
config.network_recovery_interval = 5 # seconds, base for exponential backoff
# Timeouts
config.rpc_timeout = 30 # seconds, for synchronous RPC calls
config.connection_timeout = 10
config.read_timeout = 10
config.write_timeout = 10
# AMQP defaults applied to all exchanges and queues
config.exchange_options = { durable: true }
config.queue_options = { durable: true }
# Controller namespace (default: 'BugBunny::Controllers')
config.controller_namespace = 'MyApp::RabbitHandlers'
# Logger — any object responding to debug/info/warn/error
config.logger = Rails.logger
# Health check file for Kubernetes / Docker Swarm liveness probes
config.health_check_file = '/tmp/bug_bunny_health'
endBugBunny.configure validates all required fields on exit. A missing or invalid value raises BugBunny::ConfigurationError immediately, before any connection attempt.
Routing DSL
BugBunny.routes.draw do
resources :users # GET/POST users, GET/PUT/DELETE users/:id
resources :orders, only: [:index, :show, :create]
resources :nodes do
member { put :drain } # PUT nodes/:id/drain
collection { post :rebalance } # POST nodes/rebalance
end
namespace :api do
namespace :v1 do
resources :metrics # Routes to Api::V1::MetricsController
end
end
get 'status', to: 'health#show'
post 'events/:id', to: 'events#track'
endDirect Client
pool = ConnectionPool.new(size: 5, timeout: 5) { BugBunny.create_connection }
client = BugBunny::Client.new(pool: pool) do |stack|
stack.use BugBunny::Middleware::RaiseError
stack.use BugBunny::Middleware::JsonResponse
end
# Synchronous RPC
response = client.request('users/42', method: :get)
response['body'] # => { 'id' => 42, 'name' => 'Alice' }
# Fire-and-forget
client.publish('events', body: { type: 'user.signed_in', user_id: 42 })
# With params
client.request('users', method: :get, params: { role: 'admin', page: 2 })Consumer Middleware
Middlewares run before every message reaches the router. Use them for distributed tracing, authentication, or audit logging.
class TracingMiddleware < BugBunny::ConsumerMiddleware::Base
def call(delivery_info, properties, body)
trace_id = properties.headers&.dig('X-Trace-Id')
MyTracer.with_trace(trace_id) { @app.call(delivery_info, properties, body) }
end
end
BugBunny.consumer_middlewares.use TracingMiddlewareObservability
BugBunny implementa de forma nativa las OpenTelemetry semantic conventions for messaging, inyectando automáticamente campos como messaging_system, messaging_operation, messaging_destination_name y messaging_message_id tanto en los headers AMQP como en los log events estructurados.
Todos los eventos internos se emiten como logs key=value compatibles con Datadog, CloudWatch, ELK y ExisRay.
component=bug_bunny event=consumer.message_processed status=200 duration_s=0.012 messaging_operation=process controller=NodesController action=show
component=bug_bunny event=consumer.execution_error error_class=RuntimeError error_message="..." duration_s=0.003
component=bug_bunny event=consumer.connection_error attempt_count=2 retry_in_s=10 error_message="..."
Las claves sensibles (password, token, secret, api_key, authorization, etc.) se filtran automáticamente a [FILTERED] en toda la salida de logs.
Error Handling
BugBunny maps RabbitMQ responses to a semantic exception hierarchy, similar to how HTTP clients handle status codes.
Exception Hierarchy
BugBunny::Error
├── ClientError (4xx)
│ ├── BadRequest (400)
│ ├── NotFound (404)
│ ├── NotAcceptable (406)
│ ├── RequestTimeout (408)
│ ├── Conflict (409)
│ └── UnprocessableEntity (422)
└── ServerError (5xx)
├── InternalServerError (500+)
└── RemoteError (500)
Remote Exception Propagation
When a controller raises an unhandled exception, BugBunny serializes it and sends it back to the caller as a 500 response. The client-side middleware reconstructs it as a BugBunny::RemoteError with full access to the original exception details:
begin
node = RemoteNode.find('node-123')
rescue BugBunny::RemoteError => e
e.original_class # => "TypeError"
e.original_message # => "nil can't be coerced into Integer"
e.original_backtrace # => Array<String> from the remote service
rescue BugBunny::NotFound
# Resource doesn't exist
rescue BugBunny::RequestTimeout
# Consumer didn't respond in time
endValidation Errors
Resource#save returns false on validation failure and loads remote errors into the model:
order = RemoteOrder.new(total: -1)
unless order.save
order.errors.full_messages # => ["total must be greater than 0"]
endDocumentation
- Concepts — What BugBunny is, AMQP in 5 minutes, RPC vs fire-and-forget
- Routing — Full routing DSL reference
-
Controllers — Filters,
rescue_from,render,after_action -
Resource ORM — CRUD, typed and dynamic attributes,
.withscoping - Client Middleware — Request/response middleware stack
- Consumer Middleware — Message processing middleware stack
- Distributed Tracing — Propagating trace context through RPC cycles
- Rails Setup — Full integration: Puma, Sidekiq, Zeitwerk, health checks
- Testing — Unit and integration testing with Bunny mocks