Make @RabbitListener propagate context properly (#3339)
This commit is contained in:
parent
97254f04d1
commit
108b1298cf
|
@ -0,0 +1,20 @@
|
|||
apply from: "$rootDir/gradle/instrumentation.gradle"
|
||||
|
||||
muzzle {
|
||||
pass {
|
||||
group = 'org.springframework.amqp'
|
||||
module = 'spring-rabbit'
|
||||
versions = "(,)"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
library 'org.springframework.amqp:spring-rabbit:1.0.0.RELEASE'
|
||||
|
||||
testInstrumentation project(':instrumentation:rabbitmq-2.7:javaagent')
|
||||
|
||||
// 2.1.7 adds the @RabbitListener annotation, we need that for tests
|
||||
testLibrary 'org.springframework.amqp:spring-rabbit:2.1.7.RELEASE'
|
||||
testLibrary "org.springframework.boot:spring-boot-starter-test:1.5.22.RELEASE"
|
||||
testLibrary "org.springframework.boot:spring-boot-starter:1.5.22.RELEASE"
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.spring.rabbit;
|
||||
|
||||
import static io.opentelemetry.javaagent.instrumentation.spring.rabbit.SpringRabbitSingletons.instrumenter;
|
||||
import static net.bytebuddy.matcher.ElementMatchers.named;
|
||||
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
|
||||
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
|
||||
|
||||
import io.opentelemetry.context.Context;
|
||||
import io.opentelemetry.context.Scope;
|
||||
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
|
||||
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
|
||||
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.description.type.TypeDescription;
|
||||
import net.bytebuddy.matcher.ElementMatcher;
|
||||
import org.springframework.amqp.core.Message;
|
||||
|
||||
public class AbstractMessageListenerContainerInstrumentation implements TypeInstrumentation {
|
||||
@Override
|
||||
public ElementMatcher<TypeDescription> typeMatcher() {
|
||||
return named("org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transform(TypeTransformer transformer) {
|
||||
transformer.applyAdviceToMethod(
|
||||
named("invokeListener")
|
||||
.and(
|
||||
takesArguments(2)
|
||||
.and(
|
||||
takesArgument(1, Object.class)
|
||||
.or(takesArgument(1, named("org.springframework.amqp.core.Message"))))),
|
||||
getClass().getName() + "$InvokeListenerAdvice");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static class InvokeListenerAdvice {
|
||||
@Advice.OnMethodEnter(suppress = Throwable.class)
|
||||
public static void onEnter(
|
||||
@Advice.Argument(1) Object data,
|
||||
@Advice.Local("otelContext") Context context,
|
||||
@Advice.Local("otelScope") Scope scope) {
|
||||
if (!(data instanceof Message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Context parentContext = Java8BytecodeBridge.currentContext();
|
||||
Message message = (Message) data;
|
||||
if (instrumenter().shouldStart(parentContext, message)) {
|
||||
context = instrumenter().start(parentContext, message);
|
||||
scope = context.makeCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
|
||||
public static void onEnter(
|
||||
@Advice.Argument(1) Object data,
|
||||
@Advice.Local("otelContext") Context context,
|
||||
@Advice.Local("otelScope") Scope scope,
|
||||
@Advice.Thrown Throwable throwable) {
|
||||
if (scope == null || !(data instanceof Message)) {
|
||||
return;
|
||||
}
|
||||
scope.close();
|
||||
instrumenter().end(context, (Message) data, null, throwable);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.spring.rabbit;
|
||||
|
||||
import io.opentelemetry.context.propagation.TextMapGetter;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.springframework.amqp.core.Message;
|
||||
|
||||
final class MessageHeaderGetter implements TextMapGetter<Message> {
|
||||
@Override
|
||||
public Iterable<String> keys(Message carrier) {
|
||||
return carrier.getMessageProperties().getHeaders().keySet();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String get(Message carrier, String key) {
|
||||
Object value = carrier.getMessageProperties().getHeaders().get(key);
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.spring.rabbit;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
|
||||
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
|
||||
import java.util.List;
|
||||
|
||||
@AutoService(InstrumentationModule.class)
|
||||
public class SpringRabbitInstrumentationModule extends InstrumentationModule {
|
||||
public SpringRabbitInstrumentationModule() {
|
||||
super("spring-rabbit", "spring-rabbit-1.0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TypeInstrumentation> typeInstrumentations() {
|
||||
return singletonList(new AbstractMessageListenerContainerInstrumentation());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.spring.rabbit;
|
||||
|
||||
import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessageOperation;
|
||||
import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingAttributesExtractor;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.springframework.amqp.core.Message;
|
||||
|
||||
final class SpringRabbitMessageAttributesExtractor
|
||||
extends MessagingAttributesExtractor<Message, Void> {
|
||||
@Override
|
||||
protected String system(Message message) {
|
||||
return "rabbitmq";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String destinationKind(Message message) {
|
||||
return "queue";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable String destination(Message message) {
|
||||
return message.getMessageProperties().getReceivedRoutingKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean temporaryDestination(Message message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable String protocol(Message message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable String protocolVersion(Message message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable String url(Message message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable String conversationId(Message message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Long messagePayloadSize(Message message) {
|
||||
return message.getMessageProperties().getContentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable Long messagePayloadCompressedSize(Message message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageOperation operation(Message message) {
|
||||
return MessageOperation.PROCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable String messageId(Message message, @Nullable Void unused) {
|
||||
return message.getMessageProperties().getMessageId();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.spring.rabbit;
|
||||
|
||||
import io.opentelemetry.api.GlobalOpenTelemetry;
|
||||
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
|
||||
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
|
||||
import io.opentelemetry.instrumentation.api.instrumenter.messaging.MessagingSpanNameExtractor;
|
||||
import org.springframework.amqp.core.Message;
|
||||
|
||||
public final class SpringRabbitSingletons {
|
||||
|
||||
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.spring-rabbit-1.0";
|
||||
|
||||
private static final Instrumenter<Message, Void> INSTRUMENTER;
|
||||
|
||||
static {
|
||||
SpringRabbitMessageAttributesExtractor attributesExtractor =
|
||||
new SpringRabbitMessageAttributesExtractor();
|
||||
SpanNameExtractor<Message> spanNameExtractor =
|
||||
MessagingSpanNameExtractor.create(attributesExtractor);
|
||||
|
||||
INSTRUMENTER =
|
||||
Instrumenter.<Message, Void>newBuilder(
|
||||
GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanNameExtractor)
|
||||
.addAttributesExtractor(attributesExtractor)
|
||||
.newConsumerInstrumenter(new MessageHeaderGetter());
|
||||
}
|
||||
|
||||
public static Instrumenter<Message, Void> instrumenter() {
|
||||
return INSTRUMENTER;
|
||||
}
|
||||
|
||||
private SpringRabbitSingletons() {}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
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.instrumentation.test.utils.TraceUtils.runInternalSpan
|
||||
import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace
|
||||
|
||||
import com.rabbitmq.client.ConnectionFactory
|
||||
import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
|
||||
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
|
||||
import java.time.Duration
|
||||
import org.springframework.amqp.core.AmqpTemplate
|
||||
import org.springframework.amqp.core.Queue
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitListener
|
||||
import org.springframework.boot.SpringApplication
|
||||
import org.springframework.boot.SpringBootConfiguration
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.context.ConfigurableApplicationContext
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.testcontainers.containers.GenericContainer
|
||||
import spock.lang.Shared
|
||||
|
||||
class ContextPropagationTest extends AgentInstrumentationSpecification {
|
||||
|
||||
@Shared
|
||||
GenericContainer rabbitMqContainer
|
||||
@Shared
|
||||
ConfigurableApplicationContext applicationContext
|
||||
@Shared
|
||||
ConnectionFactory connectionFactory
|
||||
|
||||
def setupSpec() {
|
||||
rabbitMqContainer = new GenericContainer('rabbitmq:latest')
|
||||
.withExposedPorts(5672)
|
||||
.withStartupTimeout(Duration.ofSeconds(120))
|
||||
rabbitMqContainer.start()
|
||||
|
||||
def app = new SpringApplication(ConsumerConfig)
|
||||
app.setDefaultProperties([
|
||||
"spring.jmx.enabled" : false,
|
||||
"spring.main.web-application-type": "none",
|
||||
"spring.rabbitmq.host" : rabbitMqContainer.containerIpAddress,
|
||||
"spring.rabbitmq.port" : rabbitMqContainer.getMappedPort(5672),
|
||||
])
|
||||
applicationContext = app.run()
|
||||
|
||||
connectionFactory = new ConnectionFactory(
|
||||
host: rabbitMqContainer.containerIpAddress,
|
||||
port: rabbitMqContainer.getMappedPort(5672)
|
||||
)
|
||||
}
|
||||
|
||||
def cleanupSpec() {
|
||||
rabbitMqContainer?.stop()
|
||||
applicationContext?.close()
|
||||
}
|
||||
|
||||
def "should propagate context to consumer"() {
|
||||
given:
|
||||
def connection = connectionFactory.newConnection()
|
||||
def channel = connection.createChannel()
|
||||
|
||||
when:
|
||||
runUnderTrace("parent") {
|
||||
applicationContext.getBean(AmqpTemplate)
|
||||
.convertAndSend(ConsumerConfig.TEST_QUEUE, "test")
|
||||
}
|
||||
|
||||
then:
|
||||
assertTraces(2) {
|
||||
trace(0, 5) {
|
||||
span(0) {
|
||||
name "parent"
|
||||
}
|
||||
span(1) {
|
||||
// created by rabbitmq instrumentation
|
||||
name "<default> -> testQueue send"
|
||||
kind PRODUCER
|
||||
childOf span(0)
|
||||
attributes {
|
||||
"${SemanticAttributes.NET_PEER_NAME.key}" "localhost"
|
||||
"${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
|
||||
"${SemanticAttributes.NET_PEER_PORT.key}" Long
|
||||
"${SemanticAttributes.MESSAGING_SYSTEM.key}" "rabbitmq"
|
||||
"${SemanticAttributes.MESSAGING_DESTINATION.key}" "<default>"
|
||||
"${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "queue"
|
||||
"${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long
|
||||
}
|
||||
}
|
||||
span(2) {
|
||||
// created by rabbitmq instrumentation
|
||||
name "testQueue process"
|
||||
kind CONSUMER
|
||||
childOf span(1)
|
||||
attributes {
|
||||
"${SemanticAttributes.MESSAGING_SYSTEM.key}" "rabbitmq"
|
||||
"${SemanticAttributes.MESSAGING_DESTINATION.key}" "<default>"
|
||||
"${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "queue"
|
||||
"${SemanticAttributes.MESSAGING_OPERATION.key}" "process"
|
||||
"${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long
|
||||
}
|
||||
}
|
||||
span(3) {
|
||||
// created by spring-amqp instrumentation
|
||||
name "testQueue process"
|
||||
kind CONSUMER
|
||||
childOf span(1)
|
||||
attributes {
|
||||
"${SemanticAttributes.MESSAGING_SYSTEM.key}" "rabbitmq"
|
||||
"${SemanticAttributes.MESSAGING_DESTINATION.key}" "testQueue"
|
||||
"${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "queue"
|
||||
"${SemanticAttributes.MESSAGING_OPERATION.key}" "process"
|
||||
"${SemanticAttributes.MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES.key}" Long
|
||||
}
|
||||
}
|
||||
span(4) {
|
||||
name "consumer"
|
||||
childOf span(3)
|
||||
}
|
||||
}
|
||||
trace(1, 1) {
|
||||
span(0) {
|
||||
// created by rabbitmq instrumentation
|
||||
name "basic.ack"
|
||||
kind CLIENT
|
||||
attributes {
|
||||
"${SemanticAttributes.NET_PEER_NAME.key}" "localhost"
|
||||
"${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
|
||||
"${SemanticAttributes.NET_PEER_PORT.key}" Long
|
||||
"${SemanticAttributes.MESSAGING_SYSTEM.key}" "rabbitmq"
|
||||
"${SemanticAttributes.MESSAGING_DESTINATION_KIND.key}" "queue"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
channel?.close()
|
||||
connection?.close()
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
static class ConsumerConfig {
|
||||
|
||||
static final String TEST_QUEUE = "testQueue"
|
||||
|
||||
@Bean
|
||||
Queue testQueue() {
|
||||
new Queue(TEST_QUEUE)
|
||||
}
|
||||
|
||||
@RabbitListener(queues = TEST_QUEUE)
|
||||
void consume(String ignored) {
|
||||
runInternalSpan("consumer")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -282,6 +282,7 @@ include ':instrumentation:spring:spring-data-1.8:javaagent'
|
|||
include ':instrumentation:spring:spring-integration-4.1:javaagent'
|
||||
include ':instrumentation:spring:spring-integration-4.1:library'
|
||||
include ':instrumentation:spring:spring-integration-4.1:testing'
|
||||
include ':instrumentation:spring:spring-rabbit-1.0:javaagent'
|
||||
include ':instrumentation:spring:spring-scheduling-3.1:javaagent'
|
||||
include ':instrumentation:spring:spring-web-3.1:library'
|
||||
include ':instrumentation:spring:spring-webmvc-3.1:javaagent'
|
||||
|
|
Loading…
Reference in New Issue