KubeMQ
ConnectorsRabbitMQ (AMQP 0-9-1)How-to guides

Routing (Direct)

Selective delivery over AMQP 0-9-1 — a direct exchange routes by exact routing-key match, unmatched keys dropped, resolved onto KubeMQ Queue channels.

Routing delivers a message selectively — only to queues whose binding key exactly matches the message's routing key. In AMQP this is a direct exchange. It sits between fanout (everyone) and topics (pattern matching). The direct exchange is virtual connector-side routing: at publish time the connector matches the routing key against the bindings and writes a copy to each matched queue's KubeMQ channel.

Overview

Declare a direct exchange and bind queues with specific keys — for example info → an info queue, error → an error queue. Publishing with a routing key delivers the message only to queues bound on that exact key. Multiple bindings on the same key all match (a key can fan out to several queues). A routing key with no matching binding is silently dropped unless you set mandatory=true, which returns a 312 NO_ROUTE.

OperationAMQP actionKubeMQ mapping
Declare exchangeexchange.declare("direct_logs", "direct")Virtual direct routing entry (no storage)
Bindqueue.bind(q, "direct_logs", "error")Maps key error → queue channel
Publishbasic.publish(exchange="direct_logs", routing-key="error")Copy written to each queue bound on error
Unmatched keyno binding on the keySilently dropped (or 312 if mandatory=true)

How it works

A publish resolves to exactly the queues bound on the message's routing key. A key bound to nobody routes to nobody and is dropped without error.

The virtual direct exchange routes each publish only to queues bound on the exact routing key; error reaches the error queue, info the info queue, and an unbound debug key is silently dropped.

Publish and route

Each example declares a direct exchange direct_logs, binds one consumer on error and another on info (each via its own server-named exclusive queue), then publishes three keys: error, info, and debug. The error and info messages reach their respective consumers; debug is bound to nobody and is silently dropped. Every client reads the broker endpoint from KUBEMQ_AMQP_URL (default amqp://guest:guest@localhost:5672/).

package main

import (
	"context"
	"log"
	"os"
	"time"

	amqp "github.com/rabbitmq/amqp091-go"
)

const exchange = "direct_logs"

func amqpURL() string {
	if v := os.Getenv("KUBEMQ_AMQP_URL"); v != "" {
		return v
	}
	return "amqp://guest:guest@localhost:5672/"
}

func bindConsumer(conn *amqp.Connection, key string) <-chan amqp.Delivery {
	ch, err := conn.Channel()
	if err != nil {
		log.Fatalf("channel for %s: %v", key, err)
	}
	q, err := ch.QueueDeclare("", false, false, true, false, nil) // server-named, exclusive
	if err != nil {
		log.Fatalf("declare %s: %v", key, err)
	}
	if err := ch.QueueBind(q.Name, key, exchange, false, nil); err != nil { // bind on the exact key
		log.Fatalf("bind %s: %v", key, err)
	}
	msgs, err := ch.Consume(q.Name, "", true, false, false, false, nil)
	if err != nil {
		log.Fatalf("consume %s: %v", key, err)
	}
	return msgs
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	conn, err := amqp.Dial(amqpURL())
	if err != nil {
		log.Fatalf("dial: %v", err)
	}
	defer func() { _ = conn.Close() }()

	ch, _ := conn.Channel()
	if err := ch.ExchangeDeclare(exchange, "direct", false, false, false, false, nil); err != nil {
		log.Fatalf("declare exchange: %v", err)
	}

	errorMsgs := bindConsumer(conn, "error")
	infoMsgs := bindConsumer(conn, "info")

	for key, body := range map[string]string{
		"error": "an error happened",
		"info":  "all is well",
		"debug": "nobody is bound to debug", // silently dropped (no binding)
	} {
		if err := ch.PublishWithContext(ctx, exchange, key, false, false, amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(body),
		}); err != nil {
			log.Fatalf("publish key=%s: %v", key, err)
		}
	}
	log.Printf(" [x] Published keys: error, info, debug")

	select {
	case d := <-errorMsgs:
		log.Printf(" [error] received %q (key=%q)", d.Body, d.RoutingKey)
	case <-ctx.Done():
		log.Fatal("error consumer timed out")
	}
	select {
	case d := <-infoMsgs:
		log.Printf(" [info]  received %q (key=%q)", d.Body, d.RoutingKey)
	case <-ctx.Done():
		log.Fatal("info consumer timed out")
	}
	log.Printf(" [✓] debug (unbound) → silently dropped (no consumer)")
}
import os

import pika

URL = os.environ.get("KUBEMQ_AMQP_URL", "amqp://guest:guest@localhost:5672/")
EXCHANGE = "direct_logs"


def main() -> None:
    conn = pika.BlockingConnection(pika.URLParameters(URL))
    ch = conn.channel()
    ch.exchange_declare(exchange=EXCHANGE, exchange_type="direct", durable=False)

    consumers = {}
    for key in ("error", "info"):
        queue = ch.queue_declare(queue="", exclusive=True).method.queue
        ch.queue_bind(exchange=EXCHANGE, queue=queue, routing_key=key)  # bind on the exact key
        consumers[key] = queue

    for key, body in {
        "error": "an error happened",
        "info": "all is well",
        "debug": "nobody is bound to debug",  # silently dropped (no binding)
    }.items():
        ch.basic_publish(exchange=EXCHANGE, routing_key=key, body=body.encode())
    print(" [x] Published keys: error, info, debug")

    for key, queue in consumers.items():
        for method, _props, body in ch.consume(queue, inactivity_timeout=10, auto_ack=True):
            if method is None:
                raise SystemExit(f"{key} consumer timed out")
            print(f" [{key}] received {body.decode()!r} (key={method.routing_key})")
            break
        ch.cancel()

    print(" [✓] debug (unbound) → silently dropped (no consumer)")
    ch.close()
    conn.close()


if __name__ == "__main__":
    main()
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public final class Main {
    private static final String EXCHANGE = "direct_logs";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri(System.getenv().getOrDefault("KUBEMQ_AMQP_URL", "amqp://guest:guest@localhost:5672/"));
        if (factory.getVirtualHost() == null || factory.getVirtualHost().isEmpty()) {
            factory.setVirtualHost("/");
        }

        try (Connection connection = factory.newConnection();
                Channel channel = connection.createChannel()) {
            channel.exchangeDeclare(EXCHANGE, "direct", false);

            Map<String, BlockingQueue<String>> received = new LinkedHashMap<>();
            for (String key : new String[] {"error", "info"}) {
                Channel ch = connection.createChannel();
                String queue = ch.queueDeclare("", false, true, true, null).getQueue();
                ch.queueBind(queue, EXCHANGE, key); // bind on the exact key
                BlockingQueue<String> sink = new ArrayBlockingQueue<>(4);
                ch.basicConsume(queue, true,
                        (tag, d) -> sink.offer(new String(d.getBody(), StandardCharsets.UTF_8)),
                        tag -> { });
                received.put(key, sink);
            }

            Map<String, String> publishes = new LinkedHashMap<>();
            publishes.put("error", "an error happened");
            publishes.put("info", "all is well");
            publishes.put("debug", "nobody is bound to debug"); // silently dropped
            for (Map.Entry<String, String> e : publishes.entrySet()) {
                channel.basicPublish(EXCHANGE, e.getKey(), null,
                        e.getValue().getBytes(StandardCharsets.UTF_8));
            }
            System.out.println("[x] Published keys: error, info, debug");

            for (Map.Entry<String, BlockingQueue<String>> e : received.entrySet()) {
                String body = e.getValue().poll(10, TimeUnit.SECONDS);
                if (body == null) throw new IllegalStateException(e.getKey() + " consumer timed out");
                System.out.println("[" + e.getKey() + "] received " + body);
            }
            System.out.println("[v] debug (unbound) → silently dropped (no consumer)");
        }
    }
}
import amqp, { type Channel } from "amqplib";

const EXCHANGE = "direct_logs";

function url(): string {
  return process.env["KUBEMQ_AMQP_URL"] ?? "amqp://guest:guest@localhost:5672/";
}

function once(ch: Channel, queue: string): Promise<string> {
  return new Promise((resolve) => {
    ch.consume(queue, (msg) => {
      if (msg) resolve(msg.content.toString());
    }, { noAck: true });
  });
}

async function main(): Promise<void> {
  const connection = await amqp.connect(url());
  const channel = await connection.createChannel();
  await channel.assertExchange(EXCHANGE, "direct", { durable: false });

  const consumers: Record<string, Promise<string>> = {};
  for (const key of ["error", "info"]) {
    const ch = await connection.createChannel();
    const q = await ch.assertQueue("", { exclusive: true });
    await ch.bindQueue(q.queue, EXCHANGE, key); // bind on the exact key
    consumers[key] = once(ch, q.queue);
  }

  const publishes: Record<string, string> = {
    error: "an error happened",
    info: "all is well",
    debug: "nobody is bound to debug", // silently dropped (no binding)
  };
  for (const [key, body] of Object.entries(publishes)) {
    channel.publish(EXCHANGE, key, Buffer.from(body));
  }
  console.log("[x] Published keys: error, info, debug");

  console.log(`[error] received ${await consumers["error"]}`);
  console.log(`[info]  received ${await consumers["info"]}`);
  console.log("[v] debug (unbound) → silently dropped (no consumer)");
  await connection.close();
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
using System.Collections.Concurrent;
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

const string exchange = "direct_logs";

static string Url() =>
    Environment.GetEnvironmentVariable("KUBEMQ_AMQP_URL") is { Length: > 0 } v
        ? v
        : "amqp://guest:guest@localhost:5672/";

var factory = new ConnectionFactory { Uri = new Uri(Url()) };
await using var connection = await factory.CreateConnectionAsync("routing-direct");
await using var channel = await connection.CreateChannelAsync();
await channel.ExchangeDeclareAsync(exchange, ExchangeType.Direct, durable: false);

var received = new Dictionary<string, BlockingCollection<string>>();
foreach (var key in new[] { "error", "info" })
{
    var ch = await connection.CreateChannelAsync();
    var queue = (await ch.QueueDeclareAsync("", durable: false, exclusive: true, autoDelete: true)).QueueName;
    await ch.QueueBindAsync(queue, exchange, key); // bind on the exact key
    var sink = new BlockingCollection<string>();
    var consumer = new AsyncEventingBasicConsumer(ch);
    consumer.ReceivedAsync += (_, ea) =>
    {
        sink.Add(Encoding.UTF8.GetString(ea.Body.Span));
        return Task.CompletedTask;
    };
    await ch.BasicConsumeAsync(queue, autoAck: true, consumer: consumer);
    received[key] = sink;
}

var publishes = new Dictionary<string, string>
{
    ["error"] = "an error happened",
    ["info"] = "all is well",
    ["debug"] = "nobody is bound to debug", // silently dropped (no binding)
};
foreach (var (key, body) in publishes)
    await channel.BasicPublishAsync(exchange, key, body: Encoding.UTF8.GetBytes(body));
Console.WriteLine("[x] Published keys: error, info, debug");

foreach (var (key, sink) in received)
    Console.WriteLine($"[{key}] received {sink.Take()}");
Console.WriteLine("[v] debug (unbound) → silently dropped (no consumer)");
# frozen_string_literal: true
require "bunny"
require "amq/uri"

EXCHANGE = "direct_logs"

opts = AMQ::URI.parse(ENV.fetch("KUBEMQ_AMQP_URL", "amqp://guest:guest@localhost:5672/"))
opts[:vhost] = "/" if opts[:vhost].nil? || opts[:vhost].to_s.empty?
conn = Bunny.new(opts)
conn.start

ch = conn.create_channel
exchange = ch.direct(EXCHANGE, durable: false)

consumers = {}
%w[error info].each do |key|
  sub_ch = conn.create_channel
  queue = sub_ch.queue("", exclusive: true)
  queue.bind(exchange, routing_key: key) # bind on the exact key
  sink = Queue.new
  queue.subscribe(manual_ack: false, block: false) { |_di, _props, body| sink.push(body) }
  consumers[key] = sink
end

{
  "error" => "an error happened",
  "info"  => "all is well",
  "debug" => "nobody is bound to debug" # silently dropped (no binding)
}.each { |key, body| exchange.publish(body, routing_key: key) }
puts " [x] Published keys: error, info, debug"

consumers.each { |key, sink| puts " [#{key}] received #{sink.pop.inspect}" }
puts " [x] debug (unbound) → silently dropped (no consumer)"

conn.close
use futures_lite::StreamExt;
use lapin::{
    options::{
        BasicConsumeOptions, BasicPublishOptions, ExchangeDeclareOptions, QueueBindOptions,
        QueueDeclareOptions,
    },
    types::FieldTable,
    BasicProperties, Connection, ConnectionProperties, ExchangeKind,
};

const EXCHANGE: &str = "direct_logs";

fn amqp_url() -> String {
    let url = std::env::var("KUBEMQ_AMQP_URL")
        .unwrap_or_else(|_| "amqp://guest:guest@localhost:5672/".into());
    match url.rsplit_once('@').map_or(url.as_str(), |(_, host)| host) {
        host if host.ends_with('/') && !host.to_lowercase().ends_with("/%2f") => format!("{url}%2f"),
        _ => url,
    }
}

async fn bind_consumer(conn: &Connection, key: &str) -> Result<lapin::Consumer, Box<dyn std::error::Error>> {
    let ch = conn.create_channel().await?;
    let queue = ch
        .queue_declare("", QueueDeclareOptions { exclusive: true, ..Default::default() }, FieldTable::default())
        .await?;
    ch.queue_bind(queue.name().as_str(), EXCHANGE, key, QueueBindOptions::default(), FieldTable::default())
        .await?; // bind on the exact key
    Ok(ch
        .basic_consume(queue.name().as_str(), "", BasicConsumeOptions { no_ack: true, ..Default::default() }, FieldTable::default())
        .await?)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let conn = Connection::connect(&amqp_url(), ConnectionProperties::default()).await?;
    let ch = conn.create_channel().await?;
    ch.exchange_declare(EXCHANGE, ExchangeKind::Direct, ExchangeDeclareOptions::default(), FieldTable::default())
        .await?;

    let mut error_c = bind_consumer(&conn, "error").await?;
    let mut info_c = bind_consumer(&conn, "info").await?;

    for (key, body) in [
        ("error", "an error happened"),
        ("info", "all is well"),
        ("debug", "nobody is bound to debug"), // silently dropped (no binding)
    ] {
        ch.basic_publish(EXCHANGE, key, BasicPublishOptions::default(), body.as_bytes(), BasicProperties::default())
            .await?;
    }
    println!("[x] Published keys: error, info, debug");

    let err = error_c.next().await.ok_or("error consumer closed")??;
    println!("[error] received {}", String::from_utf8_lossy(&err.data));
    let info = info_c.next().await.ok_or("info consumer closed")??;
    println!("[info]  received {}", String::from_utf8_lossy(&info.data));
    println!("[x] debug (unbound) → silently dropped (no consumer)");

    conn.close(0, "done").await?;
    Ok(())
}

Unmatched key — silent drop

This is the behavior to internalize: an unroutable publish without mandatory produces no error and no message. The publish succeeds at the protocol level, but the connector resolves it to an empty set of queues and writes nothing. If you need to detect unroutable publishes, set mandatory=true and handle the returned basic.return(312 NO_ROUTE).

A key can fan out to several queues. Multiple queues bound on the same key all receive a copy — direct routing is not limited to one queue per key. If you need wildcard or hierarchical keys, use a topic exchange instead of binding a long list of exact keys.

Was this page helpful?

On this page