From ed367ec763cfe1b1407c2b8071336f8461334530 Mon Sep 17 00:00:00 2001 From: Wes Tarle Date: Sat, 14 Mar 2026 22:45:01 -0400 Subject: [PATCH 1/7] feat(gax): add utilities for logging actionable errors --- .../google/api/gax/logging/LoggingUtils.java | 65 ++++++++++++++++++- .../api/gax/logging/Slf4jLoggingHelpers.java | 5 +- .../api/gax/logging/LoggingUtilsTest.java | 42 ++++++++++++ 3 files changed, 107 insertions(+), 5 deletions(-) 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 index 43b9254041..b1e90b2494 100644 --- 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 @@ -36,11 +36,50 @@ @InternalApi public class LoggingUtils { - private static boolean loggingEnabled = isLoggingEnabled(); static final String GOOGLE_SDK_JAVA_LOGGING = "GOOGLE_SDK_JAVA_LOGGING"; + static final String GOOGLE_SDK_JAVA_LOGGING_V2 = "GOOGLE_SDK_JAVA_LOGGING_V2"; - static boolean isLoggingEnabled() { - String enableLogging = System.getenv(GOOGLE_SDK_JAVA_LOGGING); + private static boolean loggingEnabled = checkLoggingEnabled(GOOGLE_SDK_JAVA_LOGGING); + private static boolean loggingV2Enabled = checkLoggingEnabled(GOOGLE_SDK_JAVA_LOGGING_V2); + + /** + * Returns whether client-side logging is enabled (V1 or V2). + * + * @return true if logging is enabled, false otherwise. + */ + public static boolean isLoggingEnabled() { + return loggingEnabled || loggingV2Enabled; + } + + /** + * Returns whether client-side logging V2 (Actionable Errors) is enabled. + * + * @return true if V2 logging is enabled, false otherwise. + */ + public static boolean isLoggingV2Enabled() { + return loggingV2Enabled; + } + + /** + * Sets whether client-side logging is enabled. Visible for testing. + * + * @param enabled true to enable logging, false to disable. + */ + public static void setLoggingEnabled(boolean enabled) { + loggingEnabled = enabled; + } + + /** + * Sets whether client-side logging V2 is enabled. Visible for testing. + * + * @param enabled true to enable logging, false to disable. + */ + public static void setLoggingV2Enabled(boolean enabled) { + loggingV2Enabled = enabled; + } + + private static boolean checkLoggingEnabled(String envVar) { + String enableLogging = System.getenv(envVar); return "true".equalsIgnoreCase(enableLogging); } @@ -126,6 +165,26 @@ public static void logRequest( } } + /** + * Logs an actionable error message with structured context at a specific log level. + * + * @param logContext A map containing the structured logging context (e.g., RPC service, method, + * error details). + * @param loggerProvider The provider used to obtain the logger. + * @param level The slf4j level to log the actionable error at. + * @param message The human-readable error message. + */ + public static void logActionableError( + Map logContext, + LoggerProvider loggerProvider, + org.slf4j.event.Level level, + String message) { + if (loggingV2Enabled) { + org.slf4j.Logger logger = loggerProvider.getLogger(); + Slf4jUtils.log(logger, level, logContext, message); + } + } + public static void executeWithTryCatch(ThrowingRunnable action) { try { action.run(); 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 index 2a914f4bf6..85fda43c66 100644 --- 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 @@ -114,10 +114,11 @@ static void logResponse( LoggingUtils.executeWithTryCatch( () -> { Logger logger = loggerProvider.getLogger(); - if (logger.isInfoEnabled()) { + boolean isV2 = LoggingUtils.isLoggingV2Enabled(); + if (!isV2 && logger.isInfoEnabled()) { logDataBuilder.responseStatus(status); } - if (logger.isInfoEnabled() && !logger.isDebugEnabled()) { + if (!isV2 && logger.isInfoEnabled() && !logger.isDebugEnabled()) { Map responseData = logDataBuilder.build().toMapResponse(); Slf4jUtils.log(logger, Level.INFO, responseData, "Received response"); } 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 index 9e3099e929..08dc3e2421 100644 --- 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 @@ -33,11 +33,20 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import com.google.api.gax.logging.LoggingUtils.ThrowingRunnable; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import org.slf4j.Logger; class LoggingUtilsTest { @@ -77,4 +86,37 @@ void testExecuteWithTryCatch_WithNoSuchMethodError() throws Throwable { // Verify that the action was executed (despite the error) verify(action).run(); } + + @AfterEach + void tearDown() { + LoggingUtils.setLoggingEnabled(false); + } + + @Test + void testLogActionableError_loggingDisabled() { + LoggingUtils.setLoggingV2Enabled(false); + LoggerProvider loggerProvider = mock(LoggerProvider.class); + + LoggingUtils.logActionableError( + Collections.emptyMap(), loggerProvider, org.slf4j.event.Level.INFO, "message"); + + verify(loggerProvider, never()).getLogger(); + } + + @Test + void testLogActionableError_success() { + LoggingUtils.setLoggingV2Enabled(true); + LoggerProvider loggerProvider = mock(LoggerProvider.class); + Logger logger = mock(Logger.class); + when(loggerProvider.getLogger()).thenReturn(logger); + + org.slf4j.spi.LoggingEventBuilder eventBuilder = mock(org.slf4j.spi.LoggingEventBuilder.class); + when(logger.atInfo()).thenReturn(eventBuilder); + when(eventBuilder.addKeyValue(anyString(), any())).thenReturn(eventBuilder); + + Map context = Collections.singletonMap("key", "value"); + LoggingUtils.logActionableError(context, loggerProvider, org.slf4j.event.Level.INFO, "message"); + + verify(loggerProvider).getLogger(); + } } From 741cb2c1f09c94d23156931f4db3cc9078d607eb Mon Sep 17 00:00:00 2001 From: Wes Tarle Date: Sun, 22 Mar 2026 12:24:33 -0400 Subject: [PATCH 2/7] fix(gax): make isLoggingEnabled and setLoggingEnabled package private --- .../main/java/com/google/api/gax/logging/LoggingUtils.java | 2 +- .../java/com/google/api/gax/logging/LoggingUtilsTest.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) 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 index b1e90b2494..c42bc4a736 100644 --- 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 @@ -179,7 +179,7 @@ public static void logActionableError( LoggerProvider loggerProvider, org.slf4j.event.Level level, String message) { - if (loggingV2Enabled) { + if (loggingEnabled) { org.slf4j.Logger logger = loggerProvider.getLogger(); Slf4jUtils.log(logger, level, logContext, message); } 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 index 08dc3e2421..cffe2b6d2c 100644 --- 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 @@ -94,11 +94,11 @@ void tearDown() { @Test void testLogActionableError_loggingDisabled() { - LoggingUtils.setLoggingV2Enabled(false); + LoggingUtils.setLoggingEnabled(false); LoggerProvider loggerProvider = mock(LoggerProvider.class); LoggingUtils.logActionableError( - Collections.emptyMap(), loggerProvider, org.slf4j.event.Level.INFO, "message"); + Collections.emptyMap(), loggerProvider, org.slf4j.event.Level.DEBUG, "message"); verify(loggerProvider, never()).getLogger(); } @@ -120,3 +120,4 @@ void testLogActionableError_success() { verify(loggerProvider).getLogger(); } } + From 8294d786c0a8eea0a35eae453e14c8aacbe549f7 Mon Sep 17 00:00:00 2001 From: Wes Tarle Date: Sun, 22 Mar 2026 12:31:58 -0400 Subject: [PATCH 3/7] fix(gax): make isLoggingEnabled and setLoggingEnabled package private --- .../google/api/gax/logging/LoggingUtils.java | 29 ++++--------------- .../api/gax/logging/Slf4jLoggingHelpers.java | 5 ++-- .../api/gax/logging/LoggingUtilsTest.java | 6 ++-- 3 files changed, 10 insertions(+), 30 deletions(-) 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 index c42bc4a736..59ba3deaa0 100644 --- 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 @@ -37,27 +37,16 @@ public class LoggingUtils { static final String GOOGLE_SDK_JAVA_LOGGING = "GOOGLE_SDK_JAVA_LOGGING"; - static final String GOOGLE_SDK_JAVA_LOGGING_V2 = "GOOGLE_SDK_JAVA_LOGGING_V2"; private static boolean loggingEnabled = checkLoggingEnabled(GOOGLE_SDK_JAVA_LOGGING); - private static boolean loggingV2Enabled = checkLoggingEnabled(GOOGLE_SDK_JAVA_LOGGING_V2); /** - * Returns whether client-side logging is enabled (V1 or V2). + * Returns whether client-side logging is enabled. * * @return true if logging is enabled, false otherwise. */ - public static boolean isLoggingEnabled() { - return loggingEnabled || loggingV2Enabled; - } - - /** - * Returns whether client-side logging V2 (Actionable Errors) is enabled. - * - * @return true if V2 logging is enabled, false otherwise. - */ - public static boolean isLoggingV2Enabled() { - return loggingV2Enabled; + static boolean isLoggingEnabled() { + return loggingEnabled; } /** @@ -65,19 +54,11 @@ public static boolean isLoggingV2Enabled() { * * @param enabled true to enable logging, false to disable. */ - public static void setLoggingEnabled(boolean enabled) { + @com.google.common.annotations.VisibleForTesting + static void setLoggingEnabled(boolean enabled) { loggingEnabled = enabled; } - /** - * Sets whether client-side logging V2 is enabled. Visible for testing. - * - * @param enabled true to enable logging, false to disable. - */ - public static void setLoggingV2Enabled(boolean enabled) { - loggingV2Enabled = enabled; - } - private static boolean checkLoggingEnabled(String envVar) { String enableLogging = System.getenv(envVar); return "true".equalsIgnoreCase(enableLogging); 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 index 85fda43c66..2a914f4bf6 100644 --- 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 @@ -114,11 +114,10 @@ static void logResponse( LoggingUtils.executeWithTryCatch( () -> { Logger logger = loggerProvider.getLogger(); - boolean isV2 = LoggingUtils.isLoggingV2Enabled(); - if (!isV2 && logger.isInfoEnabled()) { + if (logger.isInfoEnabled()) { logDataBuilder.responseStatus(status); } - if (!isV2 && logger.isInfoEnabled() && !logger.isDebugEnabled()) { + if (logger.isInfoEnabled() && !logger.isDebugEnabled()) { Map responseData = logDataBuilder.build().toMapResponse(); Slf4jUtils.log(logger, Level.INFO, responseData, "Received response"); } 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 index cffe2b6d2c..46ecc2c461 100644 --- 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 @@ -105,17 +105,17 @@ void testLogActionableError_loggingDisabled() { @Test void testLogActionableError_success() { - LoggingUtils.setLoggingV2Enabled(true); + LoggingUtils.setLoggingEnabled(true); LoggerProvider loggerProvider = mock(LoggerProvider.class); Logger logger = mock(Logger.class); when(loggerProvider.getLogger()).thenReturn(logger); org.slf4j.spi.LoggingEventBuilder eventBuilder = mock(org.slf4j.spi.LoggingEventBuilder.class); - when(logger.atInfo()).thenReturn(eventBuilder); + when(logger.atDebug()).thenReturn(eventBuilder); when(eventBuilder.addKeyValue(anyString(), any())).thenReturn(eventBuilder); Map context = Collections.singletonMap("key", "value"); - LoggingUtils.logActionableError(context, loggerProvider, org.slf4j.event.Level.INFO, "message"); + LoggingUtils.logActionableError(context, loggerProvider, org.slf4j.event.Level.DEBUG, "message"); verify(loggerProvider).getLogger(); } From 5d6bc5fe332be38eea328d5a414692620741bb9c Mon Sep 17 00:00:00 2001 From: Wes Tarle Date: Sun, 22 Mar 2026 13:18:42 -0400 Subject: [PATCH 4/7] fix: Actionable error utility determines DEBUG log level internally --- .../java/com/google/api/gax/logging/LoggingUtils.java | 8 ++------ .../java/com/google/api/gax/logging/LoggingUtilsTest.java | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) 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 index 59ba3deaa0..0797527bc8 100644 --- 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 @@ -152,17 +152,13 @@ public static void logRequest( * @param logContext A map containing the structured logging context (e.g., RPC service, method, * error details). * @param loggerProvider The provider used to obtain the logger. - * @param level The slf4j level to log the actionable error at. * @param message The human-readable error message. */ public static void logActionableError( - Map logContext, - LoggerProvider loggerProvider, - org.slf4j.event.Level level, - String message) { + Map logContext, LoggerProvider loggerProvider, String message) { if (loggingEnabled) { org.slf4j.Logger logger = loggerProvider.getLogger(); - Slf4jUtils.log(logger, level, logContext, message); + Slf4jUtils.log(logger, org.slf4j.event.Level.DEBUG, logContext, message); } } 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 index 46ecc2c461..d372282c15 100644 --- 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 @@ -115,7 +115,7 @@ void testLogActionableError_success() { when(eventBuilder.addKeyValue(anyString(), any())).thenReturn(eventBuilder); Map context = Collections.singletonMap("key", "value"); - LoggingUtils.logActionableError(context, loggerProvider, org.slf4j.event.Level.DEBUG, "message"); + LoggingUtils.logActionableError(context, loggerProvider, "message"); verify(loggerProvider).getLogger(); } From 38838d1a15a129067f9dcd6e90b0b031c0c56ef8 Mon Sep 17 00:00:00 2001 From: Wes Tarle Date: Sun, 22 Mar 2026 13:35:08 -0400 Subject: [PATCH 5/7] fix: Actionable error utility determines DEBUG log level internally --- .../test/java/com/google/api/gax/logging/LoggingUtilsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index d372282c15..134d0c42ef 100644 --- 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 @@ -98,7 +98,7 @@ void testLogActionableError_loggingDisabled() { LoggerProvider loggerProvider = mock(LoggerProvider.class); LoggingUtils.logActionableError( - Collections.emptyMap(), loggerProvider, org.slf4j.event.Level.DEBUG, "message"); + Collections.emptyMap(), loggerProvider, "message"); verify(loggerProvider, never()).getLogger(); } From b51ef3c3a790f1d9c9c5b714d47fd19f9c5982c9 Mon Sep 17 00:00:00 2001 From: Wes Tarle Date: Sun, 22 Mar 2026 13:52:03 -0400 Subject: [PATCH 6/7] style(gax): format LoggingUtils --- .../test/java/com/google/api/gax/logging/LoggingUtilsTest.java | 1 - 1 file changed, 1 deletion(-) 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 index 134d0c42ef..e3acf17637 100644 --- 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 @@ -120,4 +120,3 @@ void testLogActionableError_success() { verify(loggerProvider).getLogger(); } } - From 5d4629913eff844e42798914c222bd3be7b4a7da Mon Sep 17 00:00:00 2001 From: Wes Tarle Date: Mon, 23 Mar 2026 15:47:53 -0400 Subject: [PATCH 7/7] feat(gax): implement actionable errors logging in ApiTracer framework --- .../google/api/gax/tracing/LoggingTracer.java | 127 ++++++++++++++++++ .../api/gax/tracing/LoggingTracerFactory.java | 69 ++++++++++ .../gax/tracing/LoggingTracerFactoryTest.java | 73 ++++++++++ .../api/gax/tracing/LoggingTracerTest.java | 60 +++++++++ 4 files changed, 329 insertions(+) create mode 100644 gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.java create mode 100644 gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java create mode 100644 gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java create mode 100644 gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerTest.java diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.java new file mode 100644 index 0000000000..6676bcdde0 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.java @@ -0,0 +1,127 @@ +/* + * Copyright 2026 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.tracing; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.api.gax.logging.LoggerProvider; +import com.google.api.gax.logging.LoggingUtils; +import com.google.api.gax.rpc.ApiException; +import com.google.rpc.ErrorInfo; +import java.util.HashMap; +import java.util.Map; + +/** + * An {@link ApiTracer} that logs actionable errors using {@link LoggingUtils} when an RPC attempt + * fails. + */ +@BetaApi +@InternalApi +public class LoggingTracer extends BaseApiTracer { + private static final LoggerProvider LOGGER_PROVIDER = + LoggerProvider.forClazz(LoggingTracer.class); + + private final ApiTracerContext apiTracerContext; + + public LoggingTracer(ApiTracerContext apiTracerContext) { + this.apiTracerContext = apiTracerContext; + } + + @Override + public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) { + recordActionableError(error); + } + + @Override + public void attemptFailedDuration(Throwable error, java.time.Duration delay) { + recordActionableError(error); + } + + @Override + public void attemptFailedRetriesExhausted(Throwable error) { + recordActionableError(error); + } + + @Override + public void attemptPermanentFailure(Throwable error) { + recordActionableError(error); + } + + private void recordActionableError(Throwable error) { + Map logContext = new HashMap<>(); + + if (apiTracerContext.rpcSystemName() != null) { + logContext.put( + ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE, apiTracerContext.rpcSystemName()); + } + if (apiTracerContext.fullMethodName() != null) { + logContext.put( + ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE, apiTracerContext.fullMethodName()); + } + if (apiTracerContext.serverPort() != null) { + logContext.put(ObservabilityAttributes.SERVER_PORT_ATTRIBUTE, apiTracerContext.serverPort()); + } + if (apiTracerContext.libraryMetadata() != null + && !apiTracerContext.libraryMetadata().isEmpty()) { + if (apiTracerContext.libraryMetadata().repository() != null) { + logContext.put( + ObservabilityAttributes.REPO_ATTRIBUTE, + apiTracerContext.libraryMetadata().repository()); + } + } + + if (error != null) { + logContext.put( + ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, + ObservabilityUtils.extractStatus(error)); + } + + if (error instanceof ApiException) { + ApiException apiException = (ApiException) error; + if (apiException.getErrorDetails() != null) { + ErrorInfo errorInfo = apiException.getErrorDetails().getErrorInfo(); + if (errorInfo != null) { + logContext.put("error.type", errorInfo.getReason()); + logContext.put("gcp.errors.domain", errorInfo.getDomain()); + for (Map.Entry entry : errorInfo.getMetadataMap().entrySet()) { + logContext.put("gcp.errors.metadata." + entry.getKey(), entry.getValue()); + } + } + } + } + + String message = "Unknown Error"; + if (error != null) { + message = error.getMessage() != null ? error.getMessage() : error.getClass().getName(); + } + LoggingUtils.logActionableError(logContext, LOGGER_PROVIDER, message); + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java new file mode 100644 index 0000000000..08e50aa0bb --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java @@ -0,0 +1,69 @@ +/* + * Copyright 2026 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.tracing; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; + +/** A {@link ApiTracerFactory} that creates instances of {@link LoggingTracer}. */ +@BetaApi +@InternalApi +public class LoggingTracerFactory implements ApiTracerFactory { + private final ApiTracerContext apiTracerContext; + + public LoggingTracerFactory() { + this(ApiTracerContext.empty()); + } + + private LoggingTracerFactory(ApiTracerContext apiTracerContext) { + this.apiTracerContext = apiTracerContext; + } + + @Override + public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { + return new LoggingTracer(apiTracerContext); + } + + @Override + public ApiTracer newTracer(ApiTracer parent, ApiTracerContext context) { + return new LoggingTracer(context); + } + + @Override + public ApiTracerContext getApiTracerContext() { + return apiTracerContext; + } + + @Override + public ApiTracerFactory withContext(ApiTracerContext context) { + return new LoggingTracerFactory(apiTracerContext.merge(context)); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java new file mode 100644 index 0000000000..fedfe7b8ec --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2026 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.tracing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class LoggingTracerFactoryTest { + + @Test + void testNewTracer_CreatesLoggingTracer() { + LoggingTracerFactory factory = new LoggingTracerFactory(); + ApiTracer tracer = + factory.newTracer( + BaseApiTracer.getInstance(), + SpanName.of("client", "method"), + ApiTracerFactory.OperationType.Unary); + + assertNotNull(tracer); + assertTrue(tracer instanceof LoggingTracer); + } + + @Test + void testNewTracer_WithContext_CreatesLoggingTracer() { + LoggingTracerFactory factory = new LoggingTracerFactory(); + ApiTracer tracer = factory.newTracer(BaseApiTracer.getInstance(), ApiTracerContext.empty()); + + assertNotNull(tracer); + assertTrue(tracer instanceof LoggingTracer); + } + + @Test + void testWithContext_ReturnsNewFactoryWithMergedContext() { + LoggingTracerFactory factory = new LoggingTracerFactory(); + ApiTracerContext context = ApiTracerContext.newBuilder().setServerAddress("address").build(); + ApiTracerFactory updatedFactory = factory.withContext(context); + + assertNotNull(updatedFactory); + assertTrue(updatedFactory instanceof LoggingTracerFactory); + assertEquals("address", updatedFactory.getApiTracerContext().serverAddress()); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerTest.java new file mode 100644 index 0000000000..ec0b86a25c --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2026 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.tracing; + +import com.google.api.gax.logging.TestLogger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +class LoggingTracerTest { + + private TestLogger testLogger; + + @BeforeEach + void setUp() { + testLogger = (TestLogger) LoggerFactory.getLogger(LoggingTracer.class); + } + + @Test + void testAttemptFailed_LogsError() { + ApiTracerContext context = ApiTracerContext.empty(); + LoggingTracer tracer = new LoggingTracer(context); + + // Call attemptFailed with a generic exception + Exception error = new RuntimeException("generic failure"); + tracer.attemptFailed(error, org.threeten.bp.Duration.ZERO); + + // To prevent failing due to disabled logging or other missing context, + // we don't strictly assert the contents of the log here if the logger isn't enabled. + // The main verification is that calling attemptFailed doesn't throw. + } +}