Message Queue
The trace analysis draws a message as two calls that meet at a broker node of the
OTLP_VIRTUAL_MQ layer:
producer ──▶ broker ──▶ consumer
- A PRODUCER or CLIENT span that carries
messaging.system.nameormessaging.systemdrawsproducer → brokerwith theotlp_service_relation_client_*metrics and measures the broker withotlp_mq_access_*. The topic or queue it names inmessaging.destination.name, or the oldermessaging.destination, is an endpoint of the broker, withotlp_mq_destination_*. - A CONSUMER span that carries
messaging.system.nameormessaging.systemdrawsbroker → consumerwithotlp_service_relation_server_cpm, and counts as a request of the consumer in its service and endpoint metrics, and in its instance metrics when the span reports an instance.
Both applications need service.layer=OTLP_GENERAL on their resource for their spans to be analyzed. Nothing else is
built for a message: no edge from the producer straight to the consumer, no instance under the broker, and no broker
metric from the consumer’s side.
How the two sides meet
The producer and the consumer are both clients of the broker, so their spans are not paired as a caller’s CLIENT span
and a callee’s SERVER span are, and no network alias is learned for a broker, see Address-named
peers. The two edges meet only where both sides name the
broker with the same string. W3C trace context carries no address, unlike the SkyWalking agents’ sw8 header, so each
span names the broker from its own attributes, by the first one present:
| Span | Broker named by |
|---|---|
| producer | service.peer.name or peer.service; server.address and server.port (net.peer.name and net.peer.port); peer.address; the host and port of url.full or http.url; the messaging system |
| consumer | server.address and server.port (net.peer.name and net.peer.port); the messaging system |
The messaging system is messaging.system.name, or the older messaging.system, with aws_sqs read as aws.sqs
and aws_sns as aws.sns. A span that names the broker by none of the others lands on the node named after it,
kafka or rabbitmq, so every cluster of one system reached that way is one node.
What the agents report
As read from the source of the OpenTelemetry Java agent 2.32.0 with its default settings, and of the main branches, on 2026-10-30, of the OpenTelemetry .NET automatic instrumentation, the RabbitMQ .NET client, the OpenTelemetry Python and JavaScript contrib instrumentations and the OpenTelemetry Go automatic instrumentation.
Kafka
The Java agent, the .NET automatic instrumentation, kafkajs, the Go automatic instrumentation for segmentio/kafka-go
and kafka-python put no broker address on their Kafka spans, so both sides land on kafka. kafkajs puts the broker’s
address on its duration metric only, and kafka-python reports the bootstrap servers as messaging.url only.
confluent-kafka (Python) names the broker by the first bootstrap.servers entry on both sides, so its edges meet
when every client lists the same entry first. The topology therefore shows one kafka node shared by all the Kafka
clusters these agents reach, or, with confluent-kafka, one node per first bootstrap entry.
RabbitMQ
| Agent | Producer’s broker | Consumer’s broker |
|---|---|---|
| Java agent | rabbitmq; the connection’s address is reported as network.peer.address only, which the rule does not read |
rabbitmq |
| pika, aio-pika | the connection’s host:port |
the same; a pika consumer reading channel.consume() reports no address and lands on rabbitmq |
| amqplib | the connection’s host:port |
the same; messaging.system is the broker’s product name, and without one the spans draw no MQ edge |
| RabbitMQ .NET client | its endpoint’s host:port |
rabbitmq, since its deliver span carries no address, so the two edges do not meet |
Where both sides report an address, connect producers and consumers through the same address of the broker, such as one Kubernetes Service, so that they name the same node.
The Java agent also reports the channel calls other than a publish, queue.declare and basic.ack among them, as
CLIENT spans that carry messaging.system, and the RabbitMQ .NET client reports its connection attempts the same way.
The rule draws them as calls to the broker and counts them in its otlp_mq_access_*. With its stable messaging
conventions on, the Java agent reports an ack, nack or reject as a CLIENT span whose operation is settle instead,
which the rule drops.
Which consumer span is counted
The rule counts a CONSUMER span as the consumer’s request, except one whose operation (messaging.operation.type, or
the older messaging.operation) is receive and that is the root of its trace, and never counts a CLIENT span whose
operation is receive or settle, see Metrics. What the consumer’s response time shows depends
on the span its agent reports:
| Consumer span | Counted | Response time | Reported by |
|---|---|---|---|
process, its parent the producer’s span |
yes | the handling | Java agent; kafkajs eachMessage; RabbitMQ .NET client deliver |
process, its parent the producer’s span, ended by the acknowledgement |
yes | until the message is acknowledged or rejected, the channel closes or errors, or the instrumentation times out (consumeTimeoutMs, 1 minute by default); with noAck, until the callback returns, without what it left running asynchronously |
amqplib |
receive around the callback, its parent the producer’s span |
yes | the handling | pika and aio-pika callbacks |
receive from one fetch’s return to the next fetch, its parent the producer’s span |
yes | the handling between the two fetches | Go automatic instrumentation, segmentio/kafka-go |
receive around a blocking receive(), its parent the producer’s span |
yes | the wait for the message, included | Java agent, JMS MessageConsumer.receive() |
receive under a span the application opened |
yes | the Consume call that returned the message, its wait included |
.NET automatic instrumentation |
receive that ends before the message is handled, its parent the producer’s span |
yes | close to 0 | pika channel.consume() |
named receive but with no operation attribute, ended before the handling |
yes, even as a root | close to 0 | kafka-python |
receive that is the root of its trace |
no | .NET automatic instrumentation in a consume loop with no span around it; the Go automatic instrumentation for a message that carries no trace context; pika and aio-pika when neither the message nor the running code carries one | |
CLIENT poll |
no | kafkajs eachBatch, whose per-message process spans are counted |
confluent-kafka (Python) reports what one poll() or consume() call returned as a recv span with no attributes
and a process span under it, which stays open until the next call. Both are counted as requests, and only the
process span draws the edge from the broker.
Naming a broker cluster
A broker that clients reach at more than one address can be named as one cluster in server clusters. Only an address a span reports is renamed, so what to list follows from the agent:
| Agent | Reports for the broker | List |
|---|---|---|
| SkyWalking Java agent: Kafka | its bootstrap.servers, joined by ; in its own order |
every address, as members or aliases; a list matches in any order and as any subset |
| SkyWalking Java agent: RocketMQ | its name server list as configured | every address |
| SkyWalking Java agent: Pulsar | its service URL, pulsar://h1:6650,h2:6650 |
every address, with scheme: pulsar:// on the cluster |
| SkyWalking Java agent: NATS | its servers, nats://h1:4222,nats://h2:4222 |
every address, with scheme: nats:// on the cluster |
| SkyWalking Java agent: RabbitMQ, ActiveMQ, Artemis | the node it is connected to, as the plugin spells it | every node, as members |
| OpenTelemetry Java agent: RabbitMQ, with its stable messaging conventions on | the IP address and port it is connected to | every node’s IP address |
| pika, aio-pika, amqplib | the connection’s configured host:port |
that address, an alias when it names a Service |
| confluent-kafka (Python) | the first bootstrap.servers entry |
that entry |
| RabbitMQ .NET client | its endpoint’s host:port on publish |
that address; its deliver span reports none, so the consumer side keeps rabbitmq |
| Java agent, .NET automatic instrumentation, kafkajs, Go automatic instrumentation, kafka-python: Kafka | no address | nothing: the broker keeps the messaging system’s name, kafka |
On the native path a consumer’s broker is named by the address its producer reported, which the sw8 header
carries, so listing the producers’ addresses names both sides.
Renaming a broker cluster only for its own system
The shipped rules call findServerClusterName(address) without a kind, and the native trace analysis does not use
kinds, so a span of any messaging system is renamed to the cluster that lists its address. A TAL rule file of your own
can rename a broker only for the clients of its own system. Give the cluster, as a kind, the system its clients
report as messaging.system.name or messaging.system:
clusters:
- name: kafka-prod
kinds: [kafka]
aliases:
- kafka.prod.svc.cluster.local:9092
Then copy config/tal/otlp-general.yaml to a file of your own, such as config/tal/otlp-custom.yaml, list that file
in talFiles in place of otlp-general, see Configuration, and pass the system
at each of its three calls:
// otlp-general, the consumer block: the broker a message came through
def cluster = findServerClusterName(broker, attr("messaging.system.name", "messaging.system"))
// otlp-general, the outbound block: the database, cache or broker a client or producer calls
def cluster = findServerClusterName(peer, attr("db.system.name", "db.system", "messaging.system.name", "messaging.system"))
// otlp-slow-sql: the database a slow statement is filed under; system is the span's db.system.name or db.system
def cluster = findServerClusterName(peer, system)
Change the three together. The outbound call names databases, caches and brokers alike, so a rule that passes a kind
there and not at the consumer, or not in otlp-slow-sql, names one server two ways. Each passes the system as the span
reports it, so the two sides of a call meet at the same cluster. Kinds are compared as written. A system the conventions
renamed needs both spellings when its clients report both, such as SQS’s kinds: [aws_sqs, aws.sqs]. A cluster that
lists no kinds is still renamed for every system. The OAP reads the rule files at start, so restart it after the change.