diff --git a/dd-java-agent/instrumentation/jax-ws-annotations-1/jax-ws-annotations-1.gradle b/dd-java-agent/instrumentation/jax-ws-annotations-1/jax-ws-annotations-1.gradle new file mode 100644 index 00000000000..9097f714377 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-ws-annotations-1/jax-ws-annotations-1.gradle @@ -0,0 +1,16 @@ +muzzle { + pass { + group = "javax.jws" + module = "javax.jws-api" + versions = "[1.1,)" + } +} + +apply from: "$rootDir/gradle/java.gradle" + +dependencies { + compileOnly group: 'javax.jws', name: 'javax.jws-api', version: '1.1' + + testCompile group: 'javax.jws', name: 'javax.jws-api', version: '1.1' +} + diff --git a/dd-java-agent/instrumentation/jax-ws-annotations-1/src/main/java/datadog/trace/instrumentation/jaxws1/WebServiceDecorator.java b/dd-java-agent/instrumentation/jax-ws-annotations-1/src/main/java/datadog/trace/instrumentation/jaxws1/WebServiceDecorator.java new file mode 100644 index 00000000000..f6aa1901c9d --- /dev/null +++ b/dd-java-agent/instrumentation/jax-ws-annotations-1/src/main/java/datadog/trace/instrumentation/jaxws1/WebServiceDecorator.java @@ -0,0 +1,34 @@ +package datadog.trace.instrumentation.jaxws1; + +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.bootstrap.instrumentation.decorator.BaseDecorator; + +public class WebServiceDecorator extends BaseDecorator { + public static final WebServiceDecorator DECORATE = new WebServiceDecorator(); + + public static final CharSequence JAX_WS_REQUEST = UTF8BytesString.create("jax-ws.request"); + public static final CharSequence JAX_WS_ENDPOINT = UTF8BytesString.create("jax-ws-endpoint"); + + private WebServiceDecorator() {} + + @Override + protected String[] instrumentationNames() { + return new String[] {"jax-ws"}; + } + + @Override + protected CharSequence spanType() { + return InternalSpanTypes.SOAP; + } + + @Override + protected CharSequence component() { + return JAX_WS_ENDPOINT; + } + + public void onJaxWsSpan(final AgentSpan span, final Class target, final String method) { + span.setResourceName(spanNameForMethod(target, method)); + } +} diff --git a/dd-java-agent/instrumentation/jax-ws-annotations-1/src/main/java/datadog/trace/instrumentation/jaxws1/WebServiceInstrumentation.java b/dd-java-agent/instrumentation/jax-ws-annotations-1/src/main/java/datadog/trace/instrumentation/jaxws1/WebServiceInstrumentation.java new file mode 100644 index 00000000000..d1c17c165f1 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-ws-annotations-1/src/main/java/datadog/trace/instrumentation/jaxws1/WebServiceInstrumentation.java @@ -0,0 +1,100 @@ +package datadog.trace.instrumentation.jaxws1; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.DDElementMatchers.hasSuperMethod; +import static datadog.trace.agent.tooling.bytebuddy.matcher.DDElementMatchers.safeHasSuperType; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.jaxws1.WebServiceDecorator.DECORATE; +import static datadog.trace.instrumentation.jaxws1.WebServiceDecorator.JAX_WS_REQUEST; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.util.Map; +import javax.jws.WebService; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class WebServiceInstrumentation extends Instrumenter.Tracing { + private static final String WEB_SERVICE_ANNOTATION_NAME = "javax.jws.WebService"; + + public WebServiceInstrumentation() { + super("jax-ws"); + } + + @Override + protected boolean defaultEnabled() { + return false; + } + + @Override + public ElementMatcher typeMatcher() { + return safeHasSuperType(isAnnotatedWith(named(WEB_SERVICE_ANNOTATION_NAME))); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".WebServiceDecorator", + }; + } + + @Override + public Map, String> transformers() { + return singletonMap( + isMethod() + .and(isPublic()) + .and(not(isStatic())) + .and(hasSuperMethod(isDeclaredBy(isAnnotatedWith(named(WEB_SERVICE_ANNOTATION_NAME))))), + getClass().getName() + "$InvokeAdvice"); + } + + public static final class InvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope beginRequest( + @Advice.This Object thiz, @Advice.Origin("#m") String method) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(WebService.class); + if (callDepth > 0) { + return null; + } + + AgentSpan span = startSpan(JAX_WS_REQUEST); + span.setMeasured(true); + DECORATE.onJaxWsSpan(span, thiz.getClass(), method); + DECORATE.afterStart(span); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void finishRequest( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable error) { + if (null == scope) { + return; + } + + CallDepthThreadLocalMap.reset(WebService.class); + + AgentSpan span = scope.span(); + if (null != error) { + DECORATE.onError(span, error); + } + DECORATE.beforeFinish(span); + scope.close(); + span.finish(); + } + } +} diff --git a/dd-java-agent/instrumentation/jax-ws-annotations-1/src/test/groovy/WebServiceTest.groovy b/dd-java-agent/instrumentation/jax-ws-annotations-1/src/test/groovy/WebServiceTest.groovy new file mode 100644 index 00000000000..942f1d9445f --- /dev/null +++ b/dd-java-agent/instrumentation/jax-ws-annotations-1/src/test/groovy/WebServiceTest.groovy @@ -0,0 +1,126 @@ +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.DDSpanTypes + +import static org.junit.Assert.fail + +class WebServiceTest extends AgentTestRunner { + + @Override + void configurePreAgent() { + super.configurePreAgent() + injectSysConfig("dd.integration.jax-ws.enabled", "true") + } + + def "test successful interface request is traced"() { + when: + new TestService1Impl().send("success") + + then: + assertTraces(1) { + trace(1) { + span { + operationName "jax-ws.request" + resourceName "TestService1Impl.send" + spanType DDSpanTypes.SOAP + errored false + parent() + tags { + "component" "jax-ws-endpoint" + defaultTags() + } + } + } + } + } + + def "test successful class request is traced"() { + when: + new TestService2().send("success") + + then: + assertTraces(1) { + trace(1) { + span { + operationName "jax-ws.request" + resourceName "TestService2.send" + spanType DDSpanTypes.SOAP + errored false + parent() + tags { + "component" "jax-ws-endpoint" + defaultTags() + } + } + } + } + } + + def "test failing interface request is traced"() { + when: + try { + new TestService1Impl().send("fail") + fail("expected exception") + } catch (IllegalArgumentException e) { + // expected + } + + then: + assertTraces(1) { + trace(1) { + span { + operationName "jax-ws.request" + resourceName "TestService1Impl.send" + spanType DDSpanTypes.SOAP + errored true + parent() + tags { + "component" "jax-ws-endpoint" + "error.msg" "bad request" + "error.type" IllegalArgumentException.name + "error.stack" String + defaultTags() + } + } + } + } + } + + def "test failing class request is traced"() { + when: + try { + new TestService2().send("fail") + fail("expected exception") + } catch (IllegalArgumentException e) { + // expected + } + + then: + assertTraces(1) { + trace(1) { + span { + operationName "jax-ws.request" + resourceName "TestService2.send" + spanType DDSpanTypes.SOAP + errored true + parent() + tags { + "component" "jax-ws-endpoint" + "error.msg" "bad request" + "error.type" IllegalArgumentException.name + "error.stack" String + defaultTags() + } + } + } + } + } + + def "test other methods are not traced"() { + when: + new TestService1Impl().random() + new TestService2().random() + + then: + assertTraces(0) {} + } +} diff --git a/dd-java-agent/instrumentation/jax-ws-annotations-1/src/test/java/TestService1.java b/dd-java-agent/instrumentation/jax-ws-annotations-1/src/test/java/TestService1.java new file mode 100644 index 00000000000..299cc274d02 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-ws-annotations-1/src/test/java/TestService1.java @@ -0,0 +1,6 @@ +import javax.jws.WebService; + +@WebService +public interface TestService1 { + String send(String message); +} diff --git a/dd-java-agent/instrumentation/jax-ws-annotations-1/src/test/java/TestService1Impl.java b/dd-java-agent/instrumentation/jax-ws-annotations-1/src/test/java/TestService1Impl.java new file mode 100644 index 00000000000..d564aa3bc92 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-ws-annotations-1/src/test/java/TestService1Impl.java @@ -0,0 +1,13 @@ +public class TestService1Impl implements TestService1 { + @Override + public String send(final String request) { + if ("fail".equals(request)) { + throw new IllegalArgumentException("bad request"); + } + return random(); + } + + public String random() { + return Double.toHexString(Math.random()); + } +} diff --git a/dd-java-agent/instrumentation/jax-ws-annotations-1/src/test/java/TestService2.java b/dd-java-agent/instrumentation/jax-ws-annotations-1/src/test/java/TestService2.java new file mode 100644 index 00000000000..33ebdce2dd4 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-ws-annotations-1/src/test/java/TestService2.java @@ -0,0 +1,15 @@ +import javax.jws.WebService; + +@WebService +public class TestService2 { + public String send(final String request) { + if ("fail".equals(request)) { + throw new IllegalArgumentException("bad request"); + } + return random(); + } + + protected String random() { + return Double.toHexString(Math.random()); + } +} diff --git a/dd-java-agent/instrumentation/jax-ws-annotations-2/jax-ws-annotations-2.gradle b/dd-java-agent/instrumentation/jax-ws-annotations-2/jax-ws-annotations-2.gradle new file mode 100644 index 00000000000..0465f11b5d1 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-ws-annotations-2/jax-ws-annotations-2.gradle @@ -0,0 +1,25 @@ +muzzle { + pass { + group = "javax.xml.ws" + module = "jaxws-api" + versions = "[2.0,)" + skipVersions += ["2.1EA2", "2.1-1"] // bad releases + } +} + +apply from: "$rootDir/gradle/java.gradle" + +apply plugin: 'org.unbroken-dome.test-sets' + +testSets { + latestDepTest { + dirName = 'test' + } +} + +dependencies { + compileOnly group: 'javax.xml.ws', name: 'jaxws-api', version: '2.0' + + testCompile group: 'javax.xml.ws', name: 'jaxws-api', version: '2.0' + latestDepTestCompile group: 'javax.xml.ws', name: 'jaxws-api', version: '+' +} diff --git a/dd-java-agent/instrumentation/jax-ws-annotations-2/src/main/java/datadog/trace/instrumentation/jaxws2/WebServiceProviderDecorator.java b/dd-java-agent/instrumentation/jax-ws-annotations-2/src/main/java/datadog/trace/instrumentation/jaxws2/WebServiceProviderDecorator.java new file mode 100644 index 00000000000..2e4106e5959 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-ws-annotations-2/src/main/java/datadog/trace/instrumentation/jaxws2/WebServiceProviderDecorator.java @@ -0,0 +1,34 @@ +package datadog.trace.instrumentation.jaxws2; + +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.bootstrap.instrumentation.decorator.BaseDecorator; + +public class WebServiceProviderDecorator extends BaseDecorator { + public static final WebServiceProviderDecorator DECORATE = new WebServiceProviderDecorator(); + + public static final CharSequence JAX_WS_REQUEST = UTF8BytesString.create("jax-ws.request"); + public static final CharSequence JAX_WS_ENDPOINT = UTF8BytesString.create("jax-ws-endpoint"); + + private WebServiceProviderDecorator() {} + + @Override + protected String[] instrumentationNames() { + return new String[] {"jax-ws"}; + } + + @Override + protected CharSequence spanType() { + return InternalSpanTypes.SOAP; + } + + @Override + protected CharSequence component() { + return JAX_WS_ENDPOINT; + } + + public void onJaxWsSpan(final AgentSpan span, final Class target, final String method) { + span.setResourceName(spanNameForMethod(target, method)); + } +} diff --git a/dd-java-agent/instrumentation/jax-ws-annotations-2/src/main/java/datadog/trace/instrumentation/jaxws2/WebServiceProviderInstrumentation.java b/dd-java-agent/instrumentation/jax-ws-annotations-2/src/main/java/datadog/trace/instrumentation/jaxws2/WebServiceProviderInstrumentation.java new file mode 100644 index 00000000000..cfeb5c46c9c --- /dev/null +++ b/dd-java-agent/instrumentation/jax-ws-annotations-2/src/main/java/datadog/trace/instrumentation/jaxws2/WebServiceProviderInstrumentation.java @@ -0,0 +1,96 @@ +package datadog.trace.instrumentation.jaxws2; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.DDElementMatchers.implementsInterface; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.jaxws2.WebServiceProviderDecorator.DECORATE; +import static datadog.trace.instrumentation.jaxws2.WebServiceProviderDecorator.JAX_WS_REQUEST; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.util.Map; +import javax.xml.ws.WebServiceProvider; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class WebServiceProviderInstrumentation extends Instrumenter.Tracing { + private static final String WEB_SERVICE_PROVIDER_INTERFACE_NAME = "javax.xml.ws.Provider"; + private static final String WEB_SERVICE_PROVIDER_ANNOTATION_NAME = + "javax.xml.ws.WebServiceProvider"; + + public WebServiceProviderInstrumentation() { + super("jax-ws"); + } + + @Override + protected boolean defaultEnabled() { + return false; + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named(WEB_SERVICE_PROVIDER_INTERFACE_NAME)) + .and(isAnnotatedWith(named(WEB_SERVICE_PROVIDER_ANNOTATION_NAME))); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".WebServiceProviderDecorator", + }; + } + + @Override + public Map, String> transformers() { + return singletonMap( + isMethod().and(named("invoke")).and(takesArguments(1)), + getClass().getName() + "$InvokeAdvice"); + } + + public static final class InvokeAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope beginRequest( + @Advice.This Object thiz, @Advice.Origin("#m") String method) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(WebServiceProvider.class); + if (callDepth > 0) { + return null; + } + + AgentSpan span = startSpan(JAX_WS_REQUEST); + span.setMeasured(true); + DECORATE.onJaxWsSpan(span, thiz.getClass(), method); + DECORATE.afterStart(span); + return activateSpan(span); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void finishRequest( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable error) { + if (null == scope) { + return; + } + + CallDepthThreadLocalMap.reset(WebServiceProvider.class); + + AgentSpan span = scope.span(); + if (null != error) { + DECORATE.onError(span, error); + } + DECORATE.beforeFinish(span); + scope.close(); + span.finish(); + } + } +} diff --git a/dd-java-agent/instrumentation/jax-ws-annotations-2/src/test/groovy/WebServiceProviderTest.groovy b/dd-java-agent/instrumentation/jax-ws-annotations-2/src/test/groovy/WebServiceProviderTest.groovy new file mode 100644 index 00000000000..d04e470839b --- /dev/null +++ b/dd-java-agent/instrumentation/jax-ws-annotations-2/src/test/groovy/WebServiceProviderTest.groovy @@ -0,0 +1,73 @@ +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.DDSpanTypes + +import static org.junit.Assert.fail + +class WebServiceProviderTest extends AgentTestRunner { + + @Override + void configurePreAgent() { + super.configurePreAgent() + injectSysConfig("dd.integration.jax-ws.enabled", "true") + } + + def "test successful request is traced"() { + when: + new TestProvider().invoke("success") + + then: + assertTraces(1) { + trace(1) { + span { + operationName "jax-ws.request" + resourceName "TestProvider.invoke" + spanType DDSpanTypes.SOAP + errored false + parent() + tags { + "component" "jax-ws-endpoint" + defaultTags() + } + } + } + } + } + + def "test failing request is traced"() { + when: + try { + new TestProvider().invoke("fail") + fail("expected exception") + } catch (IllegalArgumentException e) { + // expected + } + + then: + assertTraces(1) { + trace(1) { + span { + operationName "jax-ws.request" + resourceName "TestProvider.invoke" + spanType DDSpanTypes.SOAP + errored true + parent() + tags { + "component" "jax-ws-endpoint" + "error.msg" "bad request" + "error.type" IllegalArgumentException.name + "error.stack" String + defaultTags() + } + } + } + } + } + + def "test other methods are not traced"() { + when: + new TestProvider().random() + + then: + assertTraces(0) {} + } +} diff --git a/dd-java-agent/instrumentation/jax-ws-annotations-2/src/test/java/TestProvider.java b/dd-java-agent/instrumentation/jax-ws-annotations-2/src/test/java/TestProvider.java new file mode 100644 index 00000000000..10839250690 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-ws-annotations-2/src/test/java/TestProvider.java @@ -0,0 +1,17 @@ +import javax.xml.ws.Provider; +import javax.xml.ws.WebServiceProvider; + +@WebServiceProvider +public class TestProvider implements Provider { + @Override + public String invoke(final String request) { + if ("fail".equals(request)) { + throw new IllegalArgumentException("bad request"); + } + return random(); + } + + protected String random() { + return Double.toHexString(Math.random()); + } +} diff --git a/settings.gradle b/settings.gradle index 90d8f322eac..d8d51b450ee 100644 --- a/settings.gradle +++ b/settings.gradle @@ -133,6 +133,8 @@ include ':dd-java-agent:instrumentation:jax-rs-client-1.1' include ':dd-java-agent:instrumentation:jax-rs-client-2.0' include ':dd-java-agent:instrumentation:jax-rs-client-2.0:connection-error-handling-jersey' include ':dd-java-agent:instrumentation:jax-rs-client-2.0:connection-error-handling-resteasy' +include ':dd-java-agent:instrumentation:jax-ws-annotations-1' +include ':dd-java-agent:instrumentation:jax-ws-annotations-2' include ':dd-java-agent:instrumentation:java-concurrent' include ':dd-java-agent:instrumentation:java-concurrent:java-completablefuture' include ':dd-java-agent:instrumentation:java-concurrent:lambda-testing'