diff --git a/bom/application/pom.xml b/bom/application/pom.xml index cd299d57f590d6..b2cc1c57219281 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -2489,6 +2489,16 @@ quarkus-opentelemetry-exporter-otlp ${project.version} + + io.quarkus + quarkus-resteasy-observability-deployment + ${project.version} + + + io.quarkus + quarkus-resteasy-observability + ${project.version} + diff --git a/extensions/micrometer/deployment/pom.xml b/extensions/micrometer/deployment/pom.xml index ef33c1919987c0..876b4c21d78bac 100644 --- a/extensions/micrometer/deployment/pom.xml +++ b/extensions/micrometer/deployment/pom.xml @@ -81,6 +81,12 @@ test + + io.quarkus + quarkus-resteasy-observability-deployment + test + + io.quarkus quarkus-undertow-deployment @@ -113,7 +119,7 @@ maven-surefire-plugin - INFO + DEBUG diff --git a/extensions/micrometer/deployment/src/test/resources/test-logging.properties b/extensions/micrometer/deployment/src/test/resources/test-logging.properties index 6eed6ab2596da2..7a2ecfd97109fe 100644 --- a/extensions/micrometer/deployment/src/test/resources/test-logging.properties +++ b/extensions/micrometer/deployment/src/test/resources/test-logging.properties @@ -1,4 +1,5 @@ -#quarkus.log.category."io.quarkus.micrometer".level=DEBUG +quarkus.log.category."io.quarkus.micrometer".level=DEBUG +quarkus.log.category."io.quarkus.resteasy.observability".level=DEBUG quarkus.log.category."io.quarkus.bootstrap".level=INFO -#quarkus.log.category."io.quarkus.arc".level=DEBUG +quarkus.log.category."io.quarkus.arc".level=DEBUG quarkus.log.category."io.netty".level=INFO diff --git a/extensions/micrometer/runtime/pom.xml b/extensions/micrometer/runtime/pom.xml index 3a38432c1f850f..cec6c00fb1227b 100644 --- a/extensions/micrometer/runtime/pom.xml +++ b/extensions/micrometer/runtime/pom.xml @@ -66,6 +66,11 @@ quarkus-resteasy-reactive true + + io.quarkus + quarkus-resteasy-observability + true + io.quarkus diff --git a/extensions/pom.xml b/extensions/pom.xml index 17ba5ac8418729..b49b67cdfd7e88 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -44,6 +44,7 @@ smallrye-opentracing smallrye-fault-tolerance jaeger + resteasy-observability micrometer micrometer-registry-prometheus opentelemetry diff --git a/extensions/resteasy-classic/resteasy-server-common/deployment/pom.xml b/extensions/resteasy-classic/resteasy-server-common/deployment/pom.xml index b1940b0c732023..88a865a8fa7386 100644 --- a/extensions/resteasy-classic/resteasy-server-common/deployment/pom.xml +++ b/extensions/resteasy-classic/resteasy-server-common/deployment/pom.xml @@ -37,6 +37,10 @@ io.quarkus quarkus-undertow-spi + + io.quarkus + quarkus-resteasy-observability-spi + diff --git a/extensions/resteasy-classic/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java b/extensions/resteasy-classic/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java index ed472ecaf1eeaa..f30f125f94c9b9 100755 --- a/extensions/resteasy-classic/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java +++ b/extensions/resteasy-classic/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java @@ -70,6 +70,7 @@ import io.quarkus.resteasy.common.deployment.ResteasyCommonProcessor.ResteasyCommonConfig; import io.quarkus.resteasy.common.runtime.QuarkusInjectorFactory; import io.quarkus.resteasy.common.spi.ResteasyDotNames; +import io.quarkus.resteasy.observability.spi.RestApplicationPathBuildItem; import io.quarkus.resteasy.server.common.runtime.QuarkusResteasyDeployment; import io.quarkus.resteasy.server.common.spi.AdditionalJaxRsResourceDefiningAnnotationBuildItem; import io.quarkus.resteasy.server.common.spi.AdditionalJaxRsResourceMethodAnnotationsBuildItem; @@ -182,6 +183,7 @@ public void build( BuildProducer proxyDefinition, BuildProducer transformers, BuildProducer resteasyServerConfig, + BuildProducer resteasyApplicationPath, BuildProducer resteasyDeployment, BuildProducer unremovableBeans, BuildProducer annotationsTransformer, @@ -398,6 +400,7 @@ public void build( ArcUndeclaredThrowableException.class.getName()); resteasyServerConfig.produce(new ResteasyServerConfigBuildItem(rootPath, path, resteasyInitParameters)); + resteasyApplicationPath.produce(new RestApplicationPathBuildItem(path)); Set autoInjectAnnotationNames = autoInjectAnnotations.stream().flatMap(a -> a.getAnnotationNames().stream()) .collect(Collectors.toSet()); diff --git a/extensions/resteasy-observability/deployment/pom.xml b/extensions/resteasy-observability/deployment/pom.xml new file mode 100644 index 00000000000000..24917d8283563c --- /dev/null +++ b/extensions/resteasy-observability/deployment/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + + io.quarkus + quarkus-resteasy-observability-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-resteasy-observability-deployment + Quarkus - RESTEasy Observability - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-resteasy-observability + + + io.quarkus + quarkus-resteasy-observability-spi + + + + + + io.quarkus + quarkus-junit5-internal + test + + + + + io.rest-assured + rest-assured + test + + + + + + + + maven-surefire-plugin + + + INFO + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + \ No newline at end of file diff --git a/extensions/resteasy-observability/deployment/src/main/java/io/quarkus/resteasy/observability/runtime/deployment/RestPathAnnotationProcessor.java b/extensions/resteasy-observability/deployment/src/main/java/io/quarkus/resteasy/observability/runtime/deployment/RestPathAnnotationProcessor.java new file mode 100644 index 00000000000000..b1f2e05f3989a4 --- /dev/null +++ b/extensions/resteasy-observability/deployment/src/main/java/io/quarkus/resteasy/observability/runtime/deployment/RestPathAnnotationProcessor.java @@ -0,0 +1,100 @@ +package io.quarkus.resteasy.observability.runtime.deployment; + +import java.util.Optional; +import java.util.regex.Pattern; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.MethodInfo; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.resteasy.observability.runtime.QuarkusRestPathTemplate; +import io.quarkus.resteasy.observability.runtime.QuarkusRestPathTemplateInterceptor; +import io.quarkus.resteasy.observability.spi.RestApplicationPathBuildItem; + +public class RestPathAnnotationProcessor { + + static final DotName REST_PATH = DotName.createSimple("javax.ws.rs.Path"); + static final DotName REGISTER_REST_CLIENT = DotName + .createSimple("org.eclipse.microprofile.rest.client.inject.RegisterRestClient"); + static final DotName TEMPLATE_PATH = DotName.createSimple(QuarkusRestPathTemplate.class.getName()); + static final DotName TEMPLATE_PATH_INTERCEPTOR = DotName.createSimple(QuarkusRestPathTemplateInterceptor.class.getName()); + + public static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+"); + + @BuildStep() + AdditionalBeanBuildItem registerBeanClasses() { + return AdditionalBeanBuildItem.builder() + .setUnremovable() + .addBeanClass(TEMPLATE_PATH.toString()) + .addBeanClass(TEMPLATE_PATH_INTERCEPTOR.toString()) + .build(); + } + + @BuildStep + AnnotationsTransformerBuildItem findRestPaths( + Optional restApplicationPath) { + + return new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { + @Override + public boolean appliesTo(AnnotationTarget.Kind kind) { + return kind == AnnotationTarget.Kind.METHOD; + } + + @Override + public void transform(TransformationContext ctx) { + AnnotationTarget target = ctx.getTarget(); + MethodInfo methodInfo = target.asMethod(); + ClassInfo classInfo = methodInfo.declaringClass(); + + AnnotationInstance annotation = methodInfo.annotation(REST_PATH); + if (annotation == null) { + return; + } + StringBuilder stringBuilder = new StringBuilder(slashify(annotation.value().asString())); + + // Look for @Path annotation on the class + annotation = classInfo.classAnnotation(REST_PATH); + if (annotation != null) { + stringBuilder.insert(0, slashify(annotation.value().asString())); + } + + // Add the @ApplicationPath (unless RestClient) + if (restApplicationPath.isPresent() && classInfo.classAnnotation(REGISTER_REST_CLIENT) == null) { + stringBuilder.insert(0, slashify(restApplicationPath.get().getApplicationPath())); + } + + // Now make sure there is a leading path, and no duplicates + String templatePath = MULTIPLE_SLASH_PATTERN.matcher('/' + stringBuilder.toString()).replaceAll("/"); + + // resulting path (used as observability tags) should start with a '/' + ctx.transform() + .add(TEMPLATE_PATH, AnnotationValue.createStringValue("value", templatePath)) + .done(); + } + }); + } + + String slashify(String path) { + // avoid doubles later. Empty for now + if (path == null || path.isEmpty() || "/".equals(path)) { + return ""; + } + // remove doubles + path = MULTIPLE_SLASH_PATTERN.matcher(path).replaceAll("/"); + // Label value consistency: result should not end with a slash + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + if (path.isEmpty() || path.startsWith("/")) { + return path; + } + return '/' + path; + } +} diff --git a/extensions/resteasy-observability/pom.xml b/extensions/resteasy-observability/pom.xml new file mode 100644 index 00000000000000..504fc914a0ba84 --- /dev/null +++ b/extensions/resteasy-observability/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + + quarkus-extensions-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + + quarkus-resteasy-observability-parent + Quarkus - RESTEasy Observability + pom + + + spi + runtime + deployment + + \ No newline at end of file diff --git a/extensions/resteasy-observability/runtime/pom.xml b/extensions/resteasy-observability/runtime/pom.xml new file mode 100644 index 00000000000000..72dfda4a6db085 --- /dev/null +++ b/extensions/resteasy-observability/runtime/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + + io.quarkus + quarkus-resteasy-observability-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-resteasy-observability + Quarkus - RESTEasy Observability - Runtime + Telemetry for RESTEasy workloads + + + + io.quarkus + quarkus-core + + + + io.quarkus + quarkus-arc + + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + \ No newline at end of file diff --git a/extensions/resteasy-observability/runtime/src/main/java/io/quarkus/resteasy/observability/runtime/QuarkusRestPathTemplate.java b/extensions/resteasy-observability/runtime/src/main/java/io/quarkus/resteasy/observability/runtime/QuarkusRestPathTemplate.java new file mode 100644 index 00000000000000..f234a7e3948529 --- /dev/null +++ b/extensions/resteasy-observability/runtime/src/main/java/io/quarkus/resteasy/observability/runtime/QuarkusRestPathTemplate.java @@ -0,0 +1,17 @@ +package io.quarkus.resteasy.observability.runtime; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.interceptor.InterceptorBinding; + +@Inherited +@InterceptorBinding +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface QuarkusRestPathTemplate { + String value() default ""; +} diff --git a/extensions/resteasy-observability/runtime/src/main/java/io/quarkus/resteasy/observability/runtime/QuarkusRestPathTemplateInterceptor.java b/extensions/resteasy-observability/runtime/src/main/java/io/quarkus/resteasy/observability/runtime/QuarkusRestPathTemplateInterceptor.java new file mode 100644 index 00000000000000..ad689daa22a3cc --- /dev/null +++ b/extensions/resteasy-observability/runtime/src/main/java/io/quarkus/resteasy/observability/runtime/QuarkusRestPathTemplateInterceptor.java @@ -0,0 +1,44 @@ +package io.quarkus.resteasy.observability.runtime; + +import java.lang.annotation.Annotation; +import java.util.Set; + +import javax.annotation.Priority; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.ArcInvocationContext; + +@SuppressWarnings("unused") +@QuarkusRestPathTemplate +@Interceptor +@Priority(Interceptor.Priority.LIBRARY_BEFORE + 10) +public class QuarkusRestPathTemplateInterceptor { + private static final Logger log = Logger.getLogger(QuarkusRestPathTemplateInterceptor.class); + + @AroundInvoke + Object restMethodInvoke(InvocationContext context) throws Exception { + QuarkusRestPathTemplate annotation = getAnnotation(context); + log.infof("QuarkusRestPathTemplate annotated method invoked with %s", annotation); + + if (annotation != null) { + + } + return context.proceed(); + } + + static QuarkusRestPathTemplate getAnnotation(InvocationContext context) { + Set annotations = (Set) context.getContextData() + .get(ArcInvocationContext.KEY_INTERCEPTOR_BINDINGS); + + for (Annotation a : annotations) { + if (QuarkusRestPathTemplate.class.isInstance(a)) { + return QuarkusRestPathTemplate.class.cast(a); + } + } + return null; + } +} diff --git a/extensions/resteasy-observability/spi/pom.xml b/extensions/resteasy-observability/spi/pom.xml new file mode 100644 index 00000000000000..9b3c8169aec85e --- /dev/null +++ b/extensions/resteasy-observability/spi/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + + quarkus-resteasy-observability-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + + quarkus-resteasy-observability-spi + Telemetry SPI for RESTEasy workloads + + + + io.quarkus + quarkus-core-deployment + + + + \ No newline at end of file diff --git a/extensions/resteasy-observability/spi/src/main/java/io/quarkus/resteasy/observability/spi/RestApplicationPathBuildItem.java b/extensions/resteasy-observability/spi/src/main/java/io/quarkus/resteasy/observability/spi/RestApplicationPathBuildItem.java new file mode 100644 index 00000000000000..43a2c19b5ac2b8 --- /dev/null +++ b/extensions/resteasy-observability/spi/src/main/java/io/quarkus/resteasy/observability/spi/RestApplicationPathBuildItem.java @@ -0,0 +1,23 @@ +package io.quarkus.resteasy.observability.spi; + +import io.quarkus.builder.item.SimpleBuildItem; + +final public class RestApplicationPathBuildItem extends SimpleBuildItem { + + private final String applicationPath; + + /** + * The root path for Rest applications, set either via config or {@code @ApplicationPath}. + * This should not include the http root. + */ + public RestApplicationPathBuildItem(String applicationPath) { + if (applicationPath.equals("/")) { + applicationPath = ""; + } + this.applicationPath = applicationPath; + } + + public String getApplicationPath() { + return applicationPath; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/pom.xml index a4a896e2a2f50f..79e3186aa06f9e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/pom.xml +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/pom.xml @@ -49,6 +49,10 @@ io.quarkus quarkus-resteasy-reactive-common-deployment + + io.quarkus + quarkus-resteasy-observability-spi + io.quarkus quarkus-security-deployment diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 9b893a9faaf08e..fad7a1c03cbce4 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -90,6 +90,7 @@ import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.MethodCreator; import io.quarkus.netty.deployment.MinNettyAllocatorMaxOrderBuildItem; +import io.quarkus.resteasy.observability.spi.RestApplicationPathBuildItem; import io.quarkus.resteasy.reactive.common.deployment.ApplicationResultBuildItem; import io.quarkus.resteasy.reactive.common.deployment.FactoryUtils; import io.quarkus.resteasy.reactive.common.deployment.QuarkusFactoryCreator; @@ -269,6 +270,7 @@ public void setupEndpoints(Capabilities capabilities, BeanArchiveIndexBuildItem BuildProducer reflectiveClass, BuildProducer reflectiveHierarchy, BuildProducer routes, + BuildProducer resteasyApplicationPath, ApplicationResultBuildItem applicationResultBuildItem, ResourceInterceptorsBuildItem resourceInterceptorsBuildItem, ExceptionMappersBuildItem exceptionMappersBuildItem, @@ -527,6 +529,8 @@ private boolean hasAnnotation(MethodInfo method, short paramPosition, DotName an } String deploymentPath = sanitizeApplicationPath(applicationPath); + resteasyApplicationPath.produce(new RestApplicationPathBuildItem(deploymentPath)); + // Handler used for both the default and non-default deployment path (specified as application path or resteasyConfig.path) // Routes use the order VertxHttpRecorder.DEFAULT_ROUTE_ORDER + 1 to ensure the default route is called before the resteasy one Class applicationClass = application == null ? Application.class : application.getClass();