diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/build.gradle.kts b/instrumentation/spring/spring-rmi-4.0/javaagent/build.gradle.kts index 479d420d9c6d..d6e191b1a2b2 100644 --- a/instrumentation/spring/spring-rmi-4.0/javaagent/build.gradle.kts +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/build.gradle.kts @@ -20,6 +20,9 @@ dependencies { library("org.springframework:spring-context:4.0.0.RELEASE") library("org.springframework:spring-aop:4.0.0.RELEASE") testLibrary("org.springframework.boot:spring-boot:1.1.0.RELEASE") + testImplementation("org.apache.tomee:openejb-core:8.0.16") + testImplementation("org.apache.tomee:openejb-ejbd:8.0.16") + testImplementation("org.apache.tomee:openejb-client:8.0.16") // rmi remoting was removed in spring 6 latestDepTestLibrary("org.springframework:spring-context:5.+") // documented limitation diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiIgnoredTypesConfigurer.java b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiIgnoredTypesConfigurer.java new file mode 100644 index 000000000000..c12ab641a0a4 --- /dev/null +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiIgnoredTypesConfigurer.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.rmi.v4_0; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; + +@AutoService(IgnoredTypesConfigurer.class) +public class SpringRmiIgnoredTypesConfigurer implements IgnoredTypesConfigurer { + + @Override + public void configure(IgnoredTypesBuilder builder, ConfigProperties config) { + // The Spring EJB classes are ignored in the AdditionalLibraryIgnoredTypesConfigurer, but + // are required when utilizing Spring's local-slsb and remote-slsb to access EJBs through RMI. + builder.allowClass("org.springframework.ejb.access."); + } +} diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/client/ClientInstrumentation.java b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/client/ClientInstrumentation.java index 622e453f5ebf..b04daf155bd3 100644 --- a/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/client/ClientInstrumentation.java +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/client/ClientInstrumentation.java @@ -5,6 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.spring.rmi.v4_0.client; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; import static io.opentelemetry.javaagent.instrumentation.spring.rmi.v4_0.SpringRmiSingletons.clientInstrumenter; import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -25,7 +26,8 @@ public class ClientInstrumentation implements TypeInstrumentation { @Override public ElementMatcher typeMatcher() { - return named("org.springframework.remoting.rmi.RmiClientInterceptor"); + return named("org.springframework.remoting.rmi.RmiClientInterceptor") + .or(extendsClass(named("org.springframework.ejb.access.AbstractSlsbInvokerInterceptor"))); } @Override diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/groovy/SpringRmiTest.groovy b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/groovy/SpringRmiTest.groovy deleted file mode 100644 index 50d5e99cb73a..000000000000 --- a/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/groovy/SpringRmiTest.groovy +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.api.trace.SpanKind -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.instrumentation.test.utils.PortUtils -import io.opentelemetry.semconv.ExceptionAttributes -import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes -import org.springframework.boot.SpringApplication -import org.springframework.context.ConfigurableApplicationContext -import org.springframework.context.annotation.Bean -import org.springframework.remoting.rmi.RmiProxyFactoryBean -import org.springframework.remoting.rmi.RmiServiceExporter -import org.springframework.remoting.support.RemoteExporter -import spock.lang.Shared -import springrmi.app.SpringRmiGreeter -import springrmi.app.SpringRmiGreeterImpl - -import static io.opentelemetry.api.trace.StatusCode.ERROR - -class SpringRmiTest extends AgentInstrumentationSpecification { - - @Shared - ConfigurableApplicationContext serverAppContext - - @Shared - ConfigurableApplicationContext clientAppContext - - @Shared - static int registryPort - - static class ServerConfig { - @Bean - static RemoteExporter registerRMIExporter() { - RmiServiceExporter exporter = new RmiServiceExporter() - exporter.setServiceName("springRmiGreeter") - exporter.setServiceInterface(SpringRmiGreeter) - exporter.setService(new SpringRmiGreeterImpl()) - exporter.setRegistryPort(registryPort) - return exporter - } - } - - static class ClientConfig { - @Bean - static RmiProxyFactoryBean rmiProxy() { - RmiProxyFactoryBean bean = new RmiProxyFactoryBean() - bean.setServiceInterface(SpringRmiGreeter) - bean.setServiceUrl("rmi://localhost:" + registryPort + "/springRmiGreeter") - return bean - } - } - - def setupSpec() { - registryPort = PortUtils.findOpenPort() - - def serverApp = new SpringApplication(ServerConfig) - serverApp.setDefaultProperties([ - "spring.jmx.enabled" : false, - "spring.main.web-application-type": "none", - ]) - serverAppContext = serverApp.run() - - def clientApp = new SpringApplication(ClientConfig) - clientApp.setDefaultProperties([ - "spring.jmx.enabled" : false, - "spring.main.web-application-type": "none", - ]) - clientAppContext = clientApp.run() - } - - def cleanupSpec() { - serverAppContext.close() - clientAppContext.close() - } - - def "Client call creates spans"() { - given: - SpringRmiGreeter client = clientAppContext.getBean(SpringRmiGreeter) - when: - def response = runWithSpan("parent") { client.hello("Test Name") } - then: - response == "Hello Test Name" - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "springrmi.app.SpringRmiGreeter/hello" - kind SpanKind.CLIENT - childOf span(0) - attributes { - "$RpcIncubatingAttributes.RPC_SYSTEM" "spring_rmi" - "$RpcIncubatingAttributes.RPC_SERVICE" "springrmi.app.SpringRmiGreeter" - "$RpcIncubatingAttributes.RPC_METHOD" "hello" - } - } - span(2) { - name "springrmi.app.SpringRmiGreeterImpl/hello" - kind SpanKind.SERVER - childOf span(1) - attributes { - "$RpcIncubatingAttributes.RPC_SYSTEM" "spring_rmi" - "$RpcIncubatingAttributes.RPC_SERVICE" "springrmi.app.SpringRmiGreeterImpl" - "$RpcIncubatingAttributes.RPC_METHOD" "hello" - } - } - } - } - } - - def "Throws exception"() { - given: - SpringRmiGreeter client = clientAppContext.getBean(SpringRmiGreeter) - when: - runWithSpan("parent") { client.exceptional() } - then: - def error = thrown(IllegalStateException) - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - status ERROR - hasNoParent() - event(0) { - eventName("exception") - attributes { - "$ExceptionAttributes.EXCEPTION_TYPE" error.getClass().getCanonicalName() - "$ExceptionAttributes.EXCEPTION_MESSAGE" error.getMessage() - "$ExceptionAttributes.EXCEPTION_STACKTRACE" String - } - } - } - span(1) { - name "springrmi.app.SpringRmiGreeter/exceptional" - kind SpanKind.CLIENT - status ERROR - childOf span(0) - event(0) { - eventName("exception") - attributes { - "$ExceptionAttributes.EXCEPTION_TYPE" error.getClass().getCanonicalName() - "$ExceptionAttributes.EXCEPTION_MESSAGE" error.getMessage() - "$ExceptionAttributes.EXCEPTION_STACKTRACE" String - } - } - attributes { - "$RpcIncubatingAttributes.RPC_SYSTEM" "spring_rmi" - "$RpcIncubatingAttributes.RPC_SERVICE" "springrmi.app.SpringRmiGreeter" - "$RpcIncubatingAttributes.RPC_METHOD" "exceptional" - } - } - span(2) { - name "springrmi.app.SpringRmiGreeterImpl/exceptional" - kind SpanKind.SERVER - childOf span(1) - status ERROR - event(0) { - eventName("exception") - attributes { - "$ExceptionAttributes.EXCEPTION_TYPE" error.getClass().getCanonicalName() - "$ExceptionAttributes.EXCEPTION_MESSAGE" error.getMessage() - "$ExceptionAttributes.EXCEPTION_STACKTRACE" String - } - } - attributes { - "$RpcIncubatingAttributes.RPC_SYSTEM" "spring_rmi" - "$RpcIncubatingAttributes.RPC_SERVICE" "springrmi.app.SpringRmiGreeterImpl" - "$RpcIncubatingAttributes.RPC_METHOD" "exceptional" - } - } - } - } - } - -} diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiTest.java b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiTest.java new file mode 100644 index 000000000000..c523afe93f0b --- /dev/null +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/spring/rmi/v4_0/SpringRmiTest.java @@ -0,0 +1,274 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.spring.rmi.v4_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes; +import java.rmi.RemoteException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import javax.ejb.EJBException; +import javax.ejb.embeddable.EJBContainer; +import javax.naming.spi.NamingManager; +import org.apache.openejb.OpenEjbContainer; +import org.apache.openejb.client.RemoteInitialContextFactory; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.remoting.rmi.RmiProxyFactoryBean; +import org.springframework.remoting.rmi.RmiServiceExporter; +import org.springframework.remoting.support.RemoteExporter; +import org.springframework.stereotype.Component; +import springrmi.app.SpringRmiGreeter; +import springrmi.app.SpringRmiGreeterImpl; + +class SpringRmiTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static ConfigurableApplicationContext serverAppContext; + private static ConfigurableApplicationContext clientAppContext; + private static ConfigurableApplicationContext xmlAppContext; + private static EJBContainer ejbContainer; + + static int registryPort; + + @Component + private static class ServerConfig { + @Bean + static RemoteExporter registerRmiExporter() { + SpringRmiGreeter greeter = new SpringRmiGreeterImpl(); + + RmiServiceExporter exporter = new RmiServiceExporter(); + exporter.setServiceName("springRmiGreeter"); + exporter.setServiceInterface(SpringRmiGreeter.class); + exporter.setService(greeter); + exporter.setRegistryPort(registryPort); + + return exporter; + } + } + + @Component + private static class ClientConfig { + @Bean + static RmiProxyFactoryBean rmiProxy() { + RmiProxyFactoryBean bean = new RmiProxyFactoryBean(); + bean.setServiceInterface(SpringRmiGreeter.class); + bean.setServiceUrl("rmi://localhost:" + registryPort + "/springRmiGreeter"); + return bean; + } + } + + @BeforeAll + static void beforeAll() throws Exception { + registryPort = PortUtils.findOpenPort(); + + Map map = new HashMap<>(); + map.put(EJBContainer.APP_NAME, "test"); + map.put(EJBContainer.MODULES, new java.io.File("build/classes/java/test")); + map.put(OpenEjbContainer.OPENEJB_EMBEDDED_REMOTABLE, "true"); + ejbContainer = EJBContainer.createEJBContainer(map); + // needed to ensure that remote ejb call isn't optimized to a local call + NamingManager.setInitialContextFactoryBuilder(environment -> new RemoteInitialContextFactory()); + + Map props = new HashMap<>(); + props.put("spring.jmx.enabled", false); + props.put("spring.main.web-application-type", "none"); + + SpringApplication serverApp = new SpringApplication(ServerConfig.class); + serverApp.setDefaultProperties(props); + serverAppContext = serverApp.run(); + + SpringApplication clientApp = new SpringApplication(ClientConfig.class); + clientApp.setDefaultProperties(props); + clientAppContext = clientApp.run(); + + xmlAppContext = new ClassPathXmlApplicationContext("spring-rmi.xml"); + } + + @AfterAll + static void afterAll() { + serverAppContext.close(); + clientAppContext.close(); + xmlAppContext.close(); + ejbContainer.close(); + } + + @SuppressWarnings("ImmutableEnumChecker") + private enum TestSource { + RMI( + clientAppContext, + "springrmi.app.SpringRmiGreeterImpl", + "spring_rmi", + IllegalStateException.class), + EJB(xmlAppContext, "springrmi.app.ejb.SpringRmiGreeterRemote", "java_rmi", EJBException.class); + + final ApplicationContext appContext; + final String remoteClassName; + final String serverSystem; + final Class expectedException; + + TestSource( + ApplicationContext appContext, + String remoteClassName, + String serverSystem, + Class expectedException) { + this.appContext = appContext; + this.remoteClassName = remoteClassName; + this.serverSystem = serverSystem; + this.expectedException = expectedException; + } + } + + @ParameterizedTest(autoCloseArguments = false) + @EnumSource(TestSource.class) + void clientCallCreatesSpans(TestSource testSource) throws RemoteException { + SpringRmiGreeter client = testSource.appContext.getBean(SpringRmiGreeter.class); + String response = testing.runWithSpan("parent", () -> client.hello("Test Name")); + assertEquals(response, "Hello Test Name"); + testing.waitAndAssertTraces( + trace -> { + List> assertions = new ArrayList<>(); + assertions.add(span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent()); + assertions.add( + span -> + span.hasName("springrmi.app.SpringRmiGreeter/hello") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfying( + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "spring_rmi"), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "springrmi.app.SpringRmiGreeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "hello"))); + if (testSource == TestSource.RMI) { + assertions.add( + span -> + span.hasName(testSource.remoteClassName + "/hello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfying( + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, testSource.serverSystem), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, testSource.remoteClassName), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "hello"))); + } + + trace.hasSpansSatisfyingExactly(assertions); + }); + } + + @ParameterizedTest(autoCloseArguments = false) + @EnumSource(TestSource.class) + void throwsException(TestSource testSource) { + SpringRmiGreeter client = testSource.appContext.getBean(SpringRmiGreeter.class); + Throwable error = + assertThrows( + testSource.expectedException, () -> testing.runWithSpan("parent", client::exceptional)); + testing.waitAndAssertTraces( + trace -> { + List> assertions = new ArrayList<>(); + assertions.add( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasStatus(StatusData.error()) + .hasNoParent() + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfying( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + error.getClass().getCanonicalName()), + equalTo( + ExceptionAttributes.EXCEPTION_MESSAGE, + error.getMessage()), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class))))); + assertions.add( + span -> + span.hasName("springrmi.app.SpringRmiGreeter/exceptional") + .hasKind(SpanKind.CLIENT) + .hasStatus(StatusData.error()) + .hasParent(trace.getSpan(0)) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfying( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + error.getClass().getCanonicalName()), + equalTo( + ExceptionAttributes.EXCEPTION_MESSAGE, + error.getMessage()), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class)))) + .hasAttributesSatisfying( + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, "spring_rmi"), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, + "springrmi.app.SpringRmiGreeter"), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "exceptional"))); + if (testSource == TestSource.RMI) { + assertions.add( + span -> + span.hasName(testSource.remoteClassName + "/exceptional") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfying( + equalTo( + ExceptionAttributes.EXCEPTION_TYPE, + error.getClass().getCanonicalName()), + equalTo( + ExceptionAttributes.EXCEPTION_MESSAGE, + error.getMessage()), + satisfies( + ExceptionAttributes.EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class)))) + .hasAttributesSatisfying( + equalTo(RpcIncubatingAttributes.RPC_SYSTEM, testSource.serverSystem), + equalTo( + RpcIncubatingAttributes.RPC_SERVICE, testSource.remoteClassName), + equalTo(RpcIncubatingAttributes.RPC_METHOD, "exceptional"))); + } + + trace.hasSpansSatisfyingExactly(assertions); + }); + } +} diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/springrmi/app/ejb/SpringRmiGreeterEjb.java b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/springrmi/app/ejb/SpringRmiGreeterEjb.java new file mode 100644 index 000000000000..09b7076b4310 --- /dev/null +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/springrmi/app/ejb/SpringRmiGreeterEjb.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springrmi.app.ejb; + +import javax.ejb.Stateless; +import springrmi.app.SpringRmiGreeterImpl; + +@Stateless +public class SpringRmiGreeterEjb extends SpringRmiGreeterImpl + implements SpringRmiGreeterEjbRemote {} diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/springrmi/app/ejb/SpringRmiGreeterEjbRemote.java b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/springrmi/app/ejb/SpringRmiGreeterEjbRemote.java new file mode 100644 index 000000000000..21ccee7f960e --- /dev/null +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/java/springrmi/app/ejb/SpringRmiGreeterEjbRemote.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package springrmi.app.ejb; + +import javax.ejb.Remote; +import springrmi.app.SpringRmiGreeter; + +@Remote +public interface SpringRmiGreeterEjbRemote extends SpringRmiGreeter {} diff --git a/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/resources/spring-rmi.xml b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/resources/spring-rmi.xml new file mode 100644 index 000000000000..b3f6f279f766 --- /dev/null +++ b/instrumentation/spring/spring-rmi-4.0/javaagent/src/test/resources/spring-rmi.xml @@ -0,0 +1,13 @@ + + + + + + +