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

Http client instrumentation TCK #3258

Merged
Merged
12 changes: 8 additions & 4 deletions micrometer-core/gradle.lockfile

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

2 changes: 2 additions & 0 deletions micrometer-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ dependencies {
testImplementation 'com.hazelcast:hazelcast'
testImplementation 'com.squareup.okhttp3:okhttp'
testImplementation 'io.projectreactor.netty:reactor-netty-http'
testImplementation 'org.apache.httpcomponents:httpclient'
testImplementation 'org.eclipse.jetty:jetty-client'
}
9 changes: 8 additions & 1 deletion 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,154 @@
/*
* 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 com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import io.micrometer.core.annotation.Incubating;
import io.micrometer.core.lang.Nullable;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.URI;
import java.util.concurrent.TimeUnit;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.assertj.core.api.Assertions.assertThat;

/**
* Test suite for HTTP client timing instrumentation that verifies the expected metrics
* are registered and recorded after different scenarios. Use this suite to ensure that
* your instrumentation has the expected naming and tags. A locally running server is used
* to receive real requests from an instrumented HTTP client.
*/
@WireMockTest
@Incubating(since = "1.9.2")
public abstract class HttpClientTimingInstrumentationVerificationTests extends InstrumentationVerificationTests {

enum HttpMethod {

GET, POST;

}

/**
* A default is provided that should be preferred by new instrumentations. Existing
* instrumentations that use a different value to maintain backwards compatibility may
* override this method to run tests with a different name used in assertions.
* @return name of the meter timing http client requests
*/
protected String timerName() {
return "http.client.requests";
}

/**
* Send an HTTP request using the instrumented HTTP client to the given base URL and
* path on the locally running server. The HTTP client instrumentation must be
* configured to tag the templated path to pass this test suite. The templated path
* will contain path variables surrounded by curly brackets to be substituted. For
* example, for the full templated URL {@literal http://localhost:8080/cart/{cartId}}
* the baseUrl would be {@literal http://localhost:8080}, the templatedPath would be
* {@literal /cart/{cartId}}. One string pathVariables argument is expected for
* substituting the cartId path variable. The number of pathVariables arguments SHOULD
* exactly match the number of path variables in the templatedPath.
* @param method http method to use to send the request
* @param baseUrl portion of the URL before the path where to send the request
* @param templatedPath the path portion of the URL after the baseUrl, starting with a
* forward slash, and optionally containing path variable placeholders
* @param pathVariables optional variables to substitute into the templatedPath
*/
abstract void sendHttpRequest(HttpMethod method, @Nullable byte[] body, URI baseUrl, String templatedPath,
String... pathVariables);

/**
* Convenience method provided to substitute the template placeholders for the
* provided path variables. The number of pathVariables argument SHOULD match the
* number of placeholders in the templatedPath. Substitutions will be made in order.
* @param templatedPath a URL path optionally containing placeholders in curly
* brackets
* @param pathVariables path variable values for which placeholders should be
* substituted
* @return path string with substitutions, if any, performed
*/
protected String substitutePathVariables(String templatedPath, String... pathVariables) {
if (pathVariables.length == 0) {
return templatedPath;
}
String substituted = templatedPath;
for (String substitution : pathVariables) {
substituted = substituted.replaceFirst("\\{.*?}", substitution);
}
return substituted;
}

@Test
void getTemplatedPathForUri(WireMockRuntimeInfo wmRuntimeInfo) {
stubFor(get(anyUrl()).willReturn(ok()));

String templatedPath = "/customers/{customerId}/carts/{cartId}";
sendHttpRequest(HttpMethod.GET, null, URI.create(wmRuntimeInfo.getHttpBaseUrl()), templatedPath, "112", "5");

Timer timer = getRegistry().get(timerName()).tags("method", "GET", "status", "200", "uri", templatedPath)
.timer();
assertThat(timer.count()).isEqualTo(1);
assertThat(timer.totalTime(TimeUnit.NANOSECONDS)).isPositive();
}

@Test
@Disabled("apache/jetty http client instrumentation currently fails this test")
void timedWhenServerIsMissing() throws IOException {
int unusedPort = 0;
try (ServerSocket server = new ServerSocket(0)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNENCRYPTED_SERVER_SOCKET: Unencrypted server socket (instead of SSLServerSocket)

Reply with "@sonatype-lift help" for info about LiftBot commands.
Reply with "@sonatype-lift ignore" to tell LiftBot to leave out the above finding from this PR.
Reply with "@sonatype-lift ignoreall" to tell LiftBot to leave out all the findings from this PR and from the status bar in Github.

When talking to LiftBot, you need to refresh the page to see its response. Click here to get to know more about LiftBot commands.


Was this a good recommendation?
[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]

unusedPort = server.getLocalPort();
}

try {
sendHttpRequest(HttpMethod.GET, null, URI.create("http://localhost:" + unusedPort), "/anything");
}
catch (Throwable ignore) {
}

Timer timer = getRegistry().get(timerName()).tags("method", "GET").timer();

assertThat(timer.count()).isEqualTo(1);
assertThat(timer.totalTime(TimeUnit.NANOSECONDS)).isPositive();
}

@Test
void serverException(WireMockRuntimeInfo wmRuntimeInfo) {
stubFor(get(anyUrl()).willReturn(serverError()));

sendHttpRequest(HttpMethod.GET, null, URI.create(wmRuntimeInfo.getHttpBaseUrl()), "/socks");

Timer timer = getRegistry().get(timerName()).tags("method", "GET", "status", "500").timer();
assertThat(timer.count()).isEqualTo(1);
assertThat(timer.totalTime(TimeUnit.NANOSECONDS)).isPositive();
}

@Test
void clientException(WireMockRuntimeInfo wmRuntimeInfo) {
stubFor(post(anyUrl()).willReturn(badRequest()));

sendHttpRequest(HttpMethod.POST, new byte[0], URI.create(wmRuntimeInfo.getHttpBaseUrl()), "/socks");

Timer timer = getRegistry().get(timerName()).tags("method", "POST", "status", "400").timer();
assertThat(timer.count()).isEqualTo(1);
assertThat(timer.totalTime(TimeUnit.NANOSECONDS)).isPositive();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.core.instrument.simple.SimpleMeterRegistry;

abstract class InstrumentationVerificationTests {

private final MeterRegistry registry = new SimpleMeterRegistry();

MeterRegistry getRegistry() {
return registry;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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.core.instrument.binder.httpcomponents.DefaultUriMapper;
import io.micrometer.core.instrument.binder.httpcomponents.MicrometerHttpRequestExecutor;
import io.micrometer.core.lang.Nullable;
import org.apache.http.client.HttpClient;
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.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;

class ApacheHttpClientTimingInstrumentationVerificationTests extends HttpClientTimingInstrumentationVerificationTests {

private final HttpClient httpClient = HttpClientBuilder.create()
.setRequestExecutor(MicrometerHttpRequestExecutor.builder(getRegistry()).build()).build();

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

@Override
void sendHttpRequest(HttpMethod method, @Nullable byte[] body, URI baseUri, String templatedPath,
String... pathVariables) {
try {
EntityUtils.consume(
httpClient.execute(makeRequest(method, body, baseUri, templatedPath, pathVariables)).getEntity());
}
catch (IOException 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;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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.core.instrument.binder.jetty.JettyClientMetrics;
import io.micrometer.core.lang.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.BytesContentProvider;
import org.junit.jupiter.api.BeforeEach;

import java.net.URI;

class JettyClientTimingInstrumentationVerificationTests extends HttpClientTimingInstrumentationVerificationTests {

private final HttpClient httpClient = new HttpClient();

@Override
protected String timerName() {
return "jetty.client.requests";
}

@BeforeEach
void setup() throws Exception {
httpClient.getRequestListeners().add(JettyClientMetrics
.builder(getRegistry(), result -> result.getRequest().getHeaders().get("URI_PATTERN")).build());
httpClient.start();
}

@Override
void sendHttpRequest(HttpMethod method, @Nullable byte[] body, URI baseUri, String templatedPath,
String... pathVariables) {
try {
Request request = httpClient.newRequest(baseUri + substitutePathVariables(templatedPath, pathVariables))
.method(method.name()).header("URI_PATTERN", templatedPath);
if (body != null) {
request.content(new BytesContentProvider(body));
}
request.send();
httpClient.stop();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}

}