Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix NoClassDefFoundError when using vertx without http #44071

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package io.quarkus.micrometer.deployment.binder;

import static io.restassured.RestAssured.when;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.quarkus.micrometer.test.HelloResource;
import io.quarkus.micrometer.test.PingPongResource;
import io.quarkus.micrometer.test.ServletEndpoint;
import io.quarkus.micrometer.test.Util;
import io.quarkus.micrometer.test.VertxWebEndpoint;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

public class UriTagExemplarTest {
@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withConfigurationResource("test-logging.properties")
.overrideConfigKey("quarkus.micrometer.binder-enabled-default", "false")
.overrideConfigKey("quarkus.micrometer.binder.http-client.enabled", "true")
.overrideConfigKey("quarkus.micrometer.binder.http-server.enabled", "true")
.overrideConfigKey("quarkus.micrometer.binder.http-server.match-patterns", "/one=/two")
.overrideConfigKey("quarkus.micrometer.binder.http-server.ignore-patterns", "/two")
.overrideConfigKey("quarkus.micrometer.binder.vertx.enabled", "true")
.overrideConfigKey("pingpong/mp-rest/url", "${test.url}")
.overrideConfigKey("quarkus.redis.devservices.enabled", "false")
.withApplicationRoot((jar) -> jar
.addClasses(Util.class,
PingPongResource.class,
PingPongResource.PingPongRestClient.class,
ServletEndpoint.class,
VertxWebEndpoint.class,
HelloResource.class));

static SimpleMeterRegistry registry = new SimpleMeterRegistry();

@BeforeAll
static void setRegistry() {
registry = new SimpleMeterRegistry();
Metrics.addRegistry(registry);
}

@AfterAll()
static void removeRegistry() {
Metrics.removeRegistry(registry);
}

@Test
public void testRequestUris() throws Exception {
RestAssured.basePath = "/";

// Server paths (templated)
when().get("/one").then().statusCode(200);
when().get("/two").then().statusCode(200);
when().get("/vertx/item/123").then().statusCode(200);
when().get("/vertx/item/1/123").then().statusCode(200);
when().get("/servlet/12345").then().statusCode(200);

// Server GET vs. HEAD methods -- templated
when().get("/hello/one").then().statusCode(200);
when().get("/hello/two").then().statusCode(200);
when().head("/hello/three").then().statusCode(200);
when().head("/hello/four").then().statusCode(200);
when().get("/vertx/echo/thing1").then().statusCode(200);
when().get("/vertx/echo/thing2").then().statusCode(200);
when().head("/vertx/echo/thing3").then().statusCode(200);
when().head("/vertx/echo/thing4").then().statusCode(200);

// Server -> Rest client -> Server (templated)
when().get("/ping/one").then().statusCode(200);
when().get("/ping/two").then().statusCode(200);
when().get("/ping/three").then().statusCode(200);
when().get("/ping/400").then().statusCode(200);
when().get("/ping/500").then().statusCode(200);
when().get("/async-ping/one").then().statusCode(200);
when().get("/async-ping/two").then().statusCode(200);
when().get("/async-ping/three").then().statusCode(200);

// Try to let metrics gathering finish.
// Looking for server request timers: /vertx/item/{id}, /vertx/item/{id}/{sub}, /servlet/,
// /ping/{message}, /async-ping/{message}, /pong/{message}, and 2 of both /hello/{message} and /vertx/echo/{msg}
// Looking for client request: /pong/{message}
Util.waitForMeters(registry.find("http.server.requests").timers(), 10);
Util.waitForMeters(registry.find("http.client.requests").timers(), 1);

System.out.println("Server paths\n" + Util.listMeters(registry, "http.server.requests"));
System.out.println("Client paths\n" + Util.listMeters(registry, "http.client.requests"));

// /one should map to /two, which is ignored. Neither should exist w/ timers
Assertions.assertEquals(0, registry.find("http.server.requests").tag("uri", "/one").timers().size(),
Util.foundServerRequests(registry, "/one is mapped to /two, which should be ignored."));
Assertions.assertEquals(0, registry.find("http.server.requests").tag("uri", "/two").timers().size(),
Util.foundServerRequests(registry, "/two should be ignored."));

// URIs for server: /vertx/item/{id}, /vertx/item/{id}/{sub}, /servlet/
Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/vertx/item/{id}").timers().size(),
Util.foundServerRequests(registry,
"Vert.x Web template path (/vertx/item/:id) should be detected/translated to /vertx/item/{id}."));
Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/vertx/item/{id}/{sub}").timers().size(),
Util.foundServerRequests(registry,
"Vert.x Web template path (/vertx/item/:id/:sub) should be detected/translated to /vertx/item/{id}/{sub}."));
Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/servlet").timers().size(),
Util.foundServerRequests(registry, "Servlet path (/servlet) should be used for servlet"));

// GET and HEAD are two different methods, there should be two timers for each of these URI tag values
Assertions.assertEquals(2, registry.find("http.server.requests").tag("uri", "/hello/{message}").timers().size(),
Util.foundServerRequests(registry, "/hello/{message} should have two timers (GET and HEAD)."));
Assertions.assertEquals(2, registry.find("http.server.requests").tag("uri", "/vertx/echo/{msg}").timers().size(),
Util.foundServerRequests(registry, "/vertx/echo/{msg} should have two timers (GET and HEAD)."));

// URIs to trigger REST request: /ping/{message}, /async-ping/{message},
Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/ping/{message}").timers().size(),
Util.foundServerRequests(registry, "/ping/{message} should be returned by JAX-RS."));
Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/async-ping/{message}").timers().size(),
Util.foundServerRequests(registry, "/async-ping/{message} should be returned by JAX-RS."));

// Server response for /pong/{message}
Assertions.assertEquals(1,
registry.find("http.server.requests").tag("uri", "/pong/{message}").tag("outcome", "SUCCESS").timers().size(),
Util.foundServerRequests(registry, "/pong/{message} with tag outcome=SUCCESS should be returned by JAX-RS."));
Assertions.assertEquals(1,
registry.find("http.server.requests").tag("uri", "/pong/{message}").tag("outcome", "CLIENT_ERROR").timers()
.size(),
Util.foundServerRequests(registry,
"/pong/{message} with tag outcome=CLIENT_ERROR should be returned by JAX-RS."));
Assertions.assertEquals(1,
registry.find("http.server.requests").tag("uri", "/pong/{message}").tag("outcome", "SERVER_ERROR").timers()
.size(),
Util.foundServerRequests(registry,
"/pong/{message} with tag outcome=SERVER_ERROR should be returned by JAX-RS."));

// URI for outbound client request: /pong/{message}
Assertions.assertEquals(1,
registry.find("http.client.requests").tag("uri", "/pong/{message}").tag("outcome", "SUCCESS").timers().size(),
Util.foundClientRequests(registry,
"/pong/{message} with tag outcome=SUCCESS should be returned by Rest client."));
Assertions.assertEquals(1,
registry.find("http.client.requests").tag("uri", "/pong/{message}").tag("outcome", "CLIENT_ERROR").timers()
.size(),
Util.foundClientRequests(registry,
"/pong/{message} with tag outcome=CLIENT_ERROR should be returned by Rest client."));
Assertions.assertEquals(1,
registry.find("http.client.requests").tag("uri", "/pong/{message}").tag("outcome", "SERVER_ERROR").timers()
.size(),
Util.foundClientRequests(registry,
"/pong/{message} with tag outcome=SERVER_ERROR should be returned by Rest client."));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Function;

import org.jboss.logging.Logger;

Expand All @@ -15,11 +16,14 @@
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.binder.http.Outcome;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.micrometer.runtime.HttpServerMetricsTagsContributor;
import io.quarkus.micrometer.runtime.binder.HttpBinderConfiguration;
import io.quarkus.micrometer.runtime.binder.HttpCommonTags;
import io.quarkus.opentelemetry.runtime.QuarkusContextStorage;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.ServerWebSocket;
Expand Down Expand Up @@ -164,12 +168,14 @@ public void requestReset(HttpRequestMetric requestMetric) {
if (path != null) {
Timer.Sample sample = requestMetric.getSample();

sample.stop(requestsTimer
.withTags(Tags.of(
VertxMetricsTags.method(requestMetric.request().method()),
HttpCommonTags.uri(path, requestMetric.initialPath, 0),
Outcome.CLIENT_ERROR.asTag(),
HttpCommonTags.STATUS_RESET)));
executeInContext(sample::stop,
requestsTimer
.withTags(Tags.of(
VertxMetricsTags.method(requestMetric.request().method()),
HttpCommonTags.uri(path, requestMetric.initialPath, 0),
Outcome.CLIENT_ERROR.asTag(),
HttpCommonTags.STATUS_RESET)),
requestMetric.request().context());
}
requestMetric.requestEnded();
}
Expand Down Expand Up @@ -207,11 +213,43 @@ public void responseEnd(HttpRequestMetric requestMetric, HttpResponse response,
}
}

sample.stop(requestsTimer.withTags(allTags));
executeInContext(sample::stop, requestsTimer.withTags(allTags), requestMetric.request().context());
}
requestMetric.requestEnded();
}

/**
* Called when an HTTP server response has ended.
* Makes sure exemplars are produced because they have an OTel context.
*
* @param methodReference Ex: Sample stop method reference
* @param parameter The parameter to pass to the method
* @param requestContext The request context
* @param <P> The parameter type is a type of metric, ex: Timer
* @param <R> The return type of the method pointed by the methodReference
* @return The result of the method
*/
static <P, R> R executeInContext(Function<P, R> methodReference, P parameter, io.vertx.core.Context requestContext) {
if (requestContext == null) {
return methodReference.apply(parameter);
}

Context newContext = QuarkusContextStorage.getContext(requestContext);

if (newContext == null) {
return methodReference.apply(parameter);
}

io.opentelemetry.context.Context oldContext = QuarkusContextStorage.INSTANCE.current();
try (Scope scope = QuarkusContextStorage.INSTANCE.attach(newContext)) {
return methodReference.apply(parameter);
} finally {
if (oldContext != null) {
QuarkusContextStorage.INSTANCE.attach(oldContext);
}
}
}

/**
* Called when a server web socket connects.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AdditionalBeanBuildItem> additionalBeans, OTelBuildConfig config) {
if (config.instrument().grpc()) {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -74,7 +75,15 @@ public void setupVertxTracer(BeanContainer beanContainer, boolean sqlClientAvail
}

/* STATIC INIT */
public Consumer<VertxOptions> getVertxTracingMetricsOptions() {
public Consumer<VertxOptions> getVertxHttpMetricsOptions() {
MetricsOptions metricsOptions = new MetricsOptions()
.setEnabled(true)
.setFactory(new OpenTelemetryVertxHttpMetricsFactory());
return vertxOptions -> vertxOptions.setMetricsOptions(metricsOptions);
}

/* STATIC INIT */
public Consumer<VertxOptions> getVertxMetricsOptions() {
MetricsOptions metricsOptions = new MetricsOptions()
.setEnabled(true)
.setFactory(new OpenTelemetryVertxMetricsFactory());
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Context> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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
* <code>http.route</code> 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.
* <p>
* Right now, it is not possible to register multiple <code>VertxMetrics</code>, 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
* <code>VertxHttpServerMetrics</code> 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<MetricRequest, Object, Object>,
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;
}
}
}
Loading
Loading