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..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 @@ -36,11 +36,31 @@ @InternalApi public class LoggingUtils { - private static boolean loggingEnabled = isLoggingEnabled(); static final String GOOGLE_SDK_JAVA_LOGGING = "GOOGLE_SDK_JAVA_LOGGING"; + private static boolean loggingEnabled = checkLoggingEnabled(GOOGLE_SDK_JAVA_LOGGING); + + /** + * Returns whether client-side logging is enabled. + * + * @return true if logging is enabled, false otherwise. + */ static boolean isLoggingEnabled() { - String enableLogging = System.getenv(GOOGLE_SDK_JAVA_LOGGING); + return loggingEnabled; + } + + /** + * Sets whether client-side logging is enabled. Visible for testing. + * + * @param enabled true to enable logging, false to disable. + */ + @com.google.common.annotations.VisibleForTesting + static void setLoggingEnabled(boolean enabled) { + loggingEnabled = enabled; + } + + private static boolean checkLoggingEnabled(String envVar) { + String enableLogging = System.getenv(envVar); return "true".equalsIgnoreCase(enableLogging); } @@ -126,6 +146,22 @@ 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 message The human-readable error message. + */ + public static void logActionableError( + Map logContext, LoggerProvider loggerProvider, String message) { + if (loggingEnabled) { + org.slf4j.Logger logger = loggerProvider.getLogger(); + Slf4jUtils.log(logger, org.slf4j.event.Level.DEBUG, logContext, message); + } + } + public static void executeWithTryCatch(ThrowingRunnable action) { try { action.run(); 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..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 @@ -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.setLoggingEnabled(false); + LoggerProvider loggerProvider = mock(LoggerProvider.class); + + LoggingUtils.logActionableError( + Collections.emptyMap(), loggerProvider, "message"); + + verify(loggerProvider, never()).getLogger(); + } + + @Test + void testLogActionableError_success() { + 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.atDebug()).thenReturn(eventBuilder); + when(eventBuilder.addKeyValue(anyString(), any())).thenReturn(eventBuilder); + + Map context = Collections.singletonMap("key", "value"); + LoggingUtils.logActionableError(context, loggerProvider, "message"); + + verify(loggerProvider).getLogger(); + } }