Skip to content

Commit

Permalink
Merge pull request #43321 from PhilKes/http-client-request-tags-contr…
Browse files Browse the repository at this point in the history
…ibutor

Add HttpClientMetricsTagsContributor for custom client request tags
  • Loading branch information
geoand authored Sep 17, 2024
2 parents 9e84eb9 + 54d7af1 commit 3a4ebc5
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 13 deletions.
4 changes: 4 additions & 0 deletions docs/src/main/asciidoc/telemetry-micrometer.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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();

Expand Down Expand Up @@ -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());
Expand All @@ -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()));

Expand All @@ -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);
Expand Down Expand Up @@ -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<Buffer> request = client.getAbs("http://localhost:8888/");
if (fooHeaderValue != null) {
request.putHeader("Foo", fooHeaderValue);
}
return request
.send()
.map(HttpResponse::bodyAsString)
.await().atMost(Duration.ofSeconds(10));
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,13 +11,18 @@
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;
import io.micrometer.core.instrument.MeterRegistry;
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;
Expand All @@ -28,6 +35,7 @@

class VertxHttpClientMetrics extends VertxTcpClientMetrics
implements HttpClientMetrics<VertxHttpClientMetrics.RequestTracker, String, LongTaskTimer.Sample, EventTiming> {
static final Logger log = Logger.getLogger(VertxHttpClientMetrics.class);

private final LongAdder queue = new LongAdder();

Expand All @@ -39,6 +47,8 @@ class VertxHttpClientMetrics extends VertxTcpClientMetrics

private final Meter.MeterProvider<Timer> responseTimes;

private final List<HttpClientMetricsTagsContributor> httpClientMetricsTagsContributors;

VertxHttpClientMetrics(MeterRegistry registry, String prefix, Tags tags, HttpBinderConfiguration httpBinderConfiguration) {
super(registry, prefix, tags);
this.config = httpBinderConfiguration;
Expand All @@ -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<HttpClientMetricsTagsContributor> resolveHttpClientMetricsTagsContributors() {
final List<HttpClientMetricsTagsContributor> 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<RequestTracker, EventTiming, HttpRequest, HttpResponse> createEndpointMetrics(
SocketAddress remoteAddress, int maxPoolSize) {
Expand All @@ -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());
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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() {
Expand All @@ -208,7 +250,20 @@ boolean responseEnded() {
}

public String getNormalizedUriPath(Map<Pattern, String> serverMatchPatterns, List<Pattern> 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;
}
}
}

0 comments on commit 3a4ebc5

Please sign in to comment.