diff --git a/sdk-platform-java/gax-java/gax/pom.xml b/sdk-platform-java/gax-java/gax/pom.xml
index 7a03fad54d29..2feb9c9aba81 100644
--- a/sdk-platform-java/gax-java/gax/pom.xml
+++ b/sdk-platform-java/gax-java/gax/pom.xml
@@ -139,7 +139,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,!LoggingEnabledTest
+ !EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,!LoggingEnabledTest,!LoggingTracerTest
@@ -154,7 +154,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,LoggingEnabledTest
+ EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,LoggingEnabledTest,LoggingTracerTest
diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerContext.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerContext.java
index d6f7566c7461..49bce86cd1fe 100644
--- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerContext.java
+++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerContext.java
@@ -205,6 +205,9 @@ public Map getAttemptAttributes() {
attributes.put(ObservabilityAttributes.HTTP_URL_TEMPLATE_ATTRIBUTE, httpPathTemplate());
}
}
+ if (!Strings.isNullOrEmpty(serviceName())) {
+ attributes.put(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE, serviceName());
+ }
if (!Strings.isNullOrEmpty(destinationResourceId())) {
attributes.put(
ObservabilityAttributes.DESTINATION_RESOURCE_ID_ATTRIBUTE, destinationResourceId());
diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.java
new file mode 100644
index 000000000000..ec1a39c33337
--- /dev/null
+++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.java
@@ -0,0 +1,105 @@
+/*
+ * 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.common.annotations.VisibleForTesting;
+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
+class LoggingTracer extends BaseApiTracer {
+ private static final LoggerProvider LOGGER_PROVIDER =
+ LoggerProvider.forClazz(LoggingTracer.class);
+
+ private final ApiTracerContext apiTracerContext;
+
+ LoggingTracer(ApiTracerContext apiTracerContext) {
+ this.apiTracerContext = apiTracerContext;
+ }
+
+ @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);
+ }
+
+ @VisibleForTesting
+ void recordActionableError(Throwable error) {
+ if (error == null) {
+ return;
+ }
+
+ Map logContext = new HashMap<>(apiTracerContext.getAttemptAttributes());
+
+ logContext.put(
+ ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE,
+ ObservabilityUtils.extractStatus(error));
+
+ ErrorInfo errorInfo = ObservabilityUtils.extractErrorInfo(error);
+ if (errorInfo != null) {
+ if (errorInfo.getReason() != null && !errorInfo.getReason().isEmpty()) {
+ logContext.put(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, errorInfo.getReason());
+ }
+ if (errorInfo.getDomain() != null && !errorInfo.getDomain().isEmpty()) {
+ logContext.put(ObservabilityAttributes.ERROR_DOMAIN_ATTRIBUTE, errorInfo.getDomain());
+ }
+ if (errorInfo.getMetadataMap() != null) {
+ for (Map.Entry entry : errorInfo.getMetadataMap().entrySet()) {
+ logContext.put(
+ ObservabilityAttributes.ERROR_METADATA_ATTRIBUTE_PREFIX + entry.getKey(),
+ entry.getValue());
+ }
+ }
+ }
+
+ String message = error.getMessage() != null ? error.getMessage() : error.getClass().getName();
+ LoggingUtils.logActionableError(logContext, LOGGER_PROVIDER, message);
+ }
+}
diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java
new file mode 100644
index 000000000000..101500aaf53b
--- /dev/null
+++ b/sdk-platform-java/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(apiTracerContext.merge(context));
+ }
+
+ @Override
+ public ApiTracerContext getApiTracerContext() {
+ return apiTracerContext;
+ }
+
+ @Override
+ public ApiTracerFactory withContext(ApiTracerContext context) {
+ return new LoggingTracerFactory(apiTracerContext.merge(context));
+ }
+}
diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java
index c04c76362df1..2729e147d08a 100644
--- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java
+++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java
@@ -96,4 +96,13 @@ public class ObservabilityAttributes {
/** The destination resource id of the request (e.g. projects/p/locations/l/topics/t). */
public static final String DESTINATION_RESOURCE_ID_ATTRIBUTE = "gcp.resource.destination.id";
+
+ /** The type of error that occurred (e.g., from google.rpc.ErrorInfo.reason). */
+ public static final String ERROR_TYPE_ATTRIBUTE = "error.type";
+
+ /** The domain of the error (e.g., from google.rpc.ErrorInfo.domain). */
+ public static final String ERROR_DOMAIN_ATTRIBUTE = "gcp.errors.domain";
+
+ /** The prefix for error metadata (e.g., from google.rpc.ErrorInfo.metadata). */
+ public static final String ERROR_METADATA_ATTRIBUTE_PREFIX = "gcp.errors.metadata.";
}
diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java
index 2487964370ca..f0062e38505a 100644
--- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java
+++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java
@@ -31,6 +31,7 @@
import com.google.api.gax.rpc.ApiException;
import com.google.api.gax.rpc.StatusCode;
+import com.google.rpc.ErrorInfo;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import java.util.Map;
@@ -56,6 +57,18 @@ static String extractStatus(@Nullable Throwable error) {
return statusString;
}
+ /** Function to extract the ErrorInfo payload from the error, if available */
+ @Nullable
+ static ErrorInfo extractErrorInfo(@Nullable Throwable error) {
+ if (error instanceof ApiException) {
+ ApiException apiException = (ApiException) error;
+ if (apiException.getErrorDetails() != null) {
+ return apiException.getErrorDetails().getErrorInfo();
+ }
+ }
+ return null;
+ }
+
static Attributes toOtelAttributes(Map attributes) {
AttributesBuilder attributesBuilder = Attributes.builder();
if (attributes == null) {
diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/logging/TestLogger.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/logging/TestLogger.java
index 3ee7e513cd8f..36e8d47e4827 100644
--- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/logging/TestLogger.java
+++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/logging/TestLogger.java
@@ -48,8 +48,20 @@ public class TestLogger implements Logger, LoggingEventAware {
List messageList = new ArrayList<>();
Level level;
+ public List getMessageList() {
+ return messageList;
+ }
+
Map keyValuePairsMap = new HashMap<>();
+ public Map getMDCMap() {
+ return MDCMap;
+ }
+
+ public Map getKeyValuePairsMap() {
+ return keyValuePairsMap;
+ }
+
private String loggerName;
private boolean infoEnabled;
private boolean debugEnabled;
diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/logging/TestServiceProvider.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/logging/TestServiceProvider.java
index 69cc43ba2a96..2b7f0f3b8268 100644
--- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/logging/TestServiceProvider.java
+++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/logging/TestServiceProvider.java
@@ -30,12 +30,11 @@
package com.google.api.gax.logging;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import org.slf4j.ILoggerFactory;
import org.slf4j.IMarkerFactory;
+import org.slf4j.Logger;
import org.slf4j.spi.MDCAdapter;
import org.slf4j.spi.SLF4JServiceProvider;
@@ -45,12 +44,18 @@
*/
public class TestServiceProvider implements SLF4JServiceProvider {
+ private final ConcurrentMap loggers = new ConcurrentHashMap<>();
+ private final ILoggerFactory loggerFactory =
+ new ILoggerFactory() {
+ @Override
+ public Logger getLogger(String name) {
+ return loggers.computeIfAbsent(name, TestLogger::new);
+ }
+ };
+
@Override
public ILoggerFactory getLoggerFactory() {
- // mock behavior when provider present
- ILoggerFactory mockLoggerFactory = mock(ILoggerFactory.class);
- when(mockLoggerFactory.getLogger(anyString())).thenReturn(new TestLogger("test-logger"));
- return mockLoggerFactory;
+ return loggerFactory;
}
@Override
diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java
new file mode 100644
index 000000000000..db36746ab56a
--- /dev/null
+++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.empty().toBuilder().setServerAddress("address").build();
+ ApiTracerFactory updatedFactory = factory.withContext(context);
+
+ assertNotNull(updatedFactory);
+ assertTrue(updatedFactory instanceof LoggingTracerFactory);
+ assertEquals("address", updatedFactory.getApiTracerContext().serverAddress());
+ }
+}
diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerTest.java
new file mode 100644
index 000000000000..62e4506a5247
--- /dev/null
+++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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.assertTrue;
+
+import com.google.api.gax.logging.TestLogger;
+import com.google.api.gax.rpc.ApiExceptionFactory;
+import com.google.api.gax.rpc.ErrorDetails;
+import com.google.api.gax.rpc.StatusCode;
+import com.google.api.gax.rpc.testing.FakeStatusCode;
+import com.google.protobuf.Any;
+import com.google.rpc.ErrorInfo;
+import java.util.Collections;
+import java.util.Map;
+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);
+ testLogger.getMessageList().clear();
+ testLogger.getMDCMap().clear();
+ testLogger.getKeyValuePairsMap().clear();
+ }
+
+ @Test
+ void testAttemptFailedDuration_LogsError() {
+ ApiTracerContext context = ApiTracerContext.empty();
+ LoggingTracer tracer = new LoggingTracer(context);
+
+ Exception error = new RuntimeException("generic failure duration");
+ tracer.attemptFailedDuration(error, java.time.Duration.ZERO);
+
+ assertEquals(1, testLogger.getMessageList().size());
+ assertEquals("generic failure duration", testLogger.getMessageList().get(0));
+ }
+
+ @Test
+ void testAttemptFailedRetriesExhausted_LogsError() {
+ ApiTracerContext context = ApiTracerContext.empty();
+ LoggingTracer tracer = new LoggingTracer(context);
+
+ Exception error = new RuntimeException("generic failure retries exhausted");
+ tracer.attemptFailedRetriesExhausted(error);
+
+ assertEquals(1, testLogger.getMessageList().size());
+ assertEquals("generic failure retries exhausted", testLogger.getMessageList().get(0));
+ }
+
+ @Test
+ void testAttemptPermanentFailure_LogsError() {
+ ApiTracerContext context = ApiTracerContext.empty();
+ LoggingTracer tracer = new LoggingTracer(context);
+
+ Exception error = new RuntimeException("generic permanent failure");
+ tracer.attemptPermanentFailure(error);
+
+ assertEquals(1, testLogger.getMessageList().size());
+ assertEquals("generic permanent failure", testLogger.getMessageList().get(0));
+ }
+
+ @Test
+ void testRecordActionableError_logsErrorMessage() {
+ ApiTracerContext context = ApiTracerContext.empty();
+ LoggingTracer tracer = new LoggingTracer(context);
+
+ Exception error = new RuntimeException("test error message");
+ tracer.recordActionableError(error);
+
+ assertEquals(1, testLogger.getMessageList().size());
+ assertEquals("test error message", testLogger.getMessageList().get(0));
+ }
+
+ @Test
+ void testRecordActionableError_logsStatus() {
+ ApiTracerContext context = ApiTracerContext.empty();
+ LoggingTracer tracer = new LoggingTracer(context);
+
+ Exception error =
+ ApiExceptionFactory.createException(
+ "test error message",
+ new RuntimeException("cause"),
+ FakeStatusCode.of(StatusCode.Code.INVALID_ARGUMENT),
+ false);
+
+ tracer.recordActionableError(error);
+
+ Map attributesMap = getAttributesMap();
+
+ assertTrue(attributesMap != null && !attributesMap.isEmpty());
+ assertEquals(
+ "INVALID_ARGUMENT",
+ attributesMap.get(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE));
+ }
+
+ @Test
+ void testRecordActionableError_logsAttributes() {
+ ApiTracerContext context =
+ ApiTracerContext.empty().toBuilder().setServiceName("test-service").build();
+ LoggingTracer tracer = new LoggingTracer(context);
+
+ Exception error = new RuntimeException("generic failure");
+ tracer.recordActionableError(error);
+
+ Map attributesMap = getAttributesMap();
+
+ assertTrue(attributesMap != null && !attributesMap.isEmpty());
+ assertEquals(
+ "test-service", attributesMap.get(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE));
+ }
+
+ @Test
+ void testRecordActionableError_logsErrorInfo() {
+ ApiTracerContext context = ApiTracerContext.empty();
+ LoggingTracer tracer = new LoggingTracer(context);
+
+ ErrorInfo errorInfo =
+ ErrorInfo.newBuilder()
+ .setReason("TEST_REASON")
+ .setDomain("test.domain.com")
+ .putMetadata("test_key", "test_value")
+ .build();
+
+ ErrorDetails errorDetails =
+ ErrorDetails.builder()
+ .setRawErrorMessages(Collections.singletonList(Any.pack(errorInfo)))
+ .build();
+
+ Exception error =
+ ApiExceptionFactory.createException(
+ "test error message",
+ new RuntimeException("cause"),
+ FakeStatusCode.of(StatusCode.Code.INVALID_ARGUMENT),
+ false,
+ errorDetails);
+
+ tracer.recordActionableError(error);
+
+ Map attributesMap = getAttributesMap();
+
+ assertTrue(attributesMap != null && !attributesMap.isEmpty());
+ assertEquals("TEST_REASON", attributesMap.get(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE));
+ assertEquals(
+ "test.domain.com", attributesMap.get(ObservabilityAttributes.ERROR_DOMAIN_ATTRIBUTE));
+ assertEquals(
+ "test_value",
+ attributesMap.get(ObservabilityAttributes.ERROR_METADATA_ATTRIBUTE_PREFIX + "test_key"));
+ }
+
+ private Map getAttributesMap() {
+ if (!testLogger.getMDCMap().isEmpty()) {
+ return testLogger.getMDCMap();
+ } else {
+ return testLogger.getKeyValuePairsMap();
+ }
+ }
+}