diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java index bdbb88e786c94..1f9d8ab6704ef 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java @@ -68,6 +68,16 @@ public boolean getAsBoolean() { } } + static class VertxHttpAvailable implements BooleanSupplier { + private static final boolean IS_VERTX_HTTP_AVAILABLE = isClassPresentAtRuntime( + "io.quarkus.vertx.http.runtime.VertxHttpRecorder"); + + @Override + public boolean getAsBoolean() { + return IS_VERTX_HTTP_AVAILABLE; + } + } + @BuildStep(onlyIf = GrpcExtensionAvailable.class) void grpcTracers(BuildProducer additionalBeans, OTelBuildConfig config) { if (config.instrument().grpc()) { @@ -100,10 +110,16 @@ void registerReactiveMessagingMessageDecorator( } } - @BuildStep(onlyIfNot = MetricsExtensionAvailable.class) + @BuildStep(onlyIfNot = MetricsExtensionAvailable.class, onlyIf = VertxHttpAvailable.class) + @Record(ExecutionTime.STATIC_INIT) + VertxOptionsConsumerBuildItem vertxHttpMetricsOptions(InstrumentationRecorder recorder) { + return new VertxOptionsConsumerBuildItem(recorder.getVertxHttpMetricsOptions(), LIBRARY_AFTER + 1); + } + + @BuildStep(onlyIfNot = { MetricsExtensionAvailable.class, VertxHttpAvailable.class }) @Record(ExecutionTime.STATIC_INIT) - VertxOptionsConsumerBuildItem vertxTracingMetricsOptions(InstrumentationRecorder recorder) { - return new VertxOptionsConsumerBuildItem(recorder.getVertxTracingMetricsOptions(), LIBRARY_AFTER + 1); + VertxOptionsConsumerBuildItem vertxMetricsOptions(InstrumentationRecorder recorder) { + return new VertxOptionsConsumerBuildItem(recorder.getVertxMetricsOptions(), LIBRARY_AFTER + 1); } @BuildStep diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java index abef2653061ee..a803c75acf465 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java @@ -11,6 +11,7 @@ import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.EventBusInstrumenterVertxTracer; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.HttpInstrumenterVertxTracer; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.InstrumenterVertxTracer; +import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.OpenTelemetryVertxHttpMetricsFactory; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.OpenTelemetryVertxMetricsFactory; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.OpenTelemetryVertxTracer; import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.OpenTelemetryVertxTracingFactory; @@ -74,7 +75,15 @@ public void setupVertxTracer(BeanContainer beanContainer, boolean sqlClientAvail } /* STATIC INIT */ - public Consumer getVertxTracingMetricsOptions() { + public Consumer getVertxHttpMetricsOptions() { + MetricsOptions metricsOptions = new MetricsOptions() + .setEnabled(true) + .setFactory(new OpenTelemetryVertxHttpMetricsFactory()); + return vertxOptions -> vertxOptions.setMetricsOptions(metricsOptions); + } + + /* STATIC INIT */ + public Consumer getVertxMetricsOptions() { MetricsOptions metricsOptions = new MetricsOptions() .setEnabled(true) .setFactory(new OpenTelemetryVertxMetricsFactory()); diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/MetricRequest.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/MetricRequest.java new file mode 100644 index 0000000000000..f76c13d47ab32 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/MetricRequest.java @@ -0,0 +1,27 @@ +package io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx; + +import java.util.Optional; + +import io.vertx.core.Context; +import io.vertx.core.http.impl.HttpServerRequestInternal; +import io.vertx.core.spi.observability.HttpRequest; + +public final class MetricRequest { + private final HttpRequest request; + + MetricRequest(final HttpRequest request) { + this.request = request; + } + + Optional getContext() { + if (request instanceof HttpServerRequestInternal) { + return Optional.of(((HttpServerRequestInternal) request).context()); + } else { + return Optional.empty(); + } + } + + static MetricRequest request(final HttpRequest httpRequest) { + return new MetricRequest(httpRequest); + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/OpenTelemetryVertxHttpMetricsFactory.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/OpenTelemetryVertxHttpMetricsFactory.java new file mode 100644 index 0000000000000..756a6fc807cf5 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/OpenTelemetryVertxHttpMetricsFactory.java @@ -0,0 +1,56 @@ +package io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx; + +import io.quarkus.vertx.http.runtime.ExtendedQuarkusVertxHttpMetrics; +import io.vertx.core.VertxOptions; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.SocketAddress; +import io.vertx.core.spi.VertxMetricsFactory; +import io.vertx.core.spi.metrics.HttpServerMetrics; +import io.vertx.core.spi.metrics.VertxMetrics; +import io.vertx.core.spi.observability.HttpRequest; + +/** + * This is used to retrieve the route name from Vert.x. This is useful for OpenTelemetry to generate the Span name and + * http.route attribute. Right now, there is no other way to retrieve the route name from Vert.x using the + * Telemetry SPI, so we need to rely on the Metrics SPI. + *

+ * Right now, it is not possible to register multiple VertxMetrics, meaning that only a single one is + * available per Quarkus instance. To avoid clashing with other extensions that provide Metrics data (like the + * Micrometer extension), we only register the {@link OpenTelemetryVertxHttpMetricsFactory} if the + * VertxHttpServerMetrics is not available in the runtime. + */ +public class OpenTelemetryVertxHttpMetricsFactory implements VertxMetricsFactory { + @Override + public VertxMetrics metrics(final VertxOptions options) { + return new OpenTelemetryVertxHttpServerMetrics(); + } + + public static class OpenTelemetryVertxHttpServerMetrics + implements HttpServerMetrics, + VertxMetrics, ExtendedQuarkusVertxHttpMetrics { + + @Override + public HttpServerMetrics createHttpServerMetrics(final HttpServerOptions options, + final SocketAddress localAddress) { + return this; + } + + @Override + public MetricRequest requestBegin(final Object socketMetric, final HttpRequest request) { + return MetricRequest.request(request); + } + + @Override + public void requestRouted(final MetricRequest requestMetric, final String route) { + if (route != null) { + requestMetric.getContext().ifPresent(context -> context.putLocal("VertxRoute", route)); + } + } + + @Override + public ConnectionTracker getHttpConnectionTracker() { + // To be implemented if we decide to instrument with OpenTelemetry. See VertxMeterBinderAdapter for an example. + return ExtendedQuarkusVertxHttpMetrics.NOOP_CONNECTION_TRACKER; + } + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/OpenTelemetryVertxMetricsFactory.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/OpenTelemetryVertxMetricsFactory.java index 32e7d006de6ec..6d030c609b780 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/OpenTelemetryVertxMetricsFactory.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/OpenTelemetryVertxMetricsFactory.java @@ -1,12 +1,7 @@ package io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx; -import java.util.Optional; - -import io.quarkus.vertx.http.runtime.ExtendedQuarkusVertxHttpMetrics; -import io.vertx.core.Context; import io.vertx.core.VertxOptions; import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.http.impl.HttpServerRequestInternal; import io.vertx.core.net.SocketAddress; import io.vertx.core.spi.VertxMetricsFactory; import io.vertx.core.spi.metrics.HttpServerMetrics; @@ -17,7 +12,7 @@ * This is used to retrieve the route name from Vert.x. This is useful for OpenTelemetry to generate the Span name and * http.route attribute. Right now, there is no other way to retrieve the route name from Vert.x using the * Telemetry SPI, so we need to rely on the Metrics SPI. - *

+ * * Right now, it is not possible to register multiple VertxMetrics, meaning that only a single one is * available per Quarkus instance. To avoid clashing with other extensions that provide Metrics data (like the * Micrometer extension), we only register the {@link OpenTelemetryVertxMetricsFactory} if the @@ -26,19 +21,17 @@ public class OpenTelemetryVertxMetricsFactory implements VertxMetricsFactory { @Override public VertxMetrics metrics(final VertxOptions options) { - return new OpenTelemetryHttpServerMetrics(); + return new VertxMetrics() { + @Override + public HttpServerMetrics createHttpServerMetrics(final HttpServerOptions options, + final SocketAddress localAddress) { + return new OpenTelemetryVertxServerMetrics(); + } + }; } - public static class OpenTelemetryHttpServerMetrics - implements HttpServerMetrics, - VertxMetrics, ExtendedQuarkusVertxHttpMetrics { - - @Override - public HttpServerMetrics createHttpServerMetrics(final HttpServerOptions options, - final SocketAddress localAddress) { - return this; - } - + public static class OpenTelemetryVertxServerMetrics + implements HttpServerMetrics { @Override public MetricRequest requestBegin(final Object socketMetric, final HttpRequest request) { return MetricRequest.request(request); @@ -50,31 +43,5 @@ public void requestRouted(final MetricRequest requestMetric, final String route) requestMetric.getContext().ifPresent(context -> context.putLocal("VertxRoute", route)); } } - - @Override - public ConnectionTracker getHttpConnectionTracker() { - // To be implemented if we decide to instrument with OpenTelemetry. See VertxMeterBinderAdapter for an example. - return ExtendedQuarkusVertxHttpMetrics.NOOP_CONNECTION_TRACKER; - } - - static final class MetricRequest { - private final HttpRequest request; - - MetricRequest(final HttpRequest request) { - this.request = request; - } - - Optional getContext() { - if (request instanceof HttpServerRequestInternal) { - return Optional.of(((HttpServerRequestInternal) request).context()); - } else { - return Optional.empty(); - } - } - - static MetricRequest request(final HttpRequest httpRequest) { - return new MetricRequest(httpRequest); - } - } } } diff --git a/integration-tests/opentelemetry-minimal/pom.xml b/integration-tests/opentelemetry-minimal/pom.xml new file mode 100644 index 0000000000000..7dccd1b87b7a5 --- /dev/null +++ b/integration-tests/opentelemetry-minimal/pom.xml @@ -0,0 +1,146 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-tests-parent + 999-SNAPSHOT + + + quarkus-integration-test-opentelemetry-minimal + Quarkus - Integration Tests - OpenTelemetry minimal (No HTTP) + + + + io.quarkus + quarkus-vertx + + + io.quarkus + quarkus-opentelemetry + + + io.opentelemetry + opentelemetry-sdk-testing + + + + io.quarkus + quarkus-junit5 + test + + + org.awaitility + awaitility + test + + + + + io.quarkus + + quarkus-vertx-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-opentelemetry-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + false + + + + + + + + + native-image + + + native + + + + + true + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${native.surefire.skip} + + false + + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + false + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + + + + + + \ No newline at end of file diff --git a/integration-tests/opentelemetry-minimal/src/main/java/io/quarkus/it/opentelemetry/minimal/HelloService.java b/integration-tests/opentelemetry-minimal/src/main/java/io/quarkus/it/opentelemetry/minimal/HelloService.java new file mode 100644 index 0000000000000..3535ed97271e6 --- /dev/null +++ b/integration-tests/opentelemetry-minimal/src/main/java/io/quarkus/it/opentelemetry/minimal/HelloService.java @@ -0,0 +1,42 @@ +package io.quarkus.it.opentelemetry.minimal; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricExporter; +import io.quarkus.runtime.StartupEvent; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; + +@ApplicationScoped +public class HelloService { + + @Inject + Vertx vertx; + + @Inject + InMemoryMetricExporter inMemoryMetricExporter; + + void onStart(@Observes StartupEvent ev) { + // Code executed during application startup + System.out.println("Application is starting..."); + } + + public Uni getMetricCount() { + return Uni.createFrom().emitter(e -> { + vertx.setTimer(100, x -> e.complete(inMemoryMetricExporter.getFinishedMetricItems().size())); + }); + } + + @ApplicationScoped + static class InMemoryMetricExporterProducer { + @Produces + @Singleton + InMemoryMetricExporter inMemoryMetricsExporter() { + return InMemoryMetricExporter.create(); + } + } +} diff --git a/integration-tests/opentelemetry-minimal/src/main/resources/application.properties b/integration-tests/opentelemetry-minimal/src/main/resources/application.properties new file mode 100644 index 0000000000000..ee272da7ecfae --- /dev/null +++ b/integration-tests/opentelemetry-minimal/src/main/resources/application.properties @@ -0,0 +1,8 @@ +# Setting these for tests explicitly. Not required in normal application +quarkus.application.name=opentelemetry-minimal +# speed up build +quarkus.otel.bsp.schedule.delay=100 +quarkus.otel.bsp.export.timeout=5s +quarkus.otel.metrics.enabled=true +quarkus.otel.metric.export.interval=100ms +#quarkus.otel.logs.enabled=true diff --git a/integration-tests/opentelemetry-minimal/src/test/java/io/quarkus/it/opentelemetry/minimal/HelloServiceIT.java b/integration-tests/opentelemetry-minimal/src/test/java/io/quarkus/it/opentelemetry/minimal/HelloServiceIT.java new file mode 100644 index 0000000000000..fc08b42ddb2d3 --- /dev/null +++ b/integration-tests/opentelemetry-minimal/src/test/java/io/quarkus/it/opentelemetry/minimal/HelloServiceIT.java @@ -0,0 +1,8 @@ +package io.quarkus.it.opentelemetry.minimal; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class HelloServiceIT extends HelloServiceTest { + +} diff --git a/integration-tests/opentelemetry-minimal/src/test/java/io/quarkus/it/opentelemetry/minimal/HelloServiceTest.java b/integration-tests/opentelemetry-minimal/src/test/java/io/quarkus/it/opentelemetry/minimal/HelloServiceTest.java new file mode 100644 index 0000000000000..3053dbab2ee92 --- /dev/null +++ b/integration-tests/opentelemetry-minimal/src/test/java/io/quarkus/it/opentelemetry/minimal/HelloServiceTest.java @@ -0,0 +1,26 @@ +package io.quarkus.it.opentelemetry.minimal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; + +import java.time.Duration; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class HelloServiceTest { + + @Inject + HelloService helloService; + + @Test + public void testHello() { + Integer result = helloService.getMetricCount().await().atMost(Duration.ofSeconds(5)); + System.out.println("Metric count: " + result); + assertThat(result, greaterThanOrEqualTo(1)); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 57e80f86b112c..cf618d0ab6e3c 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -360,6 +360,7 @@ opentelemetry-quickstart opentelemetry-spi opentelemetry-jdbc-instrumentation + opentelemetry-minimal opentelemetry-mongodb-client-instrumentation opentelemetry-quartz opentelemetry-scheduler