468 lines
14 KiB
Groovy
468 lines
14 KiB
Groovy
/*
|
|
* Copyright The OpenTelemetry Authors
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import com.rabbitmq.client.AMQP
|
|
import com.rabbitmq.client.Channel
|
|
import com.rabbitmq.client.Connection
|
|
import com.rabbitmq.client.Consumer
|
|
import com.rabbitmq.client.DefaultConsumer
|
|
import com.rabbitmq.client.Envelope
|
|
import com.rabbitmq.client.GetResponse
|
|
import com.rabbitmq.client.ShutdownSignalException
|
|
import io.opentelemetry.api.common.AttributeKey
|
|
import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
|
|
import io.opentelemetry.instrumentation.test.asserts.TraceAssert
|
|
import io.opentelemetry.sdk.trace.data.SpanData
|
|
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
|
|
import org.springframework.amqp.core.AmqpAdmin
|
|
import org.springframework.amqp.core.AmqpTemplate
|
|
import org.springframework.amqp.core.Queue
|
|
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory
|
|
import org.springframework.amqp.rabbit.core.RabbitAdmin
|
|
import org.springframework.amqp.rabbit.core.RabbitTemplate
|
|
|
|
import java.util.concurrent.CountDownLatch
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
import static io.opentelemetry.api.trace.SpanKind.CLIENT
|
|
import static io.opentelemetry.api.trace.SpanKind.CONSUMER
|
|
import static io.opentelemetry.api.trace.SpanKind.PRODUCER
|
|
import static io.opentelemetry.api.trace.StatusCode.ERROR
|
|
|
|
class RabbitMqTest extends AgentInstrumentationSpecification implements WithRabbitMqTrait {
|
|
|
|
Connection conn = connectionFactory.newConnection()
|
|
Channel channel = conn.createChannel()
|
|
|
|
def setupSpec() {
|
|
startRabbit()
|
|
connectionFactory.setAutomaticRecoveryEnabled(false)
|
|
}
|
|
|
|
def cleanupSpec() {
|
|
stopRabbit()
|
|
}
|
|
|
|
def cleanup() {
|
|
try {
|
|
channel.close()
|
|
conn.close()
|
|
} catch (ShutdownSignalException ignored) {
|
|
}
|
|
}
|
|
|
|
def "test rabbit publish/get"() {
|
|
setup:
|
|
String queueName = runWithSpan("producer parent") {
|
|
channel.exchangeDeclare(exchangeName, "direct", false)
|
|
String queueName = channel.queueDeclare().getQueue()
|
|
channel.queueBind(queueName, exchangeName, routingKey)
|
|
channel.basicPublish(exchangeName, routingKey, null, "Hello, world!".getBytes())
|
|
return queueName
|
|
}
|
|
GetResponse response = runWithSpan("consumer parent") {
|
|
return channel.basicGet(queueName, true)
|
|
}
|
|
|
|
expect:
|
|
new String(response.getBody()) == "Hello, world!"
|
|
|
|
and:
|
|
assertTraces(2) {
|
|
SpanData producerSpan
|
|
|
|
trace(0, 5) {
|
|
span(0) {
|
|
name "producer parent"
|
|
hasNoParent()
|
|
}
|
|
rabbitSpan(it, 1, null, null, null, "exchange.declare", span(0))
|
|
rabbitSpan(it, 2, null, null, null, "queue.declare", span(0))
|
|
rabbitSpan(it, 3, null, null, null, "queue.bind", span(0))
|
|
rabbitSpan(it, 4, exchangeName, routingKey, "send", "$exchangeName", span(0))
|
|
|
|
producerSpan = span(4)
|
|
}
|
|
|
|
trace(1, 2) {
|
|
span(0) {
|
|
name "consumer parent"
|
|
hasNoParent()
|
|
}
|
|
rabbitSpan(it, 1, exchangeName, routingKey, "receive", "<generated>", span(0), producerSpan)
|
|
}
|
|
}
|
|
|
|
where:
|
|
exchangeName | routingKey
|
|
"some-exchange" | "some-routing-key"
|
|
}
|
|
|
|
def "test rabbit publish/get default exchange"() {
|
|
setup:
|
|
String queueName = runWithSpan("producer parent") {
|
|
String queueName = channel.queueDeclare().getQueue()
|
|
channel.basicPublish("", queueName, null, "Hello, world!".getBytes())
|
|
return queueName
|
|
}
|
|
GetResponse response = runWithSpan("consumer parent") {
|
|
return channel.basicGet(queueName, true)
|
|
}
|
|
|
|
expect:
|
|
new String(response.getBody()) == "Hello, world!"
|
|
|
|
and:
|
|
assertTraces(2) {
|
|
SpanData producerSpan
|
|
|
|
trace(0, 3) {
|
|
span(0) {
|
|
name "producer parent"
|
|
hasNoParent()
|
|
}
|
|
rabbitSpan(it, 1, null, null, null, "queue.declare", span(0))
|
|
rabbitSpan(it, 2, "<default>", null, "send", "<default>", span(0))
|
|
|
|
producerSpan = span(2)
|
|
}
|
|
|
|
trace(1, 2) {
|
|
span(0) {
|
|
name "consumer parent"
|
|
hasNoParent()
|
|
}
|
|
rabbitSpan(it, 1, "<default>", null, "receive", "<generated>", span(0), producerSpan)
|
|
}
|
|
}
|
|
}
|
|
|
|
def "test rabbit consume #messageCount messages and setTimestamp=#setTimestamp"() {
|
|
setup:
|
|
channel.exchangeDeclare(exchangeName, "direct", false)
|
|
String queueName = (messageCount % 2 == 0) ?
|
|
channel.queueDeclare().getQueue() :
|
|
channel.queueDeclare("some-queue", false, true, true, null).getQueue()
|
|
channel.queueBind(queueName, exchangeName, "")
|
|
|
|
def deliveries = []
|
|
|
|
Consumer callback = new DefaultConsumer(channel) {
|
|
@Override
|
|
void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
|
|
deliveries << new String(body)
|
|
}
|
|
}
|
|
|
|
channel.basicConsume(queueName, callback)
|
|
|
|
(1..messageCount).each {
|
|
if (setTimestamp) {
|
|
channel.basicPublish(exchangeName, "",
|
|
new AMQP.BasicProperties.Builder().timestamp(new Date()).build(),
|
|
"msg $it".getBytes())
|
|
} else {
|
|
channel.basicPublish(exchangeName, "", null, "msg $it".getBytes())
|
|
}
|
|
}
|
|
def resource = messageCount % 2 == 0 ? "<generated>" : queueName
|
|
|
|
expect:
|
|
assertTraces(4 + messageCount) {
|
|
trace(0, 1) {
|
|
rabbitSpan(it, 0, null, null, null, "exchange.declare")
|
|
}
|
|
trace(1, 1) {
|
|
rabbitSpan(it, 0, null, null, null, "queue.declare")
|
|
}
|
|
trace(2, 1) {
|
|
rabbitSpan(it, 0, null, null, null, "queue.bind")
|
|
}
|
|
trace(3, 1) {
|
|
rabbitSpan(it, 0, null, null, null, "basic.consume")
|
|
}
|
|
(1..messageCount).each {
|
|
trace(3 + it, 2) {
|
|
rabbitSpan(it, 0, exchangeName, null, "send", "$exchangeName")
|
|
rabbitSpan(it, 1, exchangeName, null, "process", resource, span(0), null, null, null, setTimestamp)
|
|
}
|
|
}
|
|
}
|
|
|
|
deliveries == (1..messageCount).collect { "msg $it" }
|
|
|
|
where:
|
|
exchangeName | messageCount | setTimestamp
|
|
"some-exchange" | 1 | false
|
|
"some-exchange" | 2 | false
|
|
"some-exchange" | 3 | false
|
|
"some-exchange" | 4 | false
|
|
"some-exchange" | 1 | true
|
|
"some-exchange" | 2 | true
|
|
"some-exchange" | 3 | true
|
|
"some-exchange" | 4 | true
|
|
}
|
|
|
|
def "test rabbit consume error"() {
|
|
setup:
|
|
def error = new FileNotFoundException("Message Error")
|
|
channel.exchangeDeclare(exchangeName, "direct", false)
|
|
String queueName = channel.queueDeclare().getQueue()
|
|
channel.queueBind(queueName, exchangeName, "")
|
|
|
|
Consumer callback = new DefaultConsumer(channel) {
|
|
@Override
|
|
void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
|
|
throw error
|
|
// Unfortunately this doesn't seem to be observable in the test outside of the span generated.
|
|
}
|
|
}
|
|
|
|
channel.basicConsume(queueName, callback)
|
|
|
|
channel.basicPublish(exchangeName, "", null, "msg".getBytes())
|
|
|
|
expect:
|
|
assertTraces(5) {
|
|
trace(0, 1) {
|
|
rabbitSpan(it, 0, null, null, null, "exchange.declare")
|
|
}
|
|
trace(1, 1) {
|
|
rabbitSpan(it, 0, null, null, null, "queue.declare")
|
|
}
|
|
trace(2, 1) {
|
|
rabbitSpan(it, 0, null, null, null, "queue.bind")
|
|
}
|
|
trace(3, 1) {
|
|
rabbitSpan(it, 0, null, null, null, "basic.consume")
|
|
}
|
|
trace(4, 2) {
|
|
rabbitSpan(it, 0, exchangeName, null, "send", "$exchangeName")
|
|
rabbitSpan(it, 1, exchangeName, null, "process", "<generated>", span(0), null, error, error.message)
|
|
}
|
|
}
|
|
|
|
where:
|
|
exchangeName = "some-error-exchange"
|
|
}
|
|
|
|
def "test rabbit error (#command)"() {
|
|
when:
|
|
closure.call(channel)
|
|
|
|
then:
|
|
def error = thrown(exception)
|
|
|
|
and:
|
|
|
|
assertTraces(1) {
|
|
trace(0, 1) {
|
|
rabbitSpan(it, 0, null, null, operation, command, null, null, error, errorMsg)
|
|
}
|
|
}
|
|
|
|
where:
|
|
command | exception | errorMsg | operation | closure
|
|
"exchange.declare" | IOException | null | null | {
|
|
it.exchangeDeclare("some-exchange", "invalid-type", true)
|
|
}
|
|
"Channel.basicConsume" | IllegalStateException | "Invalid configuration: 'queue' must be non-null." | null | {
|
|
it.basicConsume(null, null)
|
|
}
|
|
"<generated>" | IOException | null | "receive" | {
|
|
it.basicGet("amq.gen-invalid-channel", true)
|
|
}
|
|
}
|
|
|
|
def "test spring rabbit"() {
|
|
setup:
|
|
def cachingConnectionFactory = new CachingConnectionFactory(connectionFactory)
|
|
AmqpAdmin admin = new RabbitAdmin(cachingConnectionFactory)
|
|
AmqpTemplate template = new RabbitTemplate(cachingConnectionFactory)
|
|
|
|
def queue = new Queue("some-routing-queue", false, true, true, null)
|
|
runWithSpan("producer parent") {
|
|
admin.declareQueue(queue)
|
|
template.convertAndSend(queue.name, "foo")
|
|
}
|
|
String message = runWithSpan("consumer parent") {
|
|
return template.receiveAndConvert(queue.name) as String
|
|
}
|
|
|
|
expect:
|
|
message == "foo"
|
|
|
|
and:
|
|
assertTraces(2) {
|
|
SpanData producerSpan
|
|
|
|
trace(0, 3) {
|
|
span(0) {
|
|
name "producer parent"
|
|
hasNoParent()
|
|
}
|
|
rabbitSpan(it, 1, null, null, null, "queue.declare", span(0))
|
|
rabbitSpan(it, 2, "<default>", "some-routing-queue", "send", "<default>", span(0))
|
|
|
|
producerSpan = span(2)
|
|
}
|
|
|
|
trace(1, 2) {
|
|
span(0) {
|
|
name "consumer parent"
|
|
hasNoParent()
|
|
}
|
|
rabbitSpan(it, 1, "<default>", "some-routing-queue", "receive", queue.name, span(0), producerSpan)
|
|
}
|
|
}
|
|
|
|
cleanup:
|
|
cachingConnectionFactory.destroy()
|
|
}
|
|
|
|
def "capture message header as span attributes"() {
|
|
setup:
|
|
String queueName = channel.queueDeclare().getQueue()
|
|
def properties = new AMQP.BasicProperties.Builder().headers(["test-message-header": "test"]).build()
|
|
channel.basicPublish("", queueName, properties, "Hello, world!".getBytes())
|
|
|
|
def latch = new CountDownLatch(1)
|
|
def deliveries = []
|
|
|
|
Consumer callback = new DefaultConsumer(channel) {
|
|
@Override
|
|
void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties props, byte[] body) throws IOException {
|
|
deliveries << new String(body)
|
|
latch.countDown()
|
|
}
|
|
}
|
|
|
|
channel.basicConsume(queueName, callback)
|
|
latch.await(10, TimeUnit.SECONDS)
|
|
expect:
|
|
deliveries[0] == "Hello, world!"
|
|
|
|
and:
|
|
assertTraces(3) {
|
|
traces.subList(1, 3).sort(orderByRootSpanKind(PRODUCER, CLIENT))
|
|
trace(0, 1) {
|
|
rabbitSpan(it, 0, null, null, null, "queue.declare")
|
|
}
|
|
trace(1, 2) {
|
|
rabbitSpan(it, 0, "<default>", null, "send", "<default>", null, null, null, null, false, true)
|
|
rabbitSpan(it, 1, "<default>", null, "process", "<generated>", span(0), null, null, null, false, true)
|
|
}
|
|
trace(2, 1) {
|
|
rabbitSpan(it, 0, null, null, null, "basic.consume")
|
|
}
|
|
}
|
|
}
|
|
|
|
def rabbitSpan(
|
|
TraceAssert trace,
|
|
int index,
|
|
String exchange,
|
|
String routingKey,
|
|
String operation,
|
|
String resource,
|
|
SpanData parentSpan = null,
|
|
SpanData linkSpan = null,
|
|
Throwable exception = null,
|
|
String errorMsg = null,
|
|
boolean expectTimestamp = false,
|
|
boolean testHeaders = false
|
|
) {
|
|
|
|
def spanName = resource
|
|
if (operation != null) {
|
|
spanName = spanName + " " + operation
|
|
}
|
|
|
|
def rabbitCommand = trace.span(index).attributes.get(AttributeKey.stringKey("rabbitmq.command"))
|
|
|
|
def spanKind
|
|
switch (rabbitCommand) {
|
|
case "basic.publish":
|
|
spanKind = PRODUCER
|
|
break
|
|
case "basic.get": // fallthrough
|
|
case "basic.deliver":
|
|
spanKind = CONSUMER
|
|
break
|
|
default:
|
|
spanKind = CLIENT
|
|
}
|
|
|
|
trace.span(index) {
|
|
name spanName
|
|
kind spanKind
|
|
|
|
if (parentSpan == null) {
|
|
hasNoParent()
|
|
} else {
|
|
childOf(parentSpan)
|
|
}
|
|
|
|
if (linkSpan == null) {
|
|
hasNoLinks()
|
|
} else {
|
|
hasLink(linkSpan)
|
|
}
|
|
|
|
if (exception) {
|
|
status ERROR
|
|
errorEvent(exception.class, errorMsg)
|
|
}
|
|
|
|
attributes {
|
|
// listener does not have access to net attributes
|
|
if (rabbitCommand != "basic.deliver") {
|
|
"$SemanticAttributes.NET_SOCK_PEER_ADDR" { it == "127.0.0.1" || it == "0:0:0:0:0:0:0:1" || it == null }
|
|
"$SemanticAttributes.NET_SOCK_PEER_PORT" Long
|
|
"$SemanticAttributes.NET_SOCK_FAMILY" { it == SemanticAttributes.NetSockFamilyValues.INET6 || it == null }
|
|
}
|
|
|
|
"$SemanticAttributes.MESSAGING_SYSTEM" "rabbitmq"
|
|
"$SemanticAttributes.MESSAGING_DESTINATION_NAME" exchange
|
|
"$SemanticAttributes.MESSAGING_DESTINATION_KIND" "queue"
|
|
|
|
"$SemanticAttributes.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY" { it == null || it == routingKey || it.startsWith("amq.gen-") }
|
|
if (operation != null && operation != "send") {
|
|
"$SemanticAttributes.MESSAGING_OPERATION" operation
|
|
}
|
|
if (expectTimestamp) {
|
|
"rabbitmq.record.queue_time_ms" { it instanceof Long && it >= 0 }
|
|
}
|
|
if (testHeaders) {
|
|
"messaging.header.test_message_header" { it == ["test"] }
|
|
}
|
|
|
|
switch (rabbitCommand) {
|
|
case "basic.publish":
|
|
"rabbitmq.command" "basic.publish"
|
|
"$SemanticAttributes.MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY" {
|
|
it == null || it == "some-routing-key" || it == "some-routing-queue" || it.startsWith("amq.gen-")
|
|
}
|
|
"rabbitmq.delivery_mode" { it == null || it == 2 }
|
|
"$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long
|
|
break
|
|
case "basic.get":
|
|
"rabbitmq.command" "basic.get"
|
|
//TODO why this queue name is not a destination for semantic convention
|
|
"rabbitmq.queue" { it == "some-queue" || it == "some-routing-queue" || it.startsWith("amq.gen-") }
|
|
"$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" { it == null || it instanceof Long }
|
|
break
|
|
case "basic.deliver":
|
|
"rabbitmq.command" "basic.deliver"
|
|
"$SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES" Long
|
|
break
|
|
default:
|
|
"rabbitmq.command" { it == null || it == resource }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|