Skip to content

Commit

Permalink
MicrometerHttpClientInterceptor has no outcome Tag (#3790) (#3791)
Browse files Browse the repository at this point in the history
Adds the `outcome` tag to align with the synchronous Apache Http Client instrumentation. Adds instrumentation verification tests to ensure instrumentation is in line with base expectations for all HTTP clients (this would have caught the missing `outcome` tag when we aligned on adding that previously).

Resolves gh-3790

Co-authored-by: Tommy Ludwig <8924140+shakuzen@users.noreply.github.com>
  • Loading branch information
cachescrubber and shakuzen authored Apr 27, 2023
1 parent 4acbcc1 commit 7927fec
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.binder.http.Outcome;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponseInterceptor;
Expand Down Expand Up @@ -78,6 +79,7 @@ public MicrometerHttpClientInterceptor(MeterRegistry meterRegistry, Function<Htt
this.responseInterceptor = (response, context) -> {
timerByHttpContext.remove(context)
.tag("status", Integer.toString(response.getStatusLine().getStatusCode()))
.tag("outcome", Outcome.forStatus(response.getStatusLine().getStatusCode()).name())
.tags(exportTagsForRoute ? HttpContextUtils.generateTagsForRoute(context) : Tags.empty())
.tags(extraTags)
.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.binder.http.Outcome;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpRequestInterceptor;
import org.apache.hc.core5.http.HttpResponseInterceptor;
Expand Down Expand Up @@ -72,6 +73,7 @@ public MicrometerHttpClientInterceptor(MeterRegistry meterRegistry, Function<Htt
this.responseInterceptor = (response, entityDetails, context) -> {
timerByHttpContext.remove(context)
.tag("status", Integer.toString(response.getCode()))
.tag("outcome", Outcome.forStatus(response.getCode()).name())
.tags(exportTagsForRoute ? HttpContextUtils.generateTagsForRoute(context) : Tags.empty())
.tags(extraTags)
.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import static com.github.tomakehurst.wiremock.client.WireMock.any;
import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;

/**
* Tests for {@link MicrometerHttpClientInterceptor}.
Expand Down Expand Up @@ -63,7 +64,14 @@ void asyncRequest(@WiremockResolver.Wiremock WireMockServer server) throws Excep
HttpResponse response = future.get();

assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
assertThat(registry.get("httpcomponents.httpclient.request").timer().count()).isEqualTo(1);
assertThatCode(() -> {
assertThat(registry.get("httpcomponents.httpclient.request")
.tag("method", "GET")
.tag("status", "200")
.tag("outcome", "SUCCESS")
.timer()
.count()).isEqualTo(1);
}).doesNotThrowAnyException();

client.close();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import static com.github.tomakehurst.wiremock.client.WireMock.any;
import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;

/**
* Tests for {@link MicrometerHttpClientInterceptor}.
Expand Down Expand Up @@ -66,7 +67,14 @@ void asyncRequest(@WiremockResolver.Wiremock WireMockServer server) throws Excep
HttpResponse response = future.get();

assertThat(response.getCode()).isEqualTo(200);
assertThat(registry.get("httpcomponents.httpclient.request").timer().count()).isEqualTo(1);
assertThatCode(() -> {
assertThat(registry.get("httpcomponents.httpclient.request")
.tag("method", "GET")
.tag("status", "200")
.tag("outcome", "SUCCESS")
.timer()
.count()).isEqualTo(1);
}).doesNotThrowAnyException();

client.close();
}
Expand Down
1 change: 1 addition & 0 deletions micrometer-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies {
testImplementation 'com.hazelcast:hazelcast'
testImplementation 'com.squareup.okhttp3:okhttp'
testImplementation 'org.apache.httpcomponents:httpclient'
testImplementation 'org.apache.httpcomponents:httpasyncclient'
testImplementation 'org.apache.httpcomponents.client5:httpclient5'
testImplementation 'org.eclipse.jetty:jetty-client'
testImplementation 'org.eclipse.jetty:jetty-server'
Expand Down
2 changes: 2 additions & 0 deletions micrometer-test/gradle.lockfile

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2022 VMware, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micrometer.core.instrument;

import io.micrometer.common.lang.Nullable;
import io.micrometer.core.instrument.binder.httpcomponents.DefaultUriMapper;
import io.micrometer.core.instrument.binder.httpcomponents.hc5.MicrometerHttpClientInterceptor;
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
import org.apache.hc.core5.http.ContentType;

import java.net.URI;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

class ApacheAsyncHttpClient5TimingInstrumentationVerificationTests
extends HttpClientTimingInstrumentationVerificationTests<CloseableHttpAsyncClient> {

@Override
protected CloseableHttpAsyncClient clientInstrumentedWithMetrics() {
MicrometerHttpClientInterceptor interceptor = new MicrometerHttpClientInterceptor(getRegistry(), Tags.empty(),
true);
CloseableHttpAsyncClient client = HttpAsyncClients.custom()
.addRequestInterceptorFirst(interceptor.getRequestInterceptor())
.addResponseInterceptorLast(interceptor.getResponseInterceptor())
.build();
client.start();
return client;
}

@Nullable
@Override
protected CloseableHttpAsyncClient clientInstrumentedWithObservations() {
return null;
}

@Override
protected String timerName() {
return "httpcomponents.httpclient.request";
}

@Override
protected void sendHttpRequest(CloseableHttpAsyncClient instrumentedClient, HttpMethod method,
@Nullable byte[] body, URI baseUri, String templatedPath, String... pathVariables) {
try {
Future<SimpleHttpResponse> future = instrumentedClient
.execute(makeRequest(method, body, baseUri, templatedPath, pathVariables), null);
future.get();
}
catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}

private SimpleHttpRequest makeRequest(HttpMethod method, @Nullable byte[] body, URI baseUri, String templatedPath,
String... pathVariables) {
SimpleRequestBuilder builder = SimpleRequestBuilder.create(method.name());
if (body != null) {
builder.setBody(body, ContentType.TEXT_PLAIN);
}
builder.setUri(URI.create(baseUri + substitutePathVariables(templatedPath, pathVariables)));
builder.setHeader(DefaultUriMapper.URI_PATTERN_HEADER, templatedPath);
return builder.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2022 VMware, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micrometer.core.instrument;

import io.micrometer.common.lang.Nullable;
import io.micrometer.core.instrument.binder.httpcomponents.DefaultUriMapper;
import io.micrometer.core.instrument.binder.httpcomponents.MicrometerHttpClientInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;

import java.io.ByteArrayInputStream;
import java.net.URI;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

class ApacheAsyncHttpClientTimingInstrumentationVerificationTests
extends HttpClientTimingInstrumentationVerificationTests<CloseableHttpAsyncClient> {

@Override
protected CloseableHttpAsyncClient clientInstrumentedWithMetrics() {
MicrometerHttpClientInterceptor interceptor = new MicrometerHttpClientInterceptor(getRegistry(), Tags.empty(),
true);
CloseableHttpAsyncClient client = HttpAsyncClients.custom()
.addInterceptorFirst(interceptor.getRequestInterceptor())
.addInterceptorLast(interceptor.getResponseInterceptor())
.build();
client.start();
return client;
}

@Nullable
@Override
protected CloseableHttpAsyncClient clientInstrumentedWithObservations() {
return null;
}

@Override
protected String timerName() {
return "httpcomponents.httpclient.request";
}

@Override
protected void sendHttpRequest(CloseableHttpAsyncClient instrumentedClient, HttpMethod method,
@Nullable byte[] body, URI baseUri, String templatedPath, String... pathVariables) {
try {
Future<HttpResponse> future = instrumentedClient
.execute(makeRequest(method, body, baseUri, templatedPath, pathVariables), null);
future.get();
}
catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}

private HttpUriRequest makeRequest(HttpMethod method, @Nullable byte[] body, URI baseUri, String templatedPath,
String... pathVariables) {
HttpEntityEnclosingRequestBase request = new HttpEntityEnclosingRequestBase() {
@Override
public String getMethod() {
return method.name();
}
};
if (body != null) {
BasicHttpEntity entity = new BasicHttpEntity();
entity.setContent(new ByteArrayInputStream(body));
request.setEntity(entity);
}
request.setURI(URI.create(baseUri + substitutePathVariables(templatedPath, pathVariables)));
request.setHeader(DefaultUriMapper.URI_PATTERN_HEADER, templatedPath);
return request;
}

}

0 comments on commit 7927fec

Please sign in to comment.