diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 83a4d773d4754..d4c3608ca16f8 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -64,7 +64,7 @@ 3.0.1 3.6.0 4.9.0 - 2.3.1 + 2.4.0-SNAPSHOT 2.1.2 2.1.1 3.0.0 diff --git a/extensions/micrometer/deployment/pom.xml b/extensions/micrometer/deployment/pom.xml index 8273cf3a321fe..c2f3a96cd625e 100644 --- a/extensions/micrometer/deployment/pom.xml +++ b/extensions/micrometer/deployment/pom.xml @@ -81,22 +81,26 @@ io.quarkus - quarkus-resteasy-deployment + quarkus-resteasy-reactive-deployment test io.quarkus - quarkus-rest-client-deployment + quarkus-rest-client-reactive-deployment test io.quarkus - quarkus-resteasy-jackson-deployment + quarkus-resteasy-reactive-jackson-deployment + test + + + io.smallrye.stork + stork-service-discovery-static-list test - io.quarkus quarkus-undertow-deployment @@ -132,6 +136,16 @@ resteasy-reactive-client test + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/StorkBinderProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/StorkBinderProcessor.java new file mode 100644 index 0000000000000..06ed79c3ea715 --- /dev/null +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/StorkBinderProcessor.java @@ -0,0 +1,30 @@ +package io.quarkus.micrometer.deployment.binder; + +import java.util.function.BooleanSupplier; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.micrometer.runtime.MicrometerRecorder; +import io.quarkus.micrometer.runtime.config.MicrometerConfig; + +public class StorkBinderProcessor { + + static final String OBSERVABLE_CLIENT = "io.smallrye.stork.api.Service"; + static final String METRICS_BEAN_CLASS = "io.quarkus.micrometer.runtime.binder.stork.StorkObservationCollectorBean"; + + static final Class OBSERVABLE_CLIENT_CLASS = MicrometerRecorder.getClassForName(OBSERVABLE_CLIENT); + + static class StorkMetricsSupportEnabled implements BooleanSupplier { + MicrometerConfig mConfig; + + public boolean getAsBoolean() { + return OBSERVABLE_CLIENT_CLASS != null && mConfig.checkBinderEnabledWithDefault(mConfig.binder.stork); + } + } + + @BuildStep(onlyIf = StorkMetricsSupportEnabled.class) + AdditionalBeanBuildItem addStorkObservationCollector() { + return AdditionalBeanBuildItem.unremovableOf(METRICS_BEAN_CLASS); + } + +} diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsDisabledTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsDisabledTest.java new file mode 100644 index 0000000000000..8264018946251 --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsDisabledTest.java @@ -0,0 +1,33 @@ +package io.quarkus.micrometer.deployment.binder; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.stork.api.observability.ObservationCollector; + +public class StorkMetricsDisabledTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("test-logging.properties") + .overrideConfigKey("quarkus.micrometer.binder.stork.enabled", "false") + .overrideConfigKey("quarkus.micrometer.binder-enabled-default", "false") + .overrideConfigKey("quarkus.micrometer.registry-enabled-default", "false") + .withEmptyApplication(); + + @Inject + Instance bean; + + @Test + void testNoInstancePresentIfNoRedisClientsClass() { + assertTrue(bean.isUnsatisfied(), + "No Stork metrics bean"); + } + +} diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsLoadBalancerFailTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsLoadBalancerFailTest.java new file mode 100644 index 0000000000000..0262201f5a416 --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsLoadBalancerFailTest.java @@ -0,0 +1,98 @@ + +package io.quarkus.micrometer.deployment.binder; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import io.quarkus.micrometer.runtime.binder.stork.StorkObservationCollectorBean; +import io.quarkus.micrometer.test.MockServiceSelectorConfiguration; +import io.quarkus.micrometer.test.MockServiceSelectorProvider; +import io.quarkus.micrometer.test.MockServiceSelectorProviderLoader; +import io.quarkus.micrometer.test.PingPongResource; +import io.quarkus.micrometer.test.Util; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.smallrye.stork.api.observability.ObservationPoints; + +@DisabledOnOs(OS.WINDOWS) +public class StorkMetricsLoadBalancerFailTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("test-logging.properties") + .overrideConfigKey("quarkus.stork.pingpong-service.service-discovery.type", "static") + .overrideConfigKey("quarkus.stork.pingpong-service.service-discovery.address-list", "${test.url}") + .overrideConfigKey("pingpong/mp-rest/url", "stork://pingpong-service") + .overrideConfigKey("quarkus.stork.pingpong-service.load-balancer.type", "mock") + .withApplicationRoot((jar) -> jar + .addClasses(PingPongResource.class, PingPongResource.PingPongRestClient.class, + MockServiceSelectorProvider.class, MockServiceSelectorConfiguration.class, + MockServiceSelectorProviderLoader.class, Util.class)); + + @Inject + MeterRegistry registry; + + @Inject + MockServiceSelectorProvider provider; + + @Test + public void shouldGetStorkMetricsWhenServiceSelectorFails() { + + Mockito.when(provider.getLoadBalancer().selectServiceInstance(Mockito.anyCollection())) + .thenThrow(new RuntimeException("Load Balancer induced failure")); + RestAssured.when().get("/ping/one").then().statusCode(500); + + //Stork metrics + assertStorkMetrics(); + + // Stork metrics exposed to Micrometer + Counter instanceCounter = registry.get("stork.instances.count").counter(); + Timer serviceDiscoveryDuration = registry.get("stork.service-discovery.duration").timer(); + Timer serviceSelectionDuration = registry.get("stork.service-selection.duration").timer(); + Timer overallDuration = registry.get("stork.overall.duration").timer(); + Gauge serviceDiscoveryFailures = registry.get("stork.service-discovery.failures").gauge(); + Gauge loadBalancerFailures = registry.get("stork.load-balancer.failures").gauge(); + + Util.assertTags(Tag.of("service-name", "pingpong-service"), instanceCounter, serviceDiscoveryDuration, + serviceSelectionDuration, overallDuration); + + assertThat(instanceCounter.count()).isEqualTo(1); + assertThat(serviceDiscoveryFailures.value()).isEqualTo(0); + assertThat(loadBalancerFailures.value()).isEqualTo(1); + assertThat(serviceDiscoveryDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + assertThat(serviceSelectionDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + assertThat(overallDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + + } + + private static void assertStorkMetrics() { + ObservationPoints.StorkResolutionEvent metrics = StorkObservationCollectorBean.STORK_METRICS + .get("pingpong-service" + StorkObservationCollectorBean.METRICS_SUFIX); + Assertions.assertThat(metrics.getDiscoveredInstancesCount()).isEqualTo(1); + Assertions.assertThat(metrics.getSelectedInstanceId()).isEqualTo(-1); + Assertions.assertThat(metrics.getServiceName()).isEqualTo("pingpong-service"); + Assertions.assertThat(metrics.isDone()).isTrue(); + Assertions.assertThat(metrics.failure().getMessage()).isEqualTo("Load Balancer induced failure"); + Assertions.assertThat(metrics.getOverallDuration()).isNotNull(); + Assertions.assertThat(metrics.getServiceDiscoveryType()).isEqualTo("static"); + Assertions.assertThat(metrics.getServiceSelectionType()).isEqualTo("mock"); + Assertions.assertThat(metrics.getServiceDiscoveryDuration()).isNotNull(); + Assertions.assertThat(metrics.getServiceSelectionDuration()).isNotNull(); + } + +} diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsServiceDiscoveryFailTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsServiceDiscoveryFailTest.java new file mode 100644 index 0000000000000..8a3bcc483ab32 --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsServiceDiscoveryFailTest.java @@ -0,0 +1,94 @@ + +package io.quarkus.micrometer.deployment.binder; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import io.quarkus.micrometer.runtime.binder.stork.StorkObservationCollectorBean; +import io.quarkus.micrometer.test.MockServiceDiscoveryConfiguration; +import io.quarkus.micrometer.test.MockServiceDiscoveryProvider; +import io.quarkus.micrometer.test.MockServiceDiscoveryProviderLoader; +import io.quarkus.micrometer.test.PingPongResource; +import io.quarkus.micrometer.test.Util; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.api.observability.ObservationPoints; + +public class StorkMetricsServiceDiscoveryFailTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("test-logging.properties") + .overrideConfigKey("quarkus.stork.pingpong-service.service-discovery.type", "mock") + .overrideConfigKey("pingpong/mp-rest/url", "stork://pingpong-service") + .withApplicationRoot((jar) -> jar + .addClasses(PingPongResource.class, PingPongResource.PingPongRestClient.class, + MockServiceDiscoveryProvider.class, MockServiceDiscoveryConfiguration.class, + MockServiceDiscoveryProviderLoader.class, Util.class)); + + @Inject + MeterRegistry registry; + + @Inject + MockServiceDiscoveryProvider provider; + + @Test + public void shouldGetStorkMetricsWhenServiceDiscoveryFails() { + + Mockito.when(provider.getServiceDiscovery().getServiceInstances()) + .thenReturn(Uni.createFrom().failure(new RuntimeException("Service Discovery induced failure"))); + RestAssured.when().get("/ping/one").then().statusCode(500); + + //Stork metrics + assertStorkMetrics(); + + // Stork metrics exposed to Micrometer + Counter instanceCounter = registry.get("stork.instances.count").counter(); + Timer serviceDiscoveryDuration = registry.get("stork.service-discovery.duration").timer(); + Timer serviceSelectionDuration = registry.get("stork.service-selection.duration").timer(); + Timer overallDuration = registry.get("stork.overall.duration").timer(); + Gauge serviceDiscoveryFailures = registry.get("stork.service-discovery.failures").gauge(); + Gauge loadBalancerFailures = registry.get("stork.load-balancer.failures").gauge(); + + Util.assertTags(Tag.of("service-name", "pingpong-service"), instanceCounter, serviceDiscoveryDuration, + serviceSelectionDuration, overallDuration); + + assertThat(instanceCounter.count()).isEqualTo(0); + assertThat(serviceDiscoveryFailures.value()).isEqualTo(1); + assertThat(loadBalancerFailures.value()).isEqualTo(0); + assertThat(serviceDiscoveryDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + assertThat(serviceSelectionDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + assertThat(overallDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + + } + + private static void assertStorkMetrics() { + ObservationPoints.StorkResolutionEvent metrics = StorkObservationCollectorBean.STORK_METRICS + .get("pingpong-service" + StorkObservationCollectorBean.METRICS_SUFIX); + Assertions.assertThat(metrics.getDiscoveredInstancesCount()).isNegative(); + Assertions.assertThat(metrics.getSelectedInstanceId()).isEqualTo(-1); + Assertions.assertThat(metrics.getServiceName()).isEqualTo("pingpong-service"); + Assertions.assertThat(metrics.isDone()).isTrue(); + Assertions.assertThat(metrics.failure().getMessage()).isEqualTo("Service Discovery induced failure"); + Assertions.assertThat(metrics.getOverallDuration()).isNotNull(); + Assertions.assertThat(metrics.getServiceDiscoveryType()).isEqualTo("mock"); + Assertions.assertThat(metrics.getServiceSelectionType()).isEqualTo("round-robin"); + Assertions.assertThat(metrics.getServiceDiscoveryDuration()).isNotNull(); + Assertions.assertThat(metrics.getServiceSelectionDuration()).isNotNull(); + } + +} diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsTest.java new file mode 100644 index 0000000000000..80eba2968436f --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsTest.java @@ -0,0 +1,96 @@ + +package io.quarkus.micrometer.deployment.binder; + +import static io.restassured.RestAssured.when; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import io.quarkus.micrometer.runtime.binder.stork.StorkObservationCollectorBean; +import io.quarkus.micrometer.test.GreetingResource; +import io.quarkus.micrometer.test.PingPongResource; +import io.quarkus.micrometer.test.Util; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.stork.api.observability.ObservationPoints; + +public class StorkMetricsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("test-logging.properties") + .overrideConfigKey("pingpong/mp-rest/url", "stork://pingpong-service") + .overrideConfigKey("quarkus.stork.pingpong-service.service-discovery.type", "static") + .overrideConfigKey("quarkus.stork.pingpong-service.service-discovery.address-list", "${test.url}") + .overrideConfigKey("greeting/mp-rest/url", "stork://greeting-service/greeting") + .overrideConfigKey("quarkus.stork.greeting-service.service-discovery.type", "static") + .overrideConfigKey("quarkus.stork.greeting-service.service-discovery.address-list", "${test.url}") + .withApplicationRoot((jar) -> jar + .addClasses(PingPongResource.class, PingPongResource.PingPongRestClient.class, GreetingResource.class, + GreetingResource.GreetingRestClient.class, Util.class)); + + @Inject + MeterRegistry registry; + + @Test + public void shouldGetStorkMetricsForTwoServicesWhenEverythingSucceded() { + when().get("/ping/one").then().statusCode(200); + when().get("greeting/hola").then().statusCode(200); + + //Stork metrics + assertStorkMetrics("pingpong-service"); + assertStorkMetrics("greeting-service"); + + // Stork metrics exposed to Micrometer + assertStorkMetricsInMicrometerRegistry("pingpong-service"); + assertStorkMetricsInMicrometerRegistry("greeting-service"); + + } + + public void assertStorkMetricsInMicrometerRegistry(String serviceName) { + Counter instanceCounter = registry.counter("stork.instances.count", "service-name", serviceName); + Timer serviceDiscoveryDuration = registry.timer("stork.service-discovery.duration", "service-name", serviceName); + Timer serviceSelectionDuration = registry.timer("stork.service-selection.duration", "service-name", serviceName); + Timer overallDuration = registry.timer("stork.overall.duration", "service-name", serviceName); + Gauge serviceDiscoveryFailures = registry.get("stork.service-discovery.failures") + .tags("service-name", serviceName).gauge(); + Gauge loadBalancerFailures = registry.get("stork.load-balancer.failures").tags("service-name", serviceName) + .gauge(); + + Util.assertTags(Tag.of("service-name", serviceName), instanceCounter, serviceDiscoveryDuration, + serviceSelectionDuration, overallDuration); + + Assertions.assertThat(instanceCounter.count()).isEqualTo(1); + Assertions.assertThat(serviceDiscoveryDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + Assertions.assertThat(serviceSelectionDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + Assertions.assertThat(overallDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + Assertions.assertThat(serviceDiscoveryFailures.value()).isEqualTo(0); + Assertions.assertThat(loadBalancerFailures.value()).isEqualTo(0); + } + + public static void assertStorkMetrics(String serviceName) { + ObservationPoints.StorkResolutionEvent metrics = StorkObservationCollectorBean.STORK_METRICS + .get(serviceName + StorkObservationCollectorBean.METRICS_SUFIX); + Assertions.assertThat(metrics.getDiscoveredInstancesCount()).isEqualTo(1); + Assertions.assertThat(metrics.getSelectedInstanceId()).isNotNegative(); + Assertions.assertThat(metrics.getServiceName()).isEqualTo(serviceName); + Assertions.assertThat(metrics.isDone()).isTrue(); + Assertions.assertThat(metrics.failure()).isNull(); + Assertions.assertThat(metrics.getOverallDuration()).isNotNull(); + Assertions.assertThat(metrics.getServiceDiscoveryType()).isEqualTo("static"); + Assertions.assertThat(metrics.getServiceSelectionType()).isEqualTo("round-robin"); + Assertions.assertThat(metrics.getServiceDiscoveryDuration()).isNotNull(); + Assertions.assertThat(metrics.getServiceSelectionDuration()).isNotNull(); + } + +} diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/GreetingResource.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/GreetingResource.java new file mode 100644 index 0000000000000..e78a85dfc8ef4 --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/GreetingResource.java @@ -0,0 +1,41 @@ +package io.quarkus.micrometer.test; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("/greeting") +@ApplicationScoped +public class GreetingResource { + + @RegisterRestClient(configKey = "greeting") + public interface GreetingRestClient { + @GET + @Path("/echo/{message}") + @Consumes(MediaType.TEXT_PLAIN) + String echo(@PathParam("message") String name); + } + + @RestClient + GreetingRestClient greetingRestClient; + + @GET + @Path("/{message}") + public String passThrough(@PathParam("message") String message) { + return greetingRestClient.echo(message + " World!"); + } + + @GET + @Path("/echo/{message}") + public Response echo(@PathParam("message") String message) { + return Response.ok(message, "text/plain").build(); + } + +} diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceDiscoveryConfiguration.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceDiscoveryConfiguration.java new file mode 100644 index 0000000000000..ed77088d030a7 --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceDiscoveryConfiguration.java @@ -0,0 +1,50 @@ +package io.quarkus.micrometer.test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Configuration for the {@code MockServiceDiscoveryProvider} ServiceDiscovery. + */ +public class MockServiceDiscoveryConfiguration implements io.smallrye.stork.api.config.ConfigWithType { + private final Map parameters; + + /** + * Creates a new MockConfiguration + * + * @param params the parameters, must not be {@code null} + */ + public MockServiceDiscoveryConfiguration(Map params) { + parameters = Collections.unmodifiableMap(params); + } + + /** + * Creates a new MockConfiguration + */ + public MockServiceDiscoveryConfiguration() { + parameters = Collections.emptyMap(); + } + + /** + * @return the type + */ + @Override + public String type() { + return "mock"; + } + + /** + * @return the parameters + */ + @Override + public Map parameters() { + return parameters; + } + + private MockServiceDiscoveryConfiguration extend(String key, String value) { + Map copy = new HashMap<>(parameters); + copy.put(key, value); + return new MockServiceDiscoveryConfiguration(copy); + } +} diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceDiscoveryProvider.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceDiscoveryProvider.java new file mode 100644 index 0000000000000..3de40aa6577cf --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceDiscoveryProvider.java @@ -0,0 +1,30 @@ +package io.quarkus.micrometer.test; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.mockito.Mockito; + +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.ServiceConfig; +import io.smallrye.stork.api.config.ServiceDiscoveryType; +import io.smallrye.stork.spi.ServiceDiscoveryProvider; +import io.smallrye.stork.spi.StorkInfrastructure; + +@ServiceDiscoveryType("mock") +@ApplicationScoped +public class MockServiceDiscoveryProvider implements ServiceDiscoveryProvider { + + public ServiceDiscovery getServiceDiscovery() { + return serviceDiscovery; + } + + private final ServiceDiscovery serviceDiscovery = Mockito.mock(ServiceDiscovery.class);; + + @Override + public ServiceDiscovery createServiceDiscovery(MockServiceDiscoveryConfiguration config, String serviceName, + ServiceConfig serviceConfig, + StorkInfrastructure storkInfrastructure) { + return serviceDiscovery; + } + +} diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceDiscoveryProviderLoader.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceDiscoveryProviderLoader.java new file mode 100644 index 0000000000000..f68fdd0813cfc --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceDiscoveryProviderLoader.java @@ -0,0 +1,44 @@ +package io.quarkus.micrometer.test; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.spi.CDI; + +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.ConfigWithType; +import io.smallrye.stork.api.config.ServiceConfig; +import io.smallrye.stork.spi.StorkInfrastructure; + +/** + * ServiceDiscoveryLoader for {@link io.quarkus.micrometer.test.MockServiceDiscoveryProvider} + */ +@ApplicationScoped +public class MockServiceDiscoveryProviderLoader implements io.smallrye.stork.spi.internal.ServiceDiscoveryLoader { + private final io.quarkus.micrometer.test.MockServiceDiscoveryProvider provider; + + public MockServiceDiscoveryProviderLoader() { + io.quarkus.micrometer.test.MockServiceDiscoveryProvider actual = null; + try { + actual = CDI.current().select(io.quarkus.micrometer.test.MockServiceDiscoveryProvider.class).get(); + } catch (Exception e) { + // Use direct instantiation + actual = new io.quarkus.micrometer.test.MockServiceDiscoveryProvider(); + } + this.provider = actual; + } + + @Override + public ServiceDiscovery createServiceDiscovery(ConfigWithType config, String serviceName, + ServiceConfig serviceConfig, StorkInfrastructure storkInfrastructure) { + MockServiceDiscoveryConfiguration typedConfig = new MockServiceDiscoveryConfiguration( + config.parameters()); + return provider.createServiceDiscovery(typedConfig, serviceName, serviceConfig, storkInfrastructure); + } + + /** + * @return the type + */ + @Override + public String type() { + return "mock"; + } +} diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceSelectorConfiguration.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceSelectorConfiguration.java new file mode 100644 index 0000000000000..5c0fcef7f6b1c --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceSelectorConfiguration.java @@ -0,0 +1,50 @@ +package io.quarkus.micrometer.test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Configuration for the {@code MockLoadBalancerProvider} LoadBalancer. + */ +public class MockServiceSelectorConfiguration implements io.smallrye.stork.api.config.ConfigWithType { + private final Map parameters; + + /** + * Creates a new FakeSelectorConfiguration + * + * @param params the parameters, must not be {@code null} + */ + public MockServiceSelectorConfiguration(Map params) { + parameters = Collections.unmodifiableMap(params); + } + + /** + * Creates a new FakeSelectorConfiguration + */ + public MockServiceSelectorConfiguration() { + parameters = Collections.emptyMap(); + } + + /** + * @return the type + */ + @Override + public String type() { + return "fake-selector"; + } + + /** + * @return the parameters + */ + @Override + public Map parameters() { + return parameters; + } + + private MockServiceSelectorConfiguration extend(String key, String value) { + Map copy = new HashMap<>(parameters); + copy.put(key, value); + return new MockServiceSelectorConfiguration(copy); + } +} diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceSelectorProvider.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceSelectorProvider.java new file mode 100644 index 0000000000000..87f4bada8e7c6 --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceSelectorProvider.java @@ -0,0 +1,26 @@ +package io.quarkus.micrometer.test; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.mockito.Mockito; + +import io.smallrye.stork.api.LoadBalancer; +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.LoadBalancerType; +import io.smallrye.stork.spi.LoadBalancerProvider; + +@LoadBalancerType("mock") +@ApplicationScoped +public class MockServiceSelectorProvider implements LoadBalancerProvider { + + private final LoadBalancer loadBalancer = Mockito.mock(LoadBalancer.class); + + @Override + public LoadBalancer createLoadBalancer(MockServiceSelectorConfiguration config, ServiceDiscovery serviceDiscovery) { + return loadBalancer; + } + + public LoadBalancer getLoadBalancer() { + return loadBalancer; + } +} diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceSelectorProviderLoader.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceSelectorProviderLoader.java new file mode 100644 index 0000000000000..c57d1991150ab --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/MockServiceSelectorProviderLoader.java @@ -0,0 +1,42 @@ +package io.quarkus.micrometer.test; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.spi.CDI; + +import io.smallrye.stork.api.LoadBalancer; +import io.smallrye.stork.api.ServiceDiscovery; +import io.smallrye.stork.api.config.ConfigWithType; + +/** + * LoadBalancerLoader for io.quarkus.it.rest.client.reactive.stork.MockLoadBalancerProvider + */ +@ApplicationScoped +public class MockServiceSelectorProviderLoader implements io.smallrye.stork.spi.internal.LoadBalancerLoader { + private final MockServiceSelectorProvider provider; + + public MockServiceSelectorProviderLoader() { + MockServiceSelectorProvider actual = null; + try { + actual = CDI.current().select(MockServiceSelectorProvider.class).get(); + } catch (Exception e) { + // Use direct instantiation + actual = new MockServiceSelectorProvider(); + } + this.provider = actual; + } + + @Override + public LoadBalancer createLoadBalancer(ConfigWithType config, ServiceDiscovery serviceDiscovery) { + io.quarkus.micrometer.test.MockServiceSelectorConfiguration typedConfig = new io.quarkus.micrometer.test.MockServiceSelectorConfiguration( + config.parameters()); + return provider.createLoadBalancer(typedConfig, serviceDiscovery); + } + + /** + * @return the type + */ + @Override + public String type() { + return "mock"; + } +} diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/Util.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/Util.java index fdf95698ebf8d..3760942d0a2fe 100644 --- a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/Util.java +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/Util.java @@ -1,5 +1,7 @@ package io.quarkus.micrometer.test; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -8,7 +10,9 @@ import org.junit.jupiter.api.Assertions; +import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; public class Util { private Util() { @@ -61,4 +65,11 @@ public static void waitForMeters(Collection collection, int count) throws Thread.sleep(3); } while (collection.size() < count && i++ < 10); } + + public static void assertTags(Tag tag, Meter... meters) { + for (Meter meter : meters) { + assertThat(meter.getId().getTags().contains(tag)); + } + } + } diff --git a/extensions/micrometer/runtime/pom.xml b/extensions/micrometer/runtime/pom.xml index c3da82799ccbc..c4f1679c24b20 100644 --- a/extensions/micrometer/runtime/pom.xml +++ b/extensions/micrometer/runtime/pom.xml @@ -113,6 +113,18 @@ true + + io.quarkus + quarkus-redis-client + true + + + + io.quarkus + quarkus-smallrye-stork + true + + io.quarkus.resteasy.reactive resteasy-reactive-client diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/stork/StorkObservationCollectorBean.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/stork/StorkObservationCollectorBean.java new file mode 100644 index 0000000000000..9e6feaef9130c --- /dev/null +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/stork/StorkObservationCollectorBean.java @@ -0,0 +1,120 @@ +package io.quarkus.micrometer.runtime.binder.stork; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Typed; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import io.smallrye.stork.api.ServiceInstance; +import io.smallrye.stork.api.observability.ObservationCollector; +import io.smallrye.stork.api.observability.ObservationPoints; + +@ApplicationScoped +@Typed(ObservationCollector.class) +public class StorkObservationCollectorBean implements ObservationCollector { + + public static final String METRICS_SUFIX = "-metrics"; + final MeterRegistry registry = Metrics.globalRegistry; + + private final EventCompletionHandler STORK_HANDLER = ev -> { + //TODO + }; + public final static Map STORK_METRICS = new ConcurrentHashMap<>(); + + @Override + public ObservationPoints.StorkResolutionEvent create(String serviceName, String serviceDiscoveryType, + String serviceSelectionType) { + return STORK_METRICS.computeIfAbsent(serviceName + METRICS_SUFIX, + key -> new ObservationPoints.StorkResolutionEvent(serviceName, serviceDiscoveryType, serviceSelectionType, + STORK_HANDLER) { + + private final Tags tags = Tags.of(Tag.of("service-name", getServiceName()));; + private final Counter instanceCounter = Counter.builder("stork.instances.count") + .description("The number of service instances discovered") + .tags(tags) + .register(registry);; + + private final Timer serviceDiscoveryTimer = Timer + .builder("stork.service-discovery.duration") + .description("The duration of the discovery operation") + .tags(tags) + .register(registry); + + private final Timer serviceSelectionTimer = Timer + .builder("stork.service-selection.duration") + .description("The duration of the selection operation ") + .tags(tags) + .register(registry); + + private final Timer overallTimer = Timer.builder("stork.overall.duration") + .description("The total duration of the Stork service discovery and selection operations") + .tags(tags) + .register(registry); + + private List serviceDiscoveryExceptions = new ArrayList<>(); + + private final Gauge serviceDiscoveryFailures = Gauge + .builder("stork.service-discovery.failures", serviceDiscoveryExceptions, + List::size) + .description("The number of failures during service discovery").tags(tags) + .register(registry); + + private List serviceSelectionExceptions = new ArrayList<>(); + + private final Gauge serviceSelectionFailures = Gauge + .builder("stork.load-balancer.failures", serviceSelectionExceptions, List::size) + .description("The number of failures during service selection.").tags(tags) + .register(registry); + + @Override + public void onServiceDiscoverySuccess(List instances) { + super.onServiceDiscoverySuccess(instances); + if (instances != null) { + instanceCounter.increment(); + } + serviceDiscoveryTimer.record(getServiceDiscoveryDuration().getNano(), TimeUnit.NANOSECONDS); + + } + + @Override + public void onServiceDiscoveryFailure(Throwable throwable) { + super.onServiceDiscoveryFailure(throwable); + serviceDiscoveryExceptions.add(throwable); + serviceDiscoveryTimer.record(getServiceDiscoveryDuration().getNano(), TimeUnit.NANOSECONDS); + overallTimer.record(getServiceDiscoveryDuration().getNano(), TimeUnit.NANOSECONDS); + + } + + @Override + public void onServiceSelectionSuccess(long id) { + super.onServiceSelectionSuccess(id); + serviceSelectionTimer.record(getServiceSelectionDuration().getNano(), TimeUnit.NANOSECONDS); + overallTimer.record(getOverallDuration().getNano(), TimeUnit.NANOSECONDS); + + } + + @Override + public void onServiceSelectionFailure(Throwable throwable) { + if (failure != throwable) { //if SD fails we don't know count the error as a LB error + serviceSelectionExceptions.add(throwable); + } + super.onServiceSelectionFailure(throwable); + serviceSelectionTimer.record(getServiceSelectionDuration().getNano(), TimeUnit.NANOSECONDS); + overallTimer.record(getServiceSelectionDuration().getNano(), TimeUnit.NANOSECONDS); + } + + }); + } + +} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java index 75042e33cf11d..7286227a22b23 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java @@ -99,6 +99,7 @@ public static class BinderConfig { public KafkaConfigGroup kafka; public RedisConfigGroup redis; + public StorkConfigGroup stork; public GrpcServerConfigGroup grpcServer; diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/StorkConfigGroup.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/StorkConfigGroup.java new file mode 100644 index 0000000000000..cca42c194fbb2 --- /dev/null +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/StorkConfigGroup.java @@ -0,0 +1,33 @@ +package io.quarkus.micrometer.runtime.config; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class StorkConfigGroup implements MicrometerConfig.CapabilityEnabled { + /** + * Stork metrics support. + *

+ * Support for Stork metrics will be enabled if Micrometer support is enabled, + * the Quarkus Stork extension is on the classpath + * and either this value is true, or this value is unset and + * {@code quarkus.micrometer.binder-enabled-default} is true. + */ + @ConfigItem + public Optional enabled; + + @Override + public Optional getEnabled() { + return enabled; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + + "{enabled=" + enabled + + '}'; + } + +} diff --git a/extensions/smallrye-stork/runtime/src/main/java/io/quarkus/stork/QuarkusStorkObservableInfrastructure.java b/extensions/smallrye-stork/runtime/src/main/java/io/quarkus/stork/QuarkusStorkObservableInfrastructure.java new file mode 100644 index 0000000000000..1030fab1b6696 --- /dev/null +++ b/extensions/smallrye-stork/runtime/src/main/java/io/quarkus/stork/QuarkusStorkObservableInfrastructure.java @@ -0,0 +1,22 @@ +package io.quarkus.stork; + +import jakarta.inject.Singleton; + +import io.smallrye.stork.api.observability.ObservationCollector; +import io.vertx.core.Vertx; + +@Singleton +public class QuarkusStorkObservableInfrastructure extends QuarkusStorkInfrastructure { + + private final ObservationCollector observationCollector; + + public QuarkusStorkObservableInfrastructure(Vertx vertx, ObservationCollector observationCollector) { + super(vertx); + this.observationCollector = observationCollector; + } + + @Override + public ObservationCollector getObservationCollector() { + return observationCollector; + } +} diff --git a/extensions/smallrye-stork/runtime/src/main/java/io/quarkus/stork/SmallRyeStorkRecorder.java b/extensions/smallrye-stork/runtime/src/main/java/io/quarkus/stork/SmallRyeStorkRecorder.java index dd82473fdac6c..4935a63c2cf29 100644 --- a/extensions/smallrye-stork/runtime/src/main/java/io/quarkus/stork/SmallRyeStorkRecorder.java +++ b/extensions/smallrye-stork/runtime/src/main/java/io/quarkus/stork/SmallRyeStorkRecorder.java @@ -2,11 +2,15 @@ import java.util.List; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.CDI; + import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.smallrye.stork.Stork; import io.smallrye.stork.api.config.ServiceConfig; +import io.smallrye.stork.api.observability.ObservationCollector; import io.vertx.core.Vertx; @Recorder @@ -15,7 +19,14 @@ public class SmallRyeStorkRecorder { public void initialize(ShutdownContext shutdown, RuntimeValue vertx, StorkConfiguration configuration) { List serviceConfigs = StorkConfigUtil.toStorkServiceConfig(configuration); StorkConfigProvider.init(serviceConfigs); - Stork.initialize(new QuarkusStorkInfrastructure(vertx.getValue())); + Instance instance = CDI.current().select(ObservationCollector.class); + if (instance.isResolvable()) { + Stork.initialize(new QuarkusStorkObservableInfrastructure(vertx.getValue(), instance.get())); + } else { + QuarkusStorkInfrastructure infrastructure = new QuarkusStorkInfrastructure(vertx.getValue()); + Stork.initialize(infrastructure); + } + shutdown.addShutdownTask(new Runnable() { @Override public void run() { diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 72498b8752c9e..3fbf2fee70c8a 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -67,7 +67,7 @@ 5.3.0 1.0.0.Final 2.15.2 - 2.3.1 + 2.4.0-SNAPSHOT 3.0.2 3.0.3 3.0.0