From 1fc3271cb0fd0e7b4fbe6d9075a0e7c3efd3b9cc Mon Sep 17 00:00:00 2001 From: Marek Skacelik Date: Wed, 26 Jul 2023 12:26:23 +0200 Subject: [PATCH] SmallRye-GraphQL metrics rework --- .../deployment/SmallRyeGraphQLProcessor.java | 28 +- .../graphql/deployment/MetricsTest.java | 7 +- .../runtime/SmallRyeGraphQLConfigMapping.java | 2 +- .../datafetcher/AbstractAsyncDataFetcher.java | 4 +- .../QuarkusDefaultDataFetcher.java | 5 +- integration-tests/smallrye-graphql/pom.xml | 5 +- .../graphql/metricresources/TestPojo.java | 60 +++ .../graphql/metricresources/TestRandom.java | 31 ++ .../graphql/metricresources/TestResource.java | 138 +++++++ .../src/main/resources/application.properties | 3 +- .../smallrye/graphql/MicrometerMetricsIT.java | 8 + .../graphql/MicrometerMetricsTest.java | 375 ++++++++++++++++++ 12 files changed, 648 insertions(+), 18 deletions(-) create mode 100644 integration-tests/smallrye-graphql/src/main/java/io/quarkus/it/smallrye/graphql/metricresources/TestPojo.java create mode 100644 integration-tests/smallrye-graphql/src/main/java/io/quarkus/it/smallrye/graphql/metricresources/TestRandom.java create mode 100644 integration-tests/smallrye-graphql/src/main/java/io/quarkus/it/smallrye/graphql/metricresources/TestResource.java create mode 100644 integration-tests/smallrye-graphql/src/test/java/io/quarkus/it/smallrye/graphql/MicrometerMetricsIT.java create mode 100644 integration-tests/smallrye-graphql/src/test/java/io/quarkus/it/smallrye/graphql/MicrometerMetricsTest.java diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java index 3febad882c0f5d..d5857405ca9b7d 100644 --- a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java @@ -57,6 +57,7 @@ import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.runtime.metrics.MetricsFactory; import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLConfig; import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLConfigMapping; import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLLocaleResolver; @@ -84,10 +85,10 @@ import io.smallrye.graphql.api.federation.Requires; import io.smallrye.graphql.api.federation.Shareable; import io.smallrye.graphql.api.federation.Tag; -import io.smallrye.graphql.cdi.config.ConfigKey; import io.smallrye.graphql.cdi.config.MicroProfileConfig; import io.smallrye.graphql.cdi.producer.GraphQLProducer; import io.smallrye.graphql.cdi.tracing.TracingService; +import io.smallrye.graphql.config.ConfigKey; import io.smallrye.graphql.schema.Annotations; import io.smallrye.graphql.schema.SchemaBuilder; import io.smallrye.graphql.schema.model.Argument; @@ -102,6 +103,7 @@ import io.smallrye.graphql.schema.model.UnionType; import io.smallrye.graphql.spi.EventingService; import io.smallrye.graphql.spi.LookupService; +import io.smallrye.graphql.spi.config.Config; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; @@ -592,15 +594,21 @@ void printDataFetcherExceptionInDevMode(SmallRyeGraphQLConfig graphQLConfig, void activateMetrics(Capabilities capabilities, Optional metricsCapability, SmallRyeGraphQLConfig graphQLConfig, - BuildProducer systemProperties) { - - boolean activate = shouldActivateService(graphQLConfig.metricsEnabled, - metricsCapability.isPresent(), - "quarkus-smallrye-metrics", - "metrics", - "quarkus.smallrye-graphql.metrics.enabled", - false); - if (activate) { + BuildProducer systemProperties, BuildProducer serviceProvider) { + + if (graphQLConfig.metricsEnabled.orElse(false) + || Config.get().getConfigValue(ConfigKey.ENABLE_METRICS, boolean.class, false)) { + metricsCapability.ifPresentOrElse(capability -> { + if (capability.metricsSupported(MetricsFactory.MICROMETER)) { + serviceProvider.produce(new ServiceProviderBuildItem("io.smallrye.graphql.spi.MetricsService", + "io.smallrye.graphql.cdi.metrics.MicrometerMetricsService")); + } + if (capability.metricsSupported(MetricsFactory.MP_METRICS)) { + serviceProvider.produce(new ServiceProviderBuildItem("io.smallrye.graphql.spi.MetricsService", + "io.smallrye.graphql.cdi.metrics.MPMetricsService")); + } + }, () -> LOG + .warn("GraphQL metrics are enabled but no supported metrics implementation is available on the classpath")); systemProperties.produce(new SystemPropertyBuildItem(ConfigKey.ENABLE_METRICS, TRUE)); } else { systemProperties.produce(new SystemPropertyBuildItem(ConfigKey.ENABLE_METRICS, FALSE)); diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/MetricsTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/MetricsTest.java index 04d1187d7ff5d8..8edc3d233821ea 100644 --- a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/MetricsTest.java +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/MetricsTest.java @@ -34,9 +34,6 @@ public class MetricsTest { @Test public void testQuery() { MetricRegistry metricRegistry = MetricRegistries.get(MetricRegistry.Type.VENDOR); - SimpleTimer metric = metricRegistry.getSimpleTimers() - .get(new MetricID("mp_graphql", new Tag("type", "QUERY"), new Tag("name", "ping"), new Tag("source", "false"))); - assertNotNull(metric, "Metrics should be registered eagerly"); String pingRequest = getPayload("{\n" + " ping {\n" + @@ -55,6 +52,10 @@ public void testQuery() { .and() .body(CoreMatchers.containsString("{\"data\":{\"ping\":{\"message\":\"pong\"}}}")); + SimpleTimer metric = metricRegistry.getSimpleTimers() + .get(new MetricID("mp_graphql", new Tag("name", "ping"), new Tag("source", "false"), new Tag("type", "QUERY"))); + assertNotNull(metric, "Metrics should be registered eagerly"); + assertEquals(1L, metric.getCount(), "Metric should be updated after querying"); } diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLConfigMapping.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLConfigMapping.java index 3c563b548b69c8..f5a41dc4f494ca 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLConfigMapping.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLConfigMapping.java @@ -9,7 +9,7 @@ import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.RelocateConfigSourceInterceptor; -import io.smallrye.graphql.cdi.config.ConfigKey; +import io.smallrye.graphql.config.ConfigKey; /** * Maps config from MicroProfile and SmallRye to Quarkus diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/AbstractAsyncDataFetcher.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/AbstractAsyncDataFetcher.java index 7bd79e776aafe4..43efc52b7a404c 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/AbstractAsyncDataFetcher.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/AbstractAsyncDataFetcher.java @@ -34,9 +34,10 @@ protected O invokeAndTransform( DataFetchingEnvironment dfe, DataFetcherResult.Builder resultBuilder, Object[] transformedArguments) throws Exception { - ManagedContext requestContext = Arc.container().requestContext(); + try { + measurementIds.add(metricsEmitter.start(c)); RequestContextHelper.reactivate(requestContext, dfe); Uni uni = handleUserMethodCall(dfe, transformedArguments); return (O) uni @@ -69,6 +70,7 @@ protected O invokeAndTransform( te.appendDataFetcherResult(resultBuilder, dfe); } finally { eventEmitter.fireAfterDataFetch(c); + metricsEmitter.end(measurementIds.remove()); } } emitter.complete(resultBuilder.build()); diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusDefaultDataFetcher.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusDefaultDataFetcher.java index 7b87e659e21be1..25a76c8599c4fd 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusDefaultDataFetcher.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/datafetcher/QuarkusDefaultDataFetcher.java @@ -77,6 +77,7 @@ private T invokeAndTransformBlocking(final io.smallrye.graphql.api.Context c final Promise result = Promise.promise(); // We need some make sure that we call given the context + measurementIds.add(metricsEmitter.start(c)); @SuppressWarnings("unchecked") Callable contextualCallable = threadContext.contextualCallable(() -> { try { @@ -101,14 +102,15 @@ private T invokeAndTransformBlocking(final io.smallrye.graphql.api.Context c throw ex; } }); - // Here call blocking with context + // measurementIds.add(metricsEmitter.start(c)); BlockingHelper.runBlocking(vc, contextualCallable, result); return (T) Uni.createFrom().completionStage(result.future().toCompletionStage()).onItemOrFailure() .invoke((item, error) -> { if (item != null) { eventEmitter.fireAfterDataFetch(c); + metricsEmitter.end(measurementIds.remove()); } else { eventEmitter.fireOnDataFetchError(c, error); } @@ -136,6 +138,7 @@ private CompletionStage> invokeBatchBlocking(DataFetchingEnvironment dfe BlockingHelper.runBlocking(vc, contextualCallable, result); return result.future().toCompletionStage() .whenComplete((resultList, error) -> { + eventEmitter.fireAfterDataFetch(dfe.getGraphQlContext().get("context")); if (error != null) { onErrorConsumer.accept(error); } diff --git a/integration-tests/smallrye-graphql/pom.xml b/integration-tests/smallrye-graphql/pom.xml index 64e2e214376a2c..2f4e00b926ffef 100644 --- a/integration-tests/smallrye-graphql/pom.xml +++ b/integration-tests/smallrye-graphql/pom.xml @@ -24,7 +24,10 @@ io.quarkus quarkus-smallrye-fault-tolerance - + + io.quarkus + quarkus-micrometer-registry-prometheus-deployment + io.quarkus quarkus-junit5 diff --git a/integration-tests/smallrye-graphql/src/main/java/io/quarkus/it/smallrye/graphql/metricresources/TestPojo.java b/integration-tests/smallrye-graphql/src/main/java/io/quarkus/it/smallrye/graphql/metricresources/TestPojo.java new file mode 100644 index 00000000000000..28eb3274eccf38 --- /dev/null +++ b/integration-tests/smallrye-graphql/src/main/java/io/quarkus/it/smallrye/graphql/metricresources/TestPojo.java @@ -0,0 +1,60 @@ +package io.quarkus.it.smallrye.graphql.metricresources; + +import java.util.Arrays; +import java.util.List; + +/** + * Just a test pojo + */ +public class TestPojo { + private String message; + private List list = Arrays.asList("a", "b", "c"); + + private Number number; + + public TestPojo() { + super(); + } + + public TestPojo(String message) { + super(); + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public Number getNumber() { + return number; + } + + public void setNumber(Number number) { + this.number = number; + } + + // + + @Override + public String toString() { + return "TestPojo{" + "message=" + message + ", list=" + list + ", number=" + number + '}'; + } + + enum Number { + ONE, + TWO, + THREE + } +} diff --git a/integration-tests/smallrye-graphql/src/main/java/io/quarkus/it/smallrye/graphql/metricresources/TestRandom.java b/integration-tests/smallrye-graphql/src/main/java/io/quarkus/it/smallrye/graphql/metricresources/TestRandom.java new file mode 100644 index 00000000000000..65aac06154016c --- /dev/null +++ b/integration-tests/smallrye-graphql/src/main/java/io/quarkus/it/smallrye/graphql/metricresources/TestRandom.java @@ -0,0 +1,31 @@ +package io.quarkus.it.smallrye.graphql.metricresources; + +/** + * Just a test pojo that contains a random number + */ +public class TestRandom { + private double value; + + public TestRandom() { + this(Math.random()); + } + + public TestRandom(double value) { + super(); + this.value = value; + } + + public double getValue() { + return value; + } + + public void setValue(double value) { + this.value = value; + } + + @Override + public String toString() { + return "TestRandom{" + "value=" + value + '}'; + } + +} diff --git a/integration-tests/smallrye-graphql/src/main/java/io/quarkus/it/smallrye/graphql/metricresources/TestResource.java b/integration-tests/smallrye-graphql/src/main/java/io/quarkus/it/smallrye/graphql/metricresources/TestResource.java new file mode 100644 index 00000000000000..f17d20708e5ca8 --- /dev/null +++ b/integration-tests/smallrye-graphql/src/main/java/io/quarkus/it/smallrye/graphql/metricresources/TestResource.java @@ -0,0 +1,138 @@ +package io.quarkus.it.smallrye.graphql.metricresources; + +import java.util.List; + +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Query; +import org.eclipse.microprofile.graphql.Source; + +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLSchema; +import io.micrometer.core.instrument.Metrics; +import io.smallrye.graphql.api.Context; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; + +/** + * Just a test endpoint + */ +@GraphQLApi +public class TestResource { + + public static final double SLEEP_TIME = 0.15; + @Inject + Context context; + + @Query + public TestPojo ping() { + return new TestPojo("pong"); + } + + @Query + public TestPojo foo() { + return new TestPojo("bar"); + } + + @Query + public TestPojo[] superMetricFoo() throws InterruptedException { + Thread.sleep(sleepTimeInMilliseconds()); + return new TestPojo[] { foo(), foo(), foo() }; + } + + @Query + public TestPojo[] foos() { + return new TestPojo[] { foo() }; + } + + @Query + public List batchFoo(@Source List testPojos) throws InterruptedException { + Thread.sleep(sleepTimeInMilliseconds()); + return List.of(new TestPojo("bar1"), new TestPojo("bar2"), new TestPojo("bar3")); + } + + @Query + public Uni> asyncBatchFoo(@Source List testPojos) { + return Uni.createFrom().item(() -> { + try { + Thread.sleep(sleepTimeInMilliseconds()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return List.of(new TestPojo("abar1"), new TestPojo("abar2"), new TestPojo("abar3")); + }); + } + + @Query("context") + public String getPathFromContext() { + return context.getPath(); + } + + @Query + public TestPojo systemserror() { + throw new RuntimeException("Some system problem"); + } + + @Mutation + public TestPojo moo(String name) { + return new TestPojo(name); + } + + @Query + public String testCharset(String characters) { + return characters; + } + + // + @Query + public Uni asyncSuperMetricFoo() throws InterruptedException { + return Uni.createFrom().item(() -> { + try { + Thread.sleep(sleepTimeInMilliseconds()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return new TestPojo[] { new TestPojo("async1"), new TestPojo("async2"), new TestPojo("async3") }; + }).runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); + + } + + @Mutation + public void clearMetrics() { + Metrics.globalRegistry.clear(); + } + + public TestRandom getRandomNumber(@Source TestPojo testPojo) throws InterruptedException { + Thread.sleep(sleepTimeInMilliseconds()); + return new TestRandom(123); + } + + public Uni getRandomNumberAsync(@Source TestPojo testPojo) throws InterruptedException { + return Uni.createFrom().item(() -> { + try { + Thread.sleep(sleepTimeInMilliseconds()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return new TestRandom(123); + }); + } + + public GraphQLSchema.Builder addMyOwnEnum(@Observes GraphQLSchema.Builder builder) { + + GraphQLEnumType myOwnEnum = GraphQLEnumType.newEnum() + .name("SomeEnum") + .description("Adding some enum type") + .value("value1") + .value("value2").build(); + + return builder.additionalType(myOwnEnum); + } + + private long sleepTimeInMilliseconds() { + return (long) (SLEEP_TIME * 1000); + } +} diff --git a/integration-tests/smallrye-graphql/src/main/resources/application.properties b/integration-tests/smallrye-graphql/src/main/resources/application.properties index 8a3936f8d443a5..e28d2319173de2 100644 --- a/integration-tests/smallrye-graphql/src/main/resources/application.properties +++ b/integration-tests/smallrye-graphql/src/main/resources/application.properties @@ -1,2 +1,3 @@ message=Production -quarkus.smallrye-graphql.show-runtime-exception-message=org.eclipse.microprofile.faulttolerance.exceptions.TimeoutException \ No newline at end of file +quarkus.smallrye-graphql.show-runtime-exception-message=org.eclipse.microprofile.faulttolerance.exceptions.TimeoutException +quarkus.smallrye-graphql.metrics.enabled=true \ No newline at end of file diff --git a/integration-tests/smallrye-graphql/src/test/java/io/quarkus/it/smallrye/graphql/MicrometerMetricsIT.java b/integration-tests/smallrye-graphql/src/test/java/io/quarkus/it/smallrye/graphql/MicrometerMetricsIT.java new file mode 100644 index 00000000000000..cbc58cf6f34261 --- /dev/null +++ b/integration-tests/smallrye-graphql/src/test/java/io/quarkus/it/smallrye/graphql/MicrometerMetricsIT.java @@ -0,0 +1,8 @@ +package io.quarkus.it.smallrye.graphql; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class MicrometerMetricsIT extends MicrometerMetricsTest { + +} diff --git a/integration-tests/smallrye-graphql/src/test/java/io/quarkus/it/smallrye/graphql/MicrometerMetricsTest.java b/integration-tests/smallrye-graphql/src/test/java/io/quarkus/it/smallrye/graphql/MicrometerMetricsTest.java new file mode 100644 index 00000000000000..01e9742e4212ee --- /dev/null +++ b/integration-tests/smallrye-graphql/src/test/java/io/quarkus/it/smallrye/graphql/MicrometerMetricsTest.java @@ -0,0 +1,375 @@ +package io.quarkus.it.smallrye.graphql; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jakarta.json.Json; +import jakarta.json.JsonObject; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.it.smallrye.graphql.metricresources.TestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +public class MicrometerMetricsTest { + + private static final double SLEEP_TIME = TestResource.SLEEP_TIME; + + @AfterEach + void resetGlobalMeterRegistry() { + String request = getPayload("mutation { clearMetrics }"); + RestAssured.given().when() + .accept("application/json") + .contentType("application/json") + .body(request) + .post("/graphql") + .then() + .assertThat() + .statusCode(200); + } + + // Run a Query and check that its corresponding metric is updated + @Test + public void shouldCreateMetricFromQuery() { + String request = getPayload("{\n" + + " superMetricFoo {\n" + + " message\n" + + " }\n" + + "}"); + assertResponse(request, + "{\"data\":{\"superMetricFoo\":[{\"message\":\"bar\"},{\"message\":\"bar\"},{\"message\":\"bar\"}]}}"); + assertMetric("superMetricFoo", false, "QUERY", 1l); + } + + @Test + public void shouldCreateMetricsFromQueryAndSourceField() { + String request = getPayload("{\n" + + " superMetricFoo {\n" + + " randomNumber {\n" + + " value\n" + + " }\n" + + " }\n" + + "}"); + + assertResponse(request, + "{\"data\":{\"superMetricFoo\":[{\"randomNumber\":{\"value\":123.0}},{\"randomNumber\":{\"value\":123.0}},{\"randomNumber\":{\"value\":123.0}}]}}"); + assertMetric("superMetricFoo", false, "QUERY", 1l); + assertMetric("randomNumber", true, "QUERY", 3l); + + } + + @Test + public void shouldCreateMetricFromAsyncQuery() { + String request = getPayload("{\n" + + " asyncSuperMetricFoo {\n" + + " message\n" + + " }\n" + + "}"); + assertResponse(request, + "{\"data\":{\"asyncSuperMetricFoo\":[{\"message\":\"async1\"},{\"message\":\"async2\"},{\"message\":\"async3\"}]}}"); + assertMetric("asyncSuperMetricFoo", false, "QUERY", 1l); + } + + @Test + public void shouldCreateMetricsFromAsyncQueryAndSourceField() { + String request = getPayload("{\n" + + " asyncSuperMetricFoo {\n" + + " randomNumber {\n" + + " value\n" + + " }\n" + + " }\n" + + "}"); + + assertResponse(request, + "{\"data\":{\"asyncSuperMetricFoo\":[{\"randomNumber\":{\"value\":123.0}},{\"randomNumber\":{\"value\":123.0}},{\"randomNumber\":{\"value\":123.0}}]}}"); + assertMetric("asyncSuperMetricFoo", false, "QUERY", 1l); + assertMetric("randomNumber", true, "QUERY", 3l); + } + + @Test + public void shouldCreateMetricsFromQueryAndAsyncSourceField() { + String request = getPayload("{\n" + + " superMetricFoo {\n" + + " randomNumberAsync {\n" + + " value\n" + + " }\n" + + " }\n" + + "}"); + + assertResponse(request, + "{\"data\":{\"superMetricFoo\":[{\"randomNumberAsync\":{\"value\":123.0}},{\"randomNumberAsync\":{\"value\":123.0}},{\"randomNumberAsync\":{\"value\":123.0}}]}}"); + assertMetric("superMetricFoo", false, "QUERY", 1l); + assertMetric("randomNumberAsync", true, "QUERY", 3l); + } + + @Test + public void shouldCreateMetricsFromAsyncQueryAndAsyncSourceField() { + String request = getPayload("{\n" + + " asyncSuperMetricFoo {\n" + + " randomNumberAsync {\n" + + " value\n" + + " }\n" + + " }\n" + + "}"); + + assertResponse(request, + "{\"data\":{\"asyncSuperMetricFoo\":[{\"randomNumberAsync\":{\"value\":123.0}},{\"randomNumberAsync\":{\"value\":123.0}},{\"randomNumberAsync\":{\"value\":123.0}}]}}"); + assertMetric("asyncSuperMetricFoo", false, "QUERY", 1l); + assertMetric("randomNumberAsync", true, "QUERY", 3l); + } + + @Test + public void shouldCreateMetricsFromQueryAndBatch() { + String request = getPayload("{\n" + + " superMetricFoo {\n" + + " message\n" + + " batchFoo {\n" + + " message\n" + + " }\n" + + " }\n" + + "}"); + + assertResponse(request, + "{\"data\":{\"superMetricFoo\":[{\"message\":\"bar\",\"batchFoo\":{\"message\":\"bar1\"}},{\"message\":\"bar\",\"batchFoo\":{\"message\":\"bar2\"}},{\"message\":\"bar\",\"batchFoo\":{\"message\":\"bar3\"}}]}}"); + assertMetric("superMetricFoo", false, "QUERY", 1l); + assertBatchMetric("batchFoo", "QUERY", 3l); + } + + @Test + public void shouldCreateMetricsFromAsyncQueryAndBatch() { + String request = getPayload("{\n" + + " asyncSuperMetricFoo {\n" + + " message\n" + + " batchFoo {\n" + + " message\n" + + " }\n" + + " }\n" + + "}"); + + assertResponse(request, + "{\"data\":{\"asyncSuperMetricFoo\":[{\"message\":\"async1\",\"batchFoo\":{\"message\":\"bar1\"}},{\"message\":\"async2\",\"batchFoo\":{\"message\":\"bar2\"}},{\"message\":\"async3\",\"batchFoo\":{\"message\":\"bar3\"}}]}}"); + assertMetric("asyncSuperMetricFoo", false, "QUERY", 1l); + assertBatchMetric("batchFoo", "QUERY", 3l); + + } + + @Test + public void shouldCreateMetricsFromQueryAndAsyncBatch() { + String request = getPayload("{\n" + + " superMetricFoo {\n" + + " message\n" + + " asyncBatchFoo {\n" + + " message\n" + + " }\n" + + " }\n" + + "}"); + + assertResponse(request, + "{\"data\":{\"superMetricFoo\":[{\"message\":\"bar\",\"asyncBatchFoo\":{\"message\":\"abar1\"}},{\"message\":\"bar\",\"asyncBatchFoo\":{\"message\":\"abar2\"}},{\"message\":\"bar\",\"asyncBatchFoo\":{\"message\":\"abar3\"}}]}}"); + assertMetric("superMetricFoo", false, "QUERY", 1l); + assertBatchMetric("asyncBatchFoo", "QUERY", 3l); + } + + @Test + public void shouldCreateMetricsFromAsyncQueryAndAsyncBatch() { + String request = getPayload("{\n" + + " asyncSuperMetricFoo {\n" + + " message\n" + + " asyncBatchFoo {\n" + + " message\n" + + " }\n" + + " }\n" + + "}"); + + assertResponse(request, + "{\"data\":{\"asyncSuperMetricFoo\":[{\"message\":\"async1\",\"asyncBatchFoo\":{\"message\":\"abar1\"}},{\"message\":\"async2\",\"asyncBatchFoo\":{\"message\":\"abar2\"}},{\"message\":\"async3\",\"asyncBatchFoo\":{\"message\":\"abar3\"}}]}}"); + assertMetric("asyncSuperMetricFoo", false, "QUERY", 1l); + assertBatchMetric("asyncBatchFoo", "QUERY", 3l); + + } + + @Test + void shouldCreateMultipleMetrics() throws ExecutionException, InterruptedException { + String request = getPayload("{\n" + + " asyncSuperMetricFoo {\n" + + " message\n" + + " }\n" + + "}"); + ExecutorService executor = Executors.newFixedThreadPool(50); + int iterations = 200; + try { + CompletableFuture[] futures = new CompletableFuture[iterations]; + for (int i = 0; i < iterations; i++) { + futures[i] = CompletableFuture.supplyAsync(() -> assertResponse(request, + "{\"data\":{\"asyncSuperMetricFoo\":[{\"message\":\"async1\"},{\"message\":\"async2\"},{\"message\":\"async3\"}]}"), + executor); + } + getTestResult(futures, iterations); + } finally { + executor.shutdown(); + } + } + + private void getTestResult(CompletableFuture[] futures, long iterations) + throws InterruptedException, ExecutionException { + CompletableFuture.allOf(futures).get(); + assertMetric("asyncSuperMetricFoo", false, "QUERY", iterations); + } + + private String getPayload(String query) { + JsonObject jsonObject = createRequestBody(query); + return jsonObject.toString(); + } + + private JsonObject createRequestBody(String graphQL) { + return createRequestBody(graphQL, null); + } + + private JsonObject createRequestBody(String graphQL, JsonObject variables) { + // Create the request + if (variables == null || variables.isEmpty()) { + variables = Json.createObjectBuilder().build(); + } + return Json.createObjectBuilder().add("query", graphQL).add("variables", variables).build(); + } + + private void assertMetric(String name, boolean source, String type, long count) { + assertMetricWrapper(name, source, type, count, false); + } + + private void assertBatchMetric(String name, String type, long count) { + assertMetricWrapper(name, true, type, count, true); + } + + private void assertMetricWrapper(String name, boolean source, String type, long count, boolean batch) { + assertMetricExists(name, source, type); + assertMetricCountValue(name, source, type, count); + assertMetricMaxValue(name, source, type, SLEEP_TIME); + assertMetricTotalValue(name, source, type, (batch ? 1 : count) * SLEEP_TIME); + } + + private void assertMetricCountValue(String name, boolean source, String type, long count) { + RestAssured.when().get("/q/metrics").then() + .body(containsString( + String.format("mp_graphql_seconds_count{name=\"%s\",source=\"%b\",type=\"%S\"} %d", name, source, type, + count))); + } + + private void assertMetricTotalValue(String name, boolean source, String type, double minimumDuration) { + String endpoint = "/q/metrics"; + String failureMessage = String.format( + "Expected metric with name '%s', source '%b', type '%s', and total minimum of %f to be present in the response body of endpoint '%s'", + name, source, type, minimumDuration, endpoint); + assertThat(failureMessage, + RestAssured.when().get(endpoint).asString(), + new TotalMetricMatcher(name, source, type, minimumDuration)); + } + + private void assertMetricMaxValue(String name, boolean source, String type, double minimumDuration) { + // mean would be better, but it is harder to get... + String endpoint = "/q/metrics"; + String failureMessage = String.format( + "Expected metric with name '%s', source '%b', type '%s', and at least or equal to maximum duration of %f to be present in the response body of endpoint '%s'", + name, source, type, minimumDuration, endpoint); + assertThat(failureMessage, + RestAssured.when().get(endpoint).asString(), + new MaxMetricMatcher(name, source, type, minimumDuration)); + } + + private void assertMetricExists(String name, boolean source, String type) { + RestAssured.when().get("/q/metrics").then() + .body(containsString( + String.format("mp_graphql_seconds_count{name=\"%s\",source=\"%b\",type=\"%S\"}", name, source, type))); + } + + private Void assertResponse(String request, String response) { + RestAssured.given().when() + .accept("application/json") + .contentType("application/json") + .body(request) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .body(containsString(response)); + + return null; + } + + abstract static class MetricMatcher extends TypeSafeMatcher { + protected final String name; + protected final boolean source; + protected final String type; + protected final double value; + + protected MetricMatcher(String name, boolean source, String type, double value) { + this.name = name; + this.source = source; + this.type = type; + this.value = value; + } + + @Override + public void describeTo(Description description) { + description.appendText("a metric with name ").appendValue(name) + .appendText(", source ").appendValue(source) + .appendText(", type ").appendValue(type) + .appendText(", and ").appendText(getValueDescription()) + .appendValue(value); + } + + protected abstract String getValueDescription(); + } + + static class TotalMetricMatcher extends MetricMatcher { + public TotalMetricMatcher(String name, boolean source, String type, double total) { + super(name, source, type, total); + } + + @Override + public boolean matchesSafely(String item) { + String pattern = String.format("mp_graphql_seconds_sum\\{name=\"%s\",source=\"%b\",type=\"%s\"\\} \\d+\\.\\d+", + name, source, type); + Matcher matcher = Pattern.compile(pattern).matcher(item); + return matcher.find() && Double.parseDouble(matcher.group().split(" ")[1]) >= value; + } + + @Override + protected String getValueDescription() { + return "total minimum of "; + } + } + + static class MaxMetricMatcher extends MetricMatcher { + public MaxMetricMatcher(String name, boolean source, String type, double maxDuration) { + super(name, source, type, maxDuration); + } + + @Override + public boolean matchesSafely(String item) { + String pattern = String.format("mp_graphql_seconds_max\\{name=\"%s\",source=\"%b\",type=\"%s\"\\} \\d+\\.\\d+", + name, source, type); + Matcher matcher = Pattern.compile(pattern).matcher(item); + return matcher.find() && Double.parseDouble(matcher.group().split(" ")[1]) >= value; + } + + @Override + protected String getValueDescription() { + return "maximum duration of "; + } + } + +}