From cd2e11a9d68fdbe99def1afa7da3d1cdfb49cdff Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Fri, 3 Jun 2022 12:49:14 +0300 Subject: [PATCH] Add instrumentation for JAX-RS 3.0 (#6136) * Add instrumentation for JAX-RS 3.0 * set min java version to 11 for jax-rs 3.0 * exclude broken version * fix muzzle range * include correct api * fix muzzle * fix muzzle * remove generics * share test code --- ...ContainerRequestFilterInstrumentation.java | 1 + .../DefaultRequestContextInstrumentation.java | 8 +- .../v2_0/JaxrsAnnotationsInstrumentation.java | 9 +- .../v2_0/JaxrsAnnotationsSingletons.java | 2 + .../JaxrsAsyncResponseInstrumentation.java | 2 + .../javaagent/build.gradle.kts | 4 +- .../jaxrs/v2_0/HandlerData.java | 161 --------- .../jaxrs/v2_0/Jaxrs2HandlerData.java | 60 ++++ .../v2_0/Jaxrs2RequestContextHelper.java | 30 ++ .../jaxrs/v2_0/JaxrsConstants.java | 15 - .../jaxrs-2.0-common/testing/build.gradle.kts | 15 +- .../src/main/groovy/JaxRsFilterTest.groovy | 133 +------ .../main/groovy/JaxRsHttpServerTest.groovy | 320 +---------------- .../CxfRequestContextInstrumentation.java | 9 +- .../jaxrs/v2_0/CxfSingletons.java | 2 + .../jaxrs/v2_0/CxfSpanName.java | 2 +- .../javaagent/build.gradle.kts | 28 +- .../JerseyRequestContextInstrumentation.java | 10 +- .../jaxrs/v2_0/JerseySingletons.java | 2 + .../jaxrs/v2_0/JerseySpanName.java | 2 +- .../src/test/groovy/JerseyFilterTest.groovy | 88 ++++- .../javaagent/build.gradle.kts | 17 +- ...steasy30RequestContextInstrumentation.java | 10 +- .../jaxrs/v2_0/Resteasy30Singletons.java | 2 + .../javaagent/build.gradle.kts | 17 +- ...steasy31RequestContextInstrumentation.java | 10 +- .../jaxrs/v2_0/Resteasy31Singletons.java | 2 + .../ResteasyRootNodeTypeInstrumentation.java | 1 + .../javaagent/build.gradle.kts | 22 ++ ...ContainerRequestFilterInstrumentation.java | 60 ++++ .../DefaultRequestContextInstrumentation.java | 95 +++++ .../v3_0/JaxrsAnnotationsInstrumentation.java | 171 +++++++++ ...JaxrsAnnotationsInstrumentationModule.java | 37 ++ .../v3_0/JaxrsAnnotationsSingletons.java | 22 ++ .../JaxrsAsyncResponseInstrumentation.java | 107 ++++++ ...JaxrsAnnotationsInstrumentationTest.groovy | 162 +++++++++ .../src/test/java/JavaInterfaces.java | 69 ++++ .../javaagent/build.gradle.kts | 10 + ...AbstractRequestContextInstrumentation.java | 42 +++ .../jaxrs/v3_0/Jaxrs3HandlerData.java | 60 ++++ .../v3_0/Jaxrs3RequestContextHelper.java | 28 ++ .../jaxrs-3.0-common/testing/build.gradle.kts | 10 + .../src/main/groovy/JaxRsFilterTest.groovy | 62 ++++ .../main/groovy/JaxRsHttpServerTest.groovy | 15 + .../groovy/JaxRsJettyHttpServerTest.groovy | 41 +++ .../main/groovy/test/JaxRsTestResource.groovy | 239 +++++++++++++ .../testing/src/main/java/Resource.java | 53 +++ .../javaagent/build.gradle.kts | 41 +++ .../v3_0/JerseyInstrumentationModule.java | 28 ++ .../JerseyRequestContextInstrumentation.java | 81 +++++ ...sourceMethodDispatcherInstrumentation.java | 47 +++ ...JerseyServletContainerInstrumentation.java | 62 ++++ .../jaxrs/v3_0/JerseySingletons.java | 22 ++ .../jaxrs/v3_0/JerseySpanName.java | 43 +++ .../src/test/groovy/JerseyFilterTest.groovy | 84 +++++ .../test/groovy/JerseyHttpServerTest.groovy | 45 +++ .../groovy/JerseyJettyHttpServerTest.groovy | 18 + .../test/groovy/JerseyStartupListener.groovy | 23 ++ .../javaagent/src/test/webapp/WEB-INF/web.xml | 10 + .../javaagent/build.gradle.kts | 43 +++ .../v3_0/ResteasyInstrumentationModule.java | 30 ++ ...ResteasyRequestContextInstrumentation.java | 75 ++++ ...ResourceLocatorInvokerInstrumentation.java | 70 ++++ ...yResourceMethodInvokerInstrumentation.java | 47 +++ .../ResteasyRootNodeTypeInstrumentation.java | 63 ++++ ...letContainerDispatcherInstrumentation.java | 56 +++ .../jaxrs/v3_0/ResteasySingletons.java | 22 ++ .../jaxrs/v3_0/ResteasySpanName.java | 37 ++ .../src/test/groovy/ResteasyFilterTest.groovy | 43 +++ .../test/groovy/ResteasyHttpServerTest.groovy | 35 ++ .../groovy/ResteasyJettyHttpServerTest.groovy | 7 + .../groovy/ResteasyStartupListener.groovy | 24 ++ .../javaagent/src/test/webapp/WEB-INF/web.xml | 10 + .../jaxrs-common/javaagent/build.gradle.kts | 10 + .../jaxrs}/AsyncResponseData.java | 2 +- .../jaxrs}/CompletionStageFinishCallback.java | 2 +- .../instrumentation/jaxrs/HandlerData.java | 141 ++++++++ .../jaxrs}/JaxrsCodeAttributesGetter.java | 2 +- .../instrumentation/jaxrs}/JaxrsConfig.java | 2 +- .../instrumentation/jaxrs/JaxrsConstants.java | 15 + .../jaxrs}/JaxrsInstrumenterFactory.java | 4 +- .../instrumentation/jaxrs}/JaxrsPathUtil.java | 2 +- .../jaxrs}/JaxrsServerSpanNaming.java | 2 +- .../jaxrs}/RequestContextHelper.java | 14 +- .../jaxrs-common/testing/build.gradle.kts | 16 + .../groovy/AbstractJaxRsFilterTest.groovy | 147 ++++++++ .../groovy/AbstractJaxRsHttpServerTest.groovy | 329 ++++++++++++++++++ settings.gradle.kts | 7 + 88 files changed, 3311 insertions(+), 717 deletions(-) delete mode 100644 instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/HandlerData.java create mode 100644 instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Jaxrs2HandlerData.java create mode 100644 instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Jaxrs2RequestContextHelper.java delete mode 100644 instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsConstants.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/build.gradle.kts create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ContainerRequestFilterInstrumentation.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/DefaultRequestContextInstrumentation.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentation.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentationModule.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsSingletons.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAsyncResponseInstrumentation.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/test/groovy/JaxrsAnnotationsInstrumentationTest.groovy create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/test/java/JavaInterfaces.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/build.gradle.kts create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/AbstractRequestContextInstrumentation.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/Jaxrs3HandlerData.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/Jaxrs3RequestContextHelper.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/build.gradle.kts create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsFilterTest.groovy create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/test/JaxRsTestResource.groovy create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/java/Resource.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/build.gradle.kts create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyInstrumentationModule.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyRequestContextInstrumentation.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyResourceMethodDispatcherInstrumentation.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyServletContainerInstrumentation.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseySingletons.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseySpanName.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyFilterTest.groovy create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyStartupListener.groovy create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/webapp/WEB-INF/web.xml create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/build.gradle.kts create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyInstrumentationModule.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyRequestContextInstrumentation.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyResourceLocatorInvokerInstrumentation.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyResourceMethodInvokerInstrumentation.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyRootNodeTypeInstrumentation.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyServletContainerDispatcherInstrumentation.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasySingletons.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasySpanName.java create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyFilterTest.groovy create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyStartupListener.groovy create mode 100644 instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/webapp/WEB-INF/web.xml create mode 100644 instrumentation/jaxrs/jaxrs-common/javaagent/build.gradle.kts rename instrumentation/jaxrs/{jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0 => jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs}/AsyncResponseData.java (88%) rename instrumentation/jaxrs/{jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0 => jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs}/CompletionStageFinishCallback.java (93%) create mode 100644 instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/HandlerData.java rename instrumentation/jaxrs/{jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0 => jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs}/JaxrsCodeAttributesGetter.java (88%) rename instrumentation/jaxrs/{jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0 => jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs}/JaxrsConfig.java (85%) create mode 100644 instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsConstants.java rename instrumentation/jaxrs/{jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0 => jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs}/JaxrsInstrumenterFactory.java (91%) rename instrumentation/jaxrs/{jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0 => jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs}/JaxrsPathUtil.java (89%) rename instrumentation/jaxrs/{jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0 => jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs}/JaxrsServerSpanNaming.java (94%) rename instrumentation/jaxrs/{jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0 => jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs}/RequestContextHelper.java (76%) create mode 100644 instrumentation/jaxrs/jaxrs-common/testing/build.gradle.kts create mode 100644 instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsFilterTest.groovy create mode 100644 instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsHttpServerTest.groovy diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ContainerRequestFilterInstrumentation.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ContainerRequestFilterInstrumentation.java index f7774bc0cac2..b421abe7ecc5 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ContainerRequestFilterInstrumentation.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ContainerRequestFilterInstrumentation.java @@ -14,6 +14,7 @@ import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConstants; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import net.bytebuddy.asm.Advice; diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/DefaultRequestContextInstrumentation.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/DefaultRequestContextInstrumentation.java index 4f17d7430b59..7c903bbd8435 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/DefaultRequestContextInstrumentation.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/DefaultRequestContextInstrumentation.java @@ -12,6 +12,8 @@ import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConstants; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsServerSpanNaming; import java.lang.reflect.Method; import javax.ws.rs.container.ContainerRequestContext; import net.bytebuddy.asm.Advice; @@ -38,7 +40,7 @@ public static class ContainerRequestContextAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void createGenericSpan( @Advice.This ContainerRequestContext requestContext, - @Local("otelHandlerData") HandlerData handlerData, + @Local("otelHandlerData") Jaxrs2HandlerData handlerData, @Local("otelContext") Context context, @Local("otelScope") Scope scope) { if (requestContext.getProperty(JaxrsConstants.ABORT_HANDLED) != null) { @@ -60,7 +62,7 @@ public static void createGenericSpan( } Context parentContext = Java8BytecodeBridge.currentContext(); - handlerData = new HandlerData(filterClass, method); + handlerData = new Jaxrs2HandlerData(filterClass, method); HttpRouteHolder.updateHttpRoute( parentContext, @@ -78,7 +80,7 @@ public static void createGenericSpan( @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void stopSpan( - @Local("otelHandlerData") HandlerData handlerData, + @Local("otelHandlerData") Jaxrs2HandlerData handlerData, @Local("otelContext") Context context, @Local("otelScope") Scope scope, @Advice.Thrown Throwable throwable) { diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsInstrumentation.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsInstrumentation.java index 83e7ca5df473..5f0a43ed53b5 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsInstrumentation.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsInstrumentation.java @@ -24,6 +24,9 @@ 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.jaxrs.AsyncResponseData; +import io.opentelemetry.javaagent.instrumentation.jaxrs.CompletionStageFinishCallback; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsServerSpanNaming; import java.lang.reflect.Method; import java.util.concurrent.CompletionStage; import javax.ws.rs.Path; @@ -74,7 +77,7 @@ public static void nameSpan( @Advice.Origin Method method, @Advice.AllArguments Object[] args, @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHandlerData") HandlerData handlerData, + @Advice.Local("otelHandlerData") Jaxrs2HandlerData handlerData, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope, @Advice.Local("otelAsyncResponse") AsyncResponse asyncResponse) { @@ -102,7 +105,7 @@ public static void nameSpan( } Context parentContext = Java8BytecodeBridge.currentContext(); - handlerData = new HandlerData(target.getClass(), method); + handlerData = new Jaxrs2HandlerData(target.getClass(), method); HttpRouteHolder.updateHttpRoute( parentContext, @@ -127,7 +130,7 @@ public static void stopSpan( @Advice.Return(readOnly = false, typing = Typing.DYNAMIC) Object returnValue, @Advice.Thrown Throwable throwable, @Advice.Local("otelCallDepth") CallDepth callDepth, - @Advice.Local("otelHandlerData") HandlerData handlerData, + @Advice.Local("otelHandlerData") Jaxrs2HandlerData handlerData, @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope, @Advice.Local("otelAsyncResponse") AsyncResponse asyncResponse) { diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsSingletons.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsSingletons.java index 3bad6d5e5be8..c5ad08c2c746 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsSingletons.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAnnotationsSingletons.java @@ -6,6 +6,8 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.instrumentation.jaxrs.HandlerData; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsInstrumenterFactory; public final class JaxrsAnnotationsSingletons { diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAsyncResponseInstrumentation.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAsyncResponseInstrumentation.java index 8e29f0d8ab94..92643f5b9c06 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAsyncResponseInstrumentation.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsAsyncResponseInstrumentation.java @@ -17,6 +17,8 @@ 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.jaxrs.AsyncResponseData; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConfig; import javax.ws.rs.container.AsyncResponse; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/build.gradle.kts index 5717bfaa125c..e0a01d3ad7b9 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/build.gradle.kts @@ -4,9 +4,7 @@ plugins { dependencies { bootstrap(project(":instrumentation:jaxrs:jaxrs-common:bootstrap")) + api(project(":instrumentation:jaxrs:jaxrs-common:javaagent")) compileOnly("javax.ws.rs:javax.ws.rs-api:2.0") - - compileOnly("com.google.auto.value:auto-value-annotations") - annotationProcessor("com.google.auto.value:auto-value") } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/HandlerData.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/HandlerData.java deleted file mode 100644 index 775163c8b8c9..000000000000 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/HandlerData.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; - -import io.opentelemetry.javaagent.bootstrap.jaxrs.ClassHierarchyIterable; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import javax.ws.rs.HttpMethod; -import javax.ws.rs.Path; - -public class HandlerData { - - private static final ClassValue> serverSpanNames = - new ClassValue>() { - @Override - protected Map computeValue(Class type) { - return new ConcurrentHashMap<>(); - } - }; - - private final Class target; - private final Method method; - - public HandlerData(Class target, Method method) { - this.target = target; - this.method = method; - } - - public Class codeClass() { - return target; - } - - public String methodName() { - return method.getName(); - } - - /** - * Returns the span name given a JaxRS annotated method. Results are cached so this method can be - * called multiple times without significantly impacting performance. - * - * @return The result can be an empty string but will never be {@code null}. - */ - String getServerSpanName() { - Map classMap = serverSpanNames.get(target); - String spanName = classMap.get(method); - if (spanName == null) { - String httpMethod = null; - Path methodPath = null; - Path classPath = findClassPath(target); - for (Class currentClass : new ClassHierarchyIterable(target)) { - Method currentMethod; - if (currentClass.equals(target)) { - currentMethod = method; - } else { - currentMethod = findMatchingMethod(method, currentClass.getDeclaredMethods()); - } - - if (currentMethod != null) { - if (httpMethod == null) { - httpMethod = locateHttpMethod(currentMethod); - } - if (methodPath == null) { - methodPath = findMethodPath(currentMethod); - } - - if (httpMethod != null && methodPath != null) { - break; - } - } - } - spanName = buildSpanName(classPath, methodPath); - classMap.put(method, spanName); - } - - return spanName; - } - - private static String locateHttpMethod(Method method) { - String httpMethod = null; - for (Annotation ann : method.getDeclaredAnnotations()) { - if (ann.annotationType().getAnnotation(HttpMethod.class) != null) { - httpMethod = ann.annotationType().getSimpleName(); - } - } - return httpMethod; - } - - private static Path findMethodPath(Method method) { - return method.getAnnotation(Path.class); - } - - private static Path findClassPath(Class target) { - for (Class currentClass : new ClassHierarchyIterable(target)) { - Path annotation = currentClass.getAnnotation(Path.class); - if (annotation != null) { - // Annotation overridden, no need to continue. - return annotation; - } - } - - return null; - } - - private static Method findMatchingMethod(Method baseMethod, Method[] methods) { - nextMethod: - for (Method method : methods) { - if (!baseMethod.getReturnType().equals(method.getReturnType())) { - continue; - } - - if (!baseMethod.getName().equals(method.getName())) { - continue; - } - - Class[] baseParameterTypes = baseMethod.getParameterTypes(); - Class[] parameterTypes = method.getParameterTypes(); - if (baseParameterTypes.length != parameterTypes.length) { - continue; - } - for (int i = 0; i < baseParameterTypes.length; i++) { - if (!baseParameterTypes[i].equals(parameterTypes[i])) { - continue nextMethod; - } - } - return method; - } - return null; - } - - private static String buildSpanName(Path classPath, Path methodPath) { - StringBuilder spanNameBuilder = new StringBuilder(); - boolean skipSlash = false; - if (classPath != null) { - String classPathValue = classPath.value(); - if (!classPathValue.startsWith("/")) { - spanNameBuilder.append("/"); - } - spanNameBuilder.append(classPathValue); - skipSlash = classPathValue.endsWith("/") || classPathValue.isEmpty(); - } - - if (methodPath != null) { - String path = methodPath.value(); - if (skipSlash) { - if (path.startsWith("/")) { - path = path.length() == 1 ? "" : path.substring(1); - } - } else if (!path.startsWith("/")) { - spanNameBuilder.append("/"); - } - spanNameBuilder.append(path); - } - - return spanNameBuilder.toString().trim(); - } -} diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Jaxrs2HandlerData.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Jaxrs2HandlerData.java new file mode 100644 index 000000000000..a040eaf6e60f --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Jaxrs2HandlerData.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import io.opentelemetry.javaagent.instrumentation.jaxrs.HandlerData; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; + +public class Jaxrs2HandlerData extends HandlerData { + + private static final ClassValue> serverSpanNames = + new ClassValue>() { + @Override + protected Map computeValue(Class type) { + return new ConcurrentHashMap<>(); + } + }; + + public Jaxrs2HandlerData(Class target, Method method) { + super(target, method); + } + + /** + * Returns the span name given a JaxRS annotated method. Results are cached so this method can be + * called multiple times without significantly impacting performance. + * + * @return The result can be an empty string but will never be {@code null}. + */ + @Override + public String getServerSpanName() { + Map classMap = serverSpanNames.get(target); + String spanName = classMap.get(method); + if (spanName == null) { + spanName = super.getServerSpanName(); + classMap.put(method, spanName); + } + + return spanName; + } + + @Override + protected Class getHttpMethodAnnotation() { + return HttpMethod.class; + } + + @Override + protected Supplier getPathAnnotation(AnnotatedElement annotatedElement) { + Path path = annotatedElement.getAnnotation(Path.class); + return path != null ? path::value : null; + } +} diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Jaxrs2RequestContextHelper.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Jaxrs2RequestContextHelper.java new file mode 100644 index 000000000000..32033dd9e696 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Jaxrs2RequestContextHelper.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.instrumentation.jaxrs.HandlerData; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConstants; +import io.opentelemetry.javaagent.instrumentation.jaxrs.RequestContextHelper; +import javax.ws.rs.container.ContainerRequestContext; + +public final class Jaxrs2RequestContextHelper { + public static Context createOrUpdateAbortSpan( + Instrumenter instrumenter, + ContainerRequestContext requestContext, + HandlerData handlerData) { + + if (handlerData == null) { + return null; + } + + requestContext.setProperty(JaxrsConstants.ABORT_HANDLED, true); + return RequestContextHelper.createOrUpdateAbortSpan(instrumenter, handlerData); + } + + private Jaxrs2RequestContextHelper() {} +} diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsConstants.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsConstants.java deleted file mode 100644 index f9c69b4d82b8..000000000000 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsConstants.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; - -final class JaxrsConstants { - public static final String ABORT_FILTER_CLASS = - "io.opentelemetry.javaagent.instrumentation.jaxrs2.filter.abort.class"; - public static final String ABORT_HANDLED = - "io.opentelemetry.javaagent.instrumentation.jaxrs2.filter.abort.handled"; - - private JaxrsConstants() {} -} diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/build.gradle.kts b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/build.gradle.kts index f508f7a2e784..9ed7dd6593b0 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/build.gradle.kts @@ -3,21 +3,8 @@ plugins { } dependencies { - api(project(":testing-common")) + api(project(":instrumentation:jaxrs:jaxrs-common:testing")) api("javax.ws.rs:javax.ws.rs-api:2.0") - implementation("org.apache.groovy:groovy") - implementation("io.opentelemetry:opentelemetry-api") - implementation("org.spockframework:spock-core") - implementation("org.slf4j:slf4j-api") - implementation("ch.qos.logback:logback-classic") - implementation("org.slf4j:log4j-over-slf4j") - implementation("org.slf4j:jcl-over-slf4j") - implementation("org.slf4j:jul-to-slf4j") - - implementation(project(":javaagent-extension-api")) - implementation(project(":instrumentation-api")) - implementation(project(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent")) - compileOnly("org.eclipse.jetty:jetty-webapp:8.0.0.v20110901") } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsFilterTest.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsFilterTest.groovy index 1577447d53ce..5629374bc870 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsFilterTest.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsFilterTest.groovy @@ -3,9 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.junit.jupiter.api.Assumptions import spock.lang.Shared import spock.lang.Unroll @@ -16,12 +13,8 @@ import javax.ws.rs.core.MediaType import javax.ws.rs.core.Response import javax.ws.rs.ext.Provider -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.SpanKind.SERVER -import static io.opentelemetry.api.trace.StatusCode.UNSET - @Unroll -abstract class JaxRsFilterTest extends AgentInstrumentationSpecification { +abstract class JaxRsFilterTest extends AbstractJaxRsFilterTest { @Shared SimpleRequestFilter simpleRequestFilter = new SimpleRequestFilter() @@ -29,130 +22,10 @@ abstract class JaxRsFilterTest extends AgentInstrumentationSpecification { @Shared PrematchRequestFilter prematchRequestFilter = new PrematchRequestFilter() - abstract makeRequest(String url) - - Tuple2 runRequest(String resource) { - if (runsOnServer()) { - return makeRequest(resource) - } - // start a trace because the test doesn't go through any servlet or other instrumentation. - return runWithHttpServerSpan("test.span") { - makeRequest(resource) - } - } - - boolean testAbortPrematch() { - true - } - - boolean runsOnServer() { - false - } - - def "test #resource, #abortNormal, #abortPrematch"() { - Assumptions.assumeTrue(!abortPrematch || testAbortPrematch()) - - given: + @Override + void setAbortStatus(boolean abortNormal, boolean abortPrematch) { simpleRequestFilter.abort = abortNormal prematchRequestFilter.abort = abortPrematch - def abort = abortNormal || abortPrematch - - when: - - def (responseText, responseStatus) = runRequest(resource) - - then: - responseText == expectedResponse - - if (abort) { - responseStatus == Response.Status.UNAUTHORIZED.statusCode - } else { - responseStatus == Response.Status.OK.statusCode - } - - assertTraces(1) { - trace(0, 2) { - span(0) { - name parentSpanName != null ? parentSpanName : "test.span" - kind SERVER - if (runsOnServer() && abortNormal) { - status UNSET - } - } - span(1) { - childOf span(0) - name controllerName - if (abortPrematch) { - attributes { - "$SemanticAttributes.CODE_NAMESPACE" "JaxRsFilterTest\$PrematchRequestFilter" - "$SemanticAttributes.CODE_FUNCTION" "filter" - } - } else { - attributes { - "$SemanticAttributes.CODE_NAMESPACE" ~/Resource[$]Test*/ - "$SemanticAttributes.CODE_FUNCTION" "hello" - } - } - } - } - } - - where: - resource | abortNormal | abortPrematch | parentSpanName | controllerName | expectedResponse - "/test/hello/bob" | false | false | "/test/hello/{name}" | "Test1.hello" | "Test1 bob!" - "/test2/hello/bob" | false | false | "/test2/hello/{name}" | "Test2.hello" | "Test2 bob!" - "/test3/hi/bob" | false | false | "/test3/hi/{name}" | "Test3.hello" | "Test3 bob!" - - // Resteasy and Jersey give different resource class names for just the below case - // Resteasy returns "SubResource.class" - // Jersey returns "Test1.class - // "/test/hello/bob" | true | false | "/test/hello/{name}" | "Test1.hello" | "Aborted" - - "/test2/hello/bob" | true | false | "/test2/hello/{name}" | "Test2.hello" | "Aborted" - "/test3/hi/bob" | true | false | "/test3/hi/{name}" | "Test3.hello" | "Aborted" - "/test/hello/bob" | false | true | null | "PrematchRequestFilter.filter" | "Aborted Prematch" - "/test2/hello/bob" | false | true | null | "PrematchRequestFilter.filter" | "Aborted Prematch" - "/test3/hi/bob" | false | true | null | "PrematchRequestFilter.filter" | "Aborted Prematch" - } - - def "test nested call"() { - given: - simpleRequestFilter.abort = false - prematchRequestFilter.abort = false - - when: - def (responseText, responseStatus) = runRequest(resource) - - then: - responseStatus == Response.Status.OK.statusCode - responseText == expectedResponse - - assertTraces(1) { - trace(0, 2) { - span(0) { - name parentResourceName - kind SERVER - if (!runsOnServer()) { - attributes { - "$SemanticAttributes.HTTP_ROUTE" parentResourceName - } - } - } - span(1) { - childOf span(0) - name controller1Name - kind INTERNAL - attributes { - "$SemanticAttributes.CODE_NAMESPACE" ~/Resource[$]Test*/ - "$SemanticAttributes.CODE_FUNCTION" "nested" - } - } - } - } - - where: - resource | parentResourceName | controller1Name | expectedResponse - "/test3/nested" | "/test3/nested" | "Test3.nested" | "Test3 nested!" } @Provider diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy index 06128e0f7b6a..e01ef29f638f 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy @@ -3,325 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.asserts.TraceAssert -import io.opentelemetry.instrumentation.test.base.HttpServerTest -import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint -import io.opentelemetry.sdk.trace.data.SpanData -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import spock.lang.Unroll +import java.util.concurrent.TimeUnit import test.JaxRsTestResource -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.SpanKind.SERVER -import static io.opentelemetry.api.trace.StatusCode.ERROR -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP -import static java.util.concurrent.TimeUnit.SECONDS -import static org.junit.jupiter.api.Assumptions.assumeTrue - -abstract class JaxRsHttpServerTest extends HttpServerTest implements AgentTestTrait { - - def "test super method without @Path"() { - given: - def response = client.get(address.resolve("test-resource-super").toString()).aggregate().join() - - expect: - response.status().code() == SUCCESS.status - response.contentUtf8() == SUCCESS.body - - assertTraces(1) { - trace(0, 2) { - span(0) { - hasNoParent() - kind SERVER - name getContextPath() + "/test-resource-super" - } - span(1) { - name "controller" - kind INTERNAL - childOf span(0) - } - } - } - } - - def "test interface method with @Path"() { - assumeTrue(testInterfaceMethodWithPath()) - - given: - def response = client.get(address.resolve("test-resource-interface/call").toString()).aggregate().join() - - expect: - response.status().code() == SUCCESS.status - response.contentUtf8() == SUCCESS.body - - assertTraces(1) { - trace(0, 2) { - span(0) { - hasNoParent() - kind SERVER - name getContextPath() + "/test-resource-interface/call" - } - span(1) { - name "controller" - kind INTERNAL - childOf span(0) - } - } - } - } - - def "test sub resource locator"() { - given: - def response = client.get(address.resolve("test-sub-resource-locator/call/sub").toString()).aggregate().join() - - expect: - response.status().code() == SUCCESS.status - response.contentUtf8() == SUCCESS.body - - assertTraces(1) { - trace(0, 5) { - span(0) { - hasNoParent() - kind SERVER - name getContextPath() + "/test-sub-resource-locator/call/sub" - } - span(1) { - name "JaxRsSubResourceLocatorTestResource.call" - kind INTERNAL - childOf span(0) - } - span(2) { - name "controller" - kind INTERNAL - childOf span(1) - } - span(3) { - name "SubResource.call" - kind INTERNAL - childOf span(0) - } - span(4) { - name "controller" - kind INTERNAL - childOf span(3) - } - } - } - } - - @Unroll - def "should handle #desc AsyncResponse"() { - given: - def url = address.resolve("async?action=${action}").toString() - - when: "async call is started" - def futureResponse = client.get(url).aggregate() - - then: "there are no traces yet" - assertTraces(0) { - } - - when: "barrier is released and resource class sends response" - JaxRsTestResource.BARRIER.await(1, SECONDS) - def response = futureResponse.join() - - then: - response.status().code() == statusCode - bodyPredicate(response.contentUtf8()) - - def spanCount = 2 - def hasSendError = asyncCancelHasSendError() && action == "cancel" - if (hasSendError) { - spanCount++ - } - assertTraces(1) { - trace(0, spanCount) { - asyncServerSpan(it, 0, url, statusCode) - handlerSpan(it, 1, span(0), "asyncOp", isCancelled, isError, errorMessage) - if (hasSendError) { - sendErrorSpan(it, 2, span(1)) - } - } - } - - where: - desc | action | statusCode | bodyPredicate | isCancelled | isError | errorMessage - "successful" | "succeed" | 200 | { it == "success" } | false | false | null - "failing" | "throw" | 500 | { it == "failure" } | false | true | "failure" - "canceled" | "cancel" | 503 | { it instanceof String } | true | false | null - } - - @Unroll - def "should handle #desc CompletionStage (JAX-RS 2.1+ only)"() { - assumeTrue(shouldTestCompletableStageAsync()) - given: - def url = address.resolve("async-completion-stage?action=${action}").toString() - - when: "async call is started" - def futureResponse = client.get(url).aggregate() - - then: "there are no traces yet" - assertTraces(0) { - } - - when: "barrier is released and resource class sends response" - JaxRsTestResource.BARRIER.await(1, SECONDS) - def response = futureResponse.join() - - then: - response.status().code() == statusCode - bodyPredicate(response.contentUtf8()) - - assertTraces(1) { - trace(0, 2) { - asyncServerSpan(it, 0, url, statusCode) - handlerSpan(it, 1, span(0), "jaxRs21Async", false, isError, errorMessage) - } - } - - where: - desc | action | statusCode | bodyPredicate | isError | errorMessage - "successful" | "succeed" | 200 | { it == "success" } | false | null - "failing" | "throw" | 500 | { it == "failure" } | true | "failure" - } - - @Override - boolean hasHandlerSpan(ServerEndpoint endpoint) { - true - } - - @Override - boolean testNotFound() { - false - } +abstract class JaxRsHttpServerTest extends AbstractJaxRsHttpServerTest { @Override - boolean testPathParam() { - true - } - - boolean testInterfaceMethodWithPath() { - true - } - - boolean asyncCancelHasSendError() { - false - } - - boolean shouldTestCompletableStageAsync() { - Boolean.getBoolean("testLatestDeps") - } - - @Override - void serverSpan(TraceAssert trace, - int index, - String traceID = null, - String parentID = null, - String method = "GET", - Long responseContentLength = null, - ServerEndpoint endpoint = SUCCESS) { - serverSpan(trace, index, traceID, parentID, method, - endpoint == PATH_PARAM ? getContextPath() + "/path/{id}/param" : endpoint.resolvePath(address).path, - endpoint.resolve(address), - endpoint.status, - endpoint.query) - } - - void asyncServerSpan(TraceAssert trace, - int index, - String url, - int statusCode) { - def rawUrl = URI.create(url).toURL() - serverSpan(trace, index, null, null, "GET", - rawUrl.path, - rawUrl.toURI(), - statusCode, - null) - } - - void serverSpan(TraceAssert trace, - int index, - String traceID, - String parentID, - String method, - String path, - URI fullUrl, - int statusCode, - String query) { - trace.span(index) { - name path - kind SERVER - if (statusCode >= 500) { - status ERROR - } - if (parentID != null) { - traceId traceID - parentSpanId parentID - } else { - hasNoParent() - } - attributes { - "$SemanticAttributes.NET_PEER_IP" { it == null || it == "127.0.0.1" } // Optional - "$SemanticAttributes.NET_PEER_PORT" Long - "$SemanticAttributes.HTTP_SCHEME" fullUrl.getScheme() - "$SemanticAttributes.HTTP_HOST" fullUrl.getHost() + ":" + fullUrl.getPort() - "$SemanticAttributes.HTTP_TARGET" fullUrl.getPath() + (fullUrl.getQuery() != null ? "?" + fullUrl.getQuery() : "") - "$SemanticAttributes.HTTP_METHOD" method - "$SemanticAttributes.HTTP_STATUS_CODE" statusCode - "$SemanticAttributes.HTTP_FLAVOR" "1.1" - "$SemanticAttributes.HTTP_USER_AGENT" TEST_USER_AGENT - "$SemanticAttributes.HTTP_CLIENT_IP" TEST_CLIENT_IP - "$SemanticAttributes.NET_TRANSPORT" IP_TCP - // Optional - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } - "$SemanticAttributes.HTTP_ROUTE" path - if (fullUrl.getPath().endsWith(ServerEndpoint.CAPTURE_HEADERS.getPath())) { - "http.request.header.x_test_request" { it == ["test"] } - "http.response.header.x_test_response" { it == ["test"] } - } - } - } - } - - @Override - void handlerSpan(TraceAssert trace, - int index, - Object parent, - String method = "GET", - ServerEndpoint endpoint = SUCCESS) { - handlerSpan(trace, index, parent, - endpoint.name().toLowerCase(), - false, - endpoint == EXCEPTION, - EXCEPTION.body) - } - - void handlerSpan(TraceAssert trace, - int index, - Object parent, - String methodName, - boolean isCancelled, - boolean isError, - String exceptionMessage = null) { - trace.span(index) { - name "JaxRsTestResource.${methodName}" - kind INTERNAL - if (isError) { - status ERROR - errorEvent(Exception, exceptionMessage) - } - childOf((SpanData) parent) - attributes { - "$SemanticAttributes.CODE_NAMESPACE" "test.JaxRsTestResource" - "$SemanticAttributes.CODE_FUNCTION" methodName - if (isCancelled) { - "jaxrs.canceled" true - } - } - } + void awaitBarrier(int amount, TimeUnit timeUnit) { + JaxRsTestResource.BARRIER.await(amount, timeUnit) } } diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfRequestContextInstrumentation.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfRequestContextInstrumentation.java index fab7e8e69fa2..b73607795095 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfRequestContextInstrumentation.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfRequestContextInstrumentation.java @@ -15,6 +15,7 @@ import io.opentelemetry.context.Scope; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConstants; import java.lang.reflect.Method; import javax.ws.rs.container.ContainerRequestContext; import net.bytebuddy.asm.Advice; @@ -59,7 +60,7 @@ public static class ContainerRequestContextAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void decorateAbortSpan( @Advice.This AbstractRequestContextImpl requestContext, - @Local("otelHandlerData") HandlerData handlerData, + @Local("otelHandlerData") Jaxrs2HandlerData handlerData, @Local("otelContext") Context context, @Local("otelScope") Scope scope) { @@ -80,9 +81,9 @@ public static void decorateAbortSpan( Method method = invocationInfo.getMethodInfo().getMethodToInvoke(); Class resourceClass = invocationInfo.getRealClass(); - handlerData = new HandlerData(resourceClass, method); + handlerData = new Jaxrs2HandlerData(resourceClass, method); context = - RequestContextHelper.createOrUpdateAbortSpan( + Jaxrs2RequestContextHelper.createOrUpdateAbortSpan( instrumenter(), (ContainerRequestContext) requestContext, handlerData); if (context != null) { scope = context.makeCurrent(); @@ -91,7 +92,7 @@ public static void decorateAbortSpan( @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void stopSpan( - @Local("otelHandlerData") HandlerData handlerData, + @Local("otelHandlerData") Jaxrs2HandlerData handlerData, @Local("otelContext") Context context, @Local("otelScope") Scope scope, @Advice.Thrown Throwable throwable) { diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfSingletons.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfSingletons.java index 66ea21dcfac1..d882135aaf73 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfSingletons.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfSingletons.java @@ -6,6 +6,8 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.instrumentation.jaxrs.HandlerData; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsInstrumenterFactory; public final class CxfSingletons { diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfSpanName.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfSpanName.java index 2b9187075cf8..c156c16190e2 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfSpanName.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfSpanName.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; -import static io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0.JaxrsPathUtil.normalizePath; +import static io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsPathUtil.normalizePath; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/build.gradle.kts index b1a1b93d9bd8..93866cff601f 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/build.gradle.kts @@ -3,19 +3,19 @@ plugins { } muzzle { - // Cant assert fails because muzzle assumes all instrumentations will fail - // Instrumentations in jaxrs-2.0-common will pass pass { group.set("org.glassfish.jersey.core") module.set("jersey-server") versions.set("[2.0,3.0.0)") extraDependency("javax.servlet:javax.servlet-api:3.1.0") + assertInverse.set(true) } pass { group.set("org.glassfish.jersey.containers") module.set("jersey-container-servlet") versions.set("[2.0,3.0.0)") extraDependency("javax.servlet:javax.servlet-api:3.1.0") + assertInverse.set(true) } } @@ -30,22 +30,30 @@ dependencies { implementation(project(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent")) testInstrumentation(project(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-annotations:javaagent")) - testInstrumentation(project(":instrumentation:servlet:servlet-3.0:javaagent")) testInstrumentation(project(":instrumentation:servlet:servlet-javax-common:javaagent")) testImplementation(project(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:testing")) - - // First version with DropwizardTestSupport: - testLibrary("io.dropwizard:dropwizard-testing:0.8.0") testImplementation("javax.xml.bind:jaxb-api:2.2.3") - testImplementation("com.fasterxml.jackson.module:jackson-module-afterburner") + testImplementation("org.eclipse.jetty:jetty-webapp:9.4.6.v20170531") latestDepTestLibrary("org.glassfish.jersey.core:jersey-server:2.+") latestDepTestLibrary("org.glassfish.jersey.containers:jersey-container-servlet:2.+") - // this is needed because dropwizard-testing version 0.8.0 (above) pulls it in transitively, - // but the latest version of dropwizard-testing does not - latestDepTestLibrary("org.eclipse.jetty:jetty-webapp:9.+") + latestDepTestLibrary("org.glassfish.jersey.containers:jersey-container-servlet:2.+") + latestDepTestLibrary("org.glassfish.jersey.inject:jersey-hk2:2.+") +} + +if (!(findProperty("testLatestDeps") as Boolean)) { + // early jersey versions require old guava + configurations.testRuntimeClasspath.resolutionStrategy.force("com.google.guava:guava:14.0.1") + + configurations { + // early jersey versions bundle asm without shading + testImplementation { + exclude("org.ow2.asm", "asm") + exclude("org.ow2.asm", "asm-commons") + } + } } tasks { diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyRequestContextInstrumentation.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyRequestContextInstrumentation.java index 9e91868d3cfe..8490b1c94aeb 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyRequestContextInstrumentation.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseyRequestContextInstrumentation.java @@ -9,6 +9,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConstants; import java.lang.reflect.Method; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ResourceInfo; @@ -37,7 +38,7 @@ public static class ContainerRequestContextAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void decorateAbortSpan( @Advice.This ContainerRequestContext requestContext, - @Local("otelHandlerData") HandlerData handlerData, + @Local("otelHandlerData") Jaxrs2HandlerData handlerData, @Local("otelContext") Context context, @Local("otelScope") Scope scope) { UriInfo uriInfo = requestContext.getUriInfo(); @@ -55,9 +56,10 @@ public static void decorateAbortSpan( return; } - handlerData = new HandlerData(resourceClass, method); + handlerData = new Jaxrs2HandlerData(resourceClass, method); context = - RequestContextHelper.createOrUpdateAbortSpan(instrumenter(), requestContext, handlerData); + Jaxrs2RequestContextHelper.createOrUpdateAbortSpan( + instrumenter(), requestContext, handlerData); if (context != null) { scope = context.makeCurrent(); } @@ -65,7 +67,7 @@ public static void decorateAbortSpan( @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void stopSpan( - @Local("otelHandlerData") HandlerData handlerData, + @Local("otelHandlerData") Jaxrs2HandlerData handlerData, @Local("otelContext") Context context, @Local("otelScope") Scope scope, @Advice.Thrown Throwable throwable) { diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseySingletons.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseySingletons.java index 842947a8c334..7bea4456ec5b 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseySingletons.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseySingletons.java @@ -6,6 +6,8 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.instrumentation.jaxrs.HandlerData; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsInstrumenterFactory; public final class JerseySingletons { diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseySpanName.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseySpanName.java index 0c7e7f6f85c8..bcb0d83523a3 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseySpanName.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JerseySpanName.java @@ -5,7 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; -import static io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0.JaxrsPathUtil.normalizePath; +import static io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsPathUtil.normalizePath; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyFilterTest.groovy b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyFilterTest.groovy index ee3a3d11191b..d2ea506345f8 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyFilterTest.groovy +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-jersey-2.0/javaagent/src/test/groovy/JerseyFilterTest.groovy @@ -3,32 +3,82 @@ * SPDX-License-Identifier: Apache-2.0 */ -import io.dropwizard.testing.junit.ResourceTestRule -import org.junit.ClassRule -import spock.lang.Shared - -import javax.ws.rs.client.Entity -import javax.ws.rs.core.Response +import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import javax.ws.rs.core.Application +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.ServletContextHandler +import org.eclipse.jetty.servlet.ServletHolder +import org.glassfish.jersey.server.ResourceConfig +import org.glassfish.jersey.servlet.ServletContainer import static Resource.Test1 import static Resource.Test2 import static Resource.Test3 -class JerseyFilterTest extends JaxRsFilterTest { - @Shared - @ClassRule - ResourceTestRule resources = ResourceTestRule.builder() - .addResource(new Test1()) - .addResource(new Test2()) - .addResource(new Test3()) - .addProvider(simpleRequestFilter) - .addProvider(prematchRequestFilter) - .build() +class JerseyFilterTest extends JaxRsFilterTest implements HttpServerTestTrait { + + def setupSpec() { + setupServer() + } + + def cleanupSpec() { + cleanupServer() + } + + @Override + Server startServer(int port) { + def servlet = new ServletContainer(ResourceConfig.forApplication(new TestApplication())) + + def handler = new ServletContextHandler(ServletContextHandler.SESSIONS) + handler.setContextPath("/") + handler.addServlet(new ServletHolder(servlet), "/*") + + def server = new Server(port) + server.setHandler(handler) + server.start() + + return server + } @Override - def makeRequest(String url) { - Response response = resources.client().target(url).request().post(Entity.text("")) + void stopServer(Server httpServer) { + httpServer.stop() + } + + @Override + boolean runsOnServer() { + true + } + + @Override + String defaultServerSpanName() { + "/*" + } + + @Override + def makeRequest(String path) { + AggregatedHttpResponse response = client.post(address.resolve(path).toString(), "").aggregate().join() + + return [response.contentUtf8(), response.status().code()] + } + + class TestApplication extends Application { + @Override + Set> getClasses() { + def classes = new HashSet() + classes.add(Test1) + classes.add(Test2) + classes.add(Test3) + return classes + } - return [response.readEntity(String), response.statusInfo.statusCode] + @Override + Set getSingletons() { + def singletons = new HashSet() + singletons.add(simpleRequestFilter) + singletons.add(prematchRequestFilter) + return singletons + } } } \ No newline at end of file diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/build.gradle.kts index 7826de444fbb..0b947828f56c 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/build.gradle.kts @@ -3,9 +3,6 @@ plugins { } muzzle { - // Cant assert fails because muzzle assumes all instrumentations will fail - // Instrumentations in jaxrs-2.0-common will pass - // Resteasy changes a class's package in 3.1.0 then moves it back in 3.5.0 and then moves it forward again in 4.0.0 // so the jaxrs-2.0-resteasy-3.0 module applies to [3.0, 3.1) and [3.5, 4.0) // and the jaxrs-2.0-resteasy-3.1 module applies to [3.1, 3.5) and [4.0, ) @@ -20,6 +17,20 @@ muzzle { module.set("resteasy-jaxrs") versions.set("[3.5.0.Final,4)") } + + fail { + group.set("org.jboss.resteasy") + module.set("resteasy-jaxrs") + versions.set("(2.1.0.GA,3.0.0.Final)") + // missing dependencies + skip("2.3.10.Final") + } + + fail { + group.set("org.jboss.resteasy") + module.set("resteasy-core") + versions.set("[4.0.0.Final,)") + } } dependencies { diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30RequestContextInstrumentation.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30RequestContextInstrumentation.java index e61067a8c944..b94d17d87901 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30RequestContextInstrumentation.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30RequestContextInstrumentation.java @@ -9,6 +9,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConstants; import java.lang.reflect.Method; import javax.ws.rs.container.ContainerRequestContext; import net.bytebuddy.asm.Advice; @@ -38,7 +39,7 @@ public static class ContainerRequestContextAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void decorateAbortSpan( @Advice.This ContainerRequestContext requestContext, - @Local("otelHandlerData") HandlerData handlerData, + @Local("otelHandlerData") Jaxrs2HandlerData handlerData, @Local("otelContext") Context context, @Local("otelScope") Scope scope) { if (requestContext.getProperty(JaxrsConstants.ABORT_HANDLED) != null @@ -51,9 +52,10 @@ public static void decorateAbortSpan( Method method = resourceMethodInvoker.getMethod(); Class resourceClass = resourceMethodInvoker.getResourceClass(); - handlerData = new HandlerData(resourceClass, method); + handlerData = new Jaxrs2HandlerData(resourceClass, method); context = - RequestContextHelper.createOrUpdateAbortSpan(instrumenter(), requestContext, handlerData); + Jaxrs2RequestContextHelper.createOrUpdateAbortSpan( + instrumenter(), requestContext, handlerData); if (context != null) { scope = context.makeCurrent(); } @@ -61,7 +63,7 @@ public static void decorateAbortSpan( @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void stopSpan( - @Local("otelHandlerData") HandlerData handlerData, + @Local("otelHandlerData") Jaxrs2HandlerData handlerData, @Local("otelContext") Context context, @Local("otelScope") Scope scope, @Advice.Thrown Throwable throwable) { diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30Singletons.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30Singletons.java index e53f57de2cb9..60b2403a28f5 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30Singletons.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30Singletons.java @@ -6,6 +6,8 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.instrumentation.jaxrs.HandlerData; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsInstrumenterFactory; public final class Resteasy30Singletons { diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/build.gradle.kts index c148b0c6240b..e42fdaa5e3db 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/build.gradle.kts +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/build.gradle.kts @@ -3,9 +3,6 @@ plugins { } muzzle { - // Cant assert fails because muzzle assumes all instrumentations will fail - // Instrumentations in jaxrs-2.0-common will pass - // Resteasy changes a class's package in 3.1.0 then moves it back in 3.5.0 and then moves it forward again in 4.0.0 // so the jaxrs-2.0-resteasy-3.0 module applies to [3.0, 3.1) and [3.5, 4.0) // and the jaxrs-2.0-resteasy-3.1 module applies to [3.1, 3.5) and [4.0, ) @@ -20,6 +17,20 @@ muzzle { module.set("resteasy-core") versions.set("[4.0.0.Final,6)") } + + fail { + group.set("org.jboss.resteasy") + module.set("resteasy-jaxrs") + versions.set("(2.1.0.GA,3.1.0.Final)") + // missing dependencies + skip("2.3.10.Final") + } + + fail { + group.set("org.jboss.resteasy") + module.set("resteasy-core") + versions.set("[6.0.0.Final,)") + } } dependencies { diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31RequestContextInstrumentation.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31RequestContextInstrumentation.java index f63d4f1bc0fe..221698307fdd 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31RequestContextInstrumentation.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31RequestContextInstrumentation.java @@ -9,6 +9,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConstants; import java.lang.reflect.Method; import javax.ws.rs.container.ContainerRequestContext; import net.bytebuddy.asm.Advice; @@ -38,7 +39,7 @@ public static class ContainerRequestContextAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void decorateAbortSpan( @Advice.This ContainerRequestContext requestContext, - @Local("otelHandlerData") HandlerData handlerData, + @Local("otelHandlerData") Jaxrs2HandlerData handlerData, @Local("otelContext") Context context, @Local("otelScope") Scope scope) { if (requestContext.getProperty(JaxrsConstants.ABORT_HANDLED) != null @@ -51,9 +52,10 @@ public static void decorateAbortSpan( Method method = resourceMethodInvoker.getMethod(); Class resourceClass = resourceMethodInvoker.getResourceClass(); - handlerData = new HandlerData(resourceClass, method); + handlerData = new Jaxrs2HandlerData(resourceClass, method); context = - RequestContextHelper.createOrUpdateAbortSpan(instrumenter(), requestContext, handlerData); + Jaxrs2RequestContextHelper.createOrUpdateAbortSpan( + instrumenter(), requestContext, handlerData); if (context != null) { scope = context.makeCurrent(); } @@ -61,7 +63,7 @@ public static void decorateAbortSpan( @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void stopSpan( - @Local("otelHandlerData") HandlerData handlerData, + @Local("otelHandlerData") Jaxrs2HandlerData handlerData, @Local("otelContext") Context context, @Local("otelScope") Scope scope, @Advice.Thrown Throwable throwable) { diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31Singletons.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31Singletons.java index 0ba9cf823741..6e3d27d41239 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31Singletons.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31Singletons.java @@ -6,6 +6,8 @@ package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.instrumentation.jaxrs.HandlerData; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsInstrumenterFactory; public final class Resteasy31Singletons { diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyRootNodeTypeInstrumentation.java b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyRootNodeTypeInstrumentation.java index aaceaacbedac..18676b12aa38 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyRootNodeTypeInstrumentation.java +++ b/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/ResteasyRootNodeTypeInstrumentation.java @@ -12,6 +12,7 @@ import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsPathUtil; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.implementation.bytecode.assign.Assigner; diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/build.gradle.kts new file mode 100644 index 000000000000..d21429e52eaf --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("jakarta.ws.rs") + module.set("jakarta.ws.rs-api") + versions.set("[3.0.0,)") + assertInverse.set(true) + } +} + +dependencies { + bootstrap(project(":instrumentation:jaxrs:jaxrs-common:bootstrap")) + + implementation(project(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-common:javaagent")) + + compileOnly("jakarta.ws.rs:jakarta.ws.rs-api:3.0.0") + + testImplementation("jakarta.ws.rs:jakarta.ws.rs-api:3.0.0") +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ContainerRequestFilterInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ContainerRequestFilterInstrumentation.java new file mode 100644 index 000000000000..8e545d9fdf64 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ContainerRequestFilterInstrumentation.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConstants; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * This adds the filter class name to the request properties. The class name is used by + * DefaultRequestContextInstrumentation + */ +public class ContainerRequestFilterInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("jakarta.ws.rs.container.ContainerRequestFilter"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("jakarta.ws.rs.container.ContainerRequestFilter")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("filter")) + .and(takesArguments(1)) + .and(takesArgument(0, named("jakarta.ws.rs.container.ContainerRequestContext"))), + ContainerRequestFilterInstrumentation.class.getName() + "$RequestFilterAdvice"); + } + + @SuppressWarnings("unused") + public static class RequestFilterAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void setFilterClass( + @Advice.This ContainerRequestFilter filter, + @Advice.Argument(0) ContainerRequestContext context) { + context.setProperty(JaxrsConstants.ABORT_FILTER_CLASS, filter.getClass()); + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/DefaultRequestContextInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/DefaultRequestContextInstrumentation.java new file mode 100644 index 000000000000..8960484733d4 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/DefaultRequestContextInstrumentation.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0.JaxrsAnnotationsSingletons.instrumenter; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConstants; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsServerSpanNaming; +import jakarta.ws.rs.container.ContainerRequestContext; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.Local; + +/** + * Default context instrumentation. + * + *

JAX-RS does not define a way to get the matched resource method from the + * ContainerRequestContext + * + *

This default instrumentation uses the class name of the filter to create the span. More + * specific instrumentations may override this value. + */ +public class DefaultRequestContextInstrumentation extends AbstractRequestContextInstrumentation { + @Override + protected String abortAdviceName() { + return getClass().getName() + "$ContainerRequestContextAdvice"; + } + + @SuppressWarnings("unused") + public static class ContainerRequestContextAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void createGenericSpan( + @Advice.This ContainerRequestContext requestContext, + @Local("otelHandlerData") Jaxrs3HandlerData handlerData, + @Local("otelContext") Context context, + @Local("otelScope") Scope scope) { + if (requestContext.getProperty(JaxrsConstants.ABORT_HANDLED) != null) { + return; + } + + Class filterClass = + (Class) requestContext.getProperty(JaxrsConstants.ABORT_FILTER_CLASS); + Method method = null; + try { + method = filterClass.getMethod("filter", ContainerRequestContext.class); + } catch (NoSuchMethodException e) { + // Unable to find the filter method. This should not be reachable because the context + // can only be aborted inside the filter method + } + + if (filterClass == null || method == null) { + return; + } + + Context parentContext = Java8BytecodeBridge.currentContext(); + handlerData = new Jaxrs3HandlerData(filterClass, method); + + HttpRouteHolder.updateHttpRoute( + parentContext, + HttpRouteSource.CONTROLLER, + JaxrsServerSpanNaming.SERVER_SPAN_NAME, + handlerData); + + if (!instrumenter().shouldStart(parentContext, handlerData)) { + return; + } + + context = instrumenter().start(parentContext, handlerData); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Local("otelHandlerData") Jaxrs3HandlerData handlerData, + @Local("otelContext") Context context, + @Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + if (scope == null) { + return; + } + + scope.close(); + instrumenter().end(context, handlerData, null, throwable); + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentation.java new file mode 100644 index 000000000000..61f6055424ff --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentation.java @@ -0,0 +1,171 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperMethod; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0.JaxrsAnnotationsSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.bootstrap.CallDepth; +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.jaxrs.AsyncResponseData; +import io.opentelemetry.javaagent.instrumentation.jaxrs.CompletionStageFinishCallback; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsServerSpanNaming; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.container.AsyncResponse; +import java.lang.reflect.Method; +import java.util.concurrent.CompletionStage; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner.Typing; +import net.bytebuddy.matcher.ElementMatcher; + +public class JaxrsAnnotationsInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("jakarta.ws.rs.Path"); + } + + @Override + public ElementMatcher typeMatcher() { + return hasSuperType( + isAnnotatedWith(named("jakarta.ws.rs.Path")) + .or(declaresMethod(isAnnotatedWith(named("jakarta.ws.rs.Path"))))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and( + hasSuperMethod( + isAnnotatedWith( + namedOneOf( + "jakarta.ws.rs.Path", + "jakarta.ws.rs.DELETE", + "jakarta.ws.rs.GET", + "jakarta.ws.rs.HEAD", + "jakarta.ws.rs.OPTIONS", + "jakarta.ws.rs.PATCH", + "jakarta.ws.rs.POST", + "jakarta.ws.rs.PUT")))), + JaxrsAnnotationsInstrumentation.class.getName() + "$JaxRsAnnotationsAdvice"); + } + + @SuppressWarnings("unused") + public static class JaxRsAnnotationsAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void nameSpan( + @Advice.This Object target, + @Advice.Origin Method method, + @Advice.AllArguments Object[] args, + @Advice.Local("otelCallDepth") CallDepth callDepth, + @Advice.Local("otelHandlerData") Jaxrs3HandlerData handlerData, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("otelAsyncResponse") AsyncResponse asyncResponse) { + callDepth = CallDepth.forClass(Path.class); + if (callDepth.getAndIncrement() > 0) { + return; + } + + VirtualField virtualField = null; + for (Object arg : args) { + if (arg instanceof AsyncResponse) { + asyncResponse = (AsyncResponse) arg; + virtualField = VirtualField.find(AsyncResponse.class, AsyncResponseData.class); + if (virtualField.get(asyncResponse) != null) { + /* + * We are probably in a recursive call and don't want to start a new span because it + * would replace the existing span in the asyncResponse and cause it to never finish. We + * could work around this by using a list instead, but we likely don't want the extra + * span anyway. + */ + return; + } + break; + } + } + + Context parentContext = Java8BytecodeBridge.currentContext(); + handlerData = new Jaxrs3HandlerData(target.getClass(), method); + + HttpRouteHolder.updateHttpRoute( + parentContext, + HttpRouteSource.CONTROLLER, + JaxrsServerSpanNaming.SERVER_SPAN_NAME, + handlerData); + + if (!instrumenter().shouldStart(parentContext, handlerData)) { + return; + } + + context = instrumenter().start(parentContext, handlerData); + scope = context.makeCurrent(); + + if (virtualField != null && asyncResponse != null) { + virtualField.set(asyncResponse, AsyncResponseData.create(context, handlerData)); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Return(readOnly = false, typing = Typing.DYNAMIC) Object returnValue, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelCallDepth") CallDepth callDepth, + @Advice.Local("otelHandlerData") Jaxrs3HandlerData handlerData, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Local("otelAsyncResponse") AsyncResponse asyncResponse) { + if (callDepth.decrementAndGet() > 0) { + return; + } + + if (scope == null) { + return; + } + + scope.close(); + + if (throwable != null) { + instrumenter().end(context, handlerData, null, throwable); + return; + } + + CompletionStage asyncReturnValue = + returnValue instanceof CompletionStage ? (CompletionStage) returnValue : null; + + if (asyncResponse != null && !asyncResponse.isSuspended()) { + // Clear span from the asyncResponse. Logically this should never happen. Added to be safe. + VirtualField.find(AsyncResponse.class, AsyncResponseData.class).set(asyncResponse, null); + } + if (asyncReturnValue != null) { + // span finished by CompletionStageFinishCallback + asyncReturnValue = + asyncReturnValue.handle( + new CompletionStageFinishCallback<>(instrumenter(), context, handlerData)); + } + if ((asyncResponse == null || !asyncResponse.isSuspended()) && asyncReturnValue == null) { + instrumenter().end(context, handlerData, null, null); + } + // else span finished by AsyncResponse*Advice + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentationModule.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentationModule.java new file mode 100644 index 000000000000..a0ac9744aace --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsInstrumentationModule.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static java.util.Arrays.asList; + +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 JaxrsAnnotationsInstrumentationModule extends InstrumentationModule { + public JaxrsAnnotationsInstrumentationModule() { + super("jaxrs-annotations", "jaxrs-3.0-annotations"); + } + + // require jax-rs 3 + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("jakarta.ws.rs.container.AsyncResponse"); + } + + @Override + public List typeInstrumentations() { + return asList( + new ContainerRequestFilterInstrumentation(), + new DefaultRequestContextInstrumentation(), + new JaxrsAnnotationsInstrumentation(), + new JaxrsAsyncResponseInstrumentation()); + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsSingletons.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsSingletons.java new file mode 100644 index 000000000000..c319f59756d5 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAnnotationsSingletons.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.instrumentation.jaxrs.HandlerData; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsInstrumenterFactory; + +public final class JaxrsAnnotationsSingletons { + + private static final Instrumenter INSTANCE = + JaxrsInstrumenterFactory.createInstrumenter("io.opentelemetry.jaxrs-annotations-3.0"); + + public static Instrumenter instrumenter() { + return INSTANCE; + } + + private JaxrsAnnotationsSingletons() {} +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAsyncResponseInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAsyncResponseInstrumentation.java new file mode 100644 index 000000000000..ebda8ceac828 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JaxrsAsyncResponseInstrumentation.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_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.jaxrs.v3_0.JaxrsAnnotationsSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.util.VirtualField; +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.jaxrs.AsyncResponseData; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConfig; +import jakarta.ws.rs.container.AsyncResponse; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JaxrsAsyncResponseInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("jakarta.ws.rs.container.AsyncResponse"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("jakarta.ws.rs.container.AsyncResponse")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("resume").and(takesArgument(0, Object.class)).and(isPublic()), + JaxrsAsyncResponseInstrumentation.class.getName() + "$AsyncResponseAdvice"); + transformer.applyAdviceToMethod( + named("resume").and(takesArgument(0, Throwable.class)).and(isPublic()), + JaxrsAsyncResponseInstrumentation.class.getName() + "$AsyncResponseThrowableAdvice"); + transformer.applyAdviceToMethod( + named("cancel"), + JaxrsAsyncResponseInstrumentation.class.getName() + "$AsyncResponseCancelAdvice"); + } + + @SuppressWarnings("unused") + public static class AsyncResponseAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void stopSpan(@Advice.This AsyncResponse asyncResponse) { + + VirtualField virtualField = + VirtualField.find(AsyncResponse.class, AsyncResponseData.class); + + AsyncResponseData data = virtualField.get(asyncResponse); + if (data != null) { + virtualField.set(asyncResponse, null); + instrumenter().end(data.getContext(), data.getHandlerData(), null, null); + } + } + } + + @SuppressWarnings("unused") + public static class AsyncResponseThrowableAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void stopSpan( + @Advice.This AsyncResponse asyncResponse, @Advice.Argument(0) Throwable throwable) { + + VirtualField virtualField = + VirtualField.find(AsyncResponse.class, AsyncResponseData.class); + + AsyncResponseData data = virtualField.get(asyncResponse); + if (data != null) { + virtualField.set(asyncResponse, null); + instrumenter().end(data.getContext(), data.getHandlerData(), null, throwable); + } + } + } + + @SuppressWarnings("unused") + public static class AsyncResponseCancelAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void stopSpan(@Advice.This AsyncResponse asyncResponse) { + + VirtualField virtualField = + VirtualField.find(AsyncResponse.class, AsyncResponseData.class); + + AsyncResponseData data = virtualField.get(asyncResponse); + if (data != null) { + virtualField.set(asyncResponse, null); + Context context = data.getContext(); + if (JaxrsConfig.CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) { + Java8BytecodeBridge.spanFromContext(context).setAttribute("jaxrs.canceled", true); + } + instrumenter().end(context, data.getHandlerData(), null, null); + } + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/test/groovy/JaxrsAnnotationsInstrumentationTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/test/groovy/JaxrsAnnotationsInstrumentationTest.groovy new file mode 100644 index 000000000000..44ffe462f59d --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/test/groovy/JaxrsAnnotationsInstrumentationTest.groovy @@ -0,0 +1,162 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import spock.lang.Unroll + +import jakarta.ws.rs.DELETE +import jakarta.ws.rs.GET +import jakarta.ws.rs.HEAD +import jakarta.ws.rs.OPTIONS +import jakarta.ws.rs.POST +import jakarta.ws.rs.PUT +import jakarta.ws.rs.Path + +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.instrumentation.test.utils.ClassUtils.getClassName + +class JaxrsAnnotationsInstrumentationTest extends AgentInstrumentationSpecification { + + @Unroll + def "span named '#paramName' from annotations on class '#className' when is not root span"() { + setup: + runWithHttpServerSpan("test") { + obj.call() + } + + expect: + assertTraces(1) { + trace(0, 2) { + span(0) { + name paramName + kind SERVER + hasNoParent() + attributes { + "$SemanticAttributes.HTTP_ROUTE" paramName + } + } + span(1) { + name "${className}.call" + childOf span(0) + attributes { + "$SemanticAttributes.CODE_NAMESPACE" obj.getClass().getName() + "$SemanticAttributes.CODE_FUNCTION" "call" + } + } + } + } + + when: "multiple calls to the same method" + runWithHttpServerSpan("test") { + (1..10).each { + obj.call() + } + } + then: "doesn't increase the cache size" + + where: + paramName | obj + "/a" | new Jax() { + @Path("/a") + void call() { + } + } + "/b" | new Jax() { + @GET + @Path("/b") + void call() { + } + } + "/interface/c" | new InterfaceWithPath() { + @POST + @Path("/c") + void call() { + } + } + "/interface" | new InterfaceWithPath() { + @HEAD + void call() { + } + } + "/abstract/d" | new AbstractClassWithPath() { + @POST + @Path("/d") + void call() { + } + } + "/abstract" | new AbstractClassWithPath() { + @PUT + void call() { + } + } + "/child/e" | new ChildClassWithPath() { + @OPTIONS + @Path("/e") + void call() { + } + } + "/child/call" | new ChildClassWithPath() { + @DELETE + void call() { + } + } + "/child/call" | new ChildClassWithPath() + "/child/call" | new JavaInterfaces.ChildClassOnInterface() + "/child/call" | new JavaInterfaces.DefaultChildClassOnInterface() + + className = getClassName(obj.class) + } + + def "no annotations has no effect"() { + setup: + runWithHttpServerSpan("test") { + obj.call() + } + + expect: + assertTraces(1) { + trace(0, 1) { + span(0) { + name "test" + kind SERVER + attributes { + } + } + } + } + + where: + obj | _ + new Jax() { + void call() { + } + } | _ + } + + interface Jax { + void call() + } + + @Path("/interface") + interface InterfaceWithPath extends Jax { + @GET + void call() + } + + @Path("/abstract") + static abstract class AbstractClassWithPath implements Jax { + @PUT + abstract void call() + } + + @Path("child") + static class ChildClassWithPath extends AbstractClassWithPath { + @Path("call") + @POST + void call() { + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/test/java/JavaInterfaces.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/test/java/JavaInterfaces.java new file mode 100644 index 000000000000..0e747d3d3d72 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/test/java/JavaInterfaces.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +public class JavaInterfaces { + + interface Jax { + + void call(); + } + + @Path("interface") + interface InterfaceWithClassMethodPath extends Jax { + + @Override + @GET + @Path("invoke") + void call(); + } + + @Path("abstract") + abstract static class AbstractClassOnInterfaceWithClassPath + implements InterfaceWithClassMethodPath { + + @GET + @Path("call") + @Override + public void call() { + // do nothing + } + + abstract void actual(); + } + + @Path("child") + static class ChildClassOnInterface extends AbstractClassOnInterfaceWithClassPath { + + @Override + void actual() { + // do nothing + } + } + + @Path("interface") + interface DefaultInterfaceWithClassMethodPath extends Jax { + + @Override + @GET + @Path("call") + default void call() { + actual(); + } + + void actual(); + } + + @Path("child") + static class DefaultChildClassOnInterface implements DefaultInterfaceWithClassMethodPath { + + @Override + public void actual() { + // do nothing + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/build.gradle.kts new file mode 100644 index 000000000000..ad7ee82e3d7f --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +dependencies { + bootstrap(project(":instrumentation:jaxrs:jaxrs-common:bootstrap")) + api(project(":instrumentation:jaxrs:jaxrs-common:javaagent")) + + compileOnly("jakarta.ws.rs:jakarta.ws.rs-api:3.0.0") +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/AbstractRequestContextInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/AbstractRequestContextInstrumentation.java new file mode 100644 index 000000000000..a6e31bb8f1c4 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/AbstractRequestContextInstrumentation.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public abstract class AbstractRequestContextInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("jakarta.ws.rs.container.ContainerRequestContext"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("jakarta.ws.rs.container.ContainerRequestContext")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("abortWith")) + .and(takesArguments(1)) + .and(takesArgument(0, named("jakarta.ws.rs.core.Response"))), + abortAdviceName()); + } + + protected abstract String abortAdviceName(); +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/Jaxrs3HandlerData.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/Jaxrs3HandlerData.java new file mode 100644 index 000000000000..3aab14a4de1c --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/Jaxrs3HandlerData.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import io.opentelemetry.javaagent.instrumentation.jaxrs.HandlerData; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.Path; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +public class Jaxrs3HandlerData extends HandlerData { + + private static final ClassValue> serverSpanNames = + new ClassValue>() { + @Override + protected Map computeValue(Class type) { + return new ConcurrentHashMap<>(); + } + }; + + public Jaxrs3HandlerData(Class target, Method method) { + super(target, method); + } + + /** + * Returns the span name given a JaxRS annotated method. Results are cached so this method can be + * called multiple times without significantly impacting performance. + * + * @return The result can be an empty string but will never be {@code null}. + */ + @Override + public String getServerSpanName() { + Map classMap = serverSpanNames.get(target); + String spanName = classMap.get(method); + if (spanName == null) { + spanName = super.getServerSpanName(); + classMap.put(method, spanName); + } + + return spanName; + } + + @Override + protected Class getHttpMethodAnnotation() { + return HttpMethod.class; + } + + @Override + protected Supplier getPathAnnotation(AnnotatedElement annotatedElement) { + Path path = annotatedElement.getAnnotation(Path.class); + return path != null ? path::value : null; + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/Jaxrs3RequestContextHelper.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/Jaxrs3RequestContextHelper.java new file mode 100644 index 000000000000..db7f91424225 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/Jaxrs3RequestContextHelper.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.instrumentation.jaxrs.HandlerData; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConstants; +import io.opentelemetry.javaagent.instrumentation.jaxrs.RequestContextHelper; +import jakarta.ws.rs.container.ContainerRequestContext; + +public final class Jaxrs3RequestContextHelper { + public static Context createOrUpdateAbortSpan( + Instrumenter instrumenter, ContainerRequestContext requestContext, T handlerData) { + + if (handlerData == null) { + return null; + } + + requestContext.setProperty(JaxrsConstants.ABORT_HANDLED, true); + return RequestContextHelper.createOrUpdateAbortSpan(instrumenter, handlerData); + } + + private Jaxrs3RequestContextHelper() {} +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/build.gradle.kts b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/build.gradle.kts new file mode 100644 index 000000000000..a1978a3d8202 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + api(project(":instrumentation:jaxrs:jaxrs-common:testing")) + api("jakarta.ws.rs:jakarta.ws.rs-api:3.0.0") + + compileOnly("org.eclipse.jetty:jetty-webapp:11.0.0") +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsFilterTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsFilterTest.groovy new file mode 100644 index 000000000000..ede5e5618a0b --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsFilterTest.groovy @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import jakarta.ws.rs.container.ContainerRequestContext +import jakarta.ws.rs.container.ContainerRequestFilter +import jakarta.ws.rs.container.PreMatching +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.Provider +import spock.lang.Shared +import spock.lang.Unroll + +@Unroll +abstract class JaxRsFilterTest extends AbstractJaxRsFilterTest { + + @Shared + SimpleRequestFilter simpleRequestFilter = new SimpleRequestFilter() + + @Shared + PrematchRequestFilter prematchRequestFilter = new PrematchRequestFilter() + + @Override + void setAbortStatus(boolean abortNormal, boolean abortPrematch) { + simpleRequestFilter.abort = abortNormal + prematchRequestFilter.abort = abortPrematch + } + + @Provider + static class SimpleRequestFilter implements ContainerRequestFilter { + boolean abort = false + + @Override + void filter(ContainerRequestContext requestContext) throws IOException { + if (abort) { + requestContext.abortWith( + Response.status(Response.Status.UNAUTHORIZED) + .entity("Aborted") + .type(MediaType.TEXT_PLAIN_TYPE) + .build()) + } + } + } + + @Provider + @PreMatching + static class PrematchRequestFilter implements ContainerRequestFilter { + boolean abort = false + + @Override + void filter(ContainerRequestContext requestContext) throws IOException { + if (abort) { + requestContext.abortWith( + Response.status(Response.Status.UNAUTHORIZED) + .entity("Aborted Prematch") + .type(MediaType.TEXT_PLAIN_TYPE) + .build()) + } + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy new file mode 100644 index 000000000000..e01ef29f638f --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsHttpServerTest.groovy @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.util.concurrent.TimeUnit +import test.JaxRsTestResource + +abstract class JaxRsHttpServerTest extends AbstractJaxRsHttpServerTest { + + @Override + void awaitBarrier(int amount, TimeUnit timeUnit) { + JaxRsTestResource.BARRIER.await(amount, timeUnit) + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy new file mode 100644 index 000000000000..ce6e380afebe --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/JaxRsJettyHttpServerTest.groovy @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.webapp.WebAppContext + +import static org.eclipse.jetty.util.resource.Resource.newResource + +abstract class JaxRsJettyHttpServerTest extends JaxRsHttpServerTest { + + @Override + Server startServer(int port) { + WebAppContext webAppContext = new WebAppContext() + webAppContext.setContextPath("/") + // set up test application + webAppContext.setBaseResource(newResource("src/test/webapp")) + + def jettyServer = new Server(port) + jettyServer.connectors.each { + it.setHost('localhost') + } + + jettyServer.setHandler(webAppContext) + jettyServer.start() + + return jettyServer + } + + @Override + void stopServer(Server server) { + server.stop() + server.destroy() + } + + @Override + String getContextPath() { + "/rest-app" + } +} \ No newline at end of file diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/test/JaxRsTestResource.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/test/JaxRsTestResource.groovy new file mode 100644 index 000000000000..558eaf3b34ed --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/groovy/test/JaxRsTestResource.groovy @@ -0,0 +1,239 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package test + +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import jakarta.ws.rs.ApplicationPath +import jakarta.ws.rs.GET +import jakarta.ws.rs.HeaderParam +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.QueryParam +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended +import jakarta.ws.rs.core.Application +import jakarta.ws.rs.core.Context +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.core.UriInfo +import jakarta.ws.rs.ext.ExceptionMapper +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionStage +import java.util.concurrent.CyclicBarrier + +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS +import static java.util.concurrent.TimeUnit.SECONDS + +@Path("") +class JaxRsTestResource { + @Path("/success") + @GET + String success() { + HttpServerTest.controller(SUCCESS) { + SUCCESS.body + } + } + + @Path("query") + @GET + String query_param(@QueryParam("some") String param) { + HttpServerTest.controller(QUERY_PARAM) { + "some=$param" + } + } + + @Path("redirect") + @GET + Response redirect(@Context UriInfo uriInfo) { + HttpServerTest.controller(REDIRECT) { + Response.status(Response.Status.FOUND) + .location(uriInfo.relativize(new URI(REDIRECT.body))) + .build() + } + } + + @Path("error-status") + @GET + Response error() { + HttpServerTest.controller(ERROR) { + Response.status(ERROR.status) + .entity(ERROR.body) + .build() + } + } + + @Path("exception") + @GET + Object exception() { + HttpServerTest.controller(EXCEPTION) { + throw new Exception(EXCEPTION.body) + } + } + + @Path("path/{id}/param") + @GET + String path_param(@PathParam("id") int id) { + HttpServerTest.controller(PATH_PARAM) { + id + } + } + + @GET + @Path("captureHeaders") + Response capture_headers(@HeaderParam("X-Test-Request") String header) { + HttpServerTest.controller(CAPTURE_HEADERS) { + Response.status(CAPTURE_HEADERS.status) + .header("X-Test-Response", header) + .entity(CAPTURE_HEADERS.body) + .build() + } + } + + @Path("/child") + @GET + void indexed_child(@Context UriInfo uriInfo, @Suspended AsyncResponse response) { + def parameters = uriInfo.queryParameters + + CompletableFuture.runAsync({ + HttpServerTest.controller(INDEXED_CHILD) { + INDEXED_CHILD.collectSpanAttributes { parameters.getFirst(it) } + response.resume("") + } + }) + } + + static final BARRIER = new CyclicBarrier(2) + + @Path("async") + @GET + void asyncOp(@Suspended AsyncResponse response, @QueryParam("action") String action) { + CompletableFuture.runAsync({ + // await for the test method to verify that there are no spans yet + BARRIER.await(1, SECONDS) + + switch (action) { + case "succeed": + response.resume("success") + break + case "throw": + response.resume(new Exception("failure")) + break + case "cancel": + response.cancel() + break + default: + response.resume(new AssertionError((Object) ("invalid action value: " + action))) + break + } + }) + } + + @Path("async-completion-stage") + @GET + CompletionStage jaxRs21Async(@QueryParam("action") String action) { + def result = new CompletableFuture() + CompletableFuture.runAsync({ + // await for the test method to verify that there are no spans yet + BARRIER.await(1, SECONDS) + + switch (action) { + case "succeed": + result.complete("success") + break + case "throw": + result.completeExceptionally(new Exception("failure")) + break + default: + result.completeExceptionally(new AssertionError((Object) ("invalid action value: " + action))) + break + } + }) + result + } +} + +@Path("test-resource-super") +class JaxRsSuperClassTestResource extends JaxRsSuperClassTestResourceSuper { +} + +class JaxRsSuperClassTestResourceSuper { + @GET + Object call() { + HttpServerTest.controller(SUCCESS) { + SUCCESS.body + } + } +} + +class JaxRsInterfaceClassTestResource extends JaxRsInterfaceClassTestResourceSuper implements JaxRsInterface { +} + +@Path("test-resource-interface") +interface JaxRsInterface { + @Path("call") + @GET + Object call() +} + +class JaxRsInterfaceClassTestResourceSuper { + Object call() { + HttpServerTest.controller(SUCCESS) { + SUCCESS.body + } + } +} + +@Path("test-sub-resource-locator") +class JaxRsSubResourceLocatorTestResource { + @Path("call") + Object call() { + HttpServerTest.controller(SUCCESS) { + return new SubResource() + } + } +} + +class SubResource { + @Path("sub") + @GET + String call() { + HttpServerTest.controller(SUCCESS) { + SUCCESS.body + } + } +} + +class JaxRsTestExceptionMapper implements ExceptionMapper { + @Override + Response toResponse(Exception exception) { + return Response.status(500) + .entity(exception.message) + .build() + } +} + +class JaxRsTestApplication extends Application { + @Override + Set> getClasses() { + def classes = new HashSet() + classes.add(JaxRsTestResource) + classes.add(JaxRsSuperClassTestResource) + classes.add(JaxRsInterfaceClassTestResource) + classes.add(JaxRsSubResourceLocatorTestResource) + classes.add(JaxRsTestExceptionMapper) + return classes + } +} + +@ApplicationPath("/rest-app") +class JaxRsApplicationPathTestApplication extends JaxRsTestApplication { +} \ No newline at end of file diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/java/Resource.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/java/Resource.java new file mode 100644 index 000000000000..1b9e231c6300 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-common/testing/src/main/java/Resource.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +@Path("/ignored") +public interface Resource { + @Path("ignored") + String hello(String name); + + @Path("/test") + interface SubResource extends Cloneable, Resource { + @Override + @POST + @Path("/hello/{name}") + String hello(@PathParam("name") String name); + } + + class Test1 implements SubResource { + @Override + public String hello(String name) { + return "Test1 " + name + "!"; + } + } + + @Path("/test2") + class Test2 implements SubResource { + @Override + public String hello(String name) { + return "Test2 " + name + "!"; + } + } + + @Path("/test3") + class Test3 implements SubResource { + @Override + @POST + @Path("/hi/{name}") + public String hello(@PathParam("name") String name) { + return "Test3 " + name + "!"; + } + + @POST + @Path("/nested") + public String nested() { + return hello("nested"); + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..f61f8cf6a95c --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.glassfish.jersey.core") + module.set("jersey-server") + versions.set("[3.0.0,)") + assertInverse.set(true) + extraDependency("jakarta.servlet:jakarta.servlet-api:5.0.0") + } +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_11) +} + +dependencies { + bootstrap(project(":instrumentation:jaxrs:jaxrs-common:bootstrap")) + + compileOnly("jakarta.ws.rs:jakarta.ws.rs-api:3.0.0") + compileOnly("jakarta.servlet:jakarta.servlet-api:5.0.0") + library("org.glassfish.jersey.core:jersey-server:3.0.0") + library("org.glassfish.jersey.containers:jersey-container-servlet:3.0.0") + library("org.glassfish.jersey.inject:jersey-hk2:3.0.0") + implementation(project(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-common:javaagent")) + + testInstrumentation(project(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-annotations:javaagent")) + testInstrumentation(project(":instrumentation:servlet:servlet-5.0:javaagent")) + + testImplementation(project(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-common:testing")) + testImplementation("org.eclipse.jetty:jetty-webapp:11.0.0") +} + +tasks { + withType().configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs("-Dotel.instrumentation.jaxrs.experimental-span-attributes=true") + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyInstrumentationModule.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyInstrumentationModule.java new file mode 100644 index 000000000000..dc42b2edc9b1 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyInstrumentationModule.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static java.util.Arrays.asList; + +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 JerseyInstrumentationModule extends InstrumentationModule { + public JerseyInstrumentationModule() { + super("jaxrs", "jaxrs-2.0", "jersey", "jersey-2.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new JerseyRequestContextInstrumentation(), + new JerseyServletContainerInstrumentation(), + new JerseyResourceMethodDispatcherInstrumentation()); + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyRequestContextInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyRequestContextInstrumentation.java new file mode 100644 index 000000000000..0212a040c148 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyRequestContextInstrumentation.java @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0.JerseySingletons.instrumenter; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConstants; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.UriInfo; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.Local; + +/** + * Jersey specific context instrumentation. + * + *

JAX-RS does not define a way to get the matched resource method from the + * ContainerRequestContext + * + *

In the Jersey implementation, UriInfo implements ResourceInfo. The + * matched resource method can be retrieved from that object + */ +public class JerseyRequestContextInstrumentation extends AbstractRequestContextInstrumentation { + @Override + protected String abortAdviceName() { + return getClass().getName() + "$ContainerRequestContextAdvice"; + } + + @SuppressWarnings("unused") + public static class ContainerRequestContextAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void decorateAbortSpan( + @Advice.This ContainerRequestContext requestContext, + @Local("otelHandlerData") Jaxrs3HandlerData handlerData, + @Local("otelContext") Context context, + @Local("otelScope") Scope scope) { + UriInfo uriInfo = requestContext.getUriInfo(); + + if (requestContext.getProperty(JaxrsConstants.ABORT_HANDLED) != null + || !(uriInfo instanceof ResourceInfo)) { + return; + } + + ResourceInfo resourceInfo = (ResourceInfo) uriInfo; + Method method = resourceInfo.getResourceMethod(); + Class resourceClass = resourceInfo.getResourceClass(); + + if (resourceClass == null || method == null) { + return; + } + + handlerData = new Jaxrs3HandlerData(resourceClass, method); + context = + Jaxrs3RequestContextHelper.createOrUpdateAbortSpan( + instrumenter(), requestContext, handlerData); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Local("otelHandlerData") Jaxrs3HandlerData handlerData, + @Local("otelContext") Context context, + @Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + if (scope == null) { + return; + } + scope.close(); + instrumenter().end(context, handlerData, null, throwable); + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyResourceMethodDispatcherInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyResourceMethodDispatcherInstrumentation.java new file mode 100644 index 000000000000..651cb2f87c88 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyResourceMethodDispatcherInstrumentation.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import jakarta.ws.rs.core.Request; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JerseyResourceMethodDispatcherInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("dispatch") + .and( + takesArgument( + 1, + namedOneOf( + "javax.ws.rs.core.Request", + "org.glassfish.jersey.server.ContainerRequest"))), + JerseyResourceMethodDispatcherInstrumentation.class.getName() + "$DispatchAdvice"); + } + + @SuppressWarnings("unused") + public static class DispatchAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(1) Request request) { + JerseySpanName.INSTANCE.updateServerSpanName(request); + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyServletContainerInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyServletContainerInstrumentation.java new file mode 100644 index 000000000000..2a16d5956030 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseyServletContainerInstrumentation.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.jaxrs.JaxrsContextPath; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import jakarta.servlet.http.HttpServletRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class JerseyServletContainerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.glassfish.jersey.servlet.ServletContainer"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("service")) + .and(takesArgument(0, named("jakarta.servlet.http.HttpServletRequest"))) + .and(takesArgument(1, named("jakarta.servlet.http.HttpServletResponse"))), + JerseyServletContainerInstrumentation.class.getName() + "$ServiceAdvice"); + } + + @SuppressWarnings("unused") + public static class ServiceAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) HttpServletRequest httpServletRequest, + @Advice.Local("otelScope") Scope scope) { + Context context = + JaxrsContextPath.init( + Java8BytecodeBridge.currentContext(), httpServletRequest.getServletPath()); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseySingletons.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseySingletons.java new file mode 100644 index 000000000000..27525ebdbb74 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseySingletons.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.instrumentation.jaxrs.HandlerData; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsInstrumenterFactory; + +public final class JerseySingletons { + + private static final Instrumenter INSTANCE = + JaxrsInstrumenterFactory.createInstrumenter("io.opentelemetry.jersey-2.0"); + + public static Instrumenter instrumenter() { + return INSTANCE; + } + + private JerseySingletons() {} +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseySpanName.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseySpanName.java new file mode 100644 index 000000000000..6089334fd555 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/JerseySpanName.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsPathUtil.normalizePath; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; +import io.opentelemetry.javaagent.bootstrap.jaxrs.JaxrsContextPath; +import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; +import jakarta.ws.rs.core.Request; +import jakarta.ws.rs.core.UriInfo; +import javax.annotation.Nullable; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ExtendedUriInfo; + +public class JerseySpanName implements HttpRouteGetter { + + public static final JerseySpanName INSTANCE = new JerseySpanName(); + + public void updateServerSpanName(Request request) { + Context context = Context.current(); + HttpRouteHolder.updateHttpRoute(context, HttpRouteSource.NESTED_CONTROLLER, this, request); + } + + @Override + @Nullable + public String get(Context context, Request request) { + ContainerRequest containerRequest = (ContainerRequest) request; + UriInfo uriInfo = containerRequest.getUriInfo(); + ExtendedUriInfo extendedUriInfo = (ExtendedUriInfo) uriInfo; + return extendedUriInfo.getMatchedTemplates().stream() + .map((uriTemplate) -> normalizePath(uriTemplate.getTemplate())) + .reduce((a, b) -> b + a) + .map(s -> ServletContextPath.prepend(context, JaxrsContextPath.prepend(context, s))) + .orElse(null); + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyFilterTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyFilterTest.groovy new file mode 100644 index 000000000000..9e5c4a9ab25d --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyFilterTest.groovy @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait +import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse +import jakarta.ws.rs.core.Application +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.ServletContextHandler +import org.eclipse.jetty.servlet.ServletHolder +import org.glassfish.jersey.server.ResourceConfig +import org.glassfish.jersey.servlet.ServletContainer + +import static Resource.Test1 +import static Resource.Test2 +import static Resource.Test3 + +class JerseyFilterTest extends JaxRsFilterTest implements HttpServerTestTrait { + + def setupSpec() { + setupServer() + } + + def cleanupSpec() { + cleanupServer() + } + + @Override + Server startServer(int port) { + def servlet = new ServletContainer(ResourceConfig.forApplication(new TestApplication())) + + def handler = new ServletContextHandler(ServletContextHandler.SESSIONS) + handler.setContextPath("/") + handler.addServlet(new ServletHolder(servlet), "/*") + + def server = new Server(port) + server.setHandler(handler) + server.start() + + return server + } + + @Override + void stopServer(Server httpServer) { + httpServer.stop() + } + + @Override + boolean runsOnServer() { + true + } + + @Override + String defaultServerSpanName() { + "/*" + } + + @Override + def makeRequest(String path) { + AggregatedHttpResponse response = client.post(address.resolve(path).toString(), "").aggregate().join() + + return [response.contentUtf8(), response.status().code()] + } + + class TestApplication extends Application { + @Override + Set> getClasses() { + def classes = new HashSet() + classes.add(Test1) + classes.add(Test2) + classes.add(Test3) + return classes + } + + @Override + Set getSingletons() { + def singletons = new HashSet() + singletons.add(simpleRequestFilter) + singletons.add(prematchRequestFilter) + return singletons + } + } +} \ No newline at end of file diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy new file mode 100644 index 000000000000..d047247a8f18 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyHttpServerTest.groovy @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.ServletContextHandler +import org.eclipse.jetty.servlet.ServletHolder +import org.glassfish.jersey.server.ResourceConfig +import org.glassfish.jersey.servlet.ServletContainer +import test.JaxRsTestApplication + +class JerseyHttpServerTest extends JaxRsHttpServerTest { + + @Override + Server startServer(int port) { + def servlet = new ServletContainer(ResourceConfig.forApplicationClass(JaxRsTestApplication)) + + def handler = new ServletContextHandler(ServletContextHandler.SESSIONS) + handler.setContextPath("/") + handler.addServlet(new ServletHolder(servlet), "/*") + + def server = new Server(port) + server.setHandler(handler) + server.start() + + return server + } + + @Override + void stopServer(Server httpServer) { + httpServer.stop() + } + + @Override + boolean asyncCancelHasSendError() { + true + } + + @Override + boolean testInterfaceMethodWithPath() { + // disables a test that jersey deems invalid + false + } +} \ No newline at end of file diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy new file mode 100644 index 000000000000..f7529b96cf33 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyJettyHttpServerTest.groovy @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class JerseyJettyHttpServerTest extends JaxRsJettyHttpServerTest { + + @Override + boolean asyncCancelHasSendError() { + true + } + + @Override + boolean testInterfaceMethodWithPath() { + // disables a test that jersey deems invalid + false + } +} \ No newline at end of file diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyStartupListener.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyStartupListener.groovy new file mode 100644 index 000000000000..fd73b0918d69 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/groovy/JerseyStartupListener.groovy @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import jakarta.servlet.ServletContextEvent +import jakarta.servlet.ServletContextListener +import org.glassfish.jersey.servlet.init.JerseyServletContainerInitializer +import test.JaxRsApplicationPathTestApplication + +// ServletContainerInitializer isn't automatically called due to the way this test is set up +// so we call it ourself +class JerseyStartupListener implements ServletContextListener { + @Override + void contextInitialized(ServletContextEvent servletContextEvent) { + new JerseyServletContainerInitializer().onStartup(Collections.singleton(JaxRsApplicationPathTestApplication), + servletContextEvent.getServletContext()) + } + + @Override + void contextDestroyed(ServletContextEvent servletContextEvent) { + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/webapp/WEB-INF/web.xml b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/webapp/WEB-INF/web.xml new file mode 100644 index 000000000000..5b492283ea2f --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-jersey-3.0/javaagent/src/test/webapp/WEB-INF/web.xml @@ -0,0 +1,10 @@ + + + + + JerseyStartupListener + + \ No newline at end of file diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..43b0dcd42191 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.jboss.resteasy") + module.set("resteasy-core") + versions.set("[6.0.0.Final,)") + assertInverse.set(true) + } +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_11) +} + +dependencies { + bootstrap(project(":instrumentation:jaxrs:jaxrs-common:bootstrap")) + + compileOnly("jakarta.ws.rs:jakarta.ws.rs-api:3.0.0") + library("org.jboss.resteasy:resteasy-core:6.0.0.Final") + + implementation(project(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-common:javaagent")) + + testInstrumentation(project(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-annotations:javaagent")) + testInstrumentation(project(":instrumentation:servlet:servlet-5.0:javaagent")) + + testImplementation(project(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-common:testing")) + testImplementation("org.eclipse.jetty:jetty-webapp:11.0.0") + testLibrary("org.jboss.resteasy:resteasy-undertow:6.0.0.Final") { + exclude("org.jboss.resteasy", "resteasy-client") + } + testLibrary("io.undertow:undertow-servlet-jakarta:2.2.17.Final") + testLibrary("org.jboss.resteasy:resteasy-servlet-initializer:6.0.0.Final") +} + +tasks { + withType().configureEach { + // TODO run tests both with and without experimental span attributes + jvmArgs("-Dotel.instrumentation.jaxrs.experimental-span-attributes=true") + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyInstrumentationModule.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyInstrumentationModule.java new file mode 100644 index 000000000000..5b1481cdc2ed --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyInstrumentationModule.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static java.util.Arrays.asList; + +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 ResteasyInstrumentationModule extends InstrumentationModule { + public ResteasyInstrumentationModule() { + super("jaxrs", "jaxrs-3.0", "resteasy", "resteasy-6.0"); + } + + @Override + public List typeInstrumentations() { + return asList( + new ResteasyRequestContextInstrumentation(), + new ResteasyServletContainerDispatcherInstrumentation(), + new ResteasyRootNodeTypeInstrumentation(), + new ResteasyResourceMethodInvokerInstrumentation(), + new ResteasyResourceLocatorInvokerInstrumentation()); + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyRequestContextInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyRequestContextInstrumentation.java new file mode 100644 index 000000000000..09213b0c2dcd --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyRequestContextInstrumentation.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsConstants; +import jakarta.ws.rs.container.ContainerRequestContext; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.Local; +import org.jboss.resteasy.core.ResourceMethodInvoker; +import org.jboss.resteasy.core.interception.jaxrs.PostMatchContainerRequestContext; + +/** + * RESTEasy specific context instrumentation. + * + *

JAX-RS does not define a way to get the matched resource method from the + * ContainerRequestContext + * + *

In the RESTEasy implementation, ContainerRequestContext is implemented by + * PostMatchContainerRequestContext. This class provides a way to get the matched resource + * method through getResourceMethod(). + */ +public class ResteasyRequestContextInstrumentation extends AbstractRequestContextInstrumentation { + @Override + protected String abortAdviceName() { + return getClass().getName() + "$ContainerRequestContextAdvice"; + } + + @SuppressWarnings("unused") + public static class ContainerRequestContextAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void decorateAbortSpan( + @Advice.This ContainerRequestContext requestContext, + @Local("otelHandlerData") Jaxrs3HandlerData handlerData, + @Local("otelContext") Context context, + @Local("otelScope") Scope scope) { + if (requestContext.getProperty(JaxrsConstants.ABORT_HANDLED) != null + || !(requestContext instanceof PostMatchContainerRequestContext)) { + return; + } + + ResourceMethodInvoker resourceMethodInvoker = + ((PostMatchContainerRequestContext) requestContext).getResourceMethod(); + Method method = resourceMethodInvoker.getMethod(); + Class resourceClass = resourceMethodInvoker.getResourceClass(); + + handlerData = new Jaxrs3HandlerData(resourceClass, method); + context = + Jaxrs3RequestContextHelper.createOrUpdateAbortSpan( + ResteasySingletons.instrumenter(), requestContext, handlerData); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Local("otelHandlerData") Jaxrs3HandlerData handlerData, + @Local("otelContext") Context context, + @Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + if (scope == null) { + return; + } + scope.close(); + ResteasySingletons.instrumenter().end(context, handlerData, null, throwable); + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyResourceLocatorInvokerInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyResourceLocatorInvokerInstrumentation.java new file mode 100644 index 000000000000..b32c428f4493 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyResourceLocatorInvokerInstrumentation.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.jaxrs.JaxrsContextPath; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.jboss.resteasy.core.ResourceLocatorInvoker; + +public class ResteasyResourceLocatorInvokerInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.jboss.resteasy.core.ResourceLocatorInvoker"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("invokeOnTargetObject") + .and(takesArgument(0, named("org.jboss.resteasy.spi.HttpRequest"))) + .and(takesArgument(1, named("org.jboss.resteasy.spi.HttpResponse"))) + .and(takesArgument(2, Object.class)), + ResteasyResourceLocatorInvokerInstrumentation.class.getName() + + "$InvokeOnTargetObjectAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeOnTargetObjectAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This ResourceLocatorInvoker resourceInvoker, + @Advice.Local("otelScope") Scope scope) { + + Context currentContext = Java8BytecodeBridge.currentContext(); + + String name = + VirtualField.find(ResourceLocatorInvoker.class, String.class).get(resourceInvoker); + ResteasySpanName.INSTANCE.updateServerSpanName(currentContext, name); + + // subresource locator returns a resources class that may have @Path annotations + // append current path to jax-rs context path so that it would be present in the final path + Context context = + JaxrsContextPath.init(currentContext, JaxrsContextPath.prepend(currentContext, name)); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyResourceMethodInvokerInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyResourceMethodInvokerInstrumentation.java new file mode 100644 index 000000000000..0088db49d697 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyResourceMethodInvokerInstrumentation.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.jboss.resteasy.core.ResourceMethodInvoker; + +public class ResteasyResourceMethodInvokerInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.jboss.resteasy.core.ResourceMethodInvoker"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("invokeOnTarget") + .and(takesArgument(0, named("org.jboss.resteasy.spi.HttpRequest"))) + .and(takesArgument(1, named("org.jboss.resteasy.spi.HttpResponse"))) + .and(takesArgument(2, Object.class)), + ResteasyResourceMethodInvokerInstrumentation.class.getName() + "$InvokeOnTargetAdvice"); + } + + @SuppressWarnings("unused") + public static class InvokeOnTargetAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.This ResourceMethodInvoker resourceInvoker) { + + String name = + VirtualField.find(ResourceMethodInvoker.class, String.class).get(resourceInvoker); + ResteasySpanName.INSTANCE.updateServerSpanName(Java8BytecodeBridge.currentContext(), name); + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyRootNodeTypeInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyRootNodeTypeInstrumentation.java new file mode 100644 index 000000000000..f9bf9956c84a --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyRootNodeTypeInstrumentation.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsPathUtil; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatcher; +import org.jboss.resteasy.core.ResourceLocatorInvoker; +import org.jboss.resteasy.core.ResourceMethodInvoker; + +public class ResteasyRootNodeTypeInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.jboss.resteasy.core.registry.RootNode"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("addInvoker") + .and(takesArgument(0, String.class)) + // package of ResourceInvoker was changed in reasteasy 4 + .and( + takesArgument( + 1, + namedOneOf( + "org.jboss.resteasy.core.ResourceInvoker", + "org.jboss.resteasy.spi.ResourceInvoker"))), + ResteasyRootNodeTypeInstrumentation.class.getName() + "$AddInvokerAdvice"); + } + + @SuppressWarnings("unused") + public static class AddInvokerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void addInvoker( + @Advice.Argument(0) String path, + @Advice.Argument(value = 1, typing = Assigner.Typing.DYNAMIC) Object invoker) { + String normalizedPath = JaxrsPathUtil.normalizePath(path); + if (invoker instanceof ResourceLocatorInvoker) { + ResourceLocatorInvoker resourceLocatorInvoker = (ResourceLocatorInvoker) invoker; + VirtualField.find(ResourceLocatorInvoker.class, String.class) + .set(resourceLocatorInvoker, normalizedPath); + } else if (invoker instanceof ResourceMethodInvoker) { + ResourceMethodInvoker resourceMethodInvoker = (ResourceMethodInvoker) invoker; + VirtualField.find(ResourceMethodInvoker.class, String.class) + .set(resourceMethodInvoker, normalizedPath); + } + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyServletContainerDispatcherInstrumentation.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyServletContainerDispatcherInstrumentation.java new file mode 100644 index 000000000000..235ca1c2ace3 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasyServletContainerDispatcherInstrumentation.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.jaxrs.JaxrsContextPath; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ResteasyServletContainerDispatcherInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(named("service")), + ResteasyServletContainerDispatcherInstrumentation.class.getName() + "$ServiceAdvice"); + } + + @SuppressWarnings("unused") + public static class ServiceAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.FieldValue("servletMappingPrefix") String servletMappingPrefix, + @Advice.Local("otelScope") Scope scope) { + Context context = + JaxrsContextPath.init(Java8BytecodeBridge.currentContext(), servletMappingPrefix); + if (context != null) { + scope = context.makeCurrent(); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan(@Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasySingletons.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasySingletons.java new file mode 100644 index 000000000000..792f71d7b16f --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasySingletons.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.javaagent.instrumentation.jaxrs.HandlerData; +import io.opentelemetry.javaagent.instrumentation.jaxrs.JaxrsInstrumenterFactory; + +public final class ResteasySingletons { + + private static final Instrumenter INSTANCE = + JaxrsInstrumenterFactory.createInstrumenter("io.opentelemetry.resteasy-6.0"); + + public static Instrumenter instrumenter() { + return INSTANCE; + } + + private ResteasySingletons() {} +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasySpanName.java b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasySpanName.java new file mode 100644 index 000000000000..9aff95c67e5d --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/ResteasySpanName.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs.v3_0; + +import static io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource.NESTED_CONTROLLER; + +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; +import io.opentelemetry.javaagent.bootstrap.jaxrs.JaxrsContextPath; +import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath; +import javax.annotation.Nullable; + +public final class ResteasySpanName implements HttpRouteGetter { + + public static final ResteasySpanName INSTANCE = new ResteasySpanName(); + + public void updateServerSpanName(Context context, String name) { + if (name != null) { + HttpRouteHolder.updateHttpRoute(context, NESTED_CONTROLLER, this, name); + } + } + + @Override + @Nullable + public String get(Context context, String name) { + if (name.isEmpty()) { + return null; + } + return ServletContextPath.prepend(context, JaxrsContextPath.prepend(context, name)); + } + + private ResteasySpanName() {} +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyFilterTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyFilterTest.groovy new file mode 100644 index 000000000000..4c2492d48375 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyFilterTest.groovy @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import jakarta.ws.rs.core.MediaType +import org.jboss.resteasy.mock.MockDispatcherFactory +import org.jboss.resteasy.mock.MockHttpRequest +import org.jboss.resteasy.mock.MockHttpResponse +import spock.lang.Shared + +import static Resource.Test1 +import static Resource.Test2 +import static Resource.Test3 + +class ResteasyFilterTest extends JaxRsFilterTest { + @Shared + def dispatcher + + def setupSpec() { + dispatcher = MockDispatcherFactory.createDispatcher() + def registry = dispatcher.getRegistry() + registry.addSingletonResource(new Test1()) + registry.addSingletonResource(new Test2()) + registry.addSingletonResource(new Test3()) + + dispatcher.getProviderFactory().register(simpleRequestFilter) + dispatcher.getProviderFactory().register(prematchRequestFilter) + } + + @Override + def makeRequest(String url) { + MockHttpRequest request = MockHttpRequest.post(url) + request.contentType(MediaType.TEXT_PLAIN_TYPE) + request.content(new byte[0]) + + MockHttpResponse response = new MockHttpResponse() + dispatcher.invoke(request, response) + + return [response.contentAsString, response.status] + } + +} \ No newline at end of file diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy new file mode 100644 index 000000000000..9788b34eb7e5 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyHttpServerTest.groovy @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.undertow.Undertow +import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer +import test.JaxRsTestApplication + +class ResteasyHttpServerTest extends JaxRsHttpServerTest { + + @Override + String getContextPath() { + "/resteasy-context" + } + + @Override + UndertowJaxrsServer startServer(int port) { + def server = new UndertowJaxrsServer() + server.deploy(JaxRsTestApplication, getContextPath()) + server.start(Undertow.builder() + .addHttpListener(port, "localhost")) + return server + } + + @Override + void stopServer(UndertowJaxrsServer server) { + server.stop() + } + + // resteasy 3.0.x does not support JAX-RS 2.1 + boolean shouldTestCompletableStageAsync() { + false + } +} \ No newline at end of file diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy new file mode 100644 index 000000000000..6afef21cd191 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyJettyHttpServerTest.groovy @@ -0,0 +1,7 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class ResteasyJettyHttpServerTest extends JaxRsJettyHttpServerTest { +} \ No newline at end of file diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyStartupListener.groovy b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyStartupListener.groovy new file mode 100644 index 000000000000..e1af7700cd66 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/groovy/ResteasyStartupListener.groovy @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.jboss.resteasy.plugins.servlet.ResteasyServletInitializer +import test.JaxRsApplicationPathTestApplication + +import jakarta.servlet.ServletContextEvent +import jakarta.servlet.ServletContextListener + +// ServletContainerInitializer isn't automatically called due to the way this test is set up +// so we call it ourself +class ResteasyStartupListener implements ServletContextListener { + @Override + void contextInitialized(ServletContextEvent servletContextEvent) { + new ResteasyServletInitializer().onStartup(Collections.singleton(JaxRsApplicationPathTestApplication), + servletContextEvent.getServletContext()) + } + + @Override + void contextDestroyed(ServletContextEvent servletContextEvent) { + } +} diff --git a/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/webapp/WEB-INF/web.xml b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/webapp/WEB-INF/web.xml new file mode 100644 index 000000000000..459aba5d0538 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-resteasy-6.0/javaagent/src/test/webapp/WEB-INF/web.xml @@ -0,0 +1,10 @@ + + + + + ResteasyStartupListener + + \ No newline at end of file diff --git a/instrumentation/jaxrs/jaxrs-common/javaagent/build.gradle.kts b/instrumentation/jaxrs/jaxrs-common/javaagent/build.gradle.kts new file mode 100644 index 000000000000..9556766ed6f3 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +dependencies { + bootstrap(project(":instrumentation:jaxrs:jaxrs-common:bootstrap")) + + compileOnly("com.google.auto.value:auto-value-annotations") + annotationProcessor("com.google.auto.value:auto-value") +} diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/AsyncResponseData.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/AsyncResponseData.java similarity index 88% rename from instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/AsyncResponseData.java rename to instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/AsyncResponseData.java index 0f330b5f2fa7..2f937724d312 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/AsyncResponseData.java +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/AsyncResponseData.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; +package io.opentelemetry.javaagent.instrumentation.jaxrs; import com.google.auto.value.AutoValue; import io.opentelemetry.context.Context; diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CompletionStageFinishCallback.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/CompletionStageFinishCallback.java similarity index 93% rename from instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CompletionStageFinishCallback.java rename to instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/CompletionStageFinishCallback.java index e4c49795db57..a3f6a5048270 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CompletionStageFinishCallback.java +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/CompletionStageFinishCallback.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; +package io.opentelemetry.javaagent.instrumentation.jaxrs; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; diff --git a/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/HandlerData.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/HandlerData.java new file mode 100644 index 000000000000..bf8ea582ddc7 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/HandlerData.java @@ -0,0 +1,141 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs; + +import io.opentelemetry.javaagent.bootstrap.jaxrs.ClassHierarchyIterable; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.function.Supplier; + +public abstract class HandlerData { + + protected final Class target; + protected final Method method; + + protected HandlerData(Class target, Method method) { + this.target = target; + this.method = method; + } + + public Class codeClass() { + return target; + } + + public String methodName() { + return method.getName(); + } + + public String getServerSpanName() { + String httpMethod = null; + String methodPath = null; + String classPath = findClassPath(target); + for (Class currentClass : new ClassHierarchyIterable(target)) { + Method currentMethod; + if (currentClass.equals(target)) { + currentMethod = method; + } else { + currentMethod = findMatchingMethod(method, currentClass.getDeclaredMethods()); + } + + if (currentMethod != null) { + if (httpMethod == null) { + httpMethod = locateHttpMethod(currentMethod); + } + if (methodPath == null) { + methodPath = findMethodPath(currentMethod); + } + + if (httpMethod != null && methodPath != null) { + break; + } + } + } + return buildSpanName(classPath, methodPath); + } + + protected abstract Class getHttpMethodAnnotation(); + + private String locateHttpMethod(Method method) { + String httpMethod = null; + for (Annotation ann : method.getDeclaredAnnotations()) { + if (ann.annotationType().getAnnotation(getHttpMethodAnnotation()) != null) { + httpMethod = ann.annotationType().getSimpleName(); + } + } + return httpMethod; + } + + private String findMethodPath(Method method) { + Supplier pathSupplier = getPathAnnotation(method); + return pathSupplier != null ? pathSupplier.get() : null; + } + + protected abstract Supplier getPathAnnotation(AnnotatedElement annotated); + + private String findClassPath(Class target) { + for (Class currentClass : new ClassHierarchyIterable(target)) { + Supplier pathSupplier = getPathAnnotation(currentClass); + if (pathSupplier != null) { + // Annotation overridden, no need to continue. + return pathSupplier.get(); + } + } + + return null; + } + + private static Method findMatchingMethod(Method baseMethod, Method[] methods) { + nextMethod: + for (Method method : methods) { + if (!baseMethod.getReturnType().equals(method.getReturnType())) { + continue; + } + + if (!baseMethod.getName().equals(method.getName())) { + continue; + } + + Class[] baseParameterTypes = baseMethod.getParameterTypes(); + Class[] parameterTypes = method.getParameterTypes(); + if (baseParameterTypes.length != parameterTypes.length) { + continue; + } + for (int i = 0; i < baseParameterTypes.length; i++) { + if (!baseParameterTypes[i].equals(parameterTypes[i])) { + continue nextMethod; + } + } + return method; + } + return null; + } + + private static String buildSpanName(String classPath, String methodPath) { + StringBuilder spanNameBuilder = new StringBuilder(); + boolean skipSlash = false; + if (classPath != null) { + if (!classPath.startsWith("/")) { + spanNameBuilder.append("/"); + } + spanNameBuilder.append(classPath); + skipSlash = classPath.endsWith("/") || classPath.isEmpty(); + } + + if (methodPath != null) { + if (skipSlash) { + if (methodPath.startsWith("/")) { + methodPath = methodPath.length() == 1 ? "" : methodPath.substring(1); + } + } else if (!methodPath.startsWith("/")) { + spanNameBuilder.append("/"); + } + spanNameBuilder.append(methodPath); + } + + return spanNameBuilder.toString().trim(); + } +} diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsCodeAttributesGetter.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsCodeAttributesGetter.java similarity index 88% rename from instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsCodeAttributesGetter.java rename to instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsCodeAttributesGetter.java index 64ec5045292c..9276c0492ca0 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsCodeAttributesGetter.java +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsCodeAttributesGetter.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; +package io.opentelemetry.javaagent.instrumentation.jaxrs; import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter; diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsConfig.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsConfig.java similarity index 85% rename from instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsConfig.java rename to instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsConfig.java index ab4dcf75a2d4..82bdcb03815b 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsConfig.java +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsConfig.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; +package io.opentelemetry.javaagent.instrumentation.jaxrs; import io.opentelemetry.instrumentation.api.config.Config; diff --git a/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsConstants.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsConstants.java new file mode 100644 index 000000000000..2844fd152550 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsConstants.java @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrs; + +public final class JaxrsConstants { + public static final String ABORT_FILTER_CLASS = + "io.opentelemetry.javaagent.instrumentation.jaxrs.filter.abort.class"; + public static final String ABORT_HANDLED = + "io.opentelemetry.javaagent.instrumentation.jaxrs.filter.abort.handled"; + + private JaxrsConstants() {} +} diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsInstrumenterFactory.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsInstrumenterFactory.java similarity index 91% rename from instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsInstrumenterFactory.java rename to instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsInstrumenterFactory.java index 145e64159dcb..71df33e5dbbf 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsInstrumenterFactory.java +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsInstrumenterFactory.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; +package io.opentelemetry.javaagent.instrumentation.jaxrs; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.config.ExperimentalConfig; @@ -11,7 +11,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor; -final class JaxrsInstrumenterFactory { +public final class JaxrsInstrumenterFactory { public static Instrumenter createInstrumenter(String instrumentationName) { JaxrsCodeAttributesGetter codeAttributesGetter = new JaxrsCodeAttributesGetter(); diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsPathUtil.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsPathUtil.java similarity index 89% rename from instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsPathUtil.java rename to instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsPathUtil.java index 03695919cc0f..99ece7aad835 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsPathUtil.java +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsPathUtil.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; +package io.opentelemetry.javaagent.instrumentation.jaxrs; public final class JaxrsPathUtil { private JaxrsPathUtil() {} diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsServerSpanNaming.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsServerSpanNaming.java similarity index 94% rename from instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsServerSpanNaming.java rename to instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsServerSpanNaming.java index 3bcb6c35c748..f65bd2f99b4a 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/JaxrsServerSpanNaming.java +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/JaxrsServerSpanNaming.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; +package io.opentelemetry.javaagent.instrumentation.jaxrs; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteGetter; import io.opentelemetry.javaagent.bootstrap.jaxrs.JaxrsContextPath; diff --git a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/RequestContextHelper.java b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/RequestContextHelper.java similarity index 76% rename from instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/RequestContextHelper.java rename to instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/RequestContextHelper.java index 31276f3ecc24..5adf1e0580ba 100644 --- a/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/RequestContextHelper.java +++ b/instrumentation/jaxrs/jaxrs-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/RequestContextHelper.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0; +package io.opentelemetry.javaagent.instrumentation.jaxrs; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; @@ -12,19 +12,11 @@ import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteHolder; import io.opentelemetry.instrumentation.api.instrumenter.http.HttpRouteSource; import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; -import javax.ws.rs.container.ContainerRequestContext; public final class RequestContextHelper { - public static Context createOrUpdateAbortSpan( - Instrumenter instrumenter, - ContainerRequestContext requestContext, - HandlerData handlerData) { + public static Context createOrUpdateAbortSpan( + Instrumenter instrumenter, T handlerData) { - if (handlerData == null) { - return null; - } - - requestContext.setProperty(JaxrsConstants.ABORT_HANDLED, true); Context parentContext = Java8BytecodeBridge.currentContext(); Span serverSpan = LocalRootSpan.fromContextOrNull(parentContext); Span currentSpan = Java8BytecodeBridge.spanFromContext(parentContext); diff --git a/instrumentation/jaxrs/jaxrs-common/testing/build.gradle.kts b/instrumentation/jaxrs/jaxrs-common/testing/build.gradle.kts new file mode 100644 index 000000000000..98dff25fa857 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-common/testing/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + api(project(":testing-common")) + + implementation("org.apache.groovy:groovy") + implementation("io.opentelemetry:opentelemetry-api") + implementation("org.spockframework:spock-core") + implementation("org.slf4j:slf4j-api") + implementation("ch.qos.logback:logback-classic") + implementation("org.slf4j:log4j-over-slf4j") + implementation("org.slf4j:jcl-over-slf4j") + implementation("org.slf4j:jul-to-slf4j") +} diff --git a/instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsFilterTest.groovy b/instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsFilterTest.groovy new file mode 100644 index 000000000000..bf83f273cdf6 --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsFilterTest.groovy @@ -0,0 +1,147 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import org.junit.jupiter.api.Assumptions +import spock.lang.Unroll + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.UNSET + +@Unroll +abstract class AbstractJaxRsFilterTest extends AgentInstrumentationSpecification { + + abstract makeRequest(String url) + + Tuple2 runRequest(String resource) { + if (runsOnServer()) { + return makeRequest(resource) + } + // start a trace because the test doesn't go through any servlet or other instrumentation. + return runWithHttpServerSpan("test.span") { + makeRequest(resource) + } + } + + boolean testAbortPrematch() { + true + } + + boolean runsOnServer() { + false + } + + String defaultServerSpanName() { + "test.span" + } + + abstract void setAbortStatus(boolean abortNormal, boolean abortPrematch) + + def "test #resource, #abortNormal, #abortPrematch"() { + Assumptions.assumeTrue(!abortPrematch || testAbortPrematch()) + + given: + setAbortStatus(abortNormal, abortPrematch) + def abort = abortNormal || abortPrematch + + when: + + def (responseText, responseStatus) = runRequest(resource) + + then: + responseText == expectedResponse + + if (abort) { + responseStatus == 401 // Response.Status.UNAUTHORIZED.statusCode + } else { + responseStatus == 200 // Response.Status.OK.statusCode + } + + assertTraces(1) { + trace(0, 2) { + span(0) { + name parentSpanName != null ? parentSpanName : defaultServerSpanName() + kind SERVER + if (runsOnServer() && abortNormal) { + status UNSET + } + } + span(1) { + childOf span(0) + name controllerName + if (abortPrematch) { + attributes { + "$SemanticAttributes.CODE_NAMESPACE" "JaxRsFilterTest\$PrematchRequestFilter" + "$SemanticAttributes.CODE_FUNCTION" "filter" + } + } else { + attributes { + "$SemanticAttributes.CODE_NAMESPACE" ~/Resource[$]Test*/ + "$SemanticAttributes.CODE_FUNCTION" "hello" + } + } + } + } + } + + where: + resource | abortNormal | abortPrematch | parentSpanName | controllerName | expectedResponse + "/test/hello/bob" | false | false | "/test/hello/{name}" | "Test1.hello" | "Test1 bob!" + "/test2/hello/bob" | false | false | "/test2/hello/{name}" | "Test2.hello" | "Test2 bob!" + "/test3/hi/bob" | false | false | "/test3/hi/{name}" | "Test3.hello" | "Test3 bob!" + + // Resteasy and Jersey give different resource class names for just the below case + // Resteasy returns "SubResource.class" + // Jersey returns "Test1.class + // "/test/hello/bob" | true | false | "/test/hello/{name}" | "Test1.hello" | "Aborted" + + "/test2/hello/bob" | true | false | "/test2/hello/{name}" | "Test2.hello" | "Aborted" + "/test3/hi/bob" | true | false | "/test3/hi/{name}" | "Test3.hello" | "Aborted" + "/test/hello/bob" | false | true | null | "PrematchRequestFilter.filter" | "Aborted Prematch" + "/test2/hello/bob" | false | true | null | "PrematchRequestFilter.filter" | "Aborted Prematch" + "/test3/hi/bob" | false | true | null | "PrematchRequestFilter.filter" | "Aborted Prematch" + } + + def "test nested call"() { + given: + setAbortStatus(false, false) + + when: + def (responseText, responseStatus) = runRequest(resource) + + then: + responseStatus == 200 // Response.Status.OK.statusCode + responseText == expectedResponse + + assertTraces(1) { + trace(0, 2) { + span(0) { + name parentResourceName + kind SERVER + if (!runsOnServer()) { + attributes { + "$SemanticAttributes.HTTP_ROUTE" parentResourceName + } + } + } + span(1) { + childOf span(0) + name controller1Name + kind INTERNAL + attributes { + "$SemanticAttributes.CODE_NAMESPACE" ~/Resource[$]Test*/ + "$SemanticAttributes.CODE_FUNCTION" "nested" + } + } + } + } + + where: + resource | parentResourceName | controller1Name | expectedResponse + "/test3/nested" | "/test3/nested" | "Test3.nested" | "Test3 nested!" + } +} diff --git a/instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsHttpServerTest.groovy b/instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsHttpServerTest.groovy new file mode 100644 index 000000000000..b4b00a04490b --- /dev/null +++ b/instrumentation/jaxrs/jaxrs-common/testing/src/main/groovy/AbstractJaxRsHttpServerTest.groovy @@ -0,0 +1,329 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.asserts.TraceAssert +import io.opentelemetry.instrumentation.test.base.HttpServerTest +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.TimeUnit +import spock.lang.Unroll + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL +import static io.opentelemetry.api.trace.SpanKind.SERVER +import static io.opentelemetry.api.trace.StatusCode.ERROR +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM +import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP +import static java.util.concurrent.TimeUnit.SECONDS +import static org.junit.jupiter.api.Assumptions.assumeTrue + +abstract class AbstractJaxRsHttpServerTest extends HttpServerTest implements AgentTestTrait { + + abstract void awaitBarrier(int amount, TimeUnit timeUnit) + + def "test super method without @Path"() { + given: + def response = client.get(address.resolve("test-resource-super").toString()).aggregate().join() + + expect: + response.status().code() == SUCCESS.status + response.contentUtf8() == SUCCESS.body + + assertTraces(1) { + trace(0, 2) { + span(0) { + hasNoParent() + kind SERVER + name getContextPath() + "/test-resource-super" + } + span(1) { + name "controller" + kind INTERNAL + childOf span(0) + } + } + } + } + + def "test interface method with @Path"() { + assumeTrue(testInterfaceMethodWithPath()) + + given: + def response = client.get(address.resolve("test-resource-interface/call").toString()).aggregate().join() + + expect: + response.status().code() == SUCCESS.status + response.contentUtf8() == SUCCESS.body + + assertTraces(1) { + trace(0, 2) { + span(0) { + hasNoParent() + kind SERVER + name getContextPath() + "/test-resource-interface/call" + } + span(1) { + name "controller" + kind INTERNAL + childOf span(0) + } + } + } + } + + def "test sub resource locator"() { + given: + def response = client.get(address.resolve("test-sub-resource-locator/call/sub").toString()).aggregate().join() + + expect: + response.status().code() == SUCCESS.status + response.contentUtf8() == SUCCESS.body + + assertTraces(1) { + trace(0, 5) { + span(0) { + hasNoParent() + kind SERVER + name getContextPath() + "/test-sub-resource-locator/call/sub" + } + span(1) { + name "JaxRsSubResourceLocatorTestResource.call" + kind INTERNAL + childOf span(0) + } + span(2) { + name "controller" + kind INTERNAL + childOf span(1) + } + span(3) { + name "SubResource.call" + kind INTERNAL + childOf span(0) + } + span(4) { + name "controller" + kind INTERNAL + childOf span(3) + } + } + } + } + + @Unroll + def "should handle #desc AsyncResponse"() { + given: + def url = address.resolve("async?action=${action}").toString() + + when: "async call is started" + def futureResponse = client.get(url).aggregate() + + then: "there are no traces yet" + assertTraces(0) { + } + + when: "barrier is released and resource class sends response" + awaitBarrier(1, SECONDS) + def response = futureResponse.join() + + then: + response.status().code() == statusCode + bodyPredicate(response.contentUtf8()) + + def spanCount = 2 + def hasSendError = asyncCancelHasSendError() && action == "cancel" + if (hasSendError) { + spanCount++ + } + assertTraces(1) { + trace(0, spanCount) { + asyncServerSpan(it, 0, url, statusCode) + handlerSpan(it, 1, span(0), "asyncOp", isCancelled, isError, errorMessage) + if (hasSendError) { + sendErrorSpan(it, 2, span(1)) + } + } + } + + where: + desc | action | statusCode | bodyPredicate | isCancelled | isError | errorMessage + "successful" | "succeed" | 200 | { it == "success" } | false | false | null + "failing" | "throw" | 500 | { it == "failure" } | false | true | "failure" + "canceled" | "cancel" | 503 | { it instanceof String } | true | false | null + } + + @Unroll + def "should handle #desc CompletionStage (JAX-RS 2.1+ only)"() { + assumeTrue(shouldTestCompletableStageAsync()) + given: + def url = address.resolve("async-completion-stage?action=${action}").toString() + + when: "async call is started" + def futureResponse = client.get(url).aggregate() + + then: "there are no traces yet" + assertTraces(0) { + } + + when: "barrier is released and resource class sends response" + awaitBarrier(1, SECONDS) + def response = futureResponse.join() + + then: + response.status().code() == statusCode + bodyPredicate(response.contentUtf8()) + + assertTraces(1) { + trace(0, 2) { + asyncServerSpan(it, 0, url, statusCode) + handlerSpan(it, 1, span(0), "jaxRs21Async", false, isError, errorMessage) + } + } + + where: + desc | action | statusCode | bodyPredicate | isError | errorMessage + "successful" | "succeed" | 200 | { it == "success" } | false | null + "failing" | "throw" | 500 | { it == "failure" } | true | "failure" + } + + @Override + boolean hasHandlerSpan(ServerEndpoint endpoint) { + true + } + + @Override + boolean testNotFound() { + false + } + + @Override + boolean testPathParam() { + true + } + + boolean testInterfaceMethodWithPath() { + true + } + + boolean asyncCancelHasSendError() { + false + } + + boolean shouldTestCompletableStageAsync() { + Boolean.getBoolean("testLatestDeps") + } + + @Override + void serverSpan(TraceAssert trace, + int index, + String traceID = null, + String parentID = null, + String method = "GET", + Long responseContentLength = null, + ServerEndpoint endpoint = SUCCESS) { + serverSpan(trace, index, traceID, parentID, method, + endpoint == PATH_PARAM ? getContextPath() + "/path/{id}/param" : endpoint.resolvePath(address).path, + endpoint.resolve(address), + endpoint.status, + endpoint.query) + } + + void asyncServerSpan(TraceAssert trace, + int index, + String url, + int statusCode) { + def rawUrl = URI.create(url).toURL() + serverSpan(trace, index, null, null, "GET", + rawUrl.path, + rawUrl.toURI(), + statusCode, + null) + } + + void serverSpan(TraceAssert trace, + int index, + String traceID, + String parentID, + String method, + String path, + URI fullUrl, + int statusCode, + String query) { + trace.span(index) { + name path + kind SERVER + if (statusCode >= 500) { + status ERROR + } + if (parentID != null) { + traceId traceID + parentSpanId parentID + } else { + hasNoParent() + } + attributes { + "$SemanticAttributes.NET_PEER_IP" { it == null || it == "127.0.0.1" } // Optional + "$SemanticAttributes.NET_PEER_PORT" Long + "$SemanticAttributes.HTTP_SCHEME" fullUrl.getScheme() + "$SemanticAttributes.HTTP_HOST" fullUrl.getHost() + ":" + fullUrl.getPort() + "$SemanticAttributes.HTTP_TARGET" fullUrl.getPath() + (fullUrl.getQuery() != null ? "?" + fullUrl.getQuery() : "") + "$SemanticAttributes.HTTP_METHOD" method + "$SemanticAttributes.HTTP_STATUS_CODE" statusCode + "$SemanticAttributes.HTTP_FLAVOR" "1.1" + "$SemanticAttributes.HTTP_USER_AGENT" TEST_USER_AGENT + "$SemanticAttributes.HTTP_CLIENT_IP" TEST_CLIENT_IP + "$SemanticAttributes.NET_TRANSPORT" IP_TCP + // Optional + "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } + "$SemanticAttributes.HTTP_ROUTE" path + if (fullUrl.getPath().endsWith(ServerEndpoint.CAPTURE_HEADERS.getPath())) { + "http.request.header.x_test_request" { it == ["test"] } + "http.response.header.x_test_response" { it == ["test"] } + } + } + } + } + + @Override + void handlerSpan(TraceAssert trace, + int index, + Object parent, + String method = "GET", + ServerEndpoint endpoint = SUCCESS) { + handlerSpan(trace, index, parent, + endpoint.name().toLowerCase(), + false, + endpoint == EXCEPTION, + EXCEPTION.body) + } + + void handlerSpan(TraceAssert trace, + int index, + Object parent, + String methodName, + boolean isCancelled, + boolean isError, + String exceptionMessage = null) { + trace.span(index) { + name "JaxRsTestResource.${methodName}" + kind INTERNAL + if (isError) { + status ERROR + errorEvent(Exception, exceptionMessage) + } + childOf((SpanData) parent) + attributes { + "$SemanticAttributes.CODE_NAMESPACE" "test.JaxRsTestResource" + "$SemanticAttributes.CODE_FUNCTION" methodName + if (isCancelled) { + "jaxrs.canceled" true + } + } + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index f2a9abda9a48..957417dbb1f7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -235,6 +235,8 @@ include(":instrumentation:java-http-client:javaagent") include(":instrumentation:java-util-logging:javaagent") include(":instrumentation:java-util-logging:shaded-stub-for-instrumenting") include(":instrumentation:jaxrs:jaxrs-common:bootstrap") +include(":instrumentation:jaxrs:jaxrs-common:javaagent") +include(":instrumentation:jaxrs:jaxrs-common:testing") include(":instrumentation:jaxrs:jaxrs-1.0:javaagent") include(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-annotations:javaagent") include(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-arquillian-testing") @@ -248,6 +250,11 @@ include(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-common:javaagent") include(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:testing") include(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-tomee-testing") include(":instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-wildfly-testing") +include(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-annotations:javaagent") +include(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-common:javaagent") +include(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-common:testing") +include(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-jersey-3.0:javaagent") +include(":instrumentation:jaxrs:jaxrs-3.0:jaxrs-3.0-resteasy-6.0:javaagent") include(":instrumentation:jaxrs-client:jaxrs-client-1.1:javaagent") include(":instrumentation:jaxrs-client:jaxrs-client-2.0-testing") include(":instrumentation:jaxws:jaxws-2.0:javaagent")