diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 854f633d24..801ed4c977 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -31,6 +31,7 @@ jobs:
env:
GOOGLE_CLOUD_UNIVERSE_DOMAIN: random.com
GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS: true
+ GOOGLE_SDK_JAVA_LOGGING: true
- run: bazelisk version
- name: Install Maven modules
run: |
@@ -82,6 +83,7 @@ jobs:
env:
GOOGLE_CLOUD_UNIVERSE_DOMAIN: random.com
GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS: true
+ GOOGLE_SDK_JAVA_LOGGING: true
- run: bazelisk version
- name: Install Maven modules
run: |
@@ -133,6 +135,7 @@ jobs:
env:
GOOGLE_CLOUD_UNIVERSE_DOMAIN: random.com
GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS: true
+ GOOGLE_SDK_JAVA_LOGGING: true
build-java8-gapic-generator-java:
name: "build(8) for gapic-generator-java"
@@ -255,6 +258,34 @@ jobs:
-P enable-integration-tests \
--batch-mode \
--no-transfer-progress
+ # The `slf4j2_logback` profile brings logging dependency and compiles logging tests, require env var to be set
+ - name: Showcase integration tests - Logging SLF4J 2.x
+ working-directory: showcase
+ run: |
+ mvn clean verify -P '!showcase,enable-integration-tests,loggingTestBase,slf4j2_logback' \
+ --batch-mode \
+ --no-transfer-progress
+ # Set the Env Var for this step only
+ env:
+ GOOGLE_SDK_JAVA_LOGGING: true
+ # The `slf4j1_logback` profile brings logging dependency and compiles logging tests, require env var to be set
+ - name: Showcase integration tests - Logging SLF4J 1.x
+ working-directory: showcase
+ run: |
+ mvn clean verify -P '!showcase,enable-integration-tests,loggingTestBase,slf4j1_logback' \
+ --batch-mode \
+ --no-transfer-progress
+ # Set the Env Var for this step only
+ env:
+ GOOGLE_SDK_JAVA_LOGGING: true
+ # The `disabledLogging` profile tests logging disabled when logging dependency present,
+ # do not set env var for this step
+ - name: Showcase integration tests - Logging disabed
+ working-directory: showcase
+ run: |
+ mvn clean verify -P '!showcase,enable-integration-tests,loggingTestBase,disabledLogging' \
+ --batch-mode \
+ --no-transfer-progress
showcase-clirr:
if: ${{ github.base_ref != '' }} # Only execute on pull_request trigger event
diff --git a/gapic-generator-java-pom-parent/pom.xml b/gapic-generator-java-pom-parent/pom.xml
index a426cb9ee9..bb87b52553 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.4
+ 2.0.16
diff --git a/gax-java/dependencies.properties b/gax-java/dependencies.properties
index 4d09063fbb..9dcb8c805d 100644
--- a/gax-java/dependencies.properties
+++ b/gax-java/dependencies.properties
@@ -76,6 +76,8 @@ 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:nativeimage:24.1.2
+maven.org_slf4j_slf4j_api=org.slf4j:slf4j-api:2.0.16
+maven.com_google_protobuf_protobuf_java_util=com.google.protobuf:protobuf-java-util:3.25.5
# Testing maven artifacts
maven.junit_junit=junit:junit:4.13.2
diff --git a/gax-java/gax-grpc/pom.xml b/gax-java/gax-grpc/pom.xml
index 01a62345ef..cb33a3dae3 100644
--- a/gax-java/gax-grpc/pom.xml
+++ b/gax-java/gax-grpc/pom.xml
@@ -92,6 +92,12 @@
auto-value-annotations
provided
+
+
+ org.slf4j
+ slf4j-api
+ true
+
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..9c305261cb
--- /dev/null
+++ b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcLoggingInterceptor.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2025 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 com.google.api.gax.logging.LoggingUtils.executeWithTryCatch;
+import static com.google.api.gax.logging.LoggingUtils.logRequest;
+import static com.google.api.gax.logging.LoggingUtils.logResponse;
+import static com.google.api.gax.logging.LoggingUtils.recordResponseHeaders;
+import static com.google.api.gax.logging.LoggingUtils.recordResponsePayload;
+import static com.google.api.gax.logging.LoggingUtils.recordServiceRpcAndRequestHeaders;
+
+import com.google.api.core.InternalApi;
+import com.google.api.gax.logging.LogData;
+import com.google.api.gax.logging.LoggerProvider;
+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.HashMap;
+import java.util.Map;
+
+@InternalApi
+public class GrpcLoggingInterceptor implements ClientInterceptor {
+
+ private static final LoggerProvider LOGGER_PROVIDER =
+ LoggerProvider.forClazz(GrpcLoggingInterceptor.class);
+
+ 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) {
+ recordServiceRpcAndRequestHeaders(
+ method.getServiceName(),
+ method.getFullMethodName(),
+ null, // endpoint is for http request only
+ metadataHeadersToMap(headers),
+ logDataBuilder,
+ LOGGER_PROVIDER);
+ SimpleForwardingClientCallListener responseLoggingListener =
+ new SimpleForwardingClientCallListener(responseListener) {
+ @Override
+ public void onHeaders(Metadata headers) {
+ recordResponseHeaders(
+ metadataHeadersToMap(headers), logDataBuilder, LOGGER_PROVIDER);
+ super.onHeaders(headers);
+ }
+
+ @Override
+ public void onMessage(RespT message) {
+ recordResponsePayload(message, logDataBuilder, LOGGER_PROVIDER);
+ super.onMessage(message);
+ }
+
+ @Override
+ public void onClose(Status status, Metadata trailers) {
+ logResponse(status.getCode().toString(), logDataBuilder, LOGGER_PROVIDER);
+ super.onClose(status, trailers);
+ }
+ };
+ currentListener = responseLoggingListener;
+ super.start(responseLoggingListener, headers);
+ }
+
+ @Override
+ public void sendMessage(ReqT message) {
+ logRequest(message, logDataBuilder, LOGGER_PROVIDER);
+ super.sendMessage(message);
+ }
+ };
+ }
+
+ // Helper methods for logging
+ private static Map metadataHeadersToMap(Metadata headers) {
+
+ Map headersMap = new HashMap<>();
+ executeWithTryCatch(
+ () -> {
+ for (String key : headers.keys()) {
+ // grpc header values can be either ASCII strings or binary
+ // https://grpc.io/docs/guides/metadata/#overview
+ // this condition identified binary headers and skip for logging
+ if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
+ continue;
+ }
+ Metadata.Key metadataKey =
+ Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
+ String headerValue = headers.get(metadataKey);
+
+ headersMap.put(key, headerValue);
+ }
+ });
+
+ return headersMap;
+ }
+}
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 b75ba746f9..2b00ade341 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
@@ -681,6 +681,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..fad4cd468b
--- /dev/null
+++ b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcLoggingInterceptorTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2025 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.mockito.ArgumentMatchers.any;
+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 com.google.api.gax.grpc.testing.FakeMethodDescriptor;
+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.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class GrpcLoggingInterceptorTest {
+ @Mock private Channel channel;
+
+ @Mock private ClientCall call;
+
+ private static final MethodDescriptor method = FakeMethodDescriptor.create();
+
+ @Test
+ void testInterceptor_basic() {
+ when(channel.newCall(Mockito.>any(), any(CallOptions.class)))
+ .thenReturn(call);
+ 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() {
+ when(channel.newCall(Mockito.>any(), any(CallOptions.class)))
+ .thenReturn(call);
+ 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());
+ }
+}
diff --git a/gax-java/gax-httpjson/pom.xml b/gax-java/gax-httpjson/pom.xml
index 268f5e1bdc..30e1389b80 100644
--- a/gax-java/gax-httpjson/pom.xml
+++ b/gax-java/gax-httpjson/pom.xml
@@ -78,6 +78,12 @@
auto-value-annotations
provided
+
+
+ org.slf4j
+ slf4j-api
+ true
+
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..5a1b1b8c2b
--- /dev/null
+++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonLoggingInterceptor.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2025 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 com.google.api.gax.logging.LoggingUtils.logRequest;
+import static com.google.api.gax.logging.LoggingUtils.logResponse;
+import static com.google.api.gax.logging.LoggingUtils.recordResponseHeaders;
+import static com.google.api.gax.logging.LoggingUtils.recordResponsePayload;
+import static com.google.api.gax.logging.LoggingUtils.recordServiceRpcAndRequestHeaders;
+
+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.LoggerProvider;
+import java.util.HashMap;
+import java.util.Map;
+
+@InternalApi
+public class HttpJsonLoggingInterceptor implements HttpJsonClientInterceptor {
+
+ private static final LoggerProvider LOGGER_PROVIDER =
+ LoggerProvider.forClazz(HttpJsonLoggingInterceptor.class);
+
+ @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) {
+ recordServiceRpcAndRequestHeaders(
+ null, // service name is not available for http requests
+ method.getFullMethodName(),
+ endpoint,
+ httpJsonMetadataToMap(headers),
+ logDataBuilder,
+ LOGGER_PROVIDER);
+
+ Listener forwardingResponseListener =
+ new SimpleForwardingHttpJsonClientCallListener(responseListener) {
+
+ @Override
+ public void onHeaders(HttpJsonMetadata responseHeaders) {
+ recordResponseHeaders(
+ httpJsonMetadataToMap(responseHeaders), logDataBuilder, LOGGER_PROVIDER);
+ super.onHeaders(responseHeaders);
+ }
+
+ @Override
+ public void onMessage(RespT message) {
+ recordResponsePayload(message, logDataBuilder, LOGGER_PROVIDER);
+ super.onMessage(message);
+ }
+
+ @Override
+ public void onClose(int statusCode, HttpJsonMetadata trailers) {
+ logResponse(String.valueOf(statusCode), logDataBuilder, LOGGER_PROVIDER);
+ super.onClose(statusCode, trailers);
+ }
+ };
+ super.start(forwardingResponseListener, headers);
+ }
+
+ @Override
+ public void sendMessage(ReqT message) {
+ logRequest(message, logDataBuilder, LOGGER_PROVIDER);
+ super.sendMessage(message);
+ }
+ };
+ }
+
+ // Helper methods for logging,
+
+ private static Map httpJsonMetadataToMap(HttpJsonMetadata headers) {
+ Map headersMap = new HashMap<>();
+ headers.getHeaders().forEach((key, value) -> headersMap.put(key, value.toString()));
+ return headersMap;
+ }
+}
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..c7e134b5ce
--- /dev/null
+++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonLoggingInterceptorTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2025 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.mockito.Mockito.mock;
+
+import com.google.api.gax.httpjson.ApiMethodDescriptor.MethodType;
+
+class HttpJsonLoggingInterceptorTest {
+
+ @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();
+}
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/BUILD.bazel b/gax-java/gax/BUILD.bazel
index 5dd3ff96bd..80b26ad785 100644
--- a/gax-java/gax/BUILD.bazel
+++ b/gax-java/gax/BUILD.bazel
@@ -28,7 +28,9 @@ _COMPILE_DEPS = [
"@com_google_code_gson_gson//jar",
"@com_google_guava_failureaccess//jar",
"@javax_annotation_javax_annotation_api//jar",
- "@org_graalvm_sdk//jar"
+ "@org_graalvm_sdk//jar",
+ "@org_slf4j_slf4j_api//jar",
+ "@com_google_protobuf_protobuf_java_util//jar"
]
_TEST_COMPILE_DEPS = [
diff --git a/gax-java/gax/pom.xml b/gax-java/gax/pom.xml
index 4c2fa5f694..7323e6315e 100644
--- a/gax-java/gax/pom.xml
+++ b/gax-java/gax/pom.xml
@@ -38,6 +38,10 @@
com.google.protobuf
protobuf-java
+
+ com.google.protobuf
+ protobuf-java-util
+
org.threeten
threetenbp
@@ -69,6 +73,12 @@
opentelemetry-api
true
+
+
+ org.slf4j
+ slf4j-api
+ true
+
@@ -112,7 +122,7 @@
@{argLine} -Djava.util.logging.SimpleFormatter.format="%1$tY %1$tl:%1$tM:%1$tS.%1$tL %2$s %4$s: %5$s%6$s%n"
- !EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority
+ !EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,!LoggingEnabledTest
@@ -127,11 +137,12 @@
org.apache.maven.plugins
maven-surefire-plugin
- EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority
+ EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,LoggingEnabledTest
+
\ No newline at end of file
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..368800dba0
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/logging/LogData.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2025 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 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 Map requestHeaders();
+
+ @Nullable
+ public abstract Map requestPayload();
+
+ @Nullable
+ public abstract String responseStatus();
+
+ @Nullable
+ public abstract Map responseHeaders();
+
+ @Nullable
+ public abstract Map 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(Map requestHeaders);
+
+ public abstract Builder requestPayload(Map requestPayload);
+
+ public abstract Builder responseStatus(String responseStatus);
+
+ public abstract Builder responseHeaders(Map responseHeaders);
+
+ public abstract Builder responsePayload(Map 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
+ 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", 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", responsePayload());
+ }
+ return map;
+ }
+}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggerProvider.java b/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggerProvider.java
new file mode 100644
index 0000000000..24745ee01e
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggerProvider.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2025 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 org.slf4j.Logger;
+
+@InternalApi
+public class LoggerProvider {
+
+ private Logger logger;
+ private final Class> clazz;
+
+ private LoggerProvider(Class> clazz) {
+ this.clazz = clazz;
+ }
+
+ public static LoggerProvider forClazz(Class> clazz) {
+ return new LoggerProvider(clazz);
+ }
+
+ public Logger getLogger() {
+ if (logger == null) {
+ this.logger = Slf4jUtils.getLogger(clazz);
+ }
+ return logger;
+ }
+}
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..43b9254041
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggingUtils.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2025 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 java.util.Map;
+
+@InternalApi
+public class LoggingUtils {
+
+ private static boolean loggingEnabled = isLoggingEnabled();
+ static final String GOOGLE_SDK_JAVA_LOGGING = "GOOGLE_SDK_JAVA_LOGGING";
+
+ static boolean isLoggingEnabled() {
+ String enableLogging = System.getenv(GOOGLE_SDK_JAVA_LOGGING);
+ return "true".equalsIgnoreCase(enableLogging);
+ }
+
+ /**
+ * Sets logDataBuilder with service name, rpc name, endpoint and request headers based on logging
+ * level
+ *
+ * @param serviceName
+ * @param rpcName
+ * @param endpoint
+ * @param requestHeaders
+ * @param logDataBuilder
+ * @param loggerProvider
+ */
+ public static void recordServiceRpcAndRequestHeaders(
+ String serviceName,
+ String rpcName,
+ String endpoint,
+ Map requestHeaders,
+ LogData.Builder logDataBuilder,
+ LoggerProvider loggerProvider) {
+ if (loggingEnabled) {
+ Slf4jLoggingHelpers.recordServiceRpcAndRequestHeaders(
+ serviceName, rpcName, endpoint, requestHeaders, logDataBuilder, loggerProvider);
+ }
+ }
+
+ /**
+ * Sets logDataBuilder with response headers based on logging level
+ *
+ * @param headers
+ * @param logDataBuilder
+ * @param loggerProvider
+ */
+ public static void recordResponseHeaders(
+ Map headers, LogData.Builder logDataBuilder, LoggerProvider loggerProvider) {
+ if (loggingEnabled) {
+ Slf4jLoggingHelpers.recordResponseHeaders(headers, logDataBuilder, loggerProvider);
+ }
+ }
+
+ /**
+ * Sets logDataBuilder with respond payload based on logging level
+ *
+ * @param message
+ * @param logDataBuilder
+ * @param loggerProvider
+ * @param
+ */
+ public static void recordResponsePayload(
+ RespT message, LogData.Builder logDataBuilder, LoggerProvider loggerProvider) {
+ if (loggingEnabled) {
+ Slf4jLoggingHelpers.recordResponsePayload(message, logDataBuilder, loggerProvider);
+ }
+ }
+
+ /**
+ * Log response based on logging level configured
+ *
+ * @param status
+ * @param logDataBuilder
+ * @param loggerProvider
+ */
+ public static void logResponse(
+ String status, LogData.Builder logDataBuilder, LoggerProvider loggerProvider) {
+ if (loggingEnabled) {
+ Slf4jLoggingHelpers.logResponse(status, logDataBuilder, loggerProvider);
+ }
+ }
+
+ /**
+ * Log request based on logging level configured
+ *
+ * @param message
+ * @param logDataBuilder
+ * @param loggerProvider
+ * @param
+ */
+ public static void logRequest(
+ RespT message, LogData.Builder logDataBuilder, LoggerProvider loggerProvider) {
+ if (loggingEnabled) {
+ Slf4jLoggingHelpers.logRequest(message, logDataBuilder, loggerProvider);
+ }
+ }
+
+ public static void executeWithTryCatch(ThrowingRunnable action) {
+ try {
+ action.run();
+ } catch (Throwable t) {
+ // let logging exceptions fail silently
+ }
+ }
+
+ @FunctionalInterface
+ public interface ThrowingRunnable {
+ void run() throws Throwable;
+ }
+}
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/logging/Slf4jLoggingHelpers.java b/gax-java/gax/src/main/java/com/google/api/gax/logging/Slf4jLoggingHelpers.java
new file mode 100644
index 0000000000..2a914f4bf6
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/logging/Slf4jLoggingHelpers.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2025 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.common.base.Strings;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Message;
+import com.google.protobuf.util.JsonFormat;
+import java.util.Map;
+import java.util.function.Consumer;
+import org.slf4j.Logger;
+import org.slf4j.event.Level;
+
+/** Contains helper methods to log requests and responses */
+@InternalApi
+class Slf4jLoggingHelpers {
+
+ static final Gson gson = new Gson();
+
+ static Map messageToMapWithGson(Message message)
+ throws InvalidProtocolBufferException {
+ String json = JsonFormat.printer().print(message);
+ return gson.fromJson(json, new TypeToken