From 54d7af16961cf87bcd2ef0c095762f51d20c4f48 Mon Sep 17 00:00:00 2001 From: PhilKes Date: Mon, 16 Sep 2024 19:19:52 +0200 Subject: [PATCH] Add HttpClientMetricsTagsContributor for custom client request tags --- .../main/asciidoc/telemetry-micrometer.adoc | 4 ++ .../binder/VertxBinderProcessor.java | 6 ++ .../binder/VertxHttpClientMetricsTest.java | 30 ++++++-- .../micrometer/test/ClientDummyTag.java | 15 ++++ .../micrometer/test/ClientHeaderTag.java | 20 ++++++ .../HttpClientMetricsTagsContributor.java | 24 +++++++ .../binder/vertx/VertxHttpClientMetrics.java | 69 +++++++++++++++++-- 7 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/ClientDummyTag.java create mode 100644 extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/ClientHeaderTag.java create mode 100644 extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/HttpClientMetricsTagsContributor.java diff --git a/docs/src/main/asciidoc/telemetry-micrometer.adoc b/docs/src/main/asciidoc/telemetry-micrometer.adoc index a61966fa16d76..472f7c16c02d8 100644 --- a/docs/src/main/asciidoc/telemetry-micrometer.adoc +++ b/docs/src/main/asciidoc/telemetry-micrometer.adoc @@ -590,6 +590,10 @@ link:https://micrometer.io/docs/concepts[official documentation]. By providing CDI beans that implement `io.quarkus.micrometer.runtime.HttpServerMetricsTagsContributor`, user code can contribute arbitrary tags based on the details of HTTP request +=== Use `HttpClientMetricsTagsContributor` for client HTTP requests + +By providing CDI beans that implement `io.quarkus.micrometer.runtime.HttpClientMetricsTagsContributor`, user code can contribute arbitrary tags based on the details of HTTP request + === Use `MeterRegistryCustomizer` for arbitrary customizations to meter registries By providing CDI beans that implement `io.quarkus.micrometer.runtime.MeterRegistryCustomizer` user code has the change to change the configuration of any `MeterRegistry` that has been activated. diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/VertxBinderProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/VertxBinderProcessor.java index 6a26f66254200..4733a7ae6e161 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/VertxBinderProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/VertxBinderProcessor.java @@ -11,6 +11,7 @@ import io.quarkus.deployment.annotations.Consume; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; +import io.quarkus.micrometer.runtime.HttpClientMetricsTagsContributor; import io.quarkus.micrometer.runtime.HttpServerMetricsTagsContributor; import io.quarkus.micrometer.runtime.MicrometerRecorder; import io.quarkus.micrometer.runtime.binder.vertx.VertxMeterBinderRecorder; @@ -41,6 +42,11 @@ UnremovableBeanBuildItem unremoveableAdditionalHttpServerMetrics() { return UnremovableBeanBuildItem.beanTypes(HttpServerMetricsTagsContributor.class); } + @BuildStep + UnremovableBeanBuildItem unremoveableAdditionalHttpClientMetrics() { + return UnremovableBeanBuildItem.beanTypes(HttpClientMetricsTagsContributor.class); + } + @BuildStep @Record(value = ExecutionTime.STATIC_INIT) VertxOptionsConsumerBuildItem build(VertxMeterBinderRecorder recorder) { diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/VertxHttpClientMetricsTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/VertxHttpClientMetricsTest.java index 05222595a003d..3910804e58056 100644 --- a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/VertxHttpClientMetricsTest.java +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/VertxHttpClientMetricsTest.java @@ -19,6 +19,8 @@ import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.search.Search; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.quarkus.micrometer.test.ClientDummyTag; +import io.quarkus.micrometer.test.ClientHeaderTag; import io.quarkus.micrometer.test.Util; import io.quarkus.test.QuarkusUnitTest; import io.vertx.core.http.HttpClientOptions; @@ -28,6 +30,7 @@ import io.vertx.mutiny.core.http.HttpServer; import io.vertx.mutiny.core.http.WebSocket; import io.vertx.mutiny.ext.web.Router; +import io.vertx.mutiny.ext.web.client.HttpRequest; import io.vertx.mutiny.ext.web.client.HttpResponse; import io.vertx.mutiny.ext.web.client.WebClient; import io.vertx.mutiny.ext.web.handler.BodyHandler; @@ -39,7 +42,8 @@ public class VertxHttpClientMetricsTest { .withConfigurationResource("test-logging.properties") .overrideConfigKey("quarkus.redis.devservices.enabled", "false") .withApplicationRoot((jar) -> jar - .addClasses(App.class, HttpClient.class, WsClient.class, Util.class)); + .addClasses(App.class, HttpClient.class, WsClient.class, Util.class, + ClientDummyTag.class, ClientHeaderTag.class)); final static SimpleMeterRegistry registry = new SimpleMeterRegistry(); @@ -80,7 +84,8 @@ void testWebClientMetrics() { } try { - Assertions.assertEquals("ok", client.get()); + Assertions.assertEquals("ok", client.get(null)); + Assertions.assertEquals("ok", client.get("bar")); Assertions.assertEquals("HELLO", client.post("hello")); Assertions.assertNotNull(getMeter("http.client.connections").longTaskTimer()); @@ -91,7 +96,7 @@ void testWebClientMetrics() { () -> Assertions.assertEquals(expectedBytesWritten, registry.find("http.client.bytes.written") .tag("clientName", "my-client").summary().totalAmount())); - await().untilAsserted(() -> Assertions.assertEquals(7, + await().untilAsserted(() -> Assertions.assertEquals(9, registry.find("http.client.bytes.read") .tag("clientName", "my-client").summary().totalAmount())); @@ -103,11 +108,20 @@ void testWebClientMetrics() { Assertions.assertEquals(1, registry.find("http.client.requests") .tag("uri", "root") + .tag("dummy", "value") + .tag("foo", "UNSET") + .tag("outcome", "SUCCESS").timers().size(), + Util.foundClientRequests(registry, "/ with tag outcome=SUCCESS.")); + + Assertions.assertEquals(1, registry.find("http.client.requests") + .tag("uri", "root") + .tag("dummy", "value") + .tag("foo", "bar") .tag("outcome", "SUCCESS").timers().size(), Util.foundClientRequests(registry, "/ with tag outcome=SUCCESS.")); // Queue - Assertions.assertEquals(2, registry.find("http.client.queue.delay") + Assertions.assertEquals(3, registry.find("http.client.queue.delay") .tag("clientName", "my-client").timer().count()); Assertions.assertTrue(registry.find("http.client.queue.delay") .tag("clientName", "my-client").timer().totalTime(TimeUnit.NANOSECONDS) > 0); @@ -202,8 +216,12 @@ public void init() { .setMetricsName("http-client|my-client")); } - public String get() { - return client.getAbs("http://localhost:8888/") + public String get(String fooHeaderValue) { + HttpRequest request = client.getAbs("http://localhost:8888/"); + if (fooHeaderValue != null) { + request.putHeader("Foo", fooHeaderValue); + } + return request .send() .map(HttpResponse::bodyAsString) .await().atMost(Duration.ofSeconds(10)); diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/ClientDummyTag.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/ClientDummyTag.java new file mode 100644 index 0000000000000..139a3df7e6bd5 --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/ClientDummyTag.java @@ -0,0 +1,15 @@ +package io.quarkus.micrometer.test; + +import jakarta.inject.Singleton; + +import io.micrometer.core.instrument.Tags; +import io.quarkus.micrometer.runtime.HttpClientMetricsTagsContributor; + +@Singleton +public class ClientDummyTag implements HttpClientMetricsTagsContributor { + + @Override + public Tags contribute(Context context) { + return Tags.of("dummy", "value"); + } +} diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/ClientHeaderTag.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/ClientHeaderTag.java new file mode 100644 index 0000000000000..ff28bbbb1fc32 --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/ClientHeaderTag.java @@ -0,0 +1,20 @@ +package io.quarkus.micrometer.test; + +import jakarta.inject.Singleton; + +import io.micrometer.core.instrument.Tags; +import io.quarkus.micrometer.runtime.HttpClientMetricsTagsContributor; + +@Singleton +public class ClientHeaderTag implements HttpClientMetricsTagsContributor { + + @Override + public Tags contribute(Context context) { + String headerValue = context.request().headers().get("Foo"); + String value = "UNSET"; + if ((headerValue != null) && !headerValue.isEmpty()) { + value = headerValue; + } + return Tags.of("foo", value); + } +} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/HttpClientMetricsTagsContributor.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/HttpClientMetricsTagsContributor.java new file mode 100644 index 0000000000000..54fc08a5ffe1b --- /dev/null +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/HttpClientMetricsTagsContributor.java @@ -0,0 +1,24 @@ +package io.quarkus.micrometer.runtime; + +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.config.MeterFilter; +import io.vertx.core.spi.observability.HttpRequest; + +/** + * Allows code to add additional Micrometer {@link Tags} to the metrics collected for completed HTTP client requests. + *

+ * The implementations of this interface are meant to be registered via CDI beans. + * + * @see MeterFilter for a more advanced and feature complete way of interacting with {@link Tags} + */ +public interface HttpClientMetricsTagsContributor { + + /** + * Called when Vert.x http client request has ended + */ + Tags contribute(Context context); + + interface Context { + HttpRequest request(); + } +} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpClientMetrics.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpClientMetrics.java index b97df62279bfb..d1cc04187e0b9 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpClientMetrics.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpClientMetrics.java @@ -1,5 +1,7 @@ package io.quarkus.micrometer.runtime.binder.vertx; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -9,6 +11,8 @@ import java.util.function.Supplier; import java.util.regex.Pattern; +import org.jboss.logging.Logger; + import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.LongTaskTimer; import io.micrometer.core.instrument.Meter; @@ -16,6 +20,9 @@ import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.Timer; +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.micrometer.runtime.HttpClientMetricsTagsContributor; import io.quarkus.micrometer.runtime.binder.HttpBinderConfiguration; import io.quarkus.micrometer.runtime.binder.HttpCommonTags; import io.quarkus.micrometer.runtime.binder.RequestMetricInfo; @@ -28,6 +35,7 @@ class VertxHttpClientMetrics extends VertxTcpClientMetrics implements HttpClientMetrics { + static final Logger log = Logger.getLogger(VertxHttpClientMetrics.class); private final LongAdder queue = new LongAdder(); @@ -39,6 +47,8 @@ class VertxHttpClientMetrics extends VertxTcpClientMetrics private final Meter.MeterProvider responseTimes; + private final List httpClientMetricsTagsContributors; + VertxHttpClientMetrics(MeterRegistry registry, String prefix, Tags tags, HttpBinderConfiguration httpBinderConfiguration) { super(registry, prefix, tags); this.config = httpBinderConfiguration; @@ -65,11 +75,32 @@ public Number get() { } }).description("Number of requests waiting for a response"); + httpClientMetricsTagsContributors = resolveHttpClientMetricsTagsContributors(); + responseTimes = Timer.builder(config.getHttpClientRequestsName()) .description("Response times") .withRegistry(registry); } + private List resolveHttpClientMetricsTagsContributors() { + final List httpClientMetricsTagsContributors; + ArcContainer arcContainer = Arc.container(); + if (arcContainer == null) { + httpClientMetricsTagsContributors = Collections.emptyList(); + } else { + var handles = arcContainer.listAll(HttpClientMetricsTagsContributor.class); + if (handles.isEmpty()) { + httpClientMetricsTagsContributors = Collections.emptyList(); + } else { + httpClientMetricsTagsContributors = new ArrayList<>(handles.size()); + for (var handle : handles) { + httpClientMetricsTagsContributors.add(handle.get()); + } + } + } + return httpClientMetricsTagsContributors; + } + @Override public ClientMetrics createEndpointMetrics( SocketAddress remoteAddress, int maxPoolSize) { @@ -89,7 +120,7 @@ public void dequeueRequest(EventTiming event) { @Override public RequestTracker requestBegin(String uri, HttpRequest request) { - RequestTracker handler = new RequestTracker(tags, remote, request.uri(), request.method().name()); + RequestTracker handler = new RequestTracker(tags, remote, request); String path = handler.getNormalizedUriPath( config.getServerMatchPatterns(), config.getServerIgnorePatterns()); @@ -140,6 +171,17 @@ public void responseEnd(RequestTracker tracker, long bytesRead) { Tags list = tracker.tags .and(HttpCommonTags.status(tracker.response.statusCode())) .and(HttpCommonTags.outcome(tracker.response.statusCode())); + if (!httpClientMetricsTagsContributors.isEmpty()) { + HttpClientMetricsTagsContributor.Context context = new DefaultContext(tracker.request); + for (int i = 0; i < httpClientMetricsTagsContributors.size(); i++) { + try { + Tags additionalTags = httpClientMetricsTagsContributors.get(i).contribute(context); + list = list.and(additionalTags); + } catch (Exception e) { + log.debug("Unable to obtain additional tags", e); + } + } + } responseTimes .withTags(list) @@ -178,19 +220,19 @@ public void disconnected(String remote) { public static class RequestTracker extends RequestMetricInfo { private final Tags tags; - private final String path; + private final HttpRequest request; private EventTiming timer; HttpResponse response; private boolean responseEnded; private boolean requestEnded; private boolean reset; - RequestTracker(Tags origin, String address, String path, String method) { - this.path = path; + RequestTracker(Tags origin, String address, HttpRequest request) { + this.request = request; this.tags = origin.and( Tag.of("address", address), - HttpCommonTags.method(method), - HttpCommonTags.uri(path, null, -1)); + HttpCommonTags.method(request.method().name()), + HttpCommonTags.uri(request.uri(), null, -1)); } void requestReset() { @@ -208,7 +250,20 @@ boolean responseEnded() { } public String getNormalizedUriPath(Map serverMatchPatterns, List serverIgnorePatterns) { - return super.getNormalizedUriPath(serverMatchPatterns, serverIgnorePatterns, path); + return super.getNormalizedUriPath(serverMatchPatterns, serverIgnorePatterns, request.uri()); + } + } + + private static class DefaultContext implements HttpClientMetricsTagsContributor.Context { + private final HttpRequest request; + + private DefaultContext(HttpRequest request) { + this.request = request; + } + + @Override + public HttpRequest request() { + return request; } } }