diff --git a/docs/src/main/asciidoc/rest-client-reactive.adoc b/docs/src/main/asciidoc/rest-client-reactive.adoc index 9dbf627dd273e..c13a2c8a97fb0 100644 --- a/docs/src/main/asciidoc/rest-client-reactive.adoc +++ b/docs/src/main/asciidoc/rest-client-reactive.adoc @@ -1364,6 +1364,8 @@ quarkus.rest-client.logging.body-limit=50 quarkus.log.category."org.jboss.resteasy.reactive.client.logging".level=DEBUG ---- +TIP: REST Client Reactive uses a default `ClientLogger` implementation. You can change it by providing a custom `ClientLogger` instance through CDI or when programmatically creating your client. + == Mocking the client for tests If you use a client injected with the `@RestClient` annotation, you can easily mock it for tests. You can do it with Mockito's `@InjectMock` or with `QuarkusMock`. diff --git a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java index 26cd9782d31c4..510b8278e01ea 100644 --- a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java +++ b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java @@ -11,6 +11,7 @@ import jakarta.ws.rs.core.MediaType; import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.resteasy.reactive.client.api.ClientLogger; import org.jboss.resteasy.reactive.client.impl.ClientBuilderImpl; import org.jboss.resteasy.reactive.client.impl.WebTargetImpl; import org.jboss.resteasy.reactive.server.jackson.JacksonBasicMessageBodyReader; @@ -74,7 +75,10 @@ private ClientBuilderImpl registerJacksonProviders(ClientBuilderImpl clientBuild .registerMessageBodyWriter(new ClientJacksonMessageBodyWriter(newObjectMapper), Object.class, HANDLED_MEDIA_TYPES, true, PROVIDER_PRIORITY); } - + InstanceHandle clientLogger = arcContainer.instance(ClientLogger.class); + if (clientLogger.isAvailable()) { + clientBuilder.clientLogger(clientLogger.get()); + } } return clientBuilder; } diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java index e562dd9a1ccbd..16fbd147b69b6 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java @@ -16,6 +16,7 @@ import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory; import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; import org.eclipse.microprofile.rest.client.spi.RestClientBuilderListener; +import org.jboss.resteasy.reactive.client.api.ClientLogger; import io.quarkus.rest.client.reactive.runtime.QuarkusRestClientBuilderImpl; import io.quarkus.rest.client.reactive.runtime.RestClientBuilderImpl; @@ -250,6 +251,14 @@ static QuarkusRestClientBuilder newBuilder() { */ QuarkusRestClientBuilder httpClientOptions(HttpClientOptions httpClientOptions); + /** + * Specifies the client logger to use. + * + * @param clientLogger the client logger to use. + * @return the current builder + */ + QuarkusRestClientBuilder clientLogger(ClientLogger clientLogger); + /** * Based on the configured QuarkusRestClientBuilder, creates a new instance of the given REST interface to invoke API calls * against. diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java index 81062447ce18a..50bb3bfc17309 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java @@ -14,6 +14,7 @@ import org.eclipse.microprofile.rest.client.RestClientDefinitionException; import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory; import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; +import org.jboss.resteasy.reactive.client.api.ClientLogger; import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; import io.quarkus.rest.client.reactive.runtime.context.ClientHeadersFactoryContextResolver; @@ -217,6 +218,12 @@ public QuarkusRestClientBuilder httpClientOptions(HttpClientOptions httpClientOp return this; } + @Override + public QuarkusRestClientBuilder clientLogger(ClientLogger clientLogger) { + proxy.clientLogger(clientLogger); + return this; + } + @Override public T build(Class clazz) throws IllegalStateException, RestClientDefinitionException { return proxy.build(clazz); diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java index 014bfc5f6e294..96ef087a691f0 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java @@ -24,6 +24,7 @@ import org.eclipse.microprofile.rest.client.RestClientDefinitionException; import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; +import org.jboss.resteasy.reactive.client.api.ClientLogger; import org.jboss.resteasy.reactive.client.api.InvalidRestClientDefinitionException; import org.jboss.resteasy.reactive.client.api.LoggingScope; import org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties; @@ -65,6 +66,8 @@ public class RestClientBuilderImpl implements RestClientBuilder { private String proxyPassword; private String nonProxyHosts; + private ClientLogger clientLogger; + @Override public RestClientBuilderImpl baseUrl(URL url) { try { @@ -156,6 +159,11 @@ public RestClientBuilderImpl nonProxyHosts(String nonProxyHosts) { return this; } + public RestClientBuilderImpl clientLogger(ClientLogger clientLogger) { + this.clientLogger = clientLogger; + return this; + } + @Override public RestClientBuilderImpl executorService(ExecutorService executor) { throw new IllegalArgumentException("Specifying executor service is not supported. " + @@ -322,6 +330,14 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi Integer loggingBodySize = logging != null ? logging.bodyLimit : 100; clientBuilder.loggingScope(loggingScope); clientBuilder.loggingBodySize(loggingBodySize); + if (clientLogger != null) { + clientBuilder.clientLogger(clientLogger); + } else { + InstanceHandle clientLoggerInstance = Arc.container().instance(ClientLogger.class); + if (clientLoggerInstance.isAvailable()) { + clientBuilder.clientLogger(clientLoggerInstance.get()); + } + } clientBuilder.multiQueryParamMode(toMultiQueryParamMode(queryParamStyle)); diff --git a/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java b/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java index 3002647b66fc6..104871aa1ef07 100644 --- a/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java +++ b/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java @@ -38,6 +38,9 @@ public class ClientCallingResource { private static final String[] RESPONSES = { "cortland", "lobo", "golden delicious" }; private final AtomicInteger count = new AtomicInteger(0); + @RestClient + ClientWithClientLogger clientWithClientLogger; + @RestClient ClientWithExceptionMapper clientWithExceptionMapper; @@ -59,13 +62,57 @@ public class ClientCallingResource { @Inject InMemorySpanExporter inMemorySpanExporter; + @Inject + MyClientLogger globalClientLogger; + void init(@Observes Router router) { router.post().handler(BodyHandler.create()); router.get("/unprocessable").handler(rc -> rc.response().setStatusCode(422).end("the entity was unprocessable")); + router.get("/client-logger").handler(rc -> { + rc.response().end("Hello World!"); + }); + + router.post("/call-client-with-global-client-logger").blockingHandler(rc -> { + String url = rc.body().asString(); + ClientWithClientLogger client = QuarkusRestClientBuilder.newBuilder().baseUri(URI.create(url)) + .build(ClientWithClientLogger.class); + globalClientLogger.reset(); + client.call(); + if (globalClientLogger.wasUsed()) { + success(rc, "global client logger was used"); + } else { + fail(rc, "global client logger was not used"); + } + }); + + router.post("/call-client-with-explicit-client-logger").blockingHandler(rc -> { + String url = rc.body().asString(); + MyClientLogger explicitClientLogger = new MyClientLogger(); + ClientWithClientLogger client = QuarkusRestClientBuilder.newBuilder().baseUri(URI.create(url)) + .clientLogger(explicitClientLogger) + .build(ClientWithClientLogger.class); + client.call(); + if (explicitClientLogger.wasUsed()) { + success(rc, "explicit client logger was used"); + } else { + fail(rc, "explicit client logger was not used"); + } + }); + + router.post("/call-cdi-client-with-global-client-logger").blockingHandler(rc -> { + globalClientLogger.reset(); + clientWithClientLogger.call(); + if (globalClientLogger.wasUsed()) { + success(rc, "global client logger was used"); + } else { + fail(rc, "global client logger was not used"); + } + }); + router.post("/call-client-with-exception-mapper").blockingHandler(rc -> { - String url = rc.getBody().toString(); + String url = rc.body().asString(); ClientWithExceptionMapper client = QuarkusRestClientBuilder.newBuilder().baseUri(URI.create(url)) .register(MyResponseExceptionMapper.class) .build(ClientWithExceptionMapper.class); @@ -81,7 +128,7 @@ void init(@Observes Router router) { }); router.route("/call-client").blockingHandler(rc -> { - String url = rc.getBody().toString(); + String url = rc.body().asString(); AppleClient client = QuarkusRestClientBuilder.newBuilder().baseUri(URI.create(url)) .build(AppleClient.class); Uni apple1 = Uni.createFrom().item(client.swapApple(new Apple("lobo"))); @@ -110,7 +157,7 @@ void init(@Observes Router router) { }); router.route("/call-client-retry").blockingHandler(rc -> { - String url = rc.getBody().toString(); + String url = rc.body().asString(); AppleClient client = QuarkusRestClientBuilder.newBuilder().baseUri(URI.create(url + "/does-not-exist")) .build(AppleClient.class); AtomicInteger count = new AtomicInteger(0); @@ -120,13 +167,13 @@ void init(@Observes Router router) { }); router.post("/hello").handler(rc -> rc.response().putHeader("content-type", MediaType.TEXT_PLAIN) - .end("Hello, " + (rc.getBodyAsString()).repeat(getCount(rc)))); + .end("Hello, " + (rc.body().asString()).repeat(getCount(rc)))); router.post("/hello/fromMessage").handler(rc -> rc.response().putHeader("content-type", MediaType.TEXT_PLAIN) .end(rc.body().asJsonObject().getString("message"))); router.route("/call-hello-client").blockingHandler(rc -> { - String url = rc.getBody().toString(); + String url = rc.body().asString(); HelloClient client = QuarkusRestClientBuilder.newBuilder().baseUri(URI.create(url)) .build(HelloClient.class); String greeting = client.greeting("John", 2); @@ -134,7 +181,7 @@ void init(@Observes Router router) { }); router.route("/call-hello-client-trace").blockingHandler(rc -> { - String url = rc.getBody().toString(); + String url = rc.body().asString(); HelloClient client = QuarkusRestClientBuilder.newBuilder().baseUri(URI.create(url)) .build(HelloClient.class); String greeting = client.greeting("Mary", 3); @@ -142,7 +189,7 @@ void init(@Observes Router router) { }); router.route("/call-helloFromMessage-client").blockingHandler(rc -> { - String url = rc.getBody().toString(); + String url = rc.body().asString(); HelloClient client = QuarkusRestClientBuilder.newBuilder().baseUri(URI.create(url)) .build(HelloClient.class); String greeting = client.fromMessage(new HelloClient.Message("Hello world")); @@ -153,7 +200,7 @@ void init(@Observes Router router) { .end(getParam(rc))); router.route("/call-params-client-with-param-first").blockingHandler(rc -> { - String url = rc.getBody().toString(); + String url = rc.body().asString(); ParamClient client = QuarkusRestClientBuilder.newBuilder().baseUri(URI.create(url)) .build(ParamClient.class); String result = client.getParam(Param.FIRST); @@ -161,7 +208,7 @@ void init(@Observes Router router) { }); router.route("/rest-response").blockingHandler(rc -> { - String url = rc.getBody().toString(); + String url = rc.body().asString(); RestResponseClient client = QuarkusRestClientBuilder.newBuilder().baseUri(URI.create(url)) .property("microprofile.rest.client.disable.default.mapper", true) .build(RestResponseClient.class); diff --git a/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientWithClientLogger.java b/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientWithClientLogger.java new file mode 100644 index 0000000000000..2b4bafe1907f9 --- /dev/null +++ b/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientWithClientLogger.java @@ -0,0 +1,13 @@ +package io.quarkus.it.rest.client.main; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@Path("/client-logger") +@RegisterRestClient(configKey = "w-client-logger") +public interface ClientWithClientLogger { + @GET + String call(); +} diff --git a/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/MyClientLogger.java b/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/MyClientLogger.java new file mode 100644 index 0000000000000..2e8c4cd8dc635 --- /dev/null +++ b/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/MyClientLogger.java @@ -0,0 +1,38 @@ +package io.quarkus.it.rest.client.main; + +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.resteasy.reactive.client.api.ClientLogger; + +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; + +@ApplicationScoped +public class MyClientLogger implements ClientLogger { + public final AtomicBoolean used = new AtomicBoolean(false); + + @Override + public void setBodySize(int bodySize) { + } + + @Override + public void logResponse(HttpClientResponse response, boolean redirect) { + used.set(true); + } + + @Override + public void logRequest(HttpClientRequest request, Buffer body, boolean omitBody) { + used.set(true); + } + + public void reset() { + used.set(false); + } + + public boolean wasUsed() { + return used.get(); + } +} diff --git a/integration-tests/rest-client-reactive/src/main/resources/application.properties b/integration-tests/rest-client-reactive/src/main/resources/application.properties index 7258d0ec3f4f9..5a438ed680a25 100644 --- a/integration-tests/rest-client-reactive/src/main/resources/application.properties +++ b/integration-tests/rest-client-reactive/src/main/resources/application.properties @@ -1,7 +1,10 @@ +w-client-logger/mp-rest/url=${test.url} w-exception-mapper/mp-rest/url=${test.url} w-fault-tolerance/mp-rest/url=${test.url} io.quarkus.it.rest.client.main.ParamClient/mp-rest/url=${test.url} io.quarkus.it.rest.client.multipart.MultipartClient/mp-rest/url=${test.url} +# global client logging scope +quarkus.rest-client.logging.scope=request-response # Self-Signed client quarkus.rest-client.self-signed.trust-store=${self-signed.trust-store} quarkus.rest-client.self-signed.trust-store-password=${self-signed.trust-store-password} diff --git a/integration-tests/rest-client-reactive/src/test/java/io/quarkus/it/rest/client/BasicTest.java b/integration-tests/rest-client-reactive/src/test/java/io/quarkus/it/rest/client/BasicTest.java index 6a9b1c6cd4037..28d67c962a4e1 100644 --- a/integration-tests/rest-client-reactive/src/test/java/io/quarkus/it/rest/client/BasicTest.java +++ b/integration-tests/rest-client-reactive/src/test/java/io/quarkus/it/rest/client/BasicTest.java @@ -77,6 +77,27 @@ void shouldRetryOnFailure() { .body(equalTo("4")); } + @Test + void shouldLogWithExplicitLogger() { + RestAssured.with().body(baseUrl).post("/call-client-with-explicit-client-logger") + .then() + .statusCode(200); + } + + @Test + void shouldLogWithGlobalLogger() { + RestAssured.with().body(baseUrl).post("/call-client-with-global-client-logger") + .then() + .statusCode(200); + } + + @Test + void shouldLogCdiWithGlobalLogger() { + RestAssured.with().body(baseUrl).post("/call-cdi-client-with-global-client-logger") + .then() + .statusCode(200); + } + @Test void shouldMapException() { RestAssured.with().body(baseUrl).post("/call-client-with-exception-mapper")