diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 82cdfc44ea..0acf6c46c5 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -255,6 +255,15 @@ jobs:
-P enable-integration-tests \
--batch-mode \
--no-transfer-progress
+ # The `envVarLoggingTest` profile runs tests that require an environment variable
+ - name: Showcase integration tests - Logging
+ run: |
+ mvn verify -P '!showcase,enable-integration-tests,envVarLoggingTest' \
+ --batch-mode \
+ --no-transfer-progress
+ # Set the Env Var for this step only
+ env:
+ GOOGLE_SDK_JAVA_LOGGING: true
showcase-native:
runs-on: ubuntu-22.04
diff --git a/gapic-generator-java-pom-parent/pom.xml b/gapic-generator-java-pom-parent/pom.xml
index b593af113b..d5fa5a552d 100644
--- a/gapic-generator-java-pom-parent/pom.xml
+++ b/gapic-generator-java-pom-parent/pom.xml
@@ -38,6 +38,7 @@
3.0.0
1.7.0
5.11.3
+ 2.0.16
diff --git a/gax-java/dependencies.properties b/gax-java/dependencies.properties
index 12224344a4..e1a3acdcc1 100644
--- a/gax-java/dependencies.properties
+++ b/gax-java/dependencies.properties
@@ -76,6 +76,7 @@ maven.com_google_http_client_google_http_client_gson=com.google.http-client:goog
maven.org_codehaus_mojo_animal_sniffer_annotations=org.codehaus.mojo:animal-sniffer-annotations:1.24
maven.javax_annotation_javax_annotation_api=javax.annotation:javax.annotation-api:1.3.2
maven.org_graalvm_sdk=org.graalvm.sdk:graal-sdk:22.3.5
+maven.org_slf4j_slf4j_api=org.slf4j:slf4j-api:2.0.16
# Testing maven artifacts
maven.junit_junit=junit:junit:4.13.2
@@ -88,3 +89,5 @@ maven.net_bytebuddy_byte_buddy=net.bytebuddy:byte-buddy:1.15.10
maven.org_objenesis_objenesis=org.objenesis:objenesis:2.6
maven.org_junit_jupiter_junit_jupiter_api=org.junit.jupiter:junit-jupiter-api:5.11.3
maven.org_junit_jupiter_junit_jupiter_params=org.junit.jupiter:junit-jupiter-params:5.11.3
+maven.ch_qos_logback_logback_classic=ch.qos.logback:logback-classic:1.3.14
+maven.ch_qos_logback_logback_core=ch.qos.logback:logback-core:1.3.14
\ No newline at end of file
diff --git a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcLoggingInterceptor.java b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcLoggingInterceptor.java
new file mode 100644
index 0000000000..2a9953a91b
--- /dev/null
+++ b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcLoggingInterceptor.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.grpc;
+
+import com.google.api.core.InternalApi;
+import com.google.api.gax.logging.LogData;
+import com.google.api.gax.logging.LoggingUtils;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.ClientCall;
+import io.grpc.ClientInterceptor;
+import io.grpc.ForwardingClientCall;
+import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
+import io.grpc.Metadata;
+import io.grpc.MethodDescriptor;
+import io.grpc.Status;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.event.Level;
+
+@InternalApi
+public class GrpcLoggingInterceptor implements ClientInterceptor {
+
+ private static final Logger LOGGER = LoggingUtils.getLogger(GrpcLoggingInterceptor.class);
+ private static final Gson GSON = new Gson();
+
+ ClientCall.Listener> currentListener; // expose for test setup
+
+ @Override
+ public ClientCall interceptCall(
+ MethodDescriptor method, CallOptions callOptions, Channel next) {
+
+ return new ForwardingClientCall.SimpleForwardingClientCall(
+ next.newCall(method, callOptions)) {
+ LogData.Builder logDataBuilder = LogData.builder();
+
+ @Override
+ public void start(Listener responseListener, Metadata headers) {
+ logRequestInfo(method, logDataBuilder, LOGGER);
+ recordRequestHeaders(headers, logDataBuilder);
+ SimpleForwardingClientCallListener responseLoggingListener =
+ new SimpleForwardingClientCallListener(responseListener) {
+ @Override
+ public void onHeaders(Metadata headers) {
+ recordResponseHeaders(headers, logDataBuilder);
+ super.onHeaders(headers);
+ }
+
+ @Override
+ public void onMessage(RespT message) {
+ recordResponsePayload(message, logDataBuilder);
+ super.onMessage(message);
+ }
+
+ @Override
+ public void onClose(Status status, Metadata trailers) {
+ logResponse(status, logDataBuilder, LOGGER);
+ super.onClose(status, trailers);
+ }
+ };
+ currentListener = responseLoggingListener;
+ super.start(responseLoggingListener, headers);
+ }
+
+ @Override
+ public void sendMessage(ReqT message) {
+ logRequestDetails(message, logDataBuilder);
+ super.sendMessage(message);
+ }
+ };
+ }
+
+ // Helper methods for logging
+ // some duplications with http equivalent to avoid exposing as public method for now
+ void logRequestInfo(
+ MethodDescriptor method, LogData.Builder logDataBuilder, Logger logger) {
+ try {
+ if (logger.isInfoEnabled()) {
+ logDataBuilder.serviceName(method.getServiceName()).rpcName(method.getFullMethodName());
+
+ if (!logger.isDebugEnabled()) {
+ LoggingUtils.logWithMDC(
+ logger, Level.INFO, logDataBuilder.build().toMapRequest(), "Sending gRPC request");
+ }
+ }
+ } catch (Exception e) {
+ logger.error("Error logging request info (and headers)", e);
+ }
+ }
+
+ private void recordRequestHeaders(Metadata headers, LogData.Builder logDataBuilder) {
+ try {
+ if (LOGGER.isDebugEnabled()) {
+ JsonObject requestHeaders = mapHeadersToJsonObject(headers);
+ logDataBuilder.requestHeaders(GSON.toJson(requestHeaders));
+ }
+ } catch (Exception e) {
+ LOGGER.error("Error recording request headers", e);
+ }
+ }
+
+ void recordResponseHeaders(Metadata headers, LogData.Builder logDataBuilder) {
+ try {
+ if (LOGGER.isDebugEnabled()) {
+ JsonObject responseHeaders = mapHeadersToJsonObject(headers);
+ logDataBuilder.responseHeaders(GSON.toJson(responseHeaders));
+ }
+ } catch (Exception e) {
+ LOGGER.error("Error recording response headers", e);
+ }
+ }
+
+ void recordResponsePayload(RespT message, LogData.Builder logDataBuilder) {
+ try {
+ if (LOGGER.isDebugEnabled()) {
+ logDataBuilder.responsePayload(GSON.toJsonTree(message));
+ }
+ } catch (Exception e) {
+ LOGGER.error("Error recording response payload", e);
+ }
+ }
+
+ void logResponse(Status status, LogData.Builder logDataBuilder, Logger logger) {
+ try {
+ if (logger.isInfoEnabled()) {
+ logDataBuilder.responseStatus(status.getCode().toString());
+ }
+ if (logger.isInfoEnabled() && !logger.isDebugEnabled()) {
+ Map responseData = logDataBuilder.build().toMapResponse();
+ LoggingUtils.logWithMDC(logger, Level.INFO, responseData, "Received Grpc response");
+ }
+ if (logger.isDebugEnabled()) {
+ Map responsedDetailsMap = logDataBuilder.build().toMapResponse();
+ LoggingUtils.logWithMDC(logger, Level.DEBUG, responsedDetailsMap, "Received Grpc response");
+ }
+ } catch (Exception e) {
+ logger.error("Error logging request response", e);
+ }
+ }
+
+ void logRequestDetails(RespT message, LogData.Builder logDataBuilder) {
+ try {
+ if (LOGGER.isDebugEnabled()) {
+ logDataBuilder.requestPayload(GSON.toJsonTree(message));
+ Map requestDetailsMap = logDataBuilder.build().toMapRequest();
+ LoggingUtils.logWithMDC(
+ LOGGER, Level.DEBUG, requestDetailsMap, "Sending gRPC request: request payload");
+ }
+ } catch (Exception e) {
+ LOGGER.error("Error logging request details", e);
+ }
+ }
+
+ private static JsonObject mapHeadersToJsonObject(Metadata headers) {
+ JsonObject jsonHeaders = new JsonObject();
+ headers
+ .keys()
+ .forEach(
+ key -> {
+ Metadata.Key metadataKey =
+ Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
+ String headerValue = headers.get(metadataKey);
+ jsonHeaders.addProperty(key, headerValue);
+ });
+ return jsonHeaders;
+ }
+}
diff --git a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java
index ae4d7f9e51..1f23868cd5 100644
--- a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java
+++ b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java
@@ -467,6 +467,7 @@ private ManagedChannel createSingleChannel() throws IOException {
builder =
builder
.intercept(new GrpcChannelUUIDInterceptor())
+ .intercept(new GrpcLoggingInterceptor())
.intercept(headerInterceptor)
.intercept(metadataHandlerInterceptor)
.userAgent(headerInterceptor.getUserAgentHeader())
diff --git a/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcLoggingInterceptorTest.java b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcLoggingInterceptorTest.java
new file mode 100644
index 0000000000..e15a69db2d
--- /dev/null
+++ b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcLoggingInterceptorTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.grpc;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import ch.qos.logback.classic.Level;
+import com.google.api.gax.grpc.testing.FakeMethodDescriptor;
+import com.google.api.gax.logging.LogData;
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.ClientCall;
+import io.grpc.ClientInterceptors;
+import io.grpc.Metadata;
+import io.grpc.MethodDescriptor;
+import io.grpc.Status;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class GrpcLoggingInterceptorTest {
+ @Mock private Channel channel;
+
+ @Mock private ClientCall call;
+
+ private static final MethodDescriptor method = FakeMethodDescriptor.create();
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(GrpcLoggingInterceptorTest.class);
+ /** Sets up mocks. */
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(channel.newCall(Mockito.>any(), any(CallOptions.class)))
+ .thenReturn(call);
+ }
+
+ @Test
+ void testInterceptor_basic() {
+ GrpcLoggingInterceptor interceptor = new GrpcLoggingInterceptor();
+ Channel intercepted = ClientInterceptors.intercept(channel, interceptor);
+ @SuppressWarnings("unchecked")
+ ClientCall.Listener listener = mock(ClientCall.Listener.class);
+ ClientCall interceptedCall = intercepted.newCall(method, CallOptions.DEFAULT);
+ // Simulate starting the call
+ interceptedCall.start(listener, new Metadata());
+ // Verify that the underlying call's start() method is invoked
+ verify(call).start(any(ClientCall.Listener.class), any(Metadata.class));
+
+ // Simulate sending a message
+ String requestMessage = "test request";
+ interceptedCall.sendMessage(requestMessage);
+ // Verify that the underlying call's sendMessage() method is invoked
+ verify(call).sendMessage(requestMessage);
+ }
+
+ @Test
+ void testInterceptor_responseListener() {
+ GrpcLoggingInterceptor interceptor = spy(new GrpcLoggingInterceptor());
+ Channel intercepted = ClientInterceptors.intercept(channel, interceptor);
+ @SuppressWarnings("unchecked")
+ ClientCall.Listener listener = mock(ClientCall.Listener.class);
+ ClientCall interceptedCall = intercepted.newCall(method, CallOptions.DEFAULT);
+ interceptedCall.start(listener, new Metadata());
+
+ // Simulate respond interceptor calls
+ Metadata responseHeaders = new Metadata();
+ responseHeaders.put(
+ Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER), "header-value");
+ interceptor.currentListener.onHeaders(responseHeaders);
+
+ interceptor.currentListener.onMessage(null);
+
+ Status status = Status.OK;
+ interceptor.currentListener.onClose(status, new Metadata());
+
+ // --- Verify that the response listener's methods were called ---
+ verify(interceptor).recordResponseHeaders(eq(responseHeaders), any(LogData.Builder.class));
+ verify(interceptor).recordResponsePayload(any(), any(LogData.Builder.class));
+ verify(interceptor).logResponse(eq(status), any(LogData.Builder.class), any(Logger.class));
+ }
+
+ @Test
+ void testLogRequestInfo() {
+
+ TestAppender testAppender = setupTestLogger(GrpcLoggingInterceptorTest.class);
+ GrpcLoggingInterceptor interceptor = new GrpcLoggingInterceptor();
+ interceptor.logRequestInfo(method, LogData.builder(), LOGGER);
+
+ Assertions.assertEquals(1, testAppender.events.size());
+ assertEquals(Level.INFO, testAppender.events.get(0).getLevel());
+ assertEquals(
+ "{\"serviceName\":\"FakeClient\",\"message\":\"Sending gRPC request\",\"rpcName\":\"FakeClient/fake-method\"}",
+ testAppender.events.get(0).getMessage());
+ testAppender.stop();
+ }
+
+ @Test
+ void TestLogResponseInfo() {
+ TestAppender testAppender = setupTestLogger(GrpcLoggingInterceptorTest.class);
+ GrpcLoggingInterceptor interceptor = new GrpcLoggingInterceptor();
+ interceptor.logResponse(Status.CANCELLED, LogData.builder(), LOGGER);
+
+ Assertions.assertEquals(1, testAppender.events.size());
+ assertEquals(Level.INFO, testAppender.events.get(0).getLevel());
+ assertEquals(
+ "{\"response.status\":\"CANCELLED\",\"message\":\"Received Grpc response\"}",
+ testAppender.events.get(0).getMessage());
+ testAppender.stop();
+ }
+
+ private TestAppender setupTestLogger(Class> clazz) {
+ TestAppender testAppender = new TestAppender();
+ testAppender.start();
+ Logger logger = LoggerFactory.getLogger(clazz);
+ ((ch.qos.logback.classic.Logger) logger).addAppender(testAppender);
+ return testAppender;
+ }
+}
diff --git a/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/TestAppender.java b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/TestAppender.java
new file mode 100644
index 0000000000..0aa34ebb5a
--- /dev/null
+++ b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/TestAppender.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.grpc;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.AppenderBase;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Logback appender used to set up tests. */
+public class TestAppender extends AppenderBase {
+ public List events = new ArrayList<>();
+
+ @Override
+ protected void append(ILoggingEvent eventObject) {
+ // triggering Logback to capture the current MDC context and store it with the log event
+ eventObject.getMDCPropertyMap();
+ events.add(eventObject);
+ }
+
+ public void clearEvents() {
+ events.clear();
+ }
+}
diff --git a/gax-java/gax-grpc/src/test/resources/logback-test.xml b/gax-java/gax-grpc/src/test/resources/logback-test.xml
new file mode 100644
index 0000000000..802d0e18a7
--- /dev/null
+++ b/gax-java/gax-grpc/src/test/resources/logback-test.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonLoggingInterceptor.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonLoggingInterceptor.java
new file mode 100644
index 0000000000..a465ab471a
--- /dev/null
+++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonLoggingInterceptor.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.httpjson;
+
+import com.google.api.core.InternalApi;
+import com.google.api.gax.httpjson.ForwardingHttpJsonClientCall.SimpleForwardingHttpJsonClientCall;
+import com.google.api.gax.httpjson.ForwardingHttpJsonClientCallListener.SimpleForwardingHttpJsonClientCallListener;
+import com.google.api.gax.logging.LogData;
+import com.google.api.gax.logging.LoggingUtils;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.event.Level;
+
+@InternalApi
+public class HttpJsonLoggingInterceptor implements HttpJsonClientInterceptor {
+
+ private static final Logger LOGGER = LoggingUtils.getLogger(HttpJsonLoggingInterceptor.class);
+ private static final Gson GSON = new Gson();
+
+ @Override
+ public HttpJsonClientCall interceptCall(
+ ApiMethodDescriptor method,
+ HttpJsonCallOptions callOptions,
+ HttpJsonChannel next) {
+
+ String endpoint = ((ManagedHttpJsonChannel) next).getEndpoint();
+
+ return new SimpleForwardingHttpJsonClientCall(next.newCall(method, callOptions)) {
+
+ LogData.Builder logDataBuilder = LogData.builder();
+
+ @Override
+ public void start(
+ HttpJsonClientCall.Listener responseListener, HttpJsonMetadata headers) {
+
+ logRequestInfo(method, endpoint, logDataBuilder, LOGGER);
+ recordRequestHeaders(headers, logDataBuilder);
+
+ Listener forwardingResponseListener =
+ new SimpleForwardingHttpJsonClientCallListener(responseListener) {
+
+ @Override
+ public void onHeaders(HttpJsonMetadata responseHeaders) {
+ recordResponseHeaders(responseHeaders, logDataBuilder);
+ super.onHeaders(responseHeaders);
+ }
+
+ @Override
+ public void onMessage(RespT message) {
+ recordResponsePayload(message, logDataBuilder);
+ super.onMessage(message);
+ }
+
+ @Override
+ public void onClose(int statusCode, HttpJsonMetadata trailers) {
+ logResponse(statusCode, logDataBuilder, LOGGER);
+ super.onClose(statusCode, trailers);
+ }
+ };
+ super.start(forwardingResponseListener, headers);
+ }
+
+ @Override
+ public void sendMessage(ReqT message) {
+ logRequestDetails(message, logDataBuilder);
+ super.sendMessage(message);
+ }
+ };
+ }
+
+ // Helper methods for logging,
+ // some duplications with grpc equivalent to avoid exposing as public method
+ void logRequestInfo(
+ ApiMethodDescriptor method,
+ String endpoint,
+ LogData.Builder logDataBuilder,
+ Logger logger) {
+ try {
+ if (logger.isInfoEnabled()) {
+ logDataBuilder
+ .rpcName(method.getFullMethodName())
+ .httpMethod(method.getHttpMethod())
+ .httpUrl(endpoint);
+
+ if (!logger.isDebugEnabled()) {
+ LoggingUtils.logWithMDC(
+ logger, Level.INFO, logDataBuilder.build().toMapRequest(), "Sending HTTP request");
+ }
+ }
+ } catch (Exception e) {
+ logger.error("Error logging request info (and headers)", e);
+ }
+ }
+
+ private void recordRequestHeaders(HttpJsonMetadata headers, LogData.Builder logDataBuilder) {
+ try {
+ if (LOGGER.isDebugEnabled()) {
+ JsonObject requestHeaders = new JsonObject();
+ headers
+ .getHeaders()
+ .forEach((key, value) -> requestHeaders.addProperty(key, value.toString()));
+ logDataBuilder.requestHeaders(GSON.toJson(requestHeaders));
+ }
+ } catch (Exception e) {
+ LOGGER.error("Error recording request headers", e);
+ }
+ }
+
+ private void recordResponseHeaders(
+ HttpJsonMetadata responseHeaders, LogData.Builder logDataBuilder) {
+ try {
+ if (LOGGER.isDebugEnabled()) {
+
+ Map> map = new HashMap<>();
+ responseHeaders.getHeaders().forEach((key, value) -> map.put(key, (List) value));
+ logDataBuilder.responseHeaders(GSON.toJson(map));
+ }
+ } catch (Exception e) {
+ LOGGER.error("Error recording response headers", e);
+ }
+ }
+
+ private void recordResponsePayload(RespT message, LogData.Builder logDataBuilder) {
+ try {
+ if (LOGGER.isDebugEnabled()) {
+ logDataBuilder.responsePayload(GSON.toJsonTree(message));
+ }
+ } catch (Exception e) {
+ LOGGER.error("Error recording response payload", e);
+ }
+ }
+
+ void logResponse(int statusCode, LogData.Builder logDataBuilder, Logger logger) {
+ try {
+ if (logger.isInfoEnabled()) {
+ logDataBuilder.responseStatus(String.valueOf(statusCode));
+ }
+ if (logger.isInfoEnabled() && !logger.isDebugEnabled()) {
+ Map responseData = logDataBuilder.build().toMapResponse();
+ LoggingUtils.logWithMDC(logger, Level.INFO, responseData, "Received HTTP response");
+ }
+ if (logger.isDebugEnabled()) {
+ Map responsedDetailsMap = logDataBuilder.build().toMapResponse();
+ LoggingUtils.logWithMDC(logger, Level.DEBUG, responsedDetailsMap, "Received HTTP response");
+ }
+ } catch (Exception e) {
+ logger.error("Error logging request response", e);
+ }
+ }
+
+ private void logRequestDetails(RespT message, LogData.Builder logDataBuilder) {
+ try {
+ if (LOGGER.isDebugEnabled()) {
+ logDataBuilder.requestPayload(GSON.toJsonTree(message));
+ Map requestDetailsMap = logDataBuilder.build().toMapRequest();
+ LoggingUtils.logWithMDC(
+ LOGGER, Level.DEBUG, requestDetailsMap, "Sending HTTP request: request payload");
+ }
+ } catch (Exception e) {
+ LOGGER.error("Error logging request details", e);
+ }
+ }
+}
diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java
index f92bdf299c..1912bc5e29 100644
--- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java
+++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java
@@ -196,6 +196,7 @@ private HttpJsonTransportChannel createChannel() throws IOException, GeneralSecu
HttpJsonClientInterceptor headerInterceptor =
new HttpJsonHeaderInterceptor(headerProvider.getHeaders());
+ channel = new ManagedHttpJsonInterceptorChannel(channel, new HttpJsonLoggingInterceptor());
channel = new ManagedHttpJsonInterceptorChannel(channel, headerInterceptor);
if (interceptorProvider != null && interceptorProvider.getInterceptors() != null) {
for (HttpJsonClientInterceptor interceptor : interceptorProvider.getInterceptors()) {
diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonChannel.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonChannel.java
index 7a2e7a2f26..bd3bed8556 100644
--- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonChannel.java
+++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ManagedHttpJsonChannel.java
@@ -57,6 +57,10 @@ protected ManagedHttpJsonChannel() {
this(null, true, null, null);
}
+ String getEndpoint() {
+ return endpoint;
+ }
+
private ManagedHttpJsonChannel(
Executor executor,
boolean usingDefaultExecutor,
diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonLoggingInterceptorTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonLoggingInterceptorTest.java
new file mode 100644
index 0000000000..0966407fba
--- /dev/null
+++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonLoggingInterceptorTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.httpjson;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+
+import ch.qos.logback.classic.Level;
+import com.google.api.gax.httpjson.ApiMethodDescriptor.MethodType;
+import com.google.api.gax.logging.LogData;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class HttpJsonLoggingInterceptorTest {
+
+ private static final Logger LOGGER =
+ LoggerFactory.getLogger(HttpJsonLoggingInterceptorTest.class);
+
+ @SuppressWarnings("unchecked")
+ private static final ApiMethodDescriptor method =
+ ApiMethodDescriptor.newBuilder()
+ .setType(MethodType.UNARY)
+ .setRequestFormatter(mock(HttpRequestFormatter.class))
+ .setRequestFormatter(mock(HttpRequestFormatter.class))
+ .setFullMethodName("FakeClient/fake-method")
+ .setResponseParser(mock(HttpResponseParser.class))
+ .build();
+
+ @Test
+ void testLogRequestInfo() {
+
+ TestAppender testAppender = setupTestLogger(HttpJsonLoggingInterceptorTest.class);
+ HttpJsonLoggingInterceptor interceptor = new HttpJsonLoggingInterceptor();
+ interceptor.logRequestInfo(method, "fake.endpoint", LogData.builder(), LOGGER);
+
+ Assertions.assertEquals(1, testAppender.events.size());
+ assertEquals(Level.INFO, testAppender.events.get(0).getLevel());
+ assertEquals(
+ "{\"request.url\":\"fake.endpoint\",\"message\":\"Sending HTTP request\",\"rpcName\":\"FakeClient/fake-method\"}",
+ testAppender.events.get(0).getMessage());
+ testAppender.stop();
+ }
+
+ @Test
+ void testLogResponseInfo() {
+
+ TestAppender testAppender = setupTestLogger(HttpJsonLoggingInterceptorTest.class);
+ HttpJsonLoggingInterceptor interceptor = new HttpJsonLoggingInterceptor();
+ interceptor.logResponse(200, LogData.builder(), LOGGER);
+
+ Assertions.assertEquals(1, testAppender.events.size());
+ assertEquals(Level.INFO, testAppender.events.get(0).getLevel());
+ assertEquals(
+ "{\"response.status\":\"200\",\"message\":\"Received HTTP response\"}",
+ testAppender.events.get(0).getMessage());
+ testAppender.stop();
+ }
+
+ private TestAppender setupTestLogger(Class> clazz) {
+ TestAppender testAppender = new TestAppender();
+ testAppender.start();
+ Logger logger = LoggerFactory.getLogger(clazz);
+ ((ch.qos.logback.classic.Logger) logger).addAppender(testAppender);
+ return testAppender;
+ }
+}
diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java
index 2e46157534..3e6b2d56d1 100644
--- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java
+++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java
@@ -118,8 +118,12 @@ void managedChannelUsesDefaultChannelExecutor() throws IOException {
// By default, the channel will be wrapped with ManagedHttpJsonInterceptorChannel
ManagedHttpJsonInterceptorChannel interceptorChannel =
(ManagedHttpJsonInterceptorChannel) httpJsonTransportChannel.getManagedChannel();
- ManagedHttpJsonChannel managedHttpJsonChannel = interceptorChannel.getChannel();
- assertThat(managedHttpJsonChannel.getExecutor()).isNotNull();
+ // call getChannel() twice because interceptors are chained in layers by recursive construction
+ // inside com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider.createChannel
+ ManagedHttpJsonInterceptorChannel managedHttpJsonChannel =
+ (ManagedHttpJsonInterceptorChannel) interceptorChannel.getChannel();
+ ManagedHttpJsonChannel channel = managedHttpJsonChannel.getChannel();
+ assertThat(channel.getExecutor()).isNotNull();
// Clean up the resources (executor, deadlineScheduler, httpTransport)
instantiatingHttpJsonChannelProvider.getTransportChannel().shutdownNow();
@@ -146,9 +150,14 @@ void managedChannelUsesCustomExecutor() throws IOException {
// By default, the channel will be wrapped with ManagedHttpJsonInterceptorChannel
ManagedHttpJsonInterceptorChannel interceptorChannel =
(ManagedHttpJsonInterceptorChannel) httpJsonTransportChannel.getManagedChannel();
- ManagedHttpJsonChannel managedHttpJsonChannel = interceptorChannel.getChannel();
- assertThat(managedHttpJsonChannel.getExecutor()).isNotNull();
- assertThat(managedHttpJsonChannel.getExecutor()).isEqualTo(executor);
+ // call getChannel() twice because interceptors are chained in layers by recursive construction
+ // inside com.google.api.gax.httpjson.InstantiatingHttpJsonChannelProvider.createChannel
+ ManagedHttpJsonInterceptorChannel managedHttpJsonChannel =
+ (ManagedHttpJsonInterceptorChannel) interceptorChannel.getChannel();
+ ManagedHttpJsonChannel channel = managedHttpJsonChannel.getChannel();
+
+ assertThat(channel.getExecutor()).isNotNull();
+ assertThat(channel.getExecutor()).isEqualTo(executor);
// Clean up the resources (executor, deadlineScheduler, httpTransport)
instantiatingHttpJsonChannelProvider.getTransportChannel().shutdownNow();
diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/TestAppender.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/TestAppender.java
new file mode 100644
index 0000000000..c89ebfb3b7
--- /dev/null
+++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/TestAppender.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.httpjson;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.AppenderBase;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Logback appender used to set up tests. */
+public class TestAppender extends AppenderBase {
+ public List events = new ArrayList<>();
+
+ @Override
+ protected void append(ILoggingEvent eventObject) {
+ // triggering Logback to capture the current MDC context and store it with the log event
+ eventObject.getMDCPropertyMap();
+ events.add(eventObject);
+ }
+
+ public void clearEvents() {
+ events.clear();
+ }
+}
diff --git a/gax-java/gax-httpjson/src/test/resources/logback-test.xml b/gax-java/gax-httpjson/src/test/resources/logback-test.xml
new file mode 100644
index 0000000000..8a6dc9b23d
--- /dev/null
+++ b/gax-java/gax-httpjson/src/test/resources/logback-test.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/gax-java/gax/pom.xml b/gax-java/gax/pom.xml
index 4a690e1094..bfec74bab1 100644
--- a/gax-java/gax/pom.xml
+++ b/gax-java/gax/pom.xml
@@ -69,6 +69,11 @@
opentelemetry-api
true
+
+
+ org.slf4j
+ slf4j-api
+
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/logging/LogData.java b/gax-java/gax/src/main/java/com/google/api/gax/logging/LogData.java
new file mode 100644
index 0000000000..57751dc6d0
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/logging/LogData.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.logging;
+
+import com.google.api.core.InternalApi;
+import com.google.auto.value.AutoValue;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+@InternalApi
+@AutoValue
+public abstract class LogData {
+ private static final Gson gson = new Gson();
+
+ @Nullable
+ public abstract String serviceName();
+
+ @Nullable
+ public abstract String rpcName();
+
+ @Nullable
+ public abstract String requestId();
+
+ @Nullable
+ public abstract String requestHeaders();
+
+ @Nullable
+ public abstract JsonElement requestPayload();
+
+ @Nullable
+ public abstract String responseStatus();
+
+ @Nullable
+ public abstract String responseHeaders();
+
+ @Nullable
+ public abstract JsonElement responsePayload();
+
+ @Nullable
+ public abstract String httpMethod();
+
+ @Nullable
+ public abstract String httpUrl();
+
+ public static Builder builder() {
+ return new AutoValue_LogData.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder serviceName(String serviceName);
+
+ public abstract Builder rpcName(String rpcName);
+
+ public abstract Builder requestId(String requestId);
+
+ public abstract Builder requestHeaders(String requestHeaders);
+
+ public abstract Builder requestPayload(JsonElement requestPayload);
+
+ public abstract Builder responseStatus(String responseStatus);
+
+ public abstract Builder responseHeaders(String responseHeaders);
+
+ public abstract Builder responsePayload(JsonElement responsePayload);
+
+ public abstract Builder httpMethod(String httpMethod);
+
+ public abstract Builder httpUrl(String httpUrl);
+
+ public abstract LogData build();
+ }
+
+ // helper functions to convert to map for logging
+ // todo: error handling?
+ public Map toMapRequest() {
+ Map map = new HashMap<>();
+ if (serviceName() != null) {
+ map.put("serviceName", serviceName());
+ }
+ if (rpcName() != null) {
+ map.put("rpcName", rpcName());
+ }
+ if (requestId() != null) {
+ map.put("requestId", requestId());
+ }
+ if (requestHeaders() != null) {
+ map.put("request.headers", requestHeaders());
+ }
+ if (requestPayload() != null) {
+ map.put("request.payload", gson.toJson(requestPayload()));
+ }
+ if (httpMethod() != null) {
+ map.put("request.method", httpMethod());
+ }
+ if (httpUrl() != null) {
+ map.put("request.url", httpUrl());
+ }
+ return map;
+ }
+
+ public Map toMapResponse() {
+ Map map = new HashMap<>();
+ if (serviceName() != null) {
+ map.put("serviceName", serviceName());
+ }
+ if (rpcName() != null) {
+ map.put("rpcName", rpcName());
+ }
+ if (requestId() != null) {
+ map.put("requestId", requestId());
+ }
+ if (responseStatus() != null) {
+ map.put("response.status", responseStatus());
+ }
+ if (responseHeaders() != null) {
+ map.put("response.headers", responseHeaders());
+ }
+ if (responsePayload() != null) {
+ map.put("response.payload", gson.toJson(responsePayload()));
+ }
+ return map;
+ }
+}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggingUtils.java b/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggingUtils.java
new file mode 100644
index 0000000000..c738426aa9
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggingUtils.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.logging;
+
+import com.google.api.core.InternalApi;
+import com.google.api.gax.rpc.internal.EnvironmentProvider;
+import com.google.api.gax.rpc.internal.SystemEnvironmentProvider;
+import com.google.gson.Gson;
+import java.util.Map;
+import org.slf4j.ILoggerFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+@InternalApi
+public class LoggingUtils {
+
+ private static EnvironmentProvider environmentProvider = SystemEnvironmentProvider.getInstance();
+ private static final Logger NO_OP_LOGGER = org.slf4j.helpers.NOPLogger.NOP_LOGGER;
+ private static boolean loggingEnabled = isLoggingEnabled();
+ static final String GOOGLE_SDK_JAVA_LOGGING = "GOOGLE_SDK_JAVA_LOGGING";
+ private static final Gson gson = new Gson();
+ // expose this setter for testing purposes
+ static void setEnvironmentProvider(EnvironmentProvider provider) {
+ environmentProvider = provider;
+ // Recalculate LOGGING_ENABLED after setting the new provider
+ loggingEnabled = isLoggingEnabled();
+ }
+
+ private LoggingUtils() {}
+
+ public static Logger getLogger(Class> clazz) {
+ return getLogger(clazz, new DefaultLoggerFactoryProvider());
+ }
+
+ // constructor with LoggerFactoryProvider to make testing easier
+ static Logger getLogger(Class> clazz, LoggerFactoryProvider factoryProvider) {
+ if (loggingEnabled) {
+ return factoryProvider.getLoggerFactory().getLogger(clazz.getName());
+ } else {
+ // use SLF4j's NOP logger regardless of bindings
+ return NO_OP_LOGGER;
+ }
+ }
+
+ public static void logWithMDC(
+ Logger logger, org.slf4j.event.Level level, Map contextMap, String message) {
+ if (!contextMap.isEmpty()) {
+ contextMap.forEach(MDC::put);
+ contextMap.put("message", message);
+ message = gson.toJson(contextMap);
+ }
+ switch (level) {
+ case TRACE:
+ logger.trace(message);
+ break;
+ case DEBUG:
+ logger.debug(message);
+ break;
+ case INFO:
+ logger.info(message);
+ break;
+ case WARN:
+ logger.warn(message);
+ break;
+ case ERROR:
+ logger.error(message);
+ break;
+ default:
+ logger.info(message);
+ // Default to INFO level
+ }
+ if (!contextMap.isEmpty()) {
+ MDC.clear();
+ }
+ }
+
+ static boolean isLoggingEnabled() {
+ String enableLogging = environmentProvider.getenv(GOOGLE_SDK_JAVA_LOGGING);
+ return "true".equalsIgnoreCase(enableLogging);
+ }
+
+ interface LoggerFactoryProvider {
+ ILoggerFactory getLoggerFactory();
+ }
+
+ static class DefaultLoggerFactoryProvider implements LoggerFactoryProvider {
+ @Override
+ public ILoggerFactory getLoggerFactory() {
+ return LoggerFactory.getILoggerFactory();
+ }
+ }
+}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/internal/SystemEnvironmentProvider.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/internal/SystemEnvironmentProvider.java
new file mode 100644
index 0000000000..29d45ba3c5
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/internal/SystemEnvironmentProvider.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.rpc.internal;
+
+import com.google.api.core.InternalApi;
+
+/** Represents the default system environment provider. */
+@InternalApi
+public class SystemEnvironmentProvider implements EnvironmentProvider {
+ static final SystemEnvironmentProvider INSTANCE = new SystemEnvironmentProvider();
+
+ private SystemEnvironmentProvider() {}
+
+ @Override
+ public String getenv(String name) {
+ return System.getenv(name);
+ }
+
+ public static SystemEnvironmentProvider getInstance() {
+ return INSTANCE;
+ }
+}
diff --git a/gax-java/gax/src/test/java/com/google/api/gax/logging/LogDataTest.java b/gax-java/gax/src/test/java/com/google/api/gax/logging/LogDataTest.java
new file mode 100644
index 0000000000..d19c74591a
--- /dev/null
+++ b/gax-java/gax/src/test/java/com/google/api/gax/logging/LogDataTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.logging;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gson.JsonParser;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class LogDataTest {
+
+ @Test
+ void toMapResponse_correctlyConvertsData() {
+ LogData logData =
+ LogData.builder()
+ .serviceName("MyService")
+ .rpcName("myMethod")
+ .requestHeaders("fake header")
+ .requestId("abcd")
+ .responsePayload(JsonParser.parseString("{\"key\": \"value\"}"))
+ .build();
+
+ Map expectedMap =
+ ImmutableMap.of(
+ "serviceName", "MyService",
+ "rpcName", "myMethod",
+ "response.payload", "{\"key\":\"value\"}",
+ "requestId", "abcd");
+
+ assertThat(logData.toMapResponse()).isEqualTo(expectedMap);
+ }
+
+ @Test
+ void toMapRequest_correctlyConvertsData() {
+ LogData logData =
+ LogData.builder()
+ .serviceName("MyService")
+ .rpcName("myMethod")
+ .requestHeaders("fake header")
+ .requestId("abcd")
+ .httpUrl("url")
+ .responsePayload(JsonParser.parseString("{\"key\": \"value\"}"))
+ .build();
+
+ Map expectedMap =
+ ImmutableMap.of(
+ "serviceName", "MyService",
+ "rpcName", "myMethod",
+ "request.headers", "fake header",
+ "requestId", "abcd",
+ "request.url", "url");
+
+ assertThat(logData.toMapRequest()).isEqualTo(expectedMap);
+ }
+}
diff --git a/gax-java/gax/src/test/java/com/google/api/gax/logging/LoggingUtilsTest.java b/gax-java/gax/src/test/java/com/google/api/gax/logging/LoggingUtilsTest.java
new file mode 100644
index 0000000000..d53bd0bc02
--- /dev/null
+++ b/gax-java/gax/src/test/java/com/google/api/gax/logging/LoggingUtilsTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.logging;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import com.google.api.gax.logging.LoggingUtils.LoggerFactoryProvider;
+import com.google.api.gax.rpc.internal.EnvironmentProvider;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.slf4j.ILoggerFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.helpers.NOPLogger;
+
+class LoggingUtilsTest {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(LoggingUtilsTest.class);
+ private EnvironmentProvider envProvider = Mockito.mock(EnvironmentProvider.class);
+
+ @AfterEach
+ void tearDown() {
+ LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
+ loggerContext.getLogger(Logger.ROOT_LOGGER_NAME).detachAppender("CONSOLE");
+ }
+
+ @Test
+ void testGetLogger_loggingEnabled_slf4jBindingPresent() {
+ Mockito.when(envProvider.getenv(LoggingUtils.GOOGLE_SDK_JAVA_LOGGING)).thenReturn("true");
+ LoggingUtils.setEnvironmentProvider(envProvider);
+ Logger logger = LoggingUtils.getLogger(LoggingUtilsTest.class);
+ Assertions.assertInstanceOf(Logger.class, logger);
+ Assertions.assertNotEquals(NOPLogger.class, logger.getClass());
+ }
+
+ @Test
+ void testGetLogger_loggingDisabled() {
+ Mockito.when(envProvider.getenv(LoggingUtils.GOOGLE_SDK_JAVA_LOGGING)).thenReturn("false");
+ LoggingUtils.setEnvironmentProvider(envProvider);
+
+ Logger logger = LoggingUtils.getLogger(LoggingUtilsTest.class);
+ Assertions.assertEquals(NOPLogger.class, logger.getClass());
+ Assertions.assertFalse(logger.isInfoEnabled());
+ Assertions.assertFalse(logger.isDebugEnabled());
+ }
+
+ @Test
+ void testGetLogger_loggingEnabled_noBinding() {
+ Mockito.when(envProvider.getenv(LoggingUtils.GOOGLE_SDK_JAVA_LOGGING)).thenReturn("true");
+ LoggingUtils.setEnvironmentProvider(envProvider);
+ // Create a mock LoggerFactoryProvider
+ LoggerFactoryProvider mockLoggerFactoryProvider = mock(LoggerFactoryProvider.class);
+ ILoggerFactory mockLoggerFactory = mock(ILoggerFactory.class);
+ when(mockLoggerFactoryProvider.getLoggerFactory()).thenReturn(mockLoggerFactory);
+ when(mockLoggerFactory.getLogger(anyString()))
+ .thenReturn(org.slf4j.helpers.NOPLogger.NOP_LOGGER);
+
+ // Use the mock LoggerFactoryProvider in getLogger()
+ Logger logger = LoggingUtils.getLogger(LoggingUtilsTest.class, mockLoggerFactoryProvider);
+
+ // Assert that the returned logger is a NOPLogger
+ Assertions.assertInstanceOf(NOPLogger.class, logger);
+ }
+
+ @Test
+ void testIsLoggingEnabled_true() {
+ Mockito.when(envProvider.getenv(LoggingUtils.GOOGLE_SDK_JAVA_LOGGING)).thenReturn("true");
+ LoggingUtils.setEnvironmentProvider(envProvider);
+ Assertions.assertTrue(LoggingUtils.isLoggingEnabled());
+ Mockito.when(envProvider.getenv(LoggingUtils.GOOGLE_SDK_JAVA_LOGGING)).thenReturn("TRUE");
+ LoggingUtils.setEnvironmentProvider(envProvider);
+ Assertions.assertTrue(LoggingUtils.isLoggingEnabled());
+ Mockito.when(envProvider.getenv(LoggingUtils.GOOGLE_SDK_JAVA_LOGGING)).thenReturn("True");
+ LoggingUtils.setEnvironmentProvider(envProvider);
+ Assertions.assertTrue(LoggingUtils.isLoggingEnabled());
+ }
+
+ @Test
+ void testIsLoggingEnabled_defaultToFalse() {
+ LoggingUtils.setEnvironmentProvider(envProvider);
+ Assertions.assertFalse(LoggingUtils.isLoggingEnabled());
+ }
+
+ private TestAppender setupTestLogger(Class> clazz) {
+ TestAppender testAppender = new TestAppender();
+ testAppender.start();
+ Logger logger = LoggerFactory.getLogger(clazz);
+ ((ch.qos.logback.classic.Logger) logger).addAppender(testAppender);
+ return testAppender;
+ }
+
+ @Test
+ void testLogWithMDC_slf4jLogger() {
+ TestAppender testAppender = setupTestLogger(LoggingUtilsTest.class);
+ Map contextMap = new HashMap<>();
+ contextMap.put("key", "value");
+ LoggingUtils.logWithMDC(LOGGER, org.slf4j.event.Level.DEBUG, contextMap, "test message");
+
+ Assertions.assertEquals(1, testAppender.events.size());
+ Assertions.assertEquals(
+ "{\"message\":\"test message\",\"key\":\"value\"}",
+ testAppender.events.get(0).getFormattedMessage());
+
+ // Verify MDC content
+ ILoggingEvent event = testAppender.events.get(0);
+ Assertions.assertEquals("value", event.getMDCPropertyMap().get("key"));
+ testAppender.stop();
+ }
+}
diff --git a/gax-java/gax/src/test/java/com/google/api/gax/logging/TestAppender.java b/gax-java/gax/src/test/java/com/google/api/gax/logging/TestAppender.java
new file mode 100644
index 0000000000..1206553ee6
--- /dev/null
+++ b/gax-java/gax/src/test/java/com/google/api/gax/logging/TestAppender.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.api.gax.logging;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.AppenderBase;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Logback appender used to set up tests. */
+public class TestAppender extends AppenderBase {
+ public List events = new ArrayList<>();
+
+ @Override
+ protected void append(ILoggingEvent eventObject) {
+ // triggering Logback to capture the current MDC context and store it with the log event
+ eventObject.getMDCPropertyMap();
+ events.add(eventObject);
+ }
+
+ public void clearEvents() {
+ events.clear();
+ }
+}
diff --git a/gax-java/pom.xml b/gax-java/pom.xml
index fd93b003c2..1b27682da0 100644
--- a/gax-java/pom.xml
+++ b/gax-java/pom.xml
@@ -134,6 +134,11 @@
graal-sdk
${graal-sdk.version}
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
com.google.http-client
google-http-client-bom
@@ -205,6 +210,20 @@
test
+
+
+ ch.qos.logback
+ logback-classic
+
+ 1.3.14
+ test
+
+
+ ch.qos.logback
+ logback-core
+ 1.3.14
+ test
+
diff --git a/showcase/gapic-showcase/pom.xml b/showcase/gapic-showcase/pom.xml
index 2549eb199f..583ab17219 100644
--- a/showcase/gapic-showcase/pom.xml
+++ b/showcase/gapic-showcase/pom.xml
@@ -215,5 +215,20 @@
opentelemetry-sdk-testing
test
+
+
+
+ ch.qos.logback
+ logback-classic
+
+ 1.3.14
+ test
+
+
+ ch.qos.logback
+ logback-core
+ 1.3.14
+ test
+
diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITLogging.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITLogging.java
new file mode 100644
index 0000000000..51e7145e99
--- /dev/null
+++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITLogging.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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 com.google.showcase.v1beta1.it;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import com.google.api.gax.grpc.GrpcLoggingInterceptor;
+import com.google.api.gax.httpjson.HttpJsonLoggingInterceptor;
+import com.google.showcase.v1beta1.EchoClient;
+import com.google.showcase.v1beta1.EchoRequest;
+import com.google.showcase.v1beta1.EchoResponse;
+import com.google.showcase.v1beta1.it.util.TestAppender;
+import com.google.showcase.v1beta1.it.util.TestClientInitializer;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
+
+// This test needs to run with GOOGLE_SDK_JAVA_LOGGING=true
+public class ITLogging {
+ private static EchoClient grpcClient;
+
+ private static EchoClient httpjsonClient;
+
+ private TestAppender setupTestLogger(Class> clazz) {
+ TestAppender testAppender = new TestAppender();
+ testAppender.start();
+ org.slf4j.Logger logger = LoggerFactory.getLogger(clazz);
+ ((ch.qos.logback.classic.Logger) logger).addAppender(testAppender);
+ return testAppender;
+ }
+
+ @BeforeAll
+ static void createClients() throws Exception {
+ // Create gRPC Echo Client
+ grpcClient = TestClientInitializer.createGrpcEchoClient();
+ // Create Http JSON Echo Client
+ httpjsonClient = TestClientInitializer.createHttpJsonEchoClient();
+ }
+
+ @AfterAll
+ static void destroyClients() throws InterruptedException {
+ grpcClient.close();
+ httpjsonClient.close();
+
+ grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
+ httpjsonClient.awaitTermination(
+ TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
+ }
+
+ @Test
+ void testGrpc_receiveContent_logDebug() {
+ TestAppender testAppender = setupTestLogger(GrpcLoggingInterceptor.class);
+ assertThat(echoGrpc("grpc-echo?")).isEqualTo("grpc-echo?");
+ assertThat(testAppender.events.size()).isEqualTo(2);
+ assertThat(testAppender.events.get(0).getMessage())
+ .isEqualTo(
+ "{\"serviceName\":\"google.showcase.v1beta1.Echo\",\"message\":\"Sending gRPC request\",\"rpcName\":\"google.showcase.v1beta1.Echo/Echo\"}");
+ assertThat(testAppender.events.get(0).getLevel()).isEqualTo(ch.qos.logback.classic.Level.INFO);
+ assertThat(testAppender.events.get(1).getMessage())
+ .isEqualTo(
+ "{\"serviceName\":\"google.showcase.v1beta1.Echo\",\"response.status\":\"OK\",\"message\":\"Received Grpc response\",\"rpcName\":\"google.showcase.v1beta1.Echo/Echo\"}");
+ testAppender.stop();
+ }
+
+ @Test
+ void testHttpJson_receiveContent_logDebug() {
+ TestAppender testAppender = setupTestLogger(HttpJsonLoggingInterceptor.class);
+ assertThat(echoHttpJson("http-echo?")).isEqualTo("http-echo?");
+ assertThat(testAppender.events.size()).isEqualTo(2);
+ ILoggingEvent loggingEvent1 = testAppender.events.get(0);
+ assertThat(loggingEvent1.getMessage())
+ .isEqualTo(
+ "{\"request.method\":\"POST\",\"request.url\":\"http://localhost:7469\",\"message\":\"Sending HTTP request\",\"rpcName\":\"google.showcase.v1beta1.Echo/Echo\"}");
+ assertThat(loggingEvent1.getLevel()).isEqualTo(ch.qos.logback.classic.Level.INFO);
+ assertThat(loggingEvent1.getMDCPropertyMap()).hasSize(3);
+ assertThat(loggingEvent1.getMDCPropertyMap()).containsKey("rpcName");
+ assertThat(testAppender.events.get(1).getMessage())
+ .isEqualTo(
+ "{\"response.status\":\"200\",\"message\":\"Received HTTP response\",\"rpcName\":\"google.showcase.v1beta1.Echo/Echo\"}");
+ testAppender.stop();
+ }
+
+ private String echoGrpc(String value) {
+ EchoResponse response = grpcClient.echo(EchoRequest.newBuilder().setContent(value).build());
+ return response.getContent();
+ }
+
+ private String echoHttpJson(String value) {
+ EchoResponse response = httpjsonClient.echo(EchoRequest.newBuilder().setContent(value).build());
+ return response.getContent();
+ }
+}
diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestAppender.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestAppender.java
new file mode 100644
index 0000000000..0032b09d9d
--- /dev/null
+++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestAppender.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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 com.google.showcase.v1beta1.it.util;
+
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.AppenderBase;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Logback appender used to set up tests. */
+public class TestAppender extends AppenderBase {
+ public List events = new ArrayList<>();
+
+ @Override
+ protected void append(ILoggingEvent eventObject) {
+ // triggering Logback to capture the current MDC context and store it with the log event
+ eventObject.getMDCPropertyMap();
+
+ events.add(eventObject);
+ }
+ public void clearEvents() {
+ events.clear();
+ }
+}
+
diff --git a/showcase/gapic-showcase/src/test/resources/logback-test.xml b/showcase/gapic-showcase/src/test/resources/logback-test.xml
new file mode 100644
index 0000000000..66ed2652ea
--- /dev/null
+++ b/showcase/gapic-showcase/src/test/resources/logback-test.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/showcase/gapic-showcase/src/test/resources/logging.properties b/showcase/gapic-showcase/src/test/resources/logging.properties
new file mode 100644
index 0000000000..ce394f934c
--- /dev/null
+++ b/showcase/gapic-showcase/src/test/resources/logging.properties
@@ -0,0 +1,5 @@
+handlers=java.util.logging.ConsoleHandler, com.google.api.gax.logging.JsonContextMapHandler
+
+java.util.logging.ConsoleHandler.level=ALL
+java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
+com.google.api.gax.logging.JsonContextMapHandler.level=ALL
diff --git a/showcase/pom.xml b/showcase/pom.xml
index 42320f4da7..bb60c0ceca 100644
--- a/showcase/pom.xml
+++ b/showcase/pom.xml
@@ -95,6 +95,15 @@
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ **/ITLogging.java
+
+
+
@@ -117,6 +126,50 @@
org.apache.maven.plugins
maven-failsafe-plugin
+
+
+ **/ITLogging.java
+
+
+
+
+
+
+ org.apache.maven.surefire
+ surefire-junit-platform
+ ${surefire.version}
+
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+
+
+
+
+
+
+ envVarLoggingTest
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.0
+
+ sponge_log
+ ${skipUnitTests}
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+ **/ITLogging.java
+
+