diff --git a/build.gradle b/build.gradle index a1ad76f66e..a74e1497a0 100644 --- a/build.gradle +++ b/build.gradle @@ -337,7 +337,7 @@ subprojects { check.dependsOn("testModules") - if (!(project.name in ['micrometer-jakarta9'])) { // add projects here that do not exist in the previous minor so should be excluded from japicmp + if (!(project.name in ['micrometer-jakarta9', 'micrometer-java11'])) { // add projects here that do not exist in the previous minor so should be excluded from japicmp apply plugin: 'me.champeau.gradle.japicmp' apply plugin: 'de.undercouch.download' diff --git a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/DefaultHttpClientObservationConvention.java b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/DefaultHttpClientObservationConvention.java index 91e4b9871c..44622f0564 100644 --- a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/DefaultHttpClientObservationConvention.java +++ b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/DefaultHttpClientObservationConvention.java @@ -29,7 +29,9 @@ * * @author Marcin Grzejszczak * @since 1.10.0 + * @deprecated since 1.13.0 use the same class in the micrometer-java11 module instead */ +@Deprecated public class DefaultHttpClientObservationConvention implements HttpClientObservationConvention { /** diff --git a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientContext.java b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientContext.java index 3ddf09dc6b..284fc119b0 100644 --- a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientContext.java +++ b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientContext.java @@ -28,7 +28,9 @@ * * @author Marcin Grzejszczak * @since 1.10.0 + * @deprecated since 1.13.0 use the same class in the micrometer-java11 module instead */ +@Deprecated public class HttpClientContext extends RequestReplySenderContext> { private final Function uriMapper; diff --git a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientObservationConvention.java b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientObservationConvention.java index c25ae6b6ea..466da1af75 100644 --- a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientObservationConvention.java +++ b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientObservationConvention.java @@ -25,7 +25,9 @@ * * @author Marcin Grzejszczak * @since 1.10.0 + * @deprecated since 1.13.0 use the same class in the micrometer-java11 module instead */ +@Deprecated public interface HttpClientObservationConvention extends ObservationConvention { @Override diff --git a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientObservationDocumentation.java b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientObservationDocumentation.java index e5c313c153..f306d754e8 100644 --- a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientObservationDocumentation.java +++ b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientObservationDocumentation.java @@ -20,6 +20,7 @@ import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.docs.ObservationDocumentation; +@SuppressWarnings("deprecation") enum HttpClientObservationDocumentation implements ObservationDocumentation { /** diff --git a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClient.java b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClient.java index 37c7a7a1cb..178ea68cb7 100644 --- a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClient.java +++ b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClient.java @@ -50,7 +50,9 @@ * * @author Marcin Grzejszczak * @since 1.10.0 + * @deprecated since 1.13.0 use the same class in the micrometer-java11 module instead */ +@Deprecated public class MicrometerHttpClient extends HttpClient { /** diff --git a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/package-info.java b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/package-info.java index 3120b9f8bf..d0e7dab6a7 100644 --- a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/package-info.java +++ b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/package-info.java @@ -15,8 +15,10 @@ */ /** - * Instrumentation of JDK classes. + * Instrumentation of JDK classes. Deprecated since 1.13.0 use the micrometer-java11 + * module instead. */ +// Note we can't use the @deprecated JavaDoc tag due to compiler bug JDK-8160601 @NonNullApi @NonNullFields package io.micrometer.core.instrument.binder.jdk; diff --git a/micrometer-core/src/test/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClientTests.java b/micrometer-core/src/test/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClientTests.java index d3a69a978b..5a61d08ec3 100644 --- a/micrometer-core/src/test/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClientTests.java +++ b/micrometer-core/src/test/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClientTests.java @@ -37,6 +37,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.*; import static org.assertj.core.api.BDDAssertions.then; +@SuppressWarnings("deprecation") @WireMockTest class MicrometerHttpClientTests { diff --git a/micrometer-java11/build.gradle b/micrometer-java11/build.gradle new file mode 100644 index 0000000000..b947fb18eb --- /dev/null +++ b/micrometer-java11/build.gradle @@ -0,0 +1,24 @@ +description 'Micrometer core classes that require Java 11' + +dependencies { + api project(":micrometer-core") + + testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation 'org.assertj:assertj-core' +} + +java { + targetCompatibility = 11 +} + +tasks.withType(JavaCompile).configureEach { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + options.release = 11 +} + +dependencies { + testImplementation 'ru.lanwen.wiremock:wiremock-junit5' + testImplementation 'com.github.tomakehurst:wiremock-jre8-standalone' + testImplementation project(":micrometer-observation-test") +} diff --git a/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/DefaultHttpClientObservationConvention.java b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/DefaultHttpClientObservationConvention.java new file mode 100644 index 0000000000..93e4963782 --- /dev/null +++ b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/DefaultHttpClientObservationConvention.java @@ -0,0 +1,84 @@ +/* + * Copyright 2024 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.java11.instrument.binder.jdk; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.lang.NonNull; +import io.micrometer.common.lang.Nullable; +import io.micrometer.core.instrument.binder.http.Outcome; + +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.function.Function; + +/** + * Default implementation of {@link HttpClientObservationConvention}. + * + * @author Marcin Grzejszczak + * @since 1.13.0 + */ +public class DefaultHttpClientObservationConvention implements HttpClientObservationConvention { + + /** + * Instance of this {@link DefaultHttpClientObservationConvention}. + */ + public static DefaultHttpClientObservationConvention INSTANCE = new DefaultHttpClientObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(HttpClientContext context) { + if (context.getCarrier() == null) { + return KeyValues.empty(); + } + HttpRequest httpRequest = context.getCarrier().build(); + KeyValues keyValues = KeyValues.of( + HttpClientObservationDocumentation.LowCardinalityKeys.METHOD.withValue(httpRequest.method()), + HttpClientObservationDocumentation.LowCardinalityKeys.URI + .withValue(getUriTag(httpRequest, context.getResponse(), context.getUriMapper()))); + if (context.getResponse() != null) { + keyValues = keyValues + .and(HttpClientObservationDocumentation.LowCardinalityKeys.STATUS + .withValue(String.valueOf(context.getResponse().statusCode()))) + .and(HttpClientObservationDocumentation.LowCardinalityKeys.OUTCOME + .withValue(Outcome.forStatus(context.getResponse().statusCode()).name())); + } + return keyValues; + } + + String getUriTag(@Nullable HttpRequest request, @Nullable HttpResponse httpResponse, + Function uriMapper) { + if (request == null) { + return null; + } + return httpResponse != null && (httpResponse.statusCode() == 404 || httpResponse.statusCode() == 301) + ? "NOT_FOUND" : uriMapper.apply(request); + } + + @Override + @NonNull + public String getName() { + return "http.client.requests"; + } + + @Nullable + @Override + public String getContextualName(HttpClientContext context) { + if (context.getCarrier() == null) { + return null; + } + return "HTTP " + context.getCarrier().build().method(); + } + +} diff --git a/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/HttpClientContext.java b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/HttpClientContext.java new file mode 100644 index 0000000000..40ed4471ee --- /dev/null +++ b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/HttpClientContext.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 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.java11.instrument.binder.jdk; + +import io.micrometer.observation.transport.RequestReplySenderContext; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Objects; +import java.util.function.Function; + +/** + * A {@link RequestReplySenderContext} for an {@link HttpClient}. + * + * @author Marcin Grzejszczak + * @since 1.13.0 + */ +public class HttpClientContext extends RequestReplySenderContext> { + + private final Function uriMapper; + + public HttpClientContext(Function uriMapper) { + super((carrier, key, value) -> Objects.requireNonNull(carrier).header(key, value)); + this.uriMapper = uriMapper; + } + + public Function getUriMapper() { + return uriMapper; + } + +} diff --git a/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/HttpClientObservationConvention.java b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/HttpClientObservationConvention.java new file mode 100644 index 0000000000..d3c4d87fe6 --- /dev/null +++ b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/HttpClientObservationConvention.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024 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.java11.instrument.binder.jdk; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +import java.net.http.HttpClient; + +/** + * An {@link ObservationConvention} for an {@link HttpClient}. + * + * @author Marcin Grzejszczak + * @since 1.13.0 + */ +public interface HttpClientObservationConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof HttpClientContext; + } + +} diff --git a/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/HttpClientObservationDocumentation.java b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/HttpClientObservationDocumentation.java new file mode 100644 index 0000000000..77ebef4be6 --- /dev/null +++ b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/HttpClientObservationDocumentation.java @@ -0,0 +1,86 @@ +/* + * Copyright 2024 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.java11.instrument.binder.jdk; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +enum HttpClientObservationDocumentation implements ObservationDocumentation { + + /** + * Observation when an HTTP call is being made. + */ + HTTP_CALL { + @Override + public Class> getDefaultConvention() { + return DefaultHttpClientObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return LowCardinalityKeys.values(); + } + + }; + + enum LowCardinalityKeys implements KeyName { + + /** + * HTTP Method. + */ + METHOD { + @Override + public String asString() { + return "method"; + } + }, + + /** + * HTTP Status. + */ + STATUS { + @Override + public String asString() { + return "status"; + } + }, + + /** + * Key name for outcome. + * @since 1.11.0 + */ + OUTCOME { + @Override + public String asString() { + return "outcome"; + } + }, + + /** + * HTTP URI. + */ + URI { + @Override + public String asString() { + return "uri"; + } + } + + } + +} diff --git a/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/MicrometerHttpClient.java b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/MicrometerHttpClient.java new file mode 100644 index 0000000000..00f38721c9 --- /dev/null +++ b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/MicrometerHttpClient.java @@ -0,0 +1,306 @@ +/* + * Copyright 2024 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.java11.instrument.binder.jdk; + +import io.micrometer.common.lang.Nullable; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.binder.http.Outcome; +import io.micrometer.core.instrument.observation.ObservationOrTimerCompatibleInstrumentation; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.io.IOException; +import java.net.Authenticator; +import java.net.CookieHandler; +import java.net.ProxySelector; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Function; + +/** + * Delegates to an {@link HttpClient} while instrumenting with Micrometer any HTTP calls + * made. Example setup:
{@code
+ * HttpClient observedClient = MicrometerHttpClient.instrumentationBuilder(HttpClient.newHttpClient(), meterRegistry).build();
+ * }
+ * + * Inspired by interceptable-http-client. + * + * @author Marcin Grzejszczak + * @since 1.13.0 + */ +public class MicrometerHttpClient extends HttpClient { + + /** + * Header name for URI pattern. + */ + public static final String URI_PATTERN_HEADER = "URI_PATTERN"; + + private final MeterRegistry meterRegistry; + + private final HttpClient client; + + @Nullable + private final ObservationRegistry observationRegistry; + + @Nullable + private final HttpClientObservationConvention customObservationConvention; + + private final Function uriMapper; + + private MicrometerHttpClient(MeterRegistry meterRegistry, HttpClient client, + @Nullable ObservationRegistry observationRegistry, + @Nullable HttpClientObservationConvention customObservationConvention, + Function uriMapper) { + this.meterRegistry = meterRegistry; + this.client = client; + this.observationRegistry = observationRegistry; + this.customObservationConvention = customObservationConvention; + this.uriMapper = uriMapper; + } + + /** + * Builder for instrumentation of {@link HttpClient}. + * @param httpClient HttpClient to wrap + * @param meterRegistry meter registry + * @return builder + */ + public static InstrumentationBuilder instrumentationBuilder(HttpClient httpClient, MeterRegistry meterRegistry) { + return new InstrumentationBuilder(httpClient, meterRegistry); + } + + /** + * Builder for {@link MicrometerHttpClient}. + */ + public static class InstrumentationBuilder { + + private final HttpClient client; + + private final MeterRegistry meterRegistry; + + @Nullable + private ObservationRegistry observationRegistry; + + @Nullable + private HttpClientObservationConvention customObservationConvention; + + private Function uriMapper = request -> request.headers() + .firstValue(URI_PATTERN_HEADER) + .orElse("UNKNOWN"); + + /** + * Creates a new instance of {@link InstrumentationBuilder}. + * @param client client to wrap + * @param meterRegistry a {@link MeterRegistry} + */ + public InstrumentationBuilder(HttpClient client, MeterRegistry meterRegistry) { + this.client = client; + this.meterRegistry = meterRegistry; + } + + /** + * Set {@link ObservationRegistry} if you want to use {@link Observation}. + * @param observationRegistry observation registry + * @return this + */ + public InstrumentationBuilder observationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + return this; + } + + /** + * When used with {@link ObservationRegistry}, it will override the default + * {@link HttpClientObservationConvention}. + * @param customObservationConvention custom observation convention + * @return this + */ + public InstrumentationBuilder customObservationConvention( + HttpClientObservationConvention customObservationConvention) { + this.customObservationConvention = customObservationConvention; + return this; + } + + /** + * Provides custom URI mapper mechanism. + * @param uriMapper URI mapper + * @return this + */ + public InstrumentationBuilder uriMapper(Function uriMapper) { + this.uriMapper = uriMapper; + return this; + } + + /** + * Builds a wrapped {@link HttpClient}. + * @return a wrapped {@link HttpClient} + */ + public HttpClient build() { + return new MicrometerHttpClient(this.meterRegistry, this.client, this.observationRegistry, + this.customObservationConvention, this.uriMapper); + } + + } + + @Override + public Optional cookieHandler() { + return client.cookieHandler(); + } + + @Override + public Optional connectTimeout() { + return client.connectTimeout(); + } + + @Override + public Redirect followRedirects() { + return client.followRedirects(); + } + + @Override + public Optional proxy() { + return client.proxy(); + } + + @Override + public SSLContext sslContext() { + return client.sslContext(); + } + + @Override + public SSLParameters sslParameters() { + return client.sslParameters(); + } + + @Override + public Optional authenticator() { + return client.authenticator(); + } + + @Override + public Version version() { + return client.version(); + } + + @Override + public Optional executor() { + return client.executor(); + } + + @Override + public HttpResponse send(HttpRequest httpRequest, HttpResponse.BodyHandler bodyHandler) + throws IOException, InterruptedException { + HttpRequest.Builder httpRequestBuilder = decorate(httpRequest); + ObservationOrTimerCompatibleInstrumentation instrumentation = observationOrTimer( + httpRequestBuilder); + HttpRequest request = httpRequestBuilder.build(); + HttpResponse response = null; + try { + response = client.send(request, bodyHandler); + instrumentation.setResponse(response); + return response; + } + catch (IOException ex) { + instrumentation.setThrowable(ex); + throw ex; + } + finally { + stopObservationOrTimer(instrumentation, request, response); + } + } + + private void stopObservationOrTimer( + ObservationOrTimerCompatibleInstrumentation instrumentation, HttpRequest request, + @Nullable HttpResponse res) { + instrumentation.stop(DefaultHttpClientObservationConvention.INSTANCE.getName(), "Timer for JDK's HttpClient", + () -> { + Tags tags = Tags.of(HttpClientObservationDocumentation.LowCardinalityKeys.METHOD.asString(), + request.method(), HttpClientObservationDocumentation.LowCardinalityKeys.URI.asString(), + DefaultHttpClientObservationConvention.INSTANCE.getUriTag(request, res, uriMapper)); + if (res != null) { + tags = tags + .and(Tag.of(HttpClientObservationDocumentation.LowCardinalityKeys.STATUS.asString(), + String.valueOf(res.statusCode()))) + .and(Tag.of(HttpClientObservationDocumentation.LowCardinalityKeys.OUTCOME.asString(), + Outcome.forStatus(res.statusCode()).name())); + } + return tags; + }); + } + + private ObservationOrTimerCompatibleInstrumentation observationOrTimer( + HttpRequest.Builder httpRequestBuilder) { + return ObservationOrTimerCompatibleInstrumentation.start(this.meterRegistry, this.observationRegistry, () -> { + HttpClientContext context = new HttpClientContext(this.uriMapper); + context.setCarrier(httpRequestBuilder); + return context; + }, this.customObservationConvention, DefaultHttpClientObservationConvention.INSTANCE); + } + + @Override + public CompletableFuture> sendAsync(HttpRequest httpRequest, + HttpResponse.BodyHandler bodyHandler) { + return sendAsync(httpRequest, bodyHandler, null); + } + + @Override + public CompletableFuture> sendAsync(HttpRequest httpRequest, + HttpResponse.BodyHandler bodyHandler, @Nullable HttpResponse.PushPromiseHandler pushPromiseHandler) { + HttpRequest.Builder httpRequestBuilder = decorate(httpRequest); + ObservationOrTimerCompatibleInstrumentation instrumentation = observationOrTimer( + httpRequestBuilder); + HttpRequest request = httpRequestBuilder.build(); + return client.sendAsync(request, bodyHandler, pushPromiseHandler).handle((response, throwable) -> { + if (throwable != null) { + instrumentation.setThrowable(throwable); + } + instrumentation.setResponse(response); + stopObservationOrTimer(instrumentation, request, response); + return response; + }); + } + + private HttpRequest.Builder decorate(HttpRequest httpRequest) { + HttpRequest.Builder builder = HttpRequest.newBuilder(httpRequest.uri()); + builder.expectContinue(httpRequest.expectContinue()); + httpRequest.headers().map().forEach((key, values) -> values.forEach(value -> builder.header(key, value))); + httpRequest.bodyPublisher() + .ifPresentOrElse(publisher -> builder.method(httpRequest.method(), publisher), () -> { + switch (httpRequest.method()) { + case "GET": + builder.GET(); + break; + case "DELETE": + builder.DELETE(); + break; + default: + throw new IllegalStateException(httpRequest.method()); + } + }); + httpRequest.timeout().ifPresent(builder::timeout); + httpRequest.version().ifPresent(builder::version); + return builder; + } + +} diff --git a/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/package-info.java b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/package-info.java new file mode 100644 index 0000000000..f50aa57223 --- /dev/null +++ b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2024 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. + */ + +/** + * Instrumentation of JDK classes. + */ +@NonNullApi +@NonNullFields +package io.micrometer.java11.instrument.binder.jdk; + +import io.micrometer.common.lang.NonNullApi; +import io.micrometer.common.lang.NonNullFields; diff --git a/micrometer-java11/src/test/java/io/micrometer/java11/instrument/binder/jdk/MicrometerHttpClientTests.java b/micrometer-java11/src/test/java/io/micrometer/java11/instrument/binder/jdk/MicrometerHttpClientTests.java new file mode 100644 index 0000000000..df509ea4cc --- /dev/null +++ b/micrometer-java11/src/test/java/io/micrometer/java11/instrument/binder/jdk/MicrometerHttpClientTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2024 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.java11.instrument.binder.jdk; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.BDDAssertions.then; + +@WireMockTest +class MicrometerHttpClientTests { + + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + + HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build(); + + @BeforeEach + void setup() { + stubFor(any(urlEqualTo("/metrics")).willReturn(ok().withBody("body"))); + } + + @Test + void shouldInstrumentHttpClientWithObservation(WireMockRuntimeInfo wmInfo) + throws IOException, InterruptedException { + ObservationRegistry observationRegistry = TestObservationRegistry.create(); + observationRegistry.observationConfig() + .observationHandler(new ObservationHandler.AllMatchingCompositeObservationHandler(headerSettingHandler(), + new DefaultMeterObservationHandler(meterRegistry))); + + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(wmInfo.getHttpBaseUrl() + "/metrics")) + .build(); + + HttpClient observedClient = MicrometerHttpClient.instrumentationBuilder(httpClient, meterRegistry) + .observationRegistry(observationRegistry) + .build(); + observedClient.send(request, HttpResponse.BodyHandlers.ofString()); + + verify(anyRequestedFor(urlEqualTo("/metrics")).withHeader("foo", equalTo("bar"))); + thenMeterRegistryContainsHttpClientTags(); + } + + @Test + void shouldInstrumentHttpClientWithTimer(WireMockRuntimeInfo wmInfo) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(wmInfo.getHttpBaseUrl() + "/metrics")) + .build(); + + HttpClient observedClient = MicrometerHttpClient.instrumentationBuilder(httpClient, meterRegistry).build(); + observedClient.send(request, HttpResponse.BodyHandlers.ofString()); + + thenMeterRegistryContainsHttpClientTags(); + } + + private void thenMeterRegistryContainsHttpClientTags() { + then(meterRegistry.find("http.client.requests") + .tag("method", "GET") + .tag("status", "200") + .tag("outcome", "SUCCESS") + .tag("uri", "UNKNOWN") + .timer()).isNotNull(); + } + + private ObservationHandler headerSettingHandler() { + return new ObservationHandler<>() { + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof HttpClientContext; + } + + @Override + public void onStart(HttpClientContext context) { + HttpRequest.Builder carrier = context.getCarrier(); + context.getSetter().set(carrier, "foo", "bar"); + } + }; + } + +} diff --git a/micrometer-test/build.gradle b/micrometer-test/build.gradle index 8741265d61..d67fa7d4e3 100644 --- a/micrometer-test/build.gradle +++ b/micrometer-test/build.gradle @@ -1,19 +1,32 @@ -plugins { - id 'me.champeau.mrjar' version "0.1.1" -} - description 'Test compatibility kit for extensions of Micrometer' -multiRelease { - targetVersions 8, 11 +testing { + suites { + java11Test(JvmTestSuite) { + test { + useJUnitJupiter() + } + sources { + java { + srcDirs = ['src/test/java11'] + } + } + dependencies { + implementation project() + implementation project(':micrometer-java11') + } + + } + } } -// Otherwise java11 tests will not see java11 code -sourceSets { - java11Test { - compileClasspath += sourceSets.java11.output - runtimeClasspath += sourceSets.java11.output - } +compileJava11TestJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +check { + dependsOn(testing.suites.java11Test) } dependencies { @@ -21,8 +34,6 @@ dependencies { api project(':micrometer-observation') api project(':micrometer-observation-test') - java11TestImplementation project(":micrometer-core").sourceSets.java11.output - api 'org.assertj:assertj-core' api 'org.junit.jupiter:junit-jupiter' @@ -30,7 +41,6 @@ dependencies { api 'ru.lanwen.wiremock:wiremock-junit5' api 'com.github.tomakehurst:wiremock-jre8-standalone' - java11TestImplementation 'com.github.tomakehurst:wiremock-jre8-standalone' api 'org.mockito:mockito-core' implementation 'org.awaitility:awaitility' @@ -62,26 +72,3 @@ dependencies { // necessary for Jersey test framework testRuntimeOnly 'org.glassfish.jersey.inject:jersey-hk2' } - -java11Test { - // set heap size for the test JVM(s) - maxHeapSize = "1500m" - - useJUnitPlatform { - excludeTags 'docker' - } - - include { - it.getFile().getAbsolutePath().contains("java11Test") - } - - retry { - maxFailures = 5 - maxRetries = 3 - } -} - -compileJava11TestJava { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} diff --git a/micrometer-test/src/test/java11/io/micrometer/core/instrument/binder/jdk/JdkHttpClientTimingInstrumentationVerificationTests.java b/micrometer-test/src/test/java11/io/micrometer/java11/instrument/binder/jdk/JdkHttpClientTimingInstrumentationVerificationTests.java similarity index 98% rename from micrometer-test/src/test/java11/io/micrometer/core/instrument/binder/jdk/JdkHttpClientTimingInstrumentationVerificationTests.java rename to micrometer-test/src/test/java11/io/micrometer/java11/instrument/binder/jdk/JdkHttpClientTimingInstrumentationVerificationTests.java index b57c5c6723..a38624e4dc 100644 --- a/micrometer-test/src/test/java11/io/micrometer/core/instrument/binder/jdk/JdkHttpClientTimingInstrumentationVerificationTests.java +++ b/micrometer-test/src/test/java11/io/micrometer/java11/instrument/binder/jdk/JdkHttpClientTimingInstrumentationVerificationTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micrometer.core.instrument.binder.jdk; +package io.micrometer.java11.instrument.binder.jdk; import io.micrometer.common.lang.Nullable; import io.micrometer.core.instrument.HttpClientTimingInstrumentationVerificationTests; diff --git a/settings.gradle b/settings.gradle index fb3afa156a..63bbe5ef99 100644 --- a/settings.gradle +++ b/settings.gradle @@ -44,6 +44,7 @@ include 'micrometer-test', 'micrometer-observation-test' include 'micrometer-bom' include 'micrometer-jakarta9' +include 'micrometer-java11' include 'micrometer-jetty11' include 'micrometer-osgi-test' include 'docs'