diff --git a/dd-java-agent/instrumentation/jakarta-ws-annotations/build.gradle b/dd-java-agent/instrumentation/jakarta-ws-annotations/build.gradle new file mode 100644 index 00000000000..50d4514d644 --- /dev/null +++ b/dd-java-agent/instrumentation/jakarta-ws-annotations/build.gradle @@ -0,0 +1,21 @@ + +muzzle { + pass { + group = "jakarta.jws" + module = "jakarta.jws-api" + versions = "[3.0.0,]" + } +} + +apply from: "$rootDir/gradle/java.gradle" + +addTestSuiteForDir('latestDepTest', 'test') + +dependencies { + // todo correct version non rs + compileOnly group: 'jakarta.jws', name: 'jakarta.jws-api', version: '3.0.0' + + //todo also make jakarta + testImplementation group: 'jakarta.jws', name: 'jakarta.jws-api', version: '3.0.0' + latestDepTestImplementation group: 'jakarta.jws', name: 'jakarta.jws-api', version: '+' +} diff --git a/dd-java-agent/instrumentation/jakarta-ws-annotations/src/main/java/datadog/trace/instrumentation/jakartaws/WebServiceDecorator.java b/dd-java-agent/instrumentation/jakarta-ws-annotations/src/main/java/datadog/trace/instrumentation/jakartaws/WebServiceDecorator.java new file mode 100644 index 00000000000..1364b4b6536 --- /dev/null +++ b/dd-java-agent/instrumentation/jakarta-ws-annotations/src/main/java/datadog/trace/instrumentation/jakartaws/WebServiceDecorator.java @@ -0,0 +1,36 @@ +package datadog.trace.instrumentation.jakartaws; + +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 JAKARTA_WS_REQUEST = + UTF8BytesString.create("jakarta-ws.request"); + public static final CharSequence JAKARTA_WS_ENDPOINT = + UTF8BytesString.create("jakarta-ws-endpoint"); + + private WebServiceDecorator() {} + + @Override + protected String[] instrumentationNames() { + return new String[] {"jakarta-ws"}; + } + + @Override + protected CharSequence spanType() { + return InternalSpanTypes.SOAP; + } + + @Override + protected CharSequence component() { + return JAKARTA_WS_ENDPOINT; + } + + public void onJakartaWsSpan(final AgentSpan span, final Class target, final String method) { + span.setResourceName(spanNameForMethod(target, method)); + } +} diff --git a/dd-java-agent/instrumentation/jakarta-ws-annotations/src/main/java/datadog/trace/instrumentation/jakartaws/WebServiceInstrumentation.java b/dd-java-agent/instrumentation/jakarta-ws-annotations/src/main/java/datadog/trace/instrumentation/jakartaws/WebServiceInstrumentation.java new file mode 100644 index 00000000000..ff5f9de2e6d --- /dev/null +++ b/dd-java-agent/instrumentation/jakarta-ws-annotations/src/main/java/datadog/trace/instrumentation/jakartaws/WebServiceInstrumentation.java @@ -0,0 +1,101 @@ +package datadog.trace.instrumentation.jakartaws; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.declaresAnnotation; +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.hasSuperMethod; +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.hasSuperType; +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.jakartaws.WebServiceDecorator.DECORATE; +import static datadog.trace.instrumentation.jakartaws.WebServiceDecorator.JAKARTA_WS_REQUEST; +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.agent.tooling.InstrumenterModule; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import jakarta.jws.WebService; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumenterModule.class) +public final class WebServiceInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForBootstrap, Instrumenter.ForTypeHierarchy { + private static final String WEB_SERVICE_ANNOTATION_NAME = "jakarta.jws.WebService"; + + public WebServiceInstrumentation() { + super("jakarta-ws"); + } + + @Override + public String hierarchyMarkerType() { + return null; // bootstrap type + } + + @Override + public ElementMatcher hierarchyMatcher() { + return hasSuperType(declaresAnnotation(named(WEB_SERVICE_ANNOTATION_NAME))); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".WebServiceDecorator", + }; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(isPublic()) + .and(not(isStatic())) + .and( + hasSuperMethod( + isDeclaredBy(declaresAnnotation(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(JAKARTA_WS_REQUEST); + span.setMeasured(true); + DECORATE.onJakartaWsSpan(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/jakarta-ws-annotations/src/test/groovy/WebServiceTest.groovy b/dd-java-agent/instrumentation/jakarta-ws-annotations/src/test/groovy/WebServiceTest.groovy new file mode 100644 index 00000000000..5b34d15fff7 --- /dev/null +++ b/dd-java-agent/instrumentation/jakarta-ws-annotations/src/test/groovy/WebServiceTest.groovy @@ -0,0 +1,120 @@ +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.DDSpanTypes + +import static org.junit.Assert.fail + +class WebServiceTest extends AgentTestRunner { + + def "test successful interface request is traced"() { + when: + new TestService1Impl().send("success") + + then: + assertTraces(1) { + trace(1) { + span { + operationName "jakarta-ws.request" + resourceName "TestService1Impl.send" + spanType DDSpanTypes.SOAP + errored false + parent() + tags { + "component" "jakarta-ws-endpoint" + defaultTags() + } + } + } + } + } + + def "test successful class request is traced"() { + when: + new TestService2().send("success") + + then: + assertTraces(1) { + trace(1) { + span { + operationName "jakarta-ws.request" + resourceName "TestService2.send" + spanType DDSpanTypes.SOAP + errored false + parent() + tags { + "component" "jakarta-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 "jakarta-ws.request" + resourceName "TestService1Impl.send" + spanType DDSpanTypes.SOAP + errored true + parent() + tags { + "component" "jakarta-ws-endpoint" + "error.message" "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 "jakarta-ws.request" + resourceName "TestService2.send" + spanType DDSpanTypes.SOAP + errored true + parent() + tags { + "component" "jakarta-ws-endpoint" + "error.message" "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/jakarta-ws-annotations/src/test/java/TestService1.java b/dd-java-agent/instrumentation/jakarta-ws-annotations/src/test/java/TestService1.java new file mode 100644 index 00000000000..e46ca7e6b43 --- /dev/null +++ b/dd-java-agent/instrumentation/jakarta-ws-annotations/src/test/java/TestService1.java @@ -0,0 +1,6 @@ +import jakarta.jws.WebService; + +@WebService +public interface TestService1 { + String send(String message); +} diff --git a/dd-java-agent/instrumentation/jakarta-ws-annotations/src/test/java/TestService1Impl.java b/dd-java-agent/instrumentation/jakarta-ws-annotations/src/test/java/TestService1Impl.java new file mode 100644 index 00000000000..d564aa3bc92 --- /dev/null +++ b/dd-java-agent/instrumentation/jakarta-ws-annotations/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/jakarta-ws-annotations/src/test/java/TestService2.java b/dd-java-agent/instrumentation/jakarta-ws-annotations/src/test/java/TestService2.java new file mode 100644 index 00000000000..3510db5c05c --- /dev/null +++ b/dd-java-agent/instrumentation/jakarta-ws-annotations/src/test/java/TestService2.java @@ -0,0 +1,15 @@ +import jakarta.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/settings.gradle b/settings.gradle index fd85c4210f6..6e6bb98a524 100644 --- a/settings.gradle +++ b/settings.gradle @@ -270,6 +270,7 @@ include ':dd-java-agent:instrumentation:jackson-core:jackson-core-2.16' include ':dd-java-agent:instrumentation:jacoco' include ':dd-java-agent:instrumentation:jakarta-jms' include ':dd-java-agent:instrumentation:jakarta-rs-annotations-3' +include ':dd-java-agent:instrumentation:jakarta-ws-annotations' include ':dd-java-agent:instrumentation:java-concurrent' include ':dd-java-agent:instrumentation:java-concurrent:java-completablefuture' include ':dd-java-agent:instrumentation:java-concurrent:java-concurrent-21'