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

Add HttpClientMetricsTagsContributor for custom client request tags #43321

Merged
merged 1 commit into from
Sep 17, 2024
Merged
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
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;
}
}
}
Loading