sondahub / AMQP test broker
AMQP test broker
An AMQP 0-9-1 broker that behaves the way RabbitMQ does — exchanges, bindings, prefetch, confirms, TTLs, dead-lettering, direct reply-to, its error codes in its words — with queues already filling from the hub’s live activity, an orders queue that takes orders through their life, and an rpc queue that answers. STOMP on the same broker. Through a bridge that lends it ports on your computer.
Free, nothing to install but the bridge. Each run of the bridge is a broker of its own — run several side by side.
Connect
- Address
- wss://api.sondahub.com/amqp
- From an AMQP client
- through the bridge: node bridge.mjs amqp [port] [stomp port]
- URL
- amqp://guest:guest@localhost:5772 (vhost /)
- STOMP
- localhost:61713, the same broker — or wss://api.sondahub.com/stomp
- Login
- PLAIN or AMQPLAIN: guest/guest, sonda/sondahub, viewer/viewer
- Tuning
- heartbeat 60 s, frame_max 131,072, channel_max 2,047
The bridge listens on port 5772 for AMQP and 61713 for STOMP — off the usual 5672 and 61613, so a broker you already run keeps them — or the next free ports, and carries every connection to the hub over one WebSocket. One bridge, one broker: what a client publishes over AMQP, another can read over STOMP. The bridge prints who connects, what is declared and bound, what is published where, and every error with its reason.
curl -O https://sondahub.com/industrial/bridge.mjs node bridge.mjs amqp AMQP 0-9-1 and 1.0 on localhost:5772 → wss://api.sondahub.com/amqp — amqp://guest:guest@localhost:5772 STOMP on localhost:61713 → wss://api.sondahub.com/amqp — localhost:61713, the same broker every run of the bridge is a world of its own the hub is an AMQP 0-9-1 and 1.0 broker that speaks STOMP too — exchanges amq.direct, amq.fanout, amq.topic, amq.headers; queues store, fleet, bank, social, helpdesk, flights, identity (the APIs’ activity), orders, orders.dead and rpc client “orders-service” — pika 1.3.2 — connected from 127.0.0.1:51760 as guest orders-service consumes from fleet, prefetch 10 orders-service → (default exchange) orders: routed to 1 queue orders ← order 900001 for 42.5: paid in 2 s, shipped in 5, delivered in 9, on amq.topic as store.orders
In an AMQP client
Start the bridge, then connect to amqp://guest:guest@localhost:5772. The queues are already there and filling: store, fleet, bank, social, helpdesk, flights, identity hold each API’s activity, the last minute of it there from the start.
import json, pika
conn = pika.BlockingConnection(pika.URLParameters('amqp://guest:guest@localhost:5772/%2F'))
ch = conn.channel()
for method, props, body in ch.consume('fleet', auto_ack=True, inactivity_timeout=5):
if method is None:
break
print(method.routing_key, json.loads(body))
import amqp from 'amqplib'
const conn = await amqp.connect('amqp://guest:guest@localhost:5772')
const ch = await conn.createChannel()
const { queue } = await ch.assertQueue('', { exclusive: true })
await ch.bindQueue(queue, 'amq.topic', 'store.orders')
await ch.consume(queue, (m) => console.log(m.fields.routingKey, m.content.toString()), { noAck: true })
ch.sendToQueue('orders', Buffer.from(JSON.stringify({ customer_id: 7, total: 42.5 })))
conn, err := amqp.Dial("amqp://guest:guest@localhost:5772/")
if err != nil {
panic(err)
}
ch, _ := conn.Channel()
msgs, _ := ch.Consume("bank", "", true, false, false, false, nil)
for m := range msgs {
fmt.Println(m.RoutingKey, string(m.Body))
}
In LockFlare Sonda: point a RabbitMQ item at localhost:5772 as guest/guest, with the bridge running.
What is in it
Queues
| Queue | What it holds |
|---|---|
| store amq.topic store.# | The store API’s live activity (store.orders, store.inventory): the last 500 messages, ten minutes at most, the last minute there from the start. |
| fleet amq.topic fleet.# | The fleet API’s live activity (fleet.telemetry, fleet.devices, fleet.alerts): the last 500 messages, ten minutes at most, the last minute there from the start. |
| bank amq.topic bank.# | The bank API’s live activity (bank.fx, bank.transactions): the last 500 messages, ten minutes at most, the last minute there from the start. |
| social amq.topic social.# | The social API’s live activity (social.posts, social.likes, social.comments): the last 500 messages, ten minutes at most, the last minute there from the start. |
| helpdesk amq.topic helpdesk.# | The helpdesk API’s live activity (helpdesk.tickets, helpdesk.messages): the last 500 messages, ten minutes at most, the last minute there from the start. |
| flights amq.topic flights.# | The flights API’s live activity (flights.board, flights.bookings): the last 500 messages, ten minutes at most, the last minute there from the start. |
| identity amq.topic identity.# | The identity API’s live activity (identity.logins, identity.directory): the last 500 messages, ten minutes at most, the last minute there from the start. |
| orders the default exchange | Publish an order (JSON with customer_id and a total above 0) and the hub takes it through paid, shipped and delivered on store.orders. Anything else it rejects, and it is dead-lettered to orders.dead. |
| orders.dead orders.dlx (fanout) | What orders rejected, with x-death saying when and why, and x-sondahub-error saying what was wrong. |
| rpc the default exchange | Requests with reply_to and correlation_id: {"method":"store.getProduct","params":{"id":1}} — answered with {"result":…} or {"error":…}, the calls JSON-RPC makes. |
The hub’s queues take any declaration — durable or not, with arguments or without — so a client that declares before it consumes just works; they cannot be deleted. Each API queue keeps 500 messages at most.
Exchanges
| Exchange | What it routes |
|---|---|
| (default) direct | Every queue by its name, as the routing key. |
| amq.direct direct | Routing key equals binding key. |
| amq.fanout fanout | Every bound queue. |
| amq.topic topic | Where the hub publishes its activity: 16 routing keys, store.orders, store.inventory, fleet.telemetry, fleet.devices… Bind with patterns: store.*, fleet.#, #.alerts. |
| amq.headers, amq.match headers | Bindings match on headers, x-match all or any (or all-with-x, any-with-x). |
| orders.dlx fanout | The dead-letter exchange of orders, bound to orders.dead. |
The routing keys on amq.topic
Each message JSON, with content_type application/json, a message_id, a timestamp, type set to its routing key, app_id sondahub, and the headers source and key.
| Routing key | What it carries |
|---|---|
| store.orders header key: order id | Orders moving through their life — paid, shipped with a tracking number, delivered — one record per change. Produce your own order here and watch it move. |
| store.inventory header key: SKU | Stock moving in the warehouses: picks and restocks. |
| fleet.telemetry header key: device serial | Readings from the IoT fleet, about one a second, with metrics that walk instead of jumping. |
| fleet.devices header key: device serial | Devices going degraded or offline and coming back. |
| fleet.alerts header key: device serial | Alerts opened and cleared: thresholds, low battery, weak signal, offline. |
| bank.fx header key: currency pair | Exchange rates with bid and ask, three a second. |
| bank.transactions header key: account number | Card transactions on checking accounts, posted or pending. |
| social.posts header key: username | New posts, with their hashtags. |
| social.likes header key: post id | Likes landing on posts, with the running count. |
| social.comments header key: post id | Comments on posts. |
| helpdesk.tickets header key: ticket number | Tickets changing status and being assigned. |
| helpdesk.messages header key: ticket number | Messages on tickets, from agents and customers. |
| flights.board header key: flight number | The departures board: boarding, departed, in the air, landed — and delayed or cancelled. |
| flights.bookings header key: flight number | Bookings and check-ins, with the seat and the seats left. |
| identity.logins header key: user name | Sign-ins: the method, and why the ones that failed did. |
| identity.directory header key: user name | People joining and leaving groups, deactivated, reactivated, retitled. |
Orders and RPC
orders listens. Publish an order to it — JSON with customer_id and a total above 0, an id if you want to pick it — and the hub takes it through its life on amq.topic as store.orders: paid two seconds later, shipped with a tracking number at five, delivered at nine. An order over 10,000 is cancelled. With reply_to set, the hub answers at once: {"accepted":true,"id":…,"status":"pending"}. Anything that is not an order the hub rejects: orders dead-letters it through orders.dlx to orders.dead, with x-death saying it was rejected and x-sondahub-error saying why.
rpc answers. Publish {"method":"store.getProduct","params":{"id":1}} with reply_to and correlation_id and the answer comes back as {"result":…} or {"error":{"code","message"}}, with your correlation_id: every call JSON-RPC makes (list, get, create, update and delete on every collection of the seven APIs), writes kept as long as the broker lives. reply_to may be a queue of yours or RabbitMQ’s direct reply-to, amq.rabbitmq.reply-to.
ch.basic_consume('amq.rabbitmq.reply-to', lambda c, m, p, b: print(p.correlation_id, json.loads(b)), auto_ack=True)
ch.basic_publish('', 'rpc', json.dumps({'method': 'store.getProduct', 'params': {'id': 1}}),
pika.BasicProperties(reply_to='amq.rabbitmq.reply-to', correlation_id='1'))
conn.process_data_events(time_limit=2)
What the broker does
Exchanges direct, fanout, topic and headers, with bindings to queues and to other exchanges. Queues server-named (amq.gen-…), exclusive to their connection, auto-deleted with their last consumer, with the arguments below. Consumers served in turn, with prefetch per consumer (basic.qos, global false) or per channel (global true), exclusive consumers, and single active consumer. basic.get, ack, nack and reject with requeue — a requeued message comes back marked redelivered — and recover. Publisher confirms (a nack when a queue refuses the message), mandatory publishing (basic.return 312 NO_ROUTE), transactions (tx.select, commit, rollback), channel.flow, heartbeats, and consumers told when their queue goes (basic.cancel). Errors are RabbitMQ’s: 404 NOT_FOUND, 403 ACCESS_REFUSED, 405 RESOURCE_LOCKED, 406 PRECONDITION_FAILED on the channel; 503, 504, 505, 530, 540 on the connection — with RabbitMQ’s words, NOT_FOUND - no queue 'x' in vhost '/'.
Queue arguments
| Argument | What it does |
|---|---|
| x-message-ttl | Milliseconds a message may wait in the queue; past it, it is dead-lettered as expired (or dropped). |
| x-expires | Accepted; the queue lives as long as the broker anyway. |
| x-max-length | The most messages the queue holds. |
| x-max-length-bytes | The most body bytes the queue holds. |
| x-overflow | drop-head (the oldest is dead-lettered as maxlen), reject-publish (the new one is refused: a nack under confirms), reject-publish-dlx (refused and dead-lettered). |
| x-dead-letter-exchange | Where rejected, expired and dropped messages go, with x-death and x-first-death-* / x-last-death-* headers. |
| x-dead-letter-routing-key | The routing key they go with (else their own). |
| x-queue-type | classic, or quorum (durable and not exclusive, as RabbitMQ insists); stream is refused. |
| x-single-active-consumer | Only the first consumer gets messages until it goes. |
Messages up to 4 MB, one vhost (/), plaintext only (no TLS). Priorities are accepted and not ordered by; stream queues are refused.
Users
| User | May |
|---|---|
| guest password guest | Everything: declare, bind, publish, consume, delete. The user every client tries first. |
| sonda password sondahub | Everything: declare, bind, publish, consume, delete. |
| viewer password viewer | Reads: consume, get, declare a server-named queue and bind it. Publishing and declaring named queues or exchanges are refused with 403 ACCESS_REFUSED. |
| Anyone else or a wrong password | AMQP: connection.close 403 ACCESS_REFUSED, as RabbitMQ answers. STOMP: an ERROR frame, then the connection closes. |
AMQP 1.0 on the same port
An AMQP 1.0 client connects to localhost:5772 too — the broker reads the protocol header and answers in the version asked for — and reads /queues/fleet or sends to /exchanges/amq.topic/store.orders, RabbitMQ’s addresses. AMQP 1.0 also comes straight over WebSocket, with no bridge. The AMQP 1.0 broker.
STOMP on the same broker
A STOMP client connects to localhost:61713 through the same bridge and meets the same queues and exchanges, with RabbitMQ’s destinations — /queue/orders, /topic/fleet.#, /exchange/amq.fanout. What it sends, an AMQP client reads, and the other way round. STOMP also comes straight over WebSocket, with no bridge: the STOMP server.
The bridge, on the wire
For anyone writing a bridge of their own: it opens wss://api.sondahub.com/amqp with the subprotocol sondahub-bridge and says hello in text — {"bridge":"amqp","version":1,"ports":{"amqp":5772,"stomp":61713}}. Then every client connection rides the WebSocket under an id: a byte for what happened (3 bytes of the stream, 4 a connection opened, 5 one closed), the id (four bytes, big-endian), two zero bytes, then for a 3 the bytes as they came off the socket, and for a 4 {"proto":"amqp","peer":"ip:port","local":"ip:port"} — which listener, who dialed, and the address they dialed. The hub sends 3 and 5 back, and {"log":…} lines in text.
Questions
Why does AMQP need the bridge?
AMQP clients open TCP connections, and only HTTP reaches sondahub — no raw TCP comes in, and AMQP 0-9-1 has no WebSocket transport of its own. So the broker runs on the hub and the bridge lends it a port on your computer, carrying every connection over one WebSocket: to the client it is a broker on localhost.
Why port 5772 and not 5672?
So a broker already running on your computer keeps 5672 (and 61613 for STOMP), and so you can run several: the bridge takes 5772 and 61713, or the next free ports, and prints them. Each run of the bridge is a broker of its own. Give it ports — node bridge.mjs amqp 5672 61613 — and it takes exactly those or stops.
Do my messages reach other people?
No. The clients you point at one bridge share its broker — publish from one, consume from another, over AMQP or STOMP — but nobody else sees it, and it ends when the bridge stops. Nothing is stored: durable queues and persistent messages are accepted and last as long as the broker does.
Which clients work?
Any AMQP 0-9-1 client: the broker answers every method of the connection, channel, exchange, queue, basic, confirm and tx classes, with RabbitMQ’s extensions (publisher confirms, basic.nack, exchange-to-exchange bindings, consumer cancel notifications, direct reply-to, per-consumer prefetch) and RabbitMQ’s error codes and wording. AMQP 1.0 clients connect to the same port — the protocol header tells them apart, as on RabbitMQ 4 — and meet the same queues: AMQP 1.0.
Is there a management UI or HTTP API?
No. There is the bridge’s log, which says who connected, what was declared, bound and published, and every channel or connection error with its reason.