Skip to content

Commit

Permalink
Merge pull request #13884 from oscarfh/add-metrics-to-rest-clients
Browse files Browse the repository at this point in the history
Add client metrics to rest client extension
  • Loading branch information
ebullient authored Dec 17, 2020
2 parents a562e27 + fd4ad93 commit 1561bde
Show file tree
Hide file tree
Showing 13 changed files with 332 additions and 76 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.quarkus.micrometer.deployment.binder;

import java.util.function.BooleanSupplier;

import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.micrometer.runtime.MicrometerRecorder;
import io.quarkus.micrometer.runtime.config.MicrometerConfig;

public class HttpClientProcessor {
// Avoid referencing optional dependencies

// Rest client listener SPI
private static final String REST_CLIENT_LISTENER_CLASS_NAME = "org.eclipse.microprofile.rest.client.spi.RestClientListener";
private static final Class<?> REST_CLIENT_LISTENER_CLASS = MicrometerRecorder
.getClassForName(REST_CLIENT_LISTENER_CLASS_NAME);

// Rest Client listener
private static final String REST_CLIENT_METRICS_LISTENER = "io.quarkus.micrometer.runtime.binder.RestClientMetrics";

static class HttpClientEnabled implements BooleanSupplier {
MicrometerConfig mConfig;

public boolean getAsBoolean() {
return REST_CLIENT_LISTENER_CLASS != null && mConfig.checkBinderEnabledWithDefault(mConfig.binder.httpClient);
}
}

@BuildStep(onlyIf = HttpClientEnabled.class)
void registerRestClientListener(BuildProducer<NativeImageResourceBuildItem> resource,
BuildProducer<ReflectiveClassBuildItem> reflectiveClass) {
resource.produce(new NativeImageResourceBuildItem(
"META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientListener"));
reflectiveClass
.produce(new ReflectiveClassBuildItem(true, true, REST_CLIENT_METRICS_LISTENER));
}
}
7 changes: 7 additions & 0 deletions extensions/micrometer/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@
<optional>true</optional>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client</artifactId>
<optional>true</optional>
</dependency>


<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.quarkus.micrometer.runtime.binder;

import java.util.regex.Pattern;

import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.binder.http.Outcome;

public class HttpTags {
public static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND");
public static final Tag URI_REDIRECTION = Tag.of("uri", "REDIRECTION");
public static final Tag URI_ROOT = Tag.of("uri", "root");
static final Tag URI_UNKNOWN = Tag.of("uri", "UNKNOWN");

static final Tag STATUS_UNKNOWN = Tag.of("status", "UNKNOWN");
public static final Tag STATUS_RESET = Tag.of("status", "RESET");

public static final Tag METHOD_UNKNOWN = Tag.of("method", "UNKNOWN");

public static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/$");
public static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+");

/**
* Creates an {@code method} {@code Tag} derived from the given {@code HTTP method}.
*
* @param method the HTTP method
* @return the outcome tag
*/
public static Tag method(String method) {
return method == null ? METHOD_UNKNOWN : Tag.of("method", method);
}

/**
* Creates a {@code status} tag based on the status of the given {@code response code}.
*
* @param statusCode the HTTP response code
* @return the status tag derived from the status of the response
*/
public static Tag status(int statusCode) {
return (statusCode > 0) ? Tag.of("status", Integer.toString(statusCode)) : STATUS_UNKNOWN;
}

/**
* Creates an {@code outcome} {@code Tag} derived from the given {@code response code}.
*
* @param statusCode the HTTP response code
* @return the outcome tag
*/
public static Tag outcome(int statusCode) {
return Outcome.forStatus(statusCode).asTag();
}

/**
* Creates a {@code uri} tag based on the URI of the given {@code request}.
* Falling back to {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND}
* for 404 responses, {@code root} for requests with no path info, and {@code UNKNOWN}
* for all other requests.
*
*
* @param pathInfo
* @param code status code of the response
* @return the uri tag derived from the request
*/
public static Tag uri(String pathInfo, int code) {
if (code > 0) {
if (code / 100 == 3) {
return URI_REDIRECTION;
} else if (code == 404) {
return URI_NOT_FOUND;
}
}
if (pathInfo == null) {
return URI_UNKNOWN;
}
if (pathInfo.isEmpty() || "/".equals(pathInfo)) {
return URI_ROOT;
}

// Use first segment of request path
return Tag.of("uri", pathInfo);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.quarkus.micrometer.runtime.binder;

import java.io.IOException;

import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.ClientResponseContext;
import javax.ws.rs.client.ClientResponseFilter;

import org.eclipse.microprofile.rest.client.RestClientBuilder;
import org.eclipse.microprofile.rest.client.spi.RestClientListener;
import org.jboss.logging.Logger;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;

public class RestClientMetrics implements RestClientListener {
private static final Logger log = Logger.getLogger(RestClientMetrics.class);
private static final String HTTP_CLIENT_METRIC_NAME = "http.client.requests";
private final static String REQUEST_TIMER_SAMPLE_PROPERTY = "requestStartTime";

final MeterRegistry registry = Metrics.globalRegistry;
final MetricsClientRequestFilter clientRequestFilter = new MetricsClientRequestFilter();
final MetricsClientResponseFilter clientResponseFilter = new MetricsClientResponseFilter();

@Override
public void onNewClient(Class<?> serviceInterface, RestClientBuilder builder) {
builder.register(clientRequestFilter);
builder.register(clientResponseFilter);
}

class MetricsClientRequestFilter implements ClientRequestFilter {
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.setProperty(REQUEST_TIMER_SAMPLE_PROPERTY, Timer.start(registry));
}
}

class MetricsClientResponseFilter implements ClientResponseFilter {
@Override
public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
Timer.Sample sample = getRequestSample(requestContext);
if (sample != null) {
String requestPath = getRequestPath(requestContext);
int statusCode = responseContext.getStatus();
Timer.Builder builder = Timer.builder(HTTP_CLIENT_METRIC_NAME)
.tags(Tags.of(
HttpTags.method(requestContext.getMethod()),
HttpTags.uri(requestPath, statusCode),
HttpTags.outcome(statusCode),
HttpTags.status(statusCode),
clientName(requestContext)));

sample.stop(builder.register(registry));
}
}

private String getRequestPath(ClientRequestContext requestContext) {
return requestContext.getUri().getPath();
}

private Timer.Sample getRequestSample(ClientRequestContext requestContext) {
return (Timer.Sample) requestContext.getProperty(REQUEST_TIMER_SAMPLE_PROPERTY);
}

private Tag clientName(ClientRequestContext requestContext) {
String host = requestContext.getUri().getHost();
if (host == null) {
host = "none";
}
return Tag.of("clientName", host);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.binder.http.Outcome;
import io.quarkus.micrometer.runtime.binder.HttpTags;
import io.quarkus.micrometer.runtime.config.runtime.VertxConfig;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpMethod;
Expand Down Expand Up @@ -96,10 +97,10 @@ public RequestMetric responsePushed(Map<String, Object> socketMetric, HttpMethod
VertxMetricsTags.parseUriPath(requestMetric, matchPatterns, ignorePatterns, uri);
if (requestMetric.measure) {
registry.counter(nameHttpServerPush, Tags.of(
VertxMetricsTags.uri(requestMetric.path, response.getStatusCode()),
HttpTags.uri(requestMetric.path, response.getStatusCode()),
VertxMetricsTags.method(method),
VertxMetricsTags.outcome(response),
VertxMetricsTags.status(response.getStatusCode())))
HttpTags.status(response.getStatusCode())))
.increment();
}
log.debugf("responsePushed %s: %s, %s", uri, socketMetric, requestMetric);
Expand Down Expand Up @@ -148,9 +149,9 @@ public void requestReset(RequestMetric requestMetric) {
Timer.Builder builder = Timer.builder(nameHttpServerRequests)
.tags(requestMetric.tags)
.tags(Tags.of(
VertxMetricsTags.uri(requestPath, 0),
HttpTags.uri(requestPath, 0),
Outcome.CLIENT_ERROR.asTag(),
VertxMetricsTags.STATUS_RESET));
HttpTags.STATUS_RESET));
sample.stop(builder.register(registry));
}
}
Expand All @@ -171,9 +172,9 @@ public void responseEnd(RequestMetric requestMetric, HttpServerResponse response
Timer.Builder builder = Timer.builder(nameHttpServerRequests)
.tags(requestMetric.tags)
.tags(Tags.of(
VertxMetricsTags.uri(requestPath, response.getStatusCode()),
HttpTags.uri(requestPath, response.getStatusCode()),
VertxMetricsTags.outcome(response),
VertxMetricsTags.status(response.getStatusCode())));
HttpTags.status(response.getStatusCode())));

sample.stop(builder.register(registry));
}
Expand All @@ -194,7 +195,7 @@ public LongTaskTimer.Sample connected(Map<String, Object> socketMetric, RequestM
String path = getServerRequestPath(requestMetric);
if (path != null) {
return LongTaskTimer.builder(nameWebsocketConnections)
.tags(Tags.of(VertxMetricsTags.uri(path, 0)))
.tags(Tags.of(HttpTags.uri(path, 0)))
.register(registry)
.start();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.binder.http.Outcome;
import io.quarkus.micrometer.runtime.binder.HttpTags;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerRequest;
Expand All @@ -16,20 +17,6 @@
public class VertxMetricsTags {
private static final Logger log = Logger.getLogger(VertxMetricsTags.class);

static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND");
static final Tag URI_REDIRECTION = Tag.of("uri", "REDIRECTION");
static final Tag URI_ROOT = Tag.of("uri", "root");
static final Tag URI_UNKNOWN = Tag.of("uri", "UNKNOWN");

static final Tag STATUS_UNKNOWN = Tag.of("status", "UNKNOWN");
static final Tag STATUS_RESET = Tag.of("status", "RESET");

static final Tag METHOD_UNKNOWN = Tag.of("method", "UNKNOWN");

private static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/$");

private static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+");

/**
* Creates a {@code method} tag based on the {@link HttpServerRequest#method()}
* method} of the given {@code request}.
Expand All @@ -38,17 +25,7 @@ public class VertxMetricsTags {
* @return the method tag whose value is a capitalized method (e.g. GET).
*/
public static Tag method(HttpMethod method) {
return (method != null) ? Tag.of("method", method.toString()) : METHOD_UNKNOWN;
}

/**
* Creates a {@code status} tag based on the status of the given {@code response}.
*
* @param statusCode the HTTP response code
* @return the status tag derived from the status of the response
*/
public static Tag status(int statusCode) {
return (statusCode > 0) ? Tag.of("status", Integer.toString(statusCode)) : STATUS_UNKNOWN;
return (method != null) ? Tag.of("method", method.toString()) : HttpTags.METHOD_UNKNOWN;
}

/**
Expand Down Expand Up @@ -77,36 +54,6 @@ public static Tag outcome(HttpClientResponse response) {
return Outcome.UNKNOWN.asTag();
}

/**
* Creates a {@code uri} tag based on the URI of the given {@code request}.
* Falling back to {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND}
* for 404 responses, {@code root} for requests with no path info, and {@code UNKNOWN}
* for all other requests.
*
*
* @param pathInfo
* @param code status code of the response
* @return the uri tag derived from the request
*/
public static Tag uri(String pathInfo, int code) {
if (code > 0) {
if (code / 100 == 3) {
return URI_REDIRECTION;
} else if (code == 404) {
return URI_NOT_FOUND;
}
}
if (pathInfo == null) {
return URI_UNKNOWN;
}
if (pathInfo.isEmpty() || "/".equals(pathInfo)) {
return URI_ROOT;
}

// Use first segment of request path
return Tag.of("uri", pathInfo);
}

/**
* Extract the path out of the uri. Return null if the path should be
* ignored.
Expand All @@ -118,8 +65,8 @@ static void parseUriPath(RequestMetric requestMetric, Map<Pattern, String> match
}

String path = "/" + extractPath(uri);
path = MULTIPLE_SLASH_PATTERN.matcher(path).replaceAll("/");
path = TRAILING_SLASH_PATTERN.matcher(path).replaceAll("");
path = HttpTags.MULTIPLE_SLASH_PATTERN.matcher(path).replaceAll("/");
path = HttpTags.TRAILING_SLASH_PATTERN.matcher(path).replaceAll("");

if (path.isEmpty()) {
path = "/";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.quarkus.micrometer.runtime.config;

import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;

/**
* Build / static runtime config for outbound HTTP requests
*/
@ConfigGroup
public class HttpClientConfig implements MicrometerConfig.CapabilityEnabled {
/**
* Outbound HTTP request metrics support.
* <p>
* Support for HTTP client metrics will be enabled if Micrometer
* support is enabled, the REST client feature is enabled,
* and either this value is true, or this value is unset and
* {@code quarkus.micrometer.binder-enabled-default} is true.
*/
@ConfigItem
public Optional<Boolean> enabled;

@Override
public Optional<Boolean> getEnabled() {
return enabled;
}

@Override
public String toString() {
return this.getClass().getSimpleName()
+ "{enabled=" + enabled
+ '}';
}
}
Loading

0 comments on commit 1561bde

Please sign in to comment.