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(); + } }