diff --git a/instrumentation/spring/spring-jms-2.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/build.gradle.kts similarity index 96% rename from instrumentation/spring/spring-jms-2.0/javaagent/build.gradle.kts rename to instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/build.gradle.kts index ddf4deee13..2fd6753649 100644 --- a/instrumentation/spring/spring-jms-2.0/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/build.gradle.kts @@ -7,8 +7,9 @@ muzzle { pass { group.set("org.springframework") module.set("spring-jms") - versions.set("[2.0,)") + versions.set("[2.0,6)") extraDependency("javax.jms:jms-api:1.1-rev-1") + excludeInstrumentationName("jms") assertInverse.set(true) } } diff --git a/instrumentation/spring/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/SpringJmsInstrumentationModule.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsInstrumentationModule.java similarity index 63% rename from instrumentation/spring/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/SpringJmsInstrumentationModule.java rename to instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsInstrumentationModule.java index 78d2b47b7d..e05e799e82 100644 --- a/instrumentation/spring/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/SpringJmsInstrumentationModule.java +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsInstrumentationModule.java @@ -3,21 +3,30 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.spring.jms; +package io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; 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; +import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) public class SpringJmsInstrumentationModule extends InstrumentationModule { + public SpringJmsInstrumentationModule() { super("spring-jms", "spring-jms-2.0"); } + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // introduced in 2.0, removed in 6.0 + return hasClassesNamed("org.springframework.jms.remoting.JmsInvokerProxyFactoryBean"); + } + @Override public List typeInstrumentations() { return singletonList(new SpringJmsMessageListenerInstrumentation()); diff --git a/instrumentation/spring/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/SpringJmsMessageListenerInstrumentation.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsMessageListenerInstrumentation.java similarity index 96% rename from instrumentation/spring/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/SpringJmsMessageListenerInstrumentation.java rename to instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsMessageListenerInstrumentation.java index 922a200427..e30bb1d4e3 100644 --- a/instrumentation/spring/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/SpringJmsMessageListenerInstrumentation.java +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsMessageListenerInstrumentation.java @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.spring.jms; +package io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; -import static io.opentelemetry.javaagent.instrumentation.spring.jms.SpringJmsSingletons.listenerInstrumenter; +import static io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0.SpringJmsSingletons.listenerInstrumenter; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; diff --git a/instrumentation/spring/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/SpringJmsSingletons.java b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsSingletons.java similarity index 93% rename from instrumentation/spring/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/SpringJmsSingletons.java rename to instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsSingletons.java index 28ddbe3d8d..52906ff910 100644 --- a/instrumentation/spring/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/SpringJmsSingletons.java +++ b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v2_0/SpringJmsSingletons.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.spring.jms; +package io.opentelemetry.javaagent.instrumentation.spring.jms.v2_0; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; diff --git a/instrumentation/spring/spring-jms-2.0/javaagent/src/test/groovy/SpringListenerTest.groovy b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/SpringListenerTest.groovy similarity index 100% rename from instrumentation/spring/spring-jms-2.0/javaagent/src/test/groovy/SpringListenerTest.groovy rename to instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/SpringListenerTest.groovy diff --git a/instrumentation/spring/spring-jms-2.0/javaagent/src/test/groovy/SpringTemplateTest.groovy b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/SpringTemplateTest.groovy similarity index 100% rename from instrumentation/spring/spring-jms-2.0/javaagent/src/test/groovy/SpringTemplateTest.groovy rename to instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/SpringTemplateTest.groovy diff --git a/instrumentation/spring/spring-jms-2.0/javaagent/src/test/groovy/listener/AbstractConfig.groovy b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/AbstractConfig.groovy similarity index 100% rename from instrumentation/spring/spring-jms-2.0/javaagent/src/test/groovy/listener/AbstractConfig.groovy rename to instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/AbstractConfig.groovy diff --git a/instrumentation/spring/spring-jms-2.0/javaagent/src/test/groovy/listener/AnnotatedListenerConfig.groovy b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/AnnotatedListenerConfig.groovy similarity index 100% rename from instrumentation/spring/spring-jms-2.0/javaagent/src/test/groovy/listener/AnnotatedListenerConfig.groovy rename to instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/AnnotatedListenerConfig.groovy diff --git a/instrumentation/spring/spring-jms-2.0/javaagent/src/test/groovy/listener/ManualListenerConfig.groovy b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/ManualListenerConfig.groovy similarity index 100% rename from instrumentation/spring/spring-jms-2.0/javaagent/src/test/groovy/listener/ManualListenerConfig.groovy rename to instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/ManualListenerConfig.groovy diff --git a/instrumentation/spring/spring-jms-2.0/javaagent/src/test/groovy/listener/TestListener.groovy b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/TestListener.groovy similarity index 100% rename from instrumentation/spring/spring-jms-2.0/javaagent/src/test/groovy/listener/TestListener.groovy rename to instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/test/groovy/listener/TestListener.groovy diff --git a/instrumentation/spring/spring-jms-2.0/javaagent/src/testReceiveSpansDisabled/groovy/SpringListenerSuppressReceiveSpansTest.groovy b/instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/testReceiveSpansDisabled/groovy/SpringListenerSuppressReceiveSpansTest.groovy similarity index 100% rename from instrumentation/spring/spring-jms-2.0/javaagent/src/testReceiveSpansDisabled/groovy/SpringListenerSuppressReceiveSpansTest.groovy rename to instrumentation/spring/spring-jms/spring-jms-2.0/javaagent/src/testReceiveSpansDisabled/groovy/SpringListenerSuppressReceiveSpansTest.groovy diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/build.gradle.kts new file mode 100644 index 0000000000..c6eb9bdd6d --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.springframework") + module.set("spring-jms") + versions.set("[6.0.0,)") + extraDependency("jakarta.jms:jakarta.jms-api:3.0.0") + excludeInstrumentationName("jms") + assertInverse.set(true) + } +} + +dependencies { + implementation(project(":instrumentation:jms:jms-common:javaagent")) + implementation(project(":instrumentation:jms:jms-3.0:javaagent")) + + library("org.springframework:spring-jms:6.0.0") + compileOnly("jakarta.jms:jakarta.jms-api:3.0.0") + + testInstrumentation(project(":instrumentation:jms:jms-3.0:javaagent")) + + testImplementation("org.apache.activemq:artemis-jakarta-client:2.27.1") + + testLibrary("org.springframework.boot:spring-boot-starter-test:3.0.0") + testLibrary("org.springframework.boot:spring-boot-starter:3.0.0") +} + +// spring 6 requires java 17 +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} + +tasks { + test { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + + jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true") + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsInstrumentationModule.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsInstrumentationModule.java new file mode 100644 index 0000000000..0cbca1836f --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsInstrumentationModule.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class SpringJmsInstrumentationModule extends InstrumentationModule { + + public SpringJmsInstrumentationModule() { + super("spring-jms", "spring-jms-6.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // removed in 6.0 + return not(hasClassesNamed("org.springframework.jms.remoting.JmsInvokerProxyFactoryBean")); + } + + @Override + public List typeInstrumentations() { + return singletonList(new SpringJmsMessageListenerInstrumentation()); + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsMessageListenerInstrumentation.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsMessageListenerInstrumentation.java new file mode 100644 index 0000000000..fd783dbbe8 --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsMessageListenerInstrumentation.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0.SpringJmsSingletons.listenerInstrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +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.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.jms.MessageWithDestination; +import io.opentelemetry.javaagent.instrumentation.jms.v3_0.JakartaMessageAdapter; +import jakarta.jms.Message; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class SpringJmsMessageListenerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.springframework.jms.listener.SessionAwareMessageListener"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface( + named("org.springframework.jms.listener.SessionAwareMessageListener")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("onMessage") + .and(isPublic()) + .and(takesArguments(2)) + .and(takesArgument(0, named("jakarta.jms.Message"))), + SpringJmsMessageListenerInstrumentation.class.getName() + "$MessageListenerAdvice"); + } + + @SuppressWarnings("unused") + public static class MessageListenerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Message message, + @Advice.Local("otelRequest") MessageWithDestination request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context parentContext = Java8BytecodeBridge.currentContext(); + request = MessageWithDestination.create(JakartaMessageAdapter.create(message), null); + + if (!listenerInstrumenter().shouldStart(parentContext, request)) { + return; + } + + context = listenerInstrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Local("otelRequest") MessageWithDestination request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + if (scope == null) { + return; + } + scope.close(); + listenerInstrumenter().end(context, request, null, throwable); + } + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsSingletons.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsSingletons.java new file mode 100644 index 0000000000..1f40560707 --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsSingletons.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; +import io.opentelemetry.javaagent.instrumentation.jms.JmsInstrumenterFactory; +import io.opentelemetry.javaagent.instrumentation.jms.MessageWithDestination; + +public final class SpringJmsSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-jms-6.0"; + + private static final Instrumenter LISTENER_INSTRUMENTER = + new JmsInstrumenterFactory(GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME) + .setCapturedHeaders(ExperimentalConfig.get().getMessagingHeaders()) + .createConsumerProcessInstrumenter(); + + public static Instrumenter listenerInstrumenter() { + return LISTENER_INSTRUMENTER; + } + + private SpringJmsSingletons() {} +} diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AbstractConfig.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AbstractConfig.java new file mode 100644 index 0000000000..38c667a2e1 --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AbstractConfig.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0; + +import jakarta.jms.ConnectionFactory; +import java.util.concurrent.CompletableFuture; +import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.jms.config.DefaultJmsListenerContainerFactory; +import org.springframework.jms.config.JmsListenerContainerFactory; + +abstract class AbstractConfig { + + @Bean + public ConnectionFactory connectionFactory(@Value("${test.broker-url}") String artemisBrokerUrl) { + ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(artemisBrokerUrl); + connectionFactory.setUser("test"); + connectionFactory.setPassword("test"); + return connectionFactory; + } + + @Bean + public JmsListenerContainerFactory jmsListenerContainerFactory( + ConnectionFactory connectionFactory) { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory); + return factory; + } + + @Bean + public CompletableFuture receivedMessage() { + return new CompletableFuture<>(); + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AnnotatedListenerConfig.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AnnotatedListenerConfig.java new file mode 100644 index 0000000000..b6c80172e6 --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AnnotatedListenerConfig.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.jms.annotation.EnableJms; + +@ComponentScan +@EnableJms +public class AnnotatedListenerConfig extends AbstractConfig {} diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AnnotatedTestListener.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AnnotatedTestListener.java new file mode 100644 index 0000000000..c012228bcf --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/AnnotatedTestListener.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0; + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; + +import java.util.concurrent.CompletableFuture; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class AnnotatedTestListener { + + private final CompletableFuture receivedMessage; + + @Autowired + public AnnotatedTestListener(CompletableFuture receivedMessage) { + this.receivedMessage = receivedMessage; + } + + @JmsListener(destination = "spring-jms-listener") + public void receiveMessage(String message) { + runWithSpan("consumer", () -> receivedMessage.complete(message)); + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/ManualListenerConfig.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/ManualListenerConfig.java new file mode 100644 index 0000000000..e329934c62 --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/ManualListenerConfig.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0; + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; + +import jakarta.jms.TextMessage; +import java.util.concurrent.CompletableFuture; +import org.springframework.context.annotation.Bean; +import org.springframework.jms.annotation.EnableJms; +import org.springframework.jms.annotation.JmsListenerConfigurer; +import org.springframework.jms.config.JmsListenerEndpoint; +import org.springframework.jms.listener.AbstractMessageListenerContainer; +import org.springframework.jms.listener.MessageListenerContainer; +import org.springframework.jms.listener.SessionAwareMessageListener; + +@EnableJms +public class ManualListenerConfig extends AbstractConfig { + + @Bean + public JmsListenerConfigurer jmsListenerConfigurer(CompletableFuture receivedMessage) { + return registrar -> + registrar.registerEndpoint( + new JmsListenerEndpoint() { + @Override + public String getId() { + return "testid"; + } + + @Override + public void setupListenerContainer(MessageListenerContainer listenerContainer) { + AbstractMessageListenerContainer container = + (AbstractMessageListenerContainer) listenerContainer; + container.setDestinationName("spring-jms-listener"); + container.setupMessageListener( + (SessionAwareMessageListener) + (message, session) -> + runWithSpan( + "consumer", () -> receivedMessage.complete(message.getText()))); + } + }); + } +} diff --git a/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsListenerTest.java b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsListenerTest.java new file mode 100644 index 0000000000..a7c397b7b0 --- /dev/null +++ b/instrumentation/spring/spring-jms/spring-jms-6.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/jms/v6_0/SpringJmsListenerTest.java @@ -0,0 +1,261 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.jms.v6_0; + +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; +import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanKind; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import jakarta.jms.ConnectionFactory; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; +import org.assertj.core.api.AbstractStringAssert; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.jms.core.JmsTemplate; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +class SpringJmsListenerTest { + + static final Logger logger = LoggerFactory.getLogger(SpringJmsListenerTest.class); + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + static GenericContainer broker; + + @BeforeAll + static void setUp() { + broker = + new GenericContainer<>("quay.io/artemiscloud/activemq-artemis-broker:artemis.2.27.0") + .withEnv("AMQ_USER", "test") + .withEnv("AMQ_PASSWORD", "test") + .withExposedPorts(61616, 8161) + .withLogConsumer(new Slf4jLogConsumer(logger)); + broker.start(); + } + + @AfterAll + static void tearDown() { + if (broker != null) { + broker.close(); + } + } + + @ArgumentsSource(ConfigClasses.class) + @ParameterizedTest + @SuppressWarnings("unchecked") + void testSpringJmsListener(Class configClass) + throws ExecutionException, InterruptedException, TimeoutException { + // given + SpringApplication app = new SpringApplication(configClass); + app.setDefaultProperties(defaultConfig()); + ConfigurableApplicationContext applicationContext = app.run(); + cleanup.deferCleanup(applicationContext); + + JmsTemplate jmsTemplate = new JmsTemplate(applicationContext.getBean(ConnectionFactory.class)); + String message = "hello there"; + + // when + testing.runWithSpan("parent", () -> jmsTemplate.convertAndSend("spring-jms-listener", message)); + + // then + CompletableFuture receivedMessage = + applicationContext.getBean("receivedMessage", CompletableFuture.class); + assertThat(receivedMessage.get(10, TimeUnit.SECONDS)).isEqualTo(message); + + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(INTERNAL, CONSUMER), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent(), + span -> + span.hasName("spring-jms-listener send") + .hasKind(PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfying( + equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + SemanticAttributes.MESSAGING_DESTINATION, "spring-jms-listener"), + equalTo(SemanticAttributes.MESSAGING_DESTINATION_KIND, "queue"), + satisfies( + SemanticAttributes.MESSAGING_MESSAGE_ID, + AbstractStringAssert::isNotBlank)), + span -> + span.hasName("spring-jms-listener process") + .hasKind(CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfying( + equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + SemanticAttributes.MESSAGING_DESTINATION, "spring-jms-listener"), + equalTo(SemanticAttributes.MESSAGING_DESTINATION_KIND, "queue"), + equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), + satisfies( + SemanticAttributes.MESSAGING_MESSAGE_ID, + AbstractStringAssert::isNotBlank)), + span -> span.hasName("consumer").hasParent(trace.getSpan(2))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("spring-jms-listener receive") + .hasKind(CONSUMER) + .hasNoParent() + .hasAttributesSatisfying( + equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + SemanticAttributes.MESSAGING_DESTINATION, "spring-jms-listener"), + equalTo(SemanticAttributes.MESSAGING_DESTINATION_KIND, "queue"), + equalTo(SemanticAttributes.MESSAGING_OPERATION, "receive"), + satisfies( + SemanticAttributes.MESSAGING_MESSAGE_ID, + AbstractStringAssert::isNotBlank)))); + } + + @ArgumentsSource(ConfigClasses.class) + @ParameterizedTest + @SuppressWarnings("unchecked") + void shouldCaptureHeaders(Class configClass) + throws ExecutionException, InterruptedException, TimeoutException { + // given + SpringApplication app = new SpringApplication(configClass); + app.setDefaultProperties(defaultConfig()); + ConfigurableApplicationContext applicationContext = app.run(); + cleanup.deferCleanup(applicationContext); + + JmsTemplate jmsTemplate = new JmsTemplate(applicationContext.getBean(ConnectionFactory.class)); + String message = "hello there"; + + // when + testing.runWithSpan( + "parent", + () -> + jmsTemplate.convertAndSend( + "spring-jms-listener", + message, + jmsMessage -> { + jmsMessage.setStringProperty("test_message_header", "test"); + jmsMessage.setIntProperty("test_message_int_header", 1234); + return jmsMessage; + })); + + // then + CompletableFuture receivedMessage = + applicationContext.getBean("receivedMessage", CompletableFuture.class); + assertThat(receivedMessage.get(10, TimeUnit.SECONDS)).isEqualTo(message); + + testing.waitAndAssertSortedTraces( + orderByRootSpanKind(INTERNAL, CONSUMER), + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent(), + span -> + span.hasName("spring-jms-listener send") + .hasKind(PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfying( + equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + SemanticAttributes.MESSAGING_DESTINATION, "spring-jms-listener"), + equalTo(SemanticAttributes.MESSAGING_DESTINATION_KIND, "queue"), + satisfies( + SemanticAttributes.MESSAGING_MESSAGE_ID, + AbstractStringAssert::isNotBlank), + equalTo( + stringArrayKey("messaging.header.test_message_header"), + singletonList("test")), + equalTo( + stringArrayKey("messaging.header.test_message_int_header"), + singletonList("1234"))), + span -> + span.hasName("spring-jms-listener process") + .hasKind(CONSUMER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfying( + equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + SemanticAttributes.MESSAGING_DESTINATION, "spring-jms-listener"), + equalTo(SemanticAttributes.MESSAGING_DESTINATION_KIND, "queue"), + equalTo(SemanticAttributes.MESSAGING_OPERATION, "process"), + satisfies( + SemanticAttributes.MESSAGING_MESSAGE_ID, + AbstractStringAssert::isNotBlank), + equalTo( + stringArrayKey("messaging.header.test_message_header"), + singletonList("test")), + equalTo( + stringArrayKey("messaging.header.test_message_int_header"), + singletonList("1234"))), + span -> span.hasName("consumer").hasParent(trace.getSpan(2))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("spring-jms-listener receive") + .hasKind(CONSUMER) + .hasNoParent() + .hasAttributesSatisfying( + equalTo(SemanticAttributes.MESSAGING_SYSTEM, "jms"), + equalTo( + SemanticAttributes.MESSAGING_DESTINATION, "spring-jms-listener"), + equalTo(SemanticAttributes.MESSAGING_DESTINATION_KIND, "queue"), + equalTo(SemanticAttributes.MESSAGING_OPERATION, "receive"), + satisfies( + SemanticAttributes.MESSAGING_MESSAGE_ID, + AbstractStringAssert::isNotBlank), + equalTo( + stringArrayKey("messaging.header.test_message_header"), + singletonList("test")), + equalTo( + stringArrayKey("messaging.header.test_message_int_header"), + singletonList("1234"))))); + } + + private static Map defaultConfig() { + Map props = new HashMap<>(); + props.put("spring.jmx.enabled", false); + props.put("spring.main.web-application-type", "none"); + props.put("test.broker-url", "tcp://localhost:" + broker.getMappedPort(61616)); + return props; + } + + static final class ConfigClasses implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + arguments(AnnotatedListenerConfig.class), arguments(ManualListenerConfig.class)); + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e8e4b7c10b..99095c815f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -442,7 +442,8 @@ hideFromDependabot(":instrumentation:spring:spring-data:spring-data-common:testi hideFromDependabot(":instrumentation:spring:spring-integration-4.1:javaagent") hideFromDependabot(":instrumentation:spring:spring-integration-4.1:library") hideFromDependabot(":instrumentation:spring:spring-integration-4.1:testing") -hideFromDependabot(":instrumentation:spring:spring-jms-2.0:javaagent") +hideFromDependabot(":instrumentation:spring:spring-jms:spring-jms-2.0:javaagent") +hideFromDependabot(":instrumentation:spring:spring-jms:spring-jms-6.0:javaagent") hideFromDependabot(":instrumentation:spring:spring-kafka-2.7:javaagent") hideFromDependabot(":instrumentation:spring:spring-kafka-2.7:library") hideFromDependabot(":instrumentation:spring:spring-kafka-2.7:testing")