sondahub

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.

Run the bridge
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.

Python (pika)
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))
Node (amqplib)
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 })))
Go (amqp091-go)
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

QueueWhat 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

ExchangeWhat 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 keyWhat 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.

RPC over direct reply-to (pika)
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

ArgumentWhat it does
x-message-ttlMilliseconds a message may wait in the queue; past it, it is dead-lettered as expired (or dropped).
x-expiresAccepted; the queue lives as long as the broker anyway.
x-max-lengthThe most messages the queue holds.
x-max-length-bytesThe most body bytes the queue holds.
x-overflowdrop-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-exchangeWhere rejected, expired and dropped messages go, with x-death and x-first-death-* / x-last-death-* headers.
x-dead-letter-routing-keyThe routing key they go with (else their own).
x-queue-typeclassic, or quorum (durable and not exclusive, as RabbitMQ insists); stream is refused.
x-single-active-consumerOnly 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

UserMay
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.