diff --git a/bom/application/pom.xml b/bom/application/pom.xml index c5fd6ad9c5a395..3fe1bde884aa9f 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -64,7 +64,7 @@ 3.0.1 3.6.0 4.10.1 - 2.3.1 + 2.4.0 2.1.2 2.1.1 3.0.0 diff --git a/docs/src/main/asciidoc/stork-reference.adoc b/docs/src/main/asciidoc/stork-reference.adoc index 69a0725ec76454..44636a78ff2e41 100644 --- a/docs/src/main/asciidoc/stork-reference.adoc +++ b/docs/src/main/asciidoc/stork-reference.adoc @@ -72,3 +72,70 @@ To learn about custom service discovery and service selection, check: - https://smallrye.io/smallrye-stork/latest/service-discovery/custom-service-discovery/[Implement a custom service discover provider] - https://smallrye.io/smallrye-stork/latest/load-balancer/custom-load-balancer/[Implement a custom service selection provider] + +== Configure Stork observability + +=== Enable metrics + +Stork metrics are automatically enabled when the application also uses the xref:telemetry-micrometer.adoc[`quarkus-micrometer`] extension. + +Micrometer collects the metrics of rest/grpc clients using Stork and the client using Stork programmatically. + +As an example, if you export the metrics to Prometheus, you will get: + +[source,text] +---- +# HELP stork_service_selection_failures_total The number of failures during service selection. +# TYPE stork_service_selection_failures_total counter +stork_service_selection_failures_total{service_name="hello-service",} 0.0 +# HELP stork_service_selection_duration_seconds The duration of the selection operation +# TYPE stork_service_selection_duration_seconds summary +stork_service_selection_duration_seconds_count{service_name="hello-service",} 13.0 +stork_service_selection_duration_seconds_sum{service_name="hello-service",} 0.001049291 +# HELP stork_service_selection_duration_seconds_max The duration of the selection operation +# TYPE stork_service_selection_duration_seconds_max gauge +stork_service_selection_duration_seconds_max{service_name="hello-service",} 0.0 +# HELP stork_overall_duration_seconds_max The total duration of the Stork service discovery and selection operations +# TYPE stork_overall_duration_seconds_max gauge +stork_overall_duration_seconds_max{service_name="hello-service",} 0.0 +# HELP stork_overall_duration_seconds The total duration of the Stork service discovery and selection operations +# TYPE stork_overall_duration_seconds summary +stork_overall_duration_seconds_count{service_name="hello-service",} 13.0 +stork_overall_duration_seconds_sum{service_name="hello-service",} 0.001049291 +# HELP stork_service_discovery_failures_total The number of failures during service discovery +# TYPE stork_service_discovery_failures_total counter +stork_service_discovery_failures_total{service_name="hello-service",} 0.0 +# HELP stork_service_discovery_duration_seconds_max The duration of the discovery operation +# TYPE stork_service_discovery_duration_seconds_max gauge +stork_service_discovery_duration_seconds_max{service_name="hello-service",} 0.0 +# HELP stork_service_discovery_duration_seconds The duration of the discovery operation +# TYPE stork_service_discovery_duration_seconds summary +stork_service_discovery_duration_seconds_count{service_name="hello-service",} 13.0 +stork_service_discovery_duration_seconds_sum{service_name="hello-service",} 6.585046209 +# HELP stork_service_discovery_instances_count_total The number of service instances discovered +# TYPE stork_service_discovery_instances_count_total counter +stork_service_discovery_instances_count_total{service_name="hello-service",} 26.0 +---- + +The Stork service name can be found in the _tags_. + +The metrics contain both the service discovery (`stork_service_discovery_*`) and the metrics about the service selection (`stork_service_selection_*`) such as the number of service instances, failures, and durations. + +=== Disable metrics + +To disable the Stork metrics when `quarkus-micrometer` is used, add the following property to the application configuration: + +[source,properties] +---- +quarkus.micrometer.binder.stork.enabled=false +---- + + +[[stork-configuration-reference]] +== Configuration reference + +include::{generated-dir}/config/quarkus-stork.adoc[opts=optional, leveloffset=+1] + + + + diff --git a/extensions/micrometer/deployment/pom.xml b/extensions/micrometer/deployment/pom.xml index 8273cf3a321fed..c2f3a96cd625eb 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 00000000000000..06ed79c3ea715b --- /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 00000000000000..8264018946251d --- /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 00000000000000..0f39211a8bd183 --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsLoadBalancerFailTest.java @@ -0,0 +1,111 @@ + +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.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.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.StorkObservation; + +@DisabledOnOs(OS.WINDOWS) +public class StorkMetricsLoadBalancerFailTest { + + @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("quarkus.stork.pingpong-service.load-balancer.type", "mock") + .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}") + .overrideConfigKey("quarkus.stork.greeting-service.load-balancer.type", "mock") + .withApplicationRoot((jar) -> jar + .addClasses(PingPongResource.class, PingPongResource.PingPongRestClient.class, + MockServiceSelectorProvider.class, MockServiceSelectorConfiguration.class, + MockServiceSelectorProviderLoader.class, GreetingResource.class, + GreetingResource.GreetingRestClient.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); + RestAssured.when().get("/greeting/hola").then().statusCode(500); + + //Stork metrics + assertStorkMetrics("pingpong-service"); + assertStorkMetrics("greeting-service"); + + // Stork metrics exposed to Micrometer + assertStorkMetricsInMicrometerRegistry("pingpong-service"); + assertStorkMetricsInMicrometerRegistry("greeting-service"); + + } + + private static void assertStorkMetrics(String serviceName) { + StorkObservation metrics = StorkObservationCollectorBean.STORK_METRICS + .get(serviceName + StorkObservationCollectorBean.METRICS_SUFIX); + Assertions.assertThat(metrics.getDiscoveredInstancesCount()).isEqualTo(1); + Assertions.assertThat(metrics.getServiceName()).isEqualTo(serviceName); + Assertions.assertThat(metrics.isDone()).isTrue(); + Assertions.assertThat(metrics.isServiceDiscoverySuccessful()).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(); + } + + private void assertStorkMetricsInMicrometerRegistry(String serviceName) { + Counter instanceCounter = registry.counter("stork.service-discovery.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); + Counter serviceDiscoveryFailures = registry.get("stork.service-discovery.failures") + .tags("service-name", serviceName).counter(); + Counter loadBalancerFailures = registry.get("stork.service-selection.failures").tags("service-name", serviceName) + .counter(); + + Util.assertTags(Tag.of("service-name", serviceName), instanceCounter, serviceDiscoveryDuration, + serviceSelectionDuration); + + Assertions.assertThat(instanceCounter.count()).isEqualTo(1); + Assertions.assertThat(loadBalancerFailures.count()).isEqualTo(1); + Assertions.assertThat(serviceDiscoveryFailures.count()).isEqualTo(0); + Assertions.assertThat(serviceDiscoveryDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + Assertions.assertThat(serviceSelectionDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + } + +} 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 00000000000000..164209cd05681a --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsServiceDiscoveryFailTest.java @@ -0,0 +1,109 @@ + +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.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.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.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.StorkObservation; + +@DisabledOnOs(OS.WINDOWS) +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") + .overrideConfigKey("greeting/mp-rest/url", "stork://greeting-service/greeting") + .overrideConfigKey("quarkus.stork.greeting-service.service-discovery.type", "mock") + .withApplicationRoot((jar) -> jar + .addClasses(PingPongResource.class, PingPongResource.PingPongRestClient.class, + MockServiceDiscoveryProvider.class, MockServiceDiscoveryConfiguration.class, + MockServiceDiscoveryProviderLoader.class, GreetingResource.class, + GreetingResource.GreetingRestClient.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); + when().get("/greeting/hola").then().statusCode(500); + + //Stork metrics + assertStorkMetrics("pingpong-service"); + assertStorkMetrics("greeting-service"); + + // Stork metrics exposed to Micrometer + assertStorkMetricsInMicrometerRegistry("pingpong-service"); + assertStorkMetricsInMicrometerRegistry("greeting-service"); + + } + + private void assertStorkMetricsInMicrometerRegistry(String serviceName) { + Counter instanceCounter = registry.counter("stork.service-discovery.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); + Counter serviceDiscoveryFailures = registry.get("stork.service-discovery.failures") + .tags("service-name", serviceName).counter(); + Counter loadBalancerFailures = registry.get("stork.service-selection.failures").tags("service-name", serviceName) + .counter(); + + Util.assertTags(Tag.of("service-name", serviceName), instanceCounter, serviceDiscoveryDuration, + serviceSelectionDuration); + + Assertions.assertThat(instanceCounter.count()).isEqualTo(0); + Assertions.assertThat(loadBalancerFailures.count()).isEqualTo(0); + Assertions.assertThat(serviceDiscoveryFailures.count()).isEqualTo(1); + Assertions.assertThat(serviceDiscoveryDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + Assertions.assertThat(serviceSelectionDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + } + + private static void assertStorkMetrics(String serviceName) { + StorkObservation metrics = StorkObservationCollectorBean.STORK_METRICS + .get(serviceName + StorkObservationCollectorBean.METRICS_SUFIX); + Assertions.assertThat(metrics.getDiscoveredInstancesCount()).isNegative(); + Assertions.assertThat(metrics.getServiceName()).isEqualTo(serviceName); + Assertions.assertThat(metrics.isDone()).isTrue(); + Assertions.assertThat(metrics.isServiceDiscoverySuccessful()).isFalse(); + 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 00000000000000..146ffa74aedb8d --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/StorkMetricsTest.java @@ -0,0 +1,94 @@ + +package io.quarkus.micrometer.deployment.binder; + +import static io.restassured.RestAssured.when; + +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 io.micrometer.core.instrument.Counter; +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.StorkObservation; + +@DisabledOnOs(OS.WINDOWS) +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"); + + } + + private void assertStorkMetricsInMicrometerRegistry(String serviceName) { + Counter instanceCounter = registry.counter("stork.service-discovery.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); + Counter serviceDiscoveryFailures = registry.get("stork.service-discovery.failures") + .tags("service-name", serviceName).counter(); + Counter loadBalancerFailures = registry.get("stork.service-selection.failures").tags("service-name", serviceName) + .counter(); + + Util.assertTags(Tag.of("service-name", serviceName), instanceCounter, serviceDiscoveryDuration, + serviceSelectionDuration); + + Assertions.assertThat(instanceCounter.count()).isEqualTo(1); + Assertions.assertThat(serviceDiscoveryDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + Assertions.assertThat(serviceSelectionDuration.totalTime(TimeUnit.NANOSECONDS)).isGreaterThan(0); + Assertions.assertThat(serviceDiscoveryFailures.count()).isEqualTo(0); + Assertions.assertThat(loadBalancerFailures.count()).isEqualTo(0); + } + + public static void assertStorkMetrics(String serviceName) { + StorkObservation metrics = StorkObservationCollectorBean.STORK_METRICS + .get(serviceName + StorkObservationCollectorBean.METRICS_SUFIX); + Assertions.assertThat(metrics.getDiscoveredInstancesCount()).isEqualTo(1); + 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 00000000000000..e78a85dfc8ef4c --- /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 00000000000000..ed77088d030a7c --- /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 00000000000000..3de40aa6577cfb --- /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 00000000000000..f68fdd0813cfc4 --- /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 00000000000000..5c0fcef7f6b1c5 --- /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 00000000000000..87f4bada8e7c64 --- /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 00000000000000..c57d1991150aba --- /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 fdf95698ebf8d2..3760942d0a2fee 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 c3da82799ccbcc..c4f1679c24b201 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 00000000000000..8d51d62eba095e --- /dev/null +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/stork/StorkObservationCollectorBean.java @@ -0,0 +1,81 @@ +package io.quarkus.micrometer.runtime.binder.stork; + +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.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.observability.ObservationCollector; +import io.smallrye.stork.api.observability.StorkEventHandler; +import io.smallrye.stork.api.observability.StorkObservation; + +@ApplicationScoped +@Typed(ObservationCollector.class) +public class StorkObservationCollectorBean implements ObservationCollector, StorkEventHandler { + + public static final String METRICS_SUFIX = "-metrics"; + final MeterRegistry registry = Metrics.globalRegistry; + + public final static Map STORK_METRICS = new ConcurrentHashMap<>(); + + @Override + public StorkObservation create(String serviceName, String serviceDiscoveryType, + String serviceSelectionType) { + return STORK_METRICS.computeIfAbsent(serviceName + METRICS_SUFIX, + key -> new StorkObservation(serviceName, serviceDiscoveryType, serviceSelectionType, + this)); + } + + @Override + public void complete(StorkObservation observation) { + Tags tags = Tags.of(Tag.of("service-name", observation.getServiceName())); + + Counter instanceCounter = Counter.builder("stork.service-discovery.instances.count") + .description("The number of service instances discovered") + .tags(tags) + .register(registry); + + Timer serviceDiscoveryTimer = Timer + .builder("stork.service-discovery.duration") + .description("The duration of the discovery operation") + .tags(tags) + .register(registry); + + Timer serviceSelectionTimer = Timer + .builder("stork.service-selection.duration") + .description("The duration of the selection operation ") + .tags(tags) + .register(registry); + + Counter serviceDiscoveryFailures = Counter + .builder("stork.service-discovery.failures") + .description("The number of failures during service discovery").tags(tags) + .register(registry); + + Counter serviceSelectionFailures = Counter + .builder("stork.service-selection.failures") + .description("The number of failures during service selection.").tags(tags) + .register(registry); + + instanceCounter.increment(observation.getDiscoveredInstancesCount()); + serviceDiscoveryTimer.record(observation.getServiceDiscoveryDuration().getNano(), TimeUnit.NANOSECONDS); + serviceSelectionTimer.record(observation.getServiceSelectionDuration().getNano(), TimeUnit.NANOSECONDS); + + if (observation.failure() != null) { + if (observation.isServiceDiscoverySuccessful()) { + serviceSelectionFailures.increment(); + } else {// SD failure + serviceDiscoveryFailures.increment(); + } + } + + } +} 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 75042e33cf11dd..7286227a22b238 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 00000000000000..cca42c194fbb2c --- /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 00000000000000..1030fab1b66962 --- /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 dd82473fdac6cc..4935a63c2cf291 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 8ecaa39f8c48a2..e698967926784a 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 3.0.2 3.0.3 3.0.0