From acd0b25b77be8b0b3b346770f10ac3089667ec56 Mon Sep 17 00:00:00 2001 From: brunobat Date: Wed, 18 Dec 2024 17:23:00 +0000 Subject: [PATCH] More compatibility tests and testing utilities --- .../MicrometerOtelBridgeProcessor.java | 2 + .../deployment/DistributionSummaryTest.java | 127 +++++++ .../MicrometerCounterInterceptorTest.java | 126 +++++++ .../MicrometerTimedInterceptorTest.java | 319 ++++++++++++++++++ .../deployment/common/CountedResource.java | 64 ++++ .../deployment/common/GuardedResult.java | 34 ++ .../common/InMemoryMetricExporter.java | 125 +++---- .../deployment/common/MetricDataFilter.java | 246 ++++++++++++++ .../deployment/common/TimedResource.java | 65 ++++ .../test/resources/test-logging.properties | 4 + .../micrometer-opentelemetry-bridge/pom.xml | 164 +++++++++ .../micrometer/opentelemetry/AppResource.java | 26 ++ .../opentelemetry/ExporterResource.java | 125 +++++++ .../opentelemetry/services/CountedBean.java | 27 ++ .../services/ManualHistogram.java | 28 ++ .../services/TestValueResolver.java | 13 + .../src/main/resources/application.properties | 16 + .../MicrometerCounterInterceptorTest.java | 104 ++++++ integration-tests/pom.xml | 1 + 19 files changed, 1532 insertions(+), 84 deletions(-) create mode 100644 extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/DistributionSummaryTest.java create mode 100644 extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerCounterInterceptorTest.java create mode 100644 extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerTimedInterceptorTest.java create mode 100644 extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/CountedResource.java create mode 100644 extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/GuardedResult.java create mode 100644 extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/MetricDataFilter.java create mode 100644 extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/TimedResource.java create mode 100644 extensions/micrometer-opentelemetry-bridge/deployment/src/test/resources/test-logging.properties create mode 100644 integration-tests/micrometer-opentelemetry-bridge/pom.xml create mode 100644 integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/AppResource.java create mode 100644 integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/ExporterResource.java create mode 100644 integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/services/CountedBean.java create mode 100644 integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/services/ManualHistogram.java create mode 100644 integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/services/TestValueResolver.java create mode 100644 integration-tests/micrometer-opentelemetry-bridge/src/main/resources/application.properties create mode 100644 integration-tests/micrometer-opentelemetry-bridge/src/test/java/io/quarkus/micrometer/opentelemetry/MicrometerCounterInterceptorTest.java diff --git a/extensions/micrometer-opentelemetry-bridge/deployment/src/main/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerOtelBridgeProcessor.java b/extensions/micrometer-opentelemetry-bridge/deployment/src/main/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerOtelBridgeProcessor.java index 0af7c3cd3f8264..4bb4a024e80bfe 100644 --- a/extensions/micrometer-opentelemetry-bridge/deployment/src/main/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerOtelBridgeProcessor.java +++ b/extensions/micrometer-opentelemetry-bridge/deployment/src/main/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerOtelBridgeProcessor.java @@ -46,6 +46,8 @@ SyntheticBeanBuildItem createBridgeBean(OTelRuntimeConfig otelRuntimeConfig, .done(); } + // FIXME disable otel metrics instrumentation + /** * No point in activating the bridge if the OTel metrics if off or the exporter is none. */ diff --git a/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/DistributionSummaryTest.java b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/DistributionSummaryTest.java new file mode 100644 index 00000000000000..aa33ba4fcbcc9d --- /dev/null +++ b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/DistributionSummaryTest.java @@ -0,0 +1,127 @@ +package io.quarkus.micrometer.opentelemetry.deployment; + +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.MeterRegistry; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.quarkus.micrometer.opentelemetry.deployment.common.InMemoryMetricExporter; +import io.quarkus.micrometer.opentelemetry.deployment.common.InMemoryMetricExporterProvider; +import io.quarkus.test.QuarkusUnitTest; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class DistributionSummaryTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addClasses(ManualHistogramBean.class) + .addClasses(InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class) + .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider") + .add(new StringAsset(""" + quarkus.otel.metrics.enabled=true\n + quarkus.otel.traces.exporter=none\n + quarkus.otel.logs.exporter=none\n + quarkus.otel.metrics.exporter=in-memory\n + quarkus.otel.metric.export.interval=300ms\n + quarkus.micrometer.binder-enabled-default=false\n + quarkus.micrometer.binder.http-client.enabled=true\n + quarkus.micrometer.binder.http-server.enabled=true\n + quarkus.micrometer.binder.http-server.match-patterns=/one=/two\n + quarkus.micrometer.binder.http-server.ignore-patterns=/two\n + quarkus.micrometer.binder.vertx.enabled=true\n + pingpong/mp-rest/url=${test.url}\n + quarkus.redis.devservices.enabled=false\n + """), + "application.properties")); + + @Inject + ManualHistogramBean manualHistogramBean; + + @Inject + InMemoryMetricExporter exporter; + + @Test + void histogramTest() { + manualHistogramBean.recordHistogram(); + + MetricData testSummary = exporter.getLastFinishedHistogramItem("testSummary", 4); + assertNotNull(testSummary); + assertThat(testSummary) + .hasDescription("This is a test distribution summary") + .hasUnit("things") + .hasHistogramSatisfying( + histogram -> histogram.hasPointsSatisfying( + points -> points + .hasSum(555.5) + .hasCount(4) + .hasAttributes(attributeEntry("tag", "value")))); + + MetricData textSummaryMax = exporter.getFinishedMetricItem("testSummary.max"); + assertNotNull(textSummaryMax); + assertThat(textSummaryMax) + .hasDescription("This is a test distribution summary") + .hasDoubleGaugeSatisfying( + gauge -> gauge.hasPointsSatisfying( + point -> point + .hasValue(500) + .hasAttributes(attributeEntry("tag", "value")))); + + MetricData testSummaryHistogram = exporter.getFinishedMetricItem("testSummary.histogram"); + assertNotNull(testSummaryHistogram); + assertThat(testSummaryHistogram) + .hasDoubleGaugeSatisfying( + gauge -> gauge.hasPointsSatisfying( + point -> point + .hasValue(1) + .hasAttributes( + attributeEntry("le", "1"), + attributeEntry("tag", "value")), + point -> point + .hasValue(2) + .hasAttributes( + attributeEntry("le", "10"), + attributeEntry("tag", "value")), + point -> point + .hasValue(3) + .hasAttributes( + attributeEntry("le", "100"), + attributeEntry("tag", "value")), + point -> point + .hasValue(4) + .hasAttributes( + attributeEntry("le", "1000"), + attributeEntry("tag", "value")))); + } + + @ApplicationScoped + public static class ManualHistogramBean { + @Inject + MeterRegistry registry; + + public void recordHistogram() { + DistributionSummary summary = DistributionSummary.builder("testSummary") + .description("This is a test distribution summary") + .baseUnit("things") + .tags("tag", "value") + .serviceLevelObjectives(1, 10, 100, 1000) + .distributionStatisticBufferLength(10) + .register(registry); + + summary.record(0.5); + summary.record(5); + summary.record(50); + summary.record(500); + } + } +} diff --git a/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerCounterInterceptorTest.java b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerCounterInterceptorTest.java new file mode 100644 index 00000000000000..377d78a45b64c4 --- /dev/null +++ b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerCounterInterceptorTest.java @@ -0,0 +1,126 @@ +package io.quarkus.micrometer.opentelemetry.deployment; + +import io.micrometer.common.annotation.ValueResolver; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.aop.MeterTag; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.quarkus.micrometer.opentelemetry.deployment.common.InMemoryMetricExporter; +import io.quarkus.micrometer.opentelemetry.deployment.common.InMemoryMetricExporterProvider; +import io.quarkus.micrometer.opentelemetry.deployment.common.Util; +import io.quarkus.test.QuarkusUnitTest; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; + + +public class MicrometerCounterInterceptorTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addClasses(Util.class, CountedBean.class, TestValueResolver.class) + .addClasses(InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class) + .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider") + .add(new StringAsset(""" + quarkus.otel.sdk.disabled=false\n + quarkus.otel.metrics.enabled=true\n + quarkus.otel.traces.exporter=none\n + quarkus.otel.logs.exporter=none\n + quarkus.otel.metrics.exporter=in-memory\n + quarkus.otel.metric.export.interval=300ms\n + quarkus.micrometer.binder.http-client.enabled=true\n + quarkus.micrometer.binder.http-server.enabled=true\n + quarkus.redis.devservices.enabled=false\n + """), + "application.properties")); + + @Inject + CountedBean countedBean; + + @Inject + InMemoryMetricExporter exporter; + + @BeforeEach + void setup() { + exporter.reset(); + } + + @Test + void testCountAllMetrics() { + countedBean.countAllInvocations(false); + Assertions.assertThrows(NullPointerException.class, () -> countedBean.countAllInvocations(true)); + + exporter.assertCountDataPointsAtLeastOrEqual("metric.all", null, 2); + + MetricData metricAll = exporter.getFinishedMetricItem("metric.all"); + assertThat(metricAll) + .isNotNull() + .hasName("metric.all") + .hasDescription("")// currently empty + .hasUnit("")// currently empty + .hasDoubleSumSatisfying(sum -> sum.hasPointsSatisfying( + point -> point + .hasValue(1d) + .hasAttributes(attributeEntry( + "class", + "io.quarkus.micrometer.opentelemetry.deployment.MicrometerCounterInterceptorTest$CountedBean"), + attributeEntry("method", "countAllInvocations"), + attributeEntry("extra", "tag"), + attributeEntry("do_fail", "prefix_false"), + attributeEntry("exception", "none"), + attributeEntry("result", "success")), + point -> point + .hasValue(1d) + .hasAttributes(attributeEntry( + "class", + "io.quarkus.micrometer.opentelemetry.deployment.MicrometerCounterInterceptorTest$CountedBean"), + attributeEntry("method", "countAllInvocations"), + attributeEntry("extra", "tag"), + attributeEntry("do_fail", "prefix_true"), + attributeEntry("exception", "NullPointerException"), + attributeEntry("result", "failure")))); + } + + @ApplicationScoped + public static class CountedBean { + @Counted(value = "metric.none", recordFailuresOnly = true) + public void onlyCountFailures() { + } + + @Counted(value = "metric.all", extraTags = {"extra", "tag"}) + public void countAllInvocations(@MeterTag(key = "do_fail", resolver = TestValueResolver.class) boolean fail) { + if (fail) { + throw new NullPointerException("Failed on purpose"); + } + } + + @Counted(description = "nice description") + public void emptyMetricName(@MeterTag boolean fail) { + if (fail) { + throw new NullPointerException("Failed on purpose"); + } + } + } + + @Singleton + public static class TestValueResolver implements ValueResolver { + @Override + public String resolve(Object parameter) { + return "prefix_" + parameter; + } + } + + +} diff --git a/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerTimedInterceptorTest.java b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerTimedInterceptorTest.java new file mode 100644 index 00000000000000..9f92e781d9254f --- /dev/null +++ b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/MicrometerTimedInterceptorTest.java @@ -0,0 +1,319 @@ +package io.quarkus.micrometer.opentelemetry.deployment; + +import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.HistogramPointData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.quarkus.micrometer.opentelemetry.deployment.common.CountedResource; +import io.quarkus.micrometer.opentelemetry.deployment.common.GuardedResult; +import io.quarkus.micrometer.opentelemetry.deployment.common.InMemoryMetricExporter; +import io.quarkus.micrometer.opentelemetry.deployment.common.InMemoryMetricExporterProvider; +import io.quarkus.micrometer.opentelemetry.deployment.common.MetricDataFilter; +import io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Inject; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class MicrometerTimedInterceptorTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("test-logging.properties") + .overrideConfigKey("quarkus.otel.sdk.disabled", "false") + .overrideConfigKey("quarkus.otel.metrics.enabled", "true") + .overrideConfigKey("quarkus.otel.traces.exporter", "none") + .overrideConfigKey("quarkus.otel.logs.exporter", "none") + .overrideConfigKey("quarkus.otel.metrics.exporter", "in-memory") + .overrideConfigKey("quarkus.otel.metric.export.interval", "300ms") +// .overrideConfigKey("quarkus.micrometer.binder.http-client.enabled", "true") +// .overrideConfigKey("quarkus.micrometer.binder.http-server.enabled", "true") + .overrideConfigKey("quarkus.redis.devservices.enabled", "false") +// .overrideConfigKey("quarkus.micrometer.binder.mp-metrics.enabled", "false") +// .overrideConfigKey("quarkus.micrometer.binder.vertx.enabled", "false") +// .overrideConfigKey("quarkus.micrometer.binder.jvm", "false") +// .overrideConfigKey("quarkus.micrometer.registry-enabled-default", "false") + .overrideConfigKey("quarkus.redis.devservices.enabled", "false") + .withApplicationRoot((jar) -> jar + .addClass(CountedResource.class) + .addClass(TimedResource.class) + .addClass(GuardedResult.class) + .addClasses(InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class, MetricDataFilter.class) + .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")); + + @Inject + TimedResource timed; + + @Inject + InMemoryMetricExporter metricExporter; + + @Test + void testTimeMethod() { + timed.call(false); + + metricExporter.assertCountDataPointsAtLeastOrEqual("call", null, 1); + assertEquals(1, metricExporter.get("call") + .tag("method", "call") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("exception", "none") + .tag("extra", "tag") + .lastReadingDataPoint(HistogramPointData.class).getCount()); + + assertThat(metricExporter.get("call.max") + .tag("method", "call") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("exception", "none") + .tag("extra", "tag") + .lastReadingDataPoint(DoublePointData.class).getValue()) + .isGreaterThan(0); + } + + @Test + void testTimeMethod_Failed() { + assertThrows(NullPointerException.class, () -> timed.call(true)); + + Supplier metricFilterSupplier = () -> metricExporter.get("call") + .tag("method", "call") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("exception", "NullPointerException") + .tag("extra", "tag"); + + metricExporter.assertCountDataPointsAtLeastOrEqual(metricFilterSupplier, 1); + assertEquals(1, metricFilterSupplier.get() + .lastReadingDataPoint(HistogramPointData.class).getCount()); + + assertThat(metricExporter.get("call.max") + .tag("method", "call") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("exception", "NullPointerException") + .tag("extra", "tag") + .lastReadingDataPoint(DoublePointData.class).getValue()) + .isGreaterThan(0); + } + + @Test + void testTimeMethod_Async() { + GuardedResult guardedResult = new GuardedResult(); + CompletableFuture completableFuture = timed.asyncCall(guardedResult); + guardedResult.complete(); + completableFuture.join(); + + Supplier metricFilterSupplier = () -> metricExporter.get("async.call") + .tag("method", "asyncCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("exception", "none") + .tag("extra", "tag"); + + metricExporter.assertCountDataPointsAtLeastOrEqual(metricFilterSupplier, 1); + assertEquals(1, metricFilterSupplier.get() + .lastReadingDataPoint(HistogramPointData.class).getCount()); + + assertThat(metricExporter.get("async.call.max") + .tag("method", "asyncCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("exception", "none") + .tag("extra", "tag") + .lastReadingDataPoint(DoublePointData.class).getValue()) + .isGreaterThan(0); + } + + @Test + void testTimeMethod_AsyncFailed() { + GuardedResult guardedResult = new GuardedResult(); + CompletableFuture completableFuture = timed.asyncCall(guardedResult); + guardedResult.complete(new NullPointerException()); + assertThrows(java.util.concurrent.CompletionException.class, () -> completableFuture.join()); + + metricExporter.assertCountDataPointsAtLeastOrEqual("async.call", null, 1); + assertEquals(1, metricExporter.get("async.call") + .tag("method", "asyncCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("exception", "NullPointerException") + .tag("extra", "tag") + .lastReadingDataPoint(HistogramPointData.class).getCount()); + + assertThat(metricExporter.get("async.call.max") + .tag("method", "asyncCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("exception", "NullPointerException") + .tag("extra", "tag") + .lastReadingDataPoint(DoublePointData.class).getValue()) + .isGreaterThan(0); + } + + @Test + void testTimeMethod_Uni() { + GuardedResult guardedResult = new GuardedResult(); + Uni uni = timed.uniCall(guardedResult); + guardedResult.complete(); + uni.subscribe().asCompletionStage().join(); + + metricExporter.assertCountDataPointsAtLeastOrEqual("uni.call", null, 1); + assertEquals(1, metricExporter.get("uni.call") + .tag("method", "uniCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("exception", "none") + .tag("extra", "tag") + .lastReadingDataPoint(HistogramPointData.class).getCount()); + + assertThat(metricExporter.get("uni.call.max") + .tag("method", "uniCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("exception", "none") + .tag("extra", "tag") + .lastReadingDataPoint(DoublePointData.class).getValue()) + .isGreaterThan(0); + } + + @Test + void testTimeMethod_UniFailed() throws InterruptedException { + GuardedResult guardedResult = new GuardedResult(); + Uni uni = timed.uniCall(guardedResult); + guardedResult.complete(new NullPointerException()); + assertThrows(java.util.concurrent.CompletionException.class, + () -> uni.subscribe().asCompletionStage().join()); + + // this needs to be executed inline, otherwise the results will be old. + Supplier metricFilterSupplier = () -> metricExporter.get("uni.call") + .tag("method", "uniCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("exception", "NullPointerException") + .tag("extra", "tag"); + + metricExporter.assertCountDataPointsAtLeastOrEqual(metricFilterSupplier, 1); + assertEquals(1, metricFilterSupplier.get() + .lastReadingDataPoint(HistogramPointData.class).getCount()); + + assertThat(metricExporter.get("uni.call.max") + .tag("method", "uniCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("exception", "NullPointerException") + .tag("extra", "tag") + .lastReadingDataPoint(DoublePointData.class).getValue()) + .isGreaterThan(0); + } + + @Test + void testTimeMethod_LongTaskTimer() { + timed.longCall(false); + metricExporter.assertCountDataPointsAtLeastOrEqual("longCall.active", null, 1); + assertEquals(0, metricExporter.get("longCall.active") + .tag("method", "longCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("extra", "tag") + .lastReadingDataPoint(LongPointData.class).getValue()); + } + + @Test + void testTimeMethod_LongTaskTimer_Failed() { + assertThrows(NullPointerException.class, () -> timed.longCall(true)); + + metricExporter.assertCountDataPointsAtLeastOrEqual("longCall.active", null, 1); + assertEquals(0, metricExporter.get("longCall.active") + .tag("method", "longCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("extra", "tag") + .lastReadingDataPoint(LongPointData.class).getValue()); + } + + @Test + void testTimeMethod_LongTaskTimer_Async() { + GuardedResult guardedResult = new GuardedResult(); + CompletableFuture completableFuture = timed.longAsyncCall(guardedResult); + guardedResult.complete(); + completableFuture.join(); + + metricExporter.assertCountDataPointsAtLeastOrEqual("async.longCall.active", null, 1); + assertEquals(0, metricExporter.get("async.longCall.active") + .tag("method", "longAsyncCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("extra", "tag") + .lastReadingDataPoint(LongPointData.class).getValue()); + } + + @Test + void testTimeMethod_LongTaskTimer_AsyncFailed() { + GuardedResult guardedResult = new GuardedResult(); + CompletableFuture completableFuture = timed.longAsyncCall(guardedResult); + guardedResult.complete(new NullPointerException()); + assertThrows(java.util.concurrent.CompletionException.class, () -> completableFuture.join()); + + metricExporter.assertCountDataPointsAtLeastOrEqual("async.longCall.active", null, 1); + assertEquals(0, metricExporter.get("async.longCall.active") + .tag("method", "longAsyncCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("extra", "tag") + .lastReadingDataPoint(LongPointData.class).getValue()); + } + + @Test + void testTimeMethod_LongTaskTimer_Uni() { + GuardedResult guardedResult = new GuardedResult(); + Uni uni = timed.longUniCall(guardedResult); + guardedResult.complete(); + uni.subscribe().asCompletionStage().join(); + + metricExporter.assertCountDataPointsAtLeastOrEqual("uni.longCall.active", null, 1); + assertEquals(0, metricExporter.get("uni.longCall.active") + .tag("method", "longUniCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("extra", "tag") + .lastReadingDataPoint(LongPointData.class).getValue()); + } + + @Test + void testTimeMethod_LongTaskTimer_UniFailed() throws InterruptedException { + GuardedResult guardedResult = new GuardedResult(); + Uni uni = timed.longUniCall(guardedResult); + guardedResult.complete(new NullPointerException()); + assertThrows(java.util.concurrent.CompletionException.class, + () -> uni.subscribe().asCompletionStage().join()); + + // Was "uni.longCall" Now is "uni.longCall.active" and "uni.longCall.duration" + // Metric was executed but now there are no active tasks + + metricExporter.assertCountDataPointsAtLeastOrEqual("uni.longCall.active", null, 1); + assertEquals(0, metricExporter.get("uni.longCall.active") + .tag("method", "longUniCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("extra", "tag") + .lastReadingDataPoint(LongPointData.class).getValue()); + + assertEquals(0, metricExporter.get("uni.longCall.duration") + .tag("method", "longUniCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("extra", "tag") + .lastReadingDataPoint(DoublePointData.class).getValue()); + + } + + @Test + void testTimeMethod_repeatable() { + timed.repeatableCall(false); + + metricExporter.assertCountDataPointsAtLeastOrEqual("alpha", null, 1); + + assertEquals(1, metricExporter.get("alpha") + .tag("method", "repeatableCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("exception", "none") + .tag("extra", "tag") + .lastReadingPointsSize()); + + assertEquals(1, metricExporter.get("bravo") + .tag("method", "repeatableCall") + .tag("class", "io.quarkus.micrometer.opentelemetry.deployment.common.TimedResource") + .tag("exception", "none") + .tag("extra", "tag") + .lastReadingPointsSize()); + } + +} diff --git a/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/CountedResource.java b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/CountedResource.java new file mode 100644 index 00000000000000..29522b7930a757 --- /dev/null +++ b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/CountedResource.java @@ -0,0 +1,64 @@ +package io.quarkus.micrometer.opentelemetry.deployment.common; + +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.aop.MeterTag; +import io.quarkus.micrometer.opentelemetry.deployment.MicrometerCounterInterceptorTest; +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.concurrent.CompletableFuture; + +import static io.quarkus.micrometer.opentelemetry.deployment.MicrometerCounterInterceptorTest.*; +import static java.util.concurrent.CompletableFuture.supplyAsync; + +@ApplicationScoped +public class CountedResource { + @Counted(value = "metric.none", recordFailuresOnly = true) + public void onlyCountFailures() { + } + + @Counted(value = "metric.all", extraTags = { "extra", "tag" }) + public void countAllInvocations(@MeterTag(key = "do_fail", resolver = TestValueResolver.class) boolean fail) { + if (fail) { + throw new NullPointerException("Failed on purpose"); + } + } + + @Counted(description = "nice description") + public void emptyMetricName(@MeterTag boolean fail) { + if (fail) { + throw new NullPointerException("Failed on purpose"); + } + } + + @Counted(value = "async.none", recordFailuresOnly = true) + public CompletableFuture onlyCountAsyncFailures(GuardedResult guardedResult) { + return supplyAsync(guardedResult::get); + } + + @Counted(value = "async.all", extraTags = { "extra", "tag" }) + public CompletableFuture countAllAsyncInvocations(GuardedResult guardedResult) { + return supplyAsync(guardedResult::get); + } + + @Counted + public CompletableFuture emptyAsyncMetricName(GuardedResult guardedResult) { + return supplyAsync(guardedResult::get); + } + + @Counted(value = "uni.none", recordFailuresOnly = true) + public Uni onlyCountUniFailures(GuardedResult guardedResult) { + return Uni.createFrom().item(guardedResult::get); + } + + @Counted(value = "uni.all", extraTags = { "extra", "tag" }) + public Uni countAllUniInvocations(GuardedResult guardedResult) { + return Uni.createFrom().item(guardedResult::get); + } + + @Counted + public Uni emptyUniMetricName(GuardedResult guardedResult) { + return Uni.createFrom().item(guardedResult::get); + } + +} diff --git a/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/GuardedResult.java b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/GuardedResult.java new file mode 100644 index 00000000000000..642bde50ba8ffa --- /dev/null +++ b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/GuardedResult.java @@ -0,0 +1,34 @@ +package io.quarkus.micrometer.opentelemetry.deployment.common; + +public class GuardedResult { + + private boolean complete; + private NullPointerException withException; + + public synchronized Object get() { + while (!complete) { + try { + wait(); + } catch (InterruptedException e) { + // Intentionally empty + } + } + + if (withException == null) { + return new Object(); + } + + throw withException; + } + + public synchronized void complete() { + complete(null); + } + + public synchronized void complete(NullPointerException withException) { + this.complete = true; + this.withException = withException; + notifyAll(); + } + +} diff --git a/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/InMemoryMetricExporter.java b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/InMemoryMetricExporter.java index 08ce36616bea65..a2ca5d097d2fc7 100644 --- a/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/InMemoryMetricExporter.java +++ b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/InMemoryMetricExporter.java @@ -1,24 +1,5 @@ package io.quarkus.micrometer.opentelemetry.deployment.common; -import static java.util.concurrent.TimeUnit.SECONDS; -import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toMap; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.stream.Collectors; - -import jakarta.enterprise.context.ApplicationScoped; - -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Assertions; - import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.common.CompletableResultCode; @@ -29,59 +10,37 @@ import io.opentelemetry.sdk.metrics.export.MetricExporter; import io.opentelemetry.semconv.SemanticAttributes; import io.quarkus.arc.Unremovable; +import jakarta.enterprise.context.ApplicationScoped; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Assertions; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static java.util.concurrent.TimeUnit.SECONDS; @Unremovable @ApplicationScoped public class InMemoryMetricExporter implements MetricExporter { - private static final List KEY_COMPONENTS = List.of(SemanticAttributes.HTTP_REQUEST_METHOD.getKey(), - SemanticAttributes.HTTP_ROUTE.getKey(), - SemanticAttributes.HTTP_RESPONSE_STATUS_CODE.getKey()); - private final Queue finishedMetricItems = new ConcurrentLinkedQueue<>(); private final AggregationTemporality aggregationTemporality = AggregationTemporality.CUMULATIVE; private boolean isStopped = false; - public static Map getPointAttributes(final MetricData metricData, final String path) { - try { - return metricData.getData().getPoints().stream() - .filter(point -> isPathFound(path, point.getAttributes())) - .map(point -> point.getAttributes()) - .map(attributes1 -> attributes1.asMap()) - .flatMap(map -> map.entrySet().stream()) - .collect(toMap(map -> map.getKey().toString(), map -> map.getValue().toString())); - } catch (Exception e) { - System.out.println("Error getting point attributes for " + metricData.getName()); - metricData.getData().getPoints().stream() - .filter(point -> isPathFound(path, point.getAttributes())) - .map(point -> point.getAttributes()) - .map(attributes1 -> attributes1.asMap()) - .flatMap(map -> map.entrySet().stream()) - .forEach(attributeKeyObjectEntry -> System.out - .println(attributeKeyObjectEntry.getKey() + " " + attributeKeyObjectEntry.getValue())); - throw e; - } + public MetricDataFilter metrics(final String name) { + return new MetricDataFilter(this, name); } - public static Map getMostRecentPointsMap(List finishedMetricItems) { - return finishedMetricItems.stream() - .flatMap(metricData -> metricData.getData().getPoints().stream()) - // exclude data from /export endpoint - .filter(InMemoryMetricExporter::notExporterPointData) - // newer first - .sorted(Comparator.comparingLong(PointData::getEpochNanos).reversed()) - .collect(toMap( - pointData -> pointData.getAttributes().asMap().entrySet().stream() - //valid attributes for the resulting map key - .filter(entry -> KEY_COMPONENTS.contains(entry.getKey().getKey())) - // ensure order - .sorted(Comparator.comparing(o -> o.getKey().getKey())) - // build key - .map(entry -> entry.getKey().getKey() + ":" + entry.getValue().toString()) - .collect(joining(",")), - pointData -> pointData, - // most recent points will surface - (older, newer) -> newer)); + public MetricDataFilter get(final String name) { + return new MetricDataFilter(this, name); } /* @@ -104,24 +63,11 @@ private static boolean isPathFound(String path, Attributes attributes) { return value.toString().equals(path); } - public void assertCount(final int count) { - Awaitility.await().atMost(5, SECONDS) - .untilAsserted(() -> Assertions.assertEquals(count, getFinishedMetricItems().size())); - } - - public void assertCount(final String name, final String target, final int count) { + public MetricData getLastFinishedHistogramItem(String testSummary, int count) { Awaitility.await().atMost(5, SECONDS) - .untilAsserted(() -> Assertions.assertEquals(count, getFinishedMetricItems(name, target).size())); - } - - public void assertCountAtLeast(final int count) { - Awaitility.await().atMost(5, SECONDS) - .untilAsserted(() -> Assertions.assertTrue(count < getFinishedMetricItems().size())); - } - - public void assertCountAtLeast(final String name, final String target, final int count) { - Awaitility.await().atMost(5, SECONDS) - .untilAsserted(() -> Assertions.assertTrue(count < getFinishedMetricItems(name, target).size())); + .untilAsserted(() -> Assertions.assertEquals(count, getFinishedMetricItems(testSummary, null).size())); + List metricData = getFinishedMetricItems(testSummary, null); + return metricData.get(metricData.size() - 1);// get last added entry which will be the most recent } public void assertCountDataPointsAtLeast(final String name, final String target, final int count) { @@ -134,13 +80,19 @@ public void assertCountDataPointsAtLeastOrEqual(final String name, final String .untilAsserted(() -> Assertions.assertTrue(count <= countMaxPoints(name, target))); } + public void assertCountDataPointsAtLeastOrEqual(Supplier tag, int count) { + Awaitility.await().atMost(50, SECONDS) + .untilAsserted(() -> Assertions.assertTrue(count <= tag.get().lastReadingPointsSize())); + } + private Integer countMaxPoints(String name, String target) { - Integer i = getFinishedMetricItems(name, target).stream() - .map(data -> data.getData().getPoints().size()) - .sorted((a, b) -> b - a) - .findFirst().orElse(0); - System.out.println("Max points: " + i); - return i; + List metricData = getFinishedMetricItems(name, target); + if (metricData.isEmpty()) { + return 0; + } + int size = metricData.get(metricData.size() - 1).getData().getPoints().size(); + System.out.println("Max points: " + size); + return size; } /** @@ -152,6 +104,11 @@ public List getFinishedMetricItems() { return Collections.unmodifiableList(new ArrayList<>(finishedMetricItems)); } + public MetricData getFinishedMetricItem(String metricName) { + List metricData = getFinishedMetricItems(metricName, null); + return metricData.get(metricData.size() - 1);// get last added entry which will be the most recent + } + public List getFinishedMetricItems(final String name, final String target) { return Collections.unmodifiableList(new ArrayList<>( finishedMetricItems.stream() diff --git a/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/MetricDataFilter.java b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/MetricDataFilter.java new file mode 100644 index 00000000000000..7717f82b684edf --- /dev/null +++ b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/MetricDataFilter.java @@ -0,0 +1,246 @@ +package io.quarkus.micrometer.opentelemetry.deployment.common; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.metrics.data.Data; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.MetricDataType; +import io.opentelemetry.sdk.metrics.data.PointData; +import io.opentelemetry.sdk.resources.Resource; + +import java.util.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE; +import static io.opentelemetry.semconv.UrlAttributes.URL_PATH; + +public class MetricDataFilter { + private Stream metricData; + + public MetricDataFilter(final InMemoryMetricExporter metricExporter, final String name) { + metricData = metricExporter.getFinishedMetricItems() + .stream() + .filter(metricData -> metricData.getName().equals(name)); + } + + public MetricDataFilter route(final String route) { + metricData = metricData.map(new Function() { + @Override + public MetricData apply(final MetricData metricData) { + return new MetricData() { + @Override + public Resource getResource() { + return metricData.getResource(); + } + + @Override + public InstrumentationScopeInfo getInstrumentationScopeInfo() { + return metricData.getInstrumentationScopeInfo(); + } + + @Override + public String getName() { + return metricData.getName(); + } + + @Override + public String getDescription() { + return metricData.getDescription(); + } + + @Override + public String getUnit() { + return metricData.getUnit(); + } + + @Override + public MetricDataType getType() { + return metricData.getType(); + } + + @Override + public Data getData() { + return new Data() { + @Override + public Collection getPoints() { + return metricData.getData().getPoints().stream().filter(new Predicate() { + @Override + public boolean test(final PointData pointData) { + String value = pointData.getAttributes().get(HTTP_ROUTE); + return value != null && value.equals(route); + } + }).collect(Collectors.toSet()); + } + }; + } + }; + } + }); + return this; + } + + public MetricDataFilter path(final String path) { + metricData = metricData.map(new Function() { + @Override + public MetricData apply(final MetricData metricData) { + return new MetricData() { + @Override + public Resource getResource() { + return metricData.getResource(); + } + + @Override + public InstrumentationScopeInfo getInstrumentationScopeInfo() { + return metricData.getInstrumentationScopeInfo(); + } + + @Override + public String getName() { + return metricData.getName(); + } + + @Override + public String getDescription() { + return metricData.getDescription(); + } + + @Override + public String getUnit() { + return metricData.getUnit(); + } + + @Override + public MetricDataType getType() { + return metricData.getType(); + } + + @Override + public Data getData() { + return new Data() { + @Override + public Collection getPoints() { + return metricData.getData().getPoints().stream().filter(new Predicate() { + @Override + public boolean test(final PointData pointData) { + String value = pointData.getAttributes().get(URL_PATH); + return value != null && value.equals(path); + } + }).collect(Collectors.toSet()); + } + }; + } + }; + } + }); + return this; + } + + public MetricDataFilter tag(final String key, final String value) { + return stringAttribute(key, value); + } + + public MetricDataFilter stringAttribute(final String key, final String value) { + metricData = metricData.map(new Function() { + @Override + public MetricData apply(final MetricData metricData) { + return new MetricData() { + @Override + public Resource getResource() { + return metricData.getResource(); + } + + @Override + public InstrumentationScopeInfo getInstrumentationScopeInfo() { + return metricData.getInstrumentationScopeInfo(); + } + + @Override + public String getName() { + return metricData.getName(); + } + + @Override + public String getDescription() { + return metricData.getDescription(); + } + + @Override + public String getUnit() { + return metricData.getUnit(); + } + + @Override + public MetricDataType getType() { + return metricData.getType(); + } + + @Override + public Data getData() { + return new Data() { + @Override + public Collection getPoints() { + return metricData.getData().getPoints().stream().filter(new Predicate() { + @Override + public boolean test(final PointData pointData) { + String v = pointData.getAttributes().get(AttributeKey.stringKey(key)); + boolean result = v != null && v.equals(value); + if (!result) { + System.out.println("\nNot Matching. Expected: " + key + " = " + value + " -> Found: " + v); + } + return result; + } + }).collect(Collectors.toSet()); + } + }; + } + }; + } + }); + return this; + } + + public List getAll() { + return metricData.collect(Collectors.toList()); + } + + public MetricData lastReading() { + return metricData.reduce((first, second) -> second) + .orElseThrow(() -> new IllegalArgumentException("Stream has no elements")); + } + + public int lastReadingPointsSize() { + return metricData.reduce((first, second) -> second) + .map(data -> data.getData().getPoints().size()) + .orElseThrow(() -> new IllegalArgumentException("Stream has no elements")); + } + + /** + * Returns the first point data of the last reading. + * Assumes only one data point can be present. + * + * @param pointDataClass + * @param + * @return + */ + public T lastReadingDataPoint(Class pointDataClass) { + List list = lastReading().getData().getPoints().stream() + .map(pointData -> (T) pointData) + .toList(); + + if (list.size() == 0) { + throw new IllegalArgumentException("Stream has no elements"); + } + if (list.size() > 1) { + throw new IllegalArgumentException("Stream has more than one element"); + } + return list.get(0); + } + + public int countPoints(final MetricData metricData) { + return metricData.getData().getPoints().size(); + } +} diff --git a/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/TimedResource.java b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/TimedResource.java new file mode 100644 index 00000000000000..2fe832b49aac1e --- /dev/null +++ b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/java/io/quarkus/micrometer/opentelemetry/deployment/common/TimedResource.java @@ -0,0 +1,65 @@ +package io.quarkus.micrometer.opentelemetry.deployment.common; + +import io.micrometer.core.annotation.Timed; +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.CompletableFuture; + +import static java.util.concurrent.CompletableFuture.supplyAsync; + +@ApplicationScoped +public class TimedResource { + @Timed(value = "call", extraTags = { "extra", "tag" }) + public void call(boolean fail) { + if (fail) { + throw new NullPointerException("Failed on purpose"); + } + + } + + @Timed(value = "longCall", extraTags = { "extra", "tag" }, longTask = true) + public void longCall(boolean fail) { + try { + Thread.sleep(3); + } catch (InterruptedException e) { + } + if (fail) { + throw new NullPointerException("Failed on purpose"); + } + } + + @Timed(value = "async.call", extraTags = { "extra", "tag" }) + public CompletableFuture asyncCall(GuardedResult guardedResult) { + return supplyAsync(guardedResult::get); + } + + @Timed(value = "uni.call", extraTags = { "extra", "tag" }) + public Uni uniCall(GuardedResult guardedResult) { + return Uni.createFrom().item(guardedResult::get); + } + + @Timed(value = "async.longCall", extraTags = { "extra", "tag" }, longTask = true) + public CompletableFuture longAsyncCall(GuardedResult guardedResult) { + try { + Thread.sleep(3); + } catch (InterruptedException e) { + } + return supplyAsync(guardedResult::get); + } + + @Timed(value = "uni.longCall", extraTags = { "extra", "tag" }, longTask = true) + public Uni longUniCall(GuardedResult guardedResult) { + return Uni.createFrom().item(guardedResult::get).onItem().delayIt().by(Duration.of(3, ChronoUnit.MILLIS)); + } + + @Timed(value = "alpha", extraTags = { "extra", "tag" }) + @Timed(value = "bravo", extraTags = { "extra", "tag" }) + public void repeatableCall(boolean fail) { + if (fail) { + throw new NullPointerException("Failed on purpose"); + } + } +} diff --git a/extensions/micrometer-opentelemetry-bridge/deployment/src/test/resources/test-logging.properties b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/resources/test-logging.properties new file mode 100644 index 00000000000000..6eed6ab2596da2 --- /dev/null +++ b/extensions/micrometer-opentelemetry-bridge/deployment/src/test/resources/test-logging.properties @@ -0,0 +1,4 @@ +#quarkus.log.category."io.quarkus.micrometer".level=DEBUG +quarkus.log.category."io.quarkus.bootstrap".level=INFO +#quarkus.log.category."io.quarkus.arc".level=DEBUG +quarkus.log.category."io.netty".level=INFO diff --git a/integration-tests/micrometer-opentelemetry-bridge/pom.xml b/integration-tests/micrometer-opentelemetry-bridge/pom.xml new file mode 100644 index 00000000000000..c7e47721bd7e49 --- /dev/null +++ b/integration-tests/micrometer-opentelemetry-bridge/pom.xml @@ -0,0 +1,164 @@ + + + 4.0.0 + + + io.quarkus + quarkus-integration-tests-parent + 999-SNAPSHOT + + + quarkus-integration-test-micrometer-opentelemetry-bridge + Quarkus - Integration Tests - Micrometer to OpenTelemetry Bridge + + + + io.quarkus + quarkus-micrometer-opentelemetry-bridge + 999-SNAPSHOT + + + + + + + + + + + + + + + io.quarkus + quarkus-rest-jackson + + + + + + + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + + + + + + + + org.awaitility + awaitility + test + + + + io.opentelemetry + opentelemetry-sdk-testing + + + org.assertj + assertj-core + compile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + io.quarkus + quarkus-micrometer-opentelemetry-bridge-deployment + 999-SNAPSHOT + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + + + + + + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + diff --git a/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/AppResource.java b/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/AppResource.java new file mode 100644 index 00000000000000..43252408e7da43 --- /dev/null +++ b/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/AppResource.java @@ -0,0 +1,26 @@ +package io.quarkus.micrometer.opentelemetry; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.micrometer.opentelemetry.services.CountedBean; +import jakarta.ws.rs.core.Response; + +@Path("") +@Produces(MediaType.APPLICATION_JSON) +public class AppResource { + + @Inject + CountedBean countedBean; + + @Path("/count") + @GET + public Response count(@QueryParam("fail") boolean fail) { + countedBean.countAllInvocations(fail); + return Response.ok().build(); + } +} diff --git a/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/ExporterResource.java b/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/ExporterResource.java new file mode 100644 index 00000000000000..7980baf8770056 --- /dev/null +++ b/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/ExporterResource.java @@ -0,0 +1,125 @@ +package io.quarkus.micrometer.opentelemetry; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Response; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricExporter; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.SemanticAttributes; + +@Path("") +public class ExporterResource { + @Inject + InMemorySpanExporter inMemorySpanExporter; + @Inject + InMemoryMetricExporter inMemoryMetricExporter; + @Inject + InMemoryLogRecordExporter inMemoryLogRecordExporter; + + @GET + @Path("/reset") + public Response reset() { + inMemorySpanExporter.reset(); + inMemoryMetricExporter.reset(); + inMemoryLogRecordExporter.reset(); + return Response.ok().build(); + } + + /** + * Will exclude export endpoint related traces + */ + @GET + @Path("/export") + public List exportTraces() { + return inMemorySpanExporter.getFinishedSpanItems() + .stream() + .filter(sd -> !sd.getName().contains("export") && !sd.getName().contains("reset")) + .collect(Collectors.toList()); + } + + /** + * Export metrics with optional filtering by name and target + */ + @GET + @Path("/export/metrics") + public List exportMetrics(@QueryParam("name") String name, @QueryParam("target") String target) { + return Collections.unmodifiableList(new ArrayList<>( + inMemoryMetricExporter.getFinishedMetricItems().stream() + .filter(metricData -> name == null ? true : metricData.getName().equals(name)) + .filter(metricData -> target == null ? true + : metricData.getData() + .getPoints().stream() + .anyMatch(point -> isPathFound(target, point.getAttributes()))) + .collect(Collectors.toList()))); + } + + /** + * Will exclude Quarkus startup logs + */ + @GET + @Path("/export/logs") + public List exportLogs(@QueryParam("body") String message) { + if (message == null) { + return inMemoryLogRecordExporter.getFinishedLogRecordItems().stream() + .collect(Collectors.toList()); + } + return inMemoryLogRecordExporter.getFinishedLogRecordItems().stream() + .filter(logRecordData -> logRecordData.getBody().asString().equals(message)) + .collect(Collectors.toList()); + } + + private static boolean isPathFound(String path, Attributes attributes) { + if (path == null) { + return true;// any match + } + Object value = attributes.asMap().get(AttributeKey.stringKey(SemanticAttributes.HTTP_ROUTE.getKey())); + if (value == null) { + return false; + } + return value.toString().equals(path); + } + + @ApplicationScoped + static class InMemorySpanExporterProducer { + @Produces + @Singleton + InMemorySpanExporter inMemorySpanExporter() { + return InMemorySpanExporter.create(); + } + } + + @ApplicationScoped + static class InMemoryMetricExporterProducer { + @Produces + @Singleton + InMemoryMetricExporter inMemoryMetricsExporter() { + return InMemoryMetricExporter.create(); + } + } + + @ApplicationScoped + static class InMemoryLogRecordExporterProducer { + @Produces + @Singleton + public InMemoryLogRecordExporter createInMemoryExporter() { + return InMemoryLogRecordExporter.create(); + } + } +} diff --git a/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/services/CountedBean.java b/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/services/CountedBean.java new file mode 100644 index 00000000000000..58b5e34a14be1f --- /dev/null +++ b/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/services/CountedBean.java @@ -0,0 +1,27 @@ +package io.quarkus.micrometer.opentelemetry.services; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.aop.MeterTag; + +@ApplicationScoped +public class CountedBean { + @Counted(value = "metric.none", recordFailuresOnly = true) + public void onlyCountFailures() { + } + + @Counted(value = "metric.all", extraTags = { "extra", "tag" }) + public void countAllInvocations(@MeterTag(key = "do_fail", resolver = TestValueResolver.class) boolean fail) { + if (fail) { + throw new NullPointerException("Failed on purpose"); + } + } + + @Counted(description = "nice description") + public void emptyMetricName(@MeterTag boolean fail) { + if (fail) { + throw new NullPointerException("Failed on purpose"); + } + } +} diff --git a/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/services/ManualHistogram.java b/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/services/ManualHistogram.java new file mode 100644 index 00000000000000..a0ab2983351b46 --- /dev/null +++ b/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/services/ManualHistogram.java @@ -0,0 +1,28 @@ +package io.quarkus.micrometer.opentelemetry.services; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.MeterRegistry; + +@ApplicationScoped +public class ManualHistogram { + @Inject + MeterRegistry registry; + + public void recordHistogram() { + DistributionSummary summary = DistributionSummary.builder("testSummary") + .description("This is a test distribution summary") + .baseUnit("things") + .tags("tag", "value") + .serviceLevelObjectives(1, 10, 100, 1000) + .distributionStatisticBufferLength(10) + .register(registry); + + summary.record(0.5); + summary.record(5); + summary.record(50); + summary.record(500); + } +} diff --git a/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/services/TestValueResolver.java b/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/services/TestValueResolver.java new file mode 100644 index 00000000000000..621a1d478bda73 --- /dev/null +++ b/integration-tests/micrometer-opentelemetry-bridge/src/main/java/io/quarkus/micrometer/opentelemetry/services/TestValueResolver.java @@ -0,0 +1,13 @@ +package io.quarkus.micrometer.opentelemetry.services; + +import jakarta.inject.Singleton; + +import io.micrometer.common.annotation.ValueResolver; + +@Singleton +public class TestValueResolver implements ValueResolver { + @Override + public String resolve(Object parameter) { + return "prefix_" + parameter; + } +} diff --git a/integration-tests/micrometer-opentelemetry-bridge/src/main/resources/application.properties b/integration-tests/micrometer-opentelemetry-bridge/src/main/resources/application.properties new file mode 100644 index 00000000000000..9cf96ba6ab540b --- /dev/null +++ b/integration-tests/micrometer-opentelemetry-bridge/src/main/resources/application.properties @@ -0,0 +1,16 @@ +# Setting these for tests explicitly. Not required in normal application +quarkus.application.name=micrometer-opentelemetry-integration-test +quarkus.application.version=999-SNAPSHOT + +#metrics on +quarkus.otel.metrics.enabled=true +#Logs on +quarkus.otel.logs.enabled=true + +# speed up tests +quarkus.otel.bsp.schedule.delay=100 +quarkus.otel.bsp.export.timeout=5s +quarkus.otel.metric.export.interval=100ms + + + diff --git a/integration-tests/micrometer-opentelemetry-bridge/src/test/java/io/quarkus/micrometer/opentelemetry/MicrometerCounterInterceptorTest.java b/integration-tests/micrometer-opentelemetry-bridge/src/test/java/io/quarkus/micrometer/opentelemetry/MicrometerCounterInterceptorTest.java new file mode 100644 index 00000000000000..45dacdb0707cdf --- /dev/null +++ b/integration-tests/micrometer-opentelemetry-bridge/src/test/java/io/quarkus/micrometer/opentelemetry/MicrometerCounterInterceptorTest.java @@ -0,0 +1,104 @@ +package io.quarkus.micrometer.opentelemetry; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry; +import static io.restassured.RestAssured.given; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.common.mapper.TypeRef; + +@QuarkusTest +public class MicrometerCounterInterceptorTest { + + @BeforeEach + @AfterEach + void reset() { + given().get("/reset").then().statusCode(HTTP_OK); + } + + @Test + void testCountAllMetrics_MetricsOnSuccess() { + given() + .when() + .get("/count") + .then() + .statusCode(200); + + await().atMost(5, SECONDS).until(() -> getMetrics("metric.all").size() > 1); + + List> metrics = getMetrics("metric.all"); + + // Counter counter = registry.get("metric.all") + // .tag("method", "countAllInvocations") + // .tag("class", "io.smallrye.opentelemetry.implementation.micrometer.cdi.CountedBean") + // .tag("extra", "tag") + // // .tag("do_fail", "prefix_true") // FIXME @MeterTag not implemented yet + // .tag("result", "success").counter(); + // assertNotNull(counter); + +// MetricData metricAll = metrics.get(metrics.size() - 1); // get last +// assertThat(metricAll) +// .isNotNull() +// .hasName("metric.all") +// .hasDescription("Total number of invocations for method") +// .hasUnit("invocations") +// .hasDoubleSumSatisfying(sum -> sum.hasPointsSatisfying(point -> point.hasValue(1) +// .hasAttributes(attributeEntry( +// "class", +// "io.smallrye.opentelemetry.implementation.micrometer.cdi.CountedBean"), +// attributeEntry("method", "countAllInvocations"), +// attributeEntry("extra", "tag"), +// attributeEntry("exception", "none"), +// attributeEntry("result", "success")))); + } + + // @Test + // void testCountAllMetrics_MetricsOnFailure() { + // Assertions.assertThrows(NullPointerException.class, () -> countedBean.countAllInvocations(true)); + // + // Counter counter = registry.get("metric.all") + // .tag("method", "countAllInvocations") + // .tag("class", "io.smallrye.opentelemetry.implementation.micrometer.cdi.CountedBean") + // .tag("extra", "tag") + // // .tag("do_fail", "prefix_true") // FIXME @MeterTag not implemented yet + // .tag("exception", "NullPointerException") + // .tag("result", "failure").counter(); + // assertNotNull(counter); + // + // MetricData metricAll = exporter.getFinishedMetricItem("metric.all"); + // assertThat(metricAll) + // .isNotNull() + // .hasName("metric.all") + // .hasDescription("Total number of invocations for method") + // .hasUnit("invocations") + // .hasDoubleSumSatisfying(sum -> sum.hasPointsSatisfying(point -> point.hasValue(1) + // .hasAttributes(attributeEntry( + // "class", + // "io.smallrye.opentelemetry.implementation.micrometer.cdi.CountedBean"), + // attributeEntry("method", "countAllInvocations"), + // attributeEntry("extra", "tag"), + // attributeEntry("exception", "NullPointerException"), + // attributeEntry("result", "failure")))); + // } + + private List> getMetrics(String metricName) { + return given() + .when() + .queryParam("name", metricName) + .get("/export/metrics") + .body().as(new TypeRef<>() { + }); + } + +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 6376d450f75faa..edc691500dbbb7 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -358,6 +358,7 @@ micrometer-mp-metrics micrometer-prometheus micrometer-security + micrometer-opentelemetry-bridge opentelemetry opentelemetry-quickstart opentelemetry-spi