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/main/java/com/google/api/gax/rpc/ClientContext.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java index 43fdd848b6..7f07b633f1 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java @@ -281,6 +281,13 @@ public static ClientContext create(StubSettings settings) throws IOException { if (apiTracerFactory instanceof SpanTracerFactory) { apiTracerFactory = apiTracerFactory.withContext(apiTracerContext); } + if (com.google.api.gax.logging.LoggingUtils.isLoggingV2Enabled()) { + com.google.api.gax.tracing.ApiTracerFactory loggingTracerFactory = + new com.google.api.gax.tracing.LoggingTracerFactory().withContext(apiTracerContext); + apiTracerFactory = + new com.google.api.gax.tracing.CompositeTracerFactory( + apiTracerFactory, loggingTracerFactory); + } return newBuilder() .setBackgroundResources(backgroundResources.build()) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java new file mode 100644 index 0000000000..b2856931f2 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java @@ -0,0 +1,183 @@ +/* + * 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 java.util.List; + +/** A composite {@link ApiTracer} that delegates to a list of {@link ApiTracer}s. */ +@BetaApi +@InternalApi +public class CompositeTracer implements ApiTracer { + private final List tracers; + + public CompositeTracer(List tracers) { + this.tracers = tracers; + } + + @Override + public Scope inScope() { + // Returning a scope that closes all sub-scopes + final Scope[] scopes = new Scope[tracers.size()]; + for (int i = 0; i < tracers.size(); i++) { + scopes[i] = tracers.get(i).inScope(); + } + return () -> { + for (Scope scope : scopes) { + if (scope != null) { + scope.close(); + } + } + }; + } + + @Override + public void operationSucceeded() { + for (ApiTracer tracer : tracers) { + tracer.operationSucceeded(); + } + } + + @Override + public void operationCancelled() { + for (ApiTracer tracer : tracers) { + tracer.operationCancelled(); + } + } + + @Override + public void operationFailed(Throwable error) { + for (ApiTracer tracer : tracers) { + tracer.operationFailed(error); + } + } + + @Override + public void connectionSelected(String id) { + for (ApiTracer tracer : tracers) { + tracer.connectionSelected(id); + } + } + + @Override + @SuppressWarnings("deprecation") + public void attemptStarted(int attemptNumber) { + for (ApiTracer tracer : tracers) { + tracer.attemptStarted(attemptNumber); + } + } + + @Override + public void attemptStarted(Object request, int attemptNumber) { + for (ApiTracer tracer : tracers) { + tracer.attemptStarted(request, attemptNumber); + } + } + + @Override + public void attemptSucceeded() { + for (ApiTracer tracer : tracers) { + tracer.attemptSucceeded(); + } + } + + @Override + public void attemptCancelled() { + for (ApiTracer tracer : tracers) { + tracer.attemptCancelled(); + } + } + + @Override + @SuppressWarnings("deprecation") + public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) { + for (ApiTracer tracer : tracers) { + tracer.attemptFailed(error, delay); + } + } + + @Override + public void attemptFailedDuration(Throwable error, java.time.Duration delay) { + for (ApiTracer tracer : tracers) { + tracer.attemptFailedDuration(error, delay); + } + } + + @Override + public void attemptFailedRetriesExhausted(Throwable error) { + for (ApiTracer tracer : tracers) { + tracer.attemptFailedRetriesExhausted(error); + } + } + + @Override + public void attemptPermanentFailure(Throwable error) { + for (ApiTracer tracer : tracers) { + tracer.attemptPermanentFailure(error); + } + } + + @Override + public void lroStartFailed(Throwable error) { + for (ApiTracer tracer : tracers) { + tracer.lroStartFailed(error); + } + } + + @Override + public void lroStartSucceeded() { + for (ApiTracer tracer : tracers) { + tracer.lroStartSucceeded(); + } + } + + @Override + public void responseReceived() { + for (ApiTracer tracer : tracers) { + tracer.responseReceived(); + } + } + + @Override + public void requestSent() { + for (ApiTracer tracer : tracers) { + tracer.requestSent(); + } + } + + @Override + public void batchRequestSent(long elementCount, long requestSize) { + for (ApiTracer tracer : tracers) { + tracer.batchRequestSent(elementCount, requestSize); + } + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracerFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracerFactory.java new file mode 100644 index 0000000000..3261957b99 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracerFactory.java @@ -0,0 +1,94 @@ +/* + * 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 java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A composite {@link ApiTracerFactory} that creates a {@link CompositeTracer} containing multiple + * {@link ApiTracer} instances. + */ +@BetaApi +@InternalApi +public class CompositeTracerFactory implements ApiTracerFactory { + private final List factories; + private final ApiTracerContext apiTracerContext; + + public CompositeTracerFactory(ApiTracerFactory... factories) { + this(Arrays.asList(factories), ApiTracerContext.empty()); + } + + public CompositeTracerFactory(List factories) { + this(factories, ApiTracerContext.empty()); + } + + private CompositeTracerFactory( + List factories, ApiTracerContext apiTracerContext) { + this.factories = factories; + this.apiTracerContext = apiTracerContext; + } + + @Override + public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { + List tracers = new ArrayList<>(factories.size()); + for (ApiTracerFactory factory : factories) { + tracers.add(factory.newTracer(parent, spanName, operationType)); + } + return new CompositeTracer(tracers); + } + + @Override + public ApiTracer newTracer(ApiTracer parent, ApiTracerContext context) { + List tracers = new ArrayList<>(factories.size()); + for (ApiTracerFactory factory : factories) { + tracers.add(factory.newTracer(parent, context)); + } + return new CompositeTracer(tracers); + } + + @Override + public ApiTracerContext getApiTracerContext() { + return apiTracerContext; + } + + @Override + public ApiTracerFactory withContext(ApiTracerContext context) { + List updatedFactories = new ArrayList<>(factories.size()); + for (ApiTracerFactory factory : factories) { + updatedFactories.add(factory.withContext(context)); + } + return new CompositeTracerFactory(updatedFactories, apiTracerContext.merge(context)); + } +} 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..aed5c5e617 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.java @@ -0,0 +1,123 @@ +/* + * 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 instanceof ApiException) { + ApiException apiException = (ApiException) error; + logContext.put( + ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, + apiException.getStatusCode().getCode().toString()); + + 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 = error.getMessage() != null ? error.getMessage() : error.getClass().getName(); + LoggingUtils.logActionableError( + logContext, LOGGER_PROVIDER, org.slf4j.event.Level.INFO, 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/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(); + } }