Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import java.nio.channels.UnresolvedAddressException;
import java.security.GeneralSecurityException;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.net.ssl.SSLHandshakeException;

Expand Down Expand Up @@ -112,15 +113,10 @@ enum ErrorType {
* </ol>
*
* @param error the Throwable from which to extract the error type string.
* @return a low-cardinality string representing the specific error type, or {@code null} if the
* provided error is {@code null} or non-determined.
* @return a low-cardinality string representing the specific error type
*/
// Requirement source: go/clo:product-requirements-v1
public static String extractErrorType(@Nullable Throwable error) {
if (error == null) {
// No information about the error; return null so the attribute is not recorded.
return null;
}
public static String extractErrorType(@Nonnull Throwable error) {

// 1. Unwrap standard wrapper exceptions if present
Throwable realError = getRealCause(error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import com.google.api.gax.logging.LoggerProvider;
import com.google.api.gax.logging.LoggingUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.rpc.ErrorInfo;
import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -77,17 +78,16 @@ void recordActionableError(Throwable error) {
}

Map<String, Object> logContext = new HashMap<>(apiTracerContext.getAttemptAttributes());
logContext.putAll(
ObservabilityUtils.getResponseAttributes(error, apiTracerContext.transport()));

logContext.put(
ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE,
ObservabilityUtils.extractStatus(error).toString());
if (!Strings.isNullOrEmpty(error.getMessage())) {
logContext.put(ObservabilityAttributes.EXCEPTION_MESSAGE_ATTRIBUTE, error.getMessage());
}

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()) {
if (!Strings.isNullOrEmpty(errorInfo.getDomain())) {
logContext.put(ObservabilityAttributes.ERROR_DOMAIN_ATTRIBUTE, errorInfo.getDomain());
}
if (errorInfo.getMetadataMap() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ public class ObservabilityAttributes {
/** If the error was caused by an exception, the exception class name. */
public static final String EXCEPTION_TYPE_ATTRIBUTE = "exception.type";

/** If the error was caused by an exception, the exception message. */
public static final String EXCEPTION_MESSAGE_ATTRIBUTE = "exception.message";

/** Size of the response body in bytes. */
public static final String HTTP_RESPONSE_BODY_SIZE = "http.response.body.size";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,6 @@

final class ObservabilityUtils {

/**
* Extracts a low-cardinality string representing the specific classification of the error to be
* used in the {@link ObservabilityAttributes#ERROR_TYPE_ATTRIBUTE} attribute. See {@link
* ErrorTypeUtil#extractErrorType} for extended documentation.
*/
static String extractErrorType(@Nullable Throwable error) {
return ErrorTypeUtil.extractErrorType(error);
}

/** Function to extract the status of the error as a canonical code. */
static StatusCode.Code extractStatus(@Nullable Throwable error) {
if (error == null) {
Expand Down Expand Up @@ -182,7 +173,7 @@ static Map<String, Object> getResponseAttributes(
}
if (error != null) {
attributes.put(
ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, ObservabilityUtils.extractErrorType(error));
ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, ErrorTypeUtil.extractErrorType(error));
attributes.put(ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE, error.getClass().getName());
}
return attributes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,6 @@

class ErrorTypeUtilTest {

@Test
void testExtractErrorType_null() {
assertThat(ErrorTypeUtil.extractErrorType(null)).isNull();
}

@Test
void testExtractErrorType_apiException_noReason() {
ApiException exception =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
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;
Expand Down Expand Up @@ -121,8 +120,6 @@ void testRecordActionableError_logsStatus() {
tracer.recordActionableError(error);

Map<String, ?> attributesMap = getAttributesMap();

assertTrue(attributesMap != null && !attributesMap.isEmpty());
assertEquals(
"INVALID_ARGUMENT",
attributesMap.get(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE));
Expand All @@ -138,8 +135,6 @@ void testRecordActionableError_logsAttributes() {
tracer.recordActionableError(error);

Map<String, ?> attributesMap = getAttributesMap();

assertTrue(attributesMap != null && !attributesMap.isEmpty());
assertEquals(
"test-service", attributesMap.get(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE));
}
Expand Down Expand Up @@ -172,8 +167,6 @@ void testRecordActionableError_logsErrorInfo() {
tracer.recordActionableError(error);

Map<String, ?> 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));
Expand All @@ -182,6 +175,45 @@ void testRecordActionableError_logsErrorInfo() {
attributesMap.get(ObservabilityAttributes.ERROR_METADATA_ATTRIBUTE_PREFIX + "test_key"));
}

@Test
void testRecordActionableError_logsExceptionDetails() {
ApiTracerContext context = ApiTracerContext.empty();
LoggingTracer tracer = new LoggingTracer(context);

Exception error = new RuntimeException("test error message");
tracer.recordActionableError(error);

Map<String, ?> attributesMap = getAttributesMap();
assertEquals(
"java.lang.RuntimeException",
attributesMap.get(ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE));
assertEquals(
"test error message",
attributesMap.get(ObservabilityAttributes.EXCEPTION_MESSAGE_ATTRIBUTE));
}

@Test
void testRecordActionableError_logsHttpStatus() {
ApiTracerContext context =
ApiTracerContext.empty().toBuilder().setTransport(ApiTracerContext.Transport.HTTP).build();
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<String, ?> attributesMap = getAttributesMap();
assertEquals(
"INVALID_ARGUMENT",
attributesMap.get(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE));
assertEquals(400L, attributesMap.get(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE));
}

private Map<String, ?> getAttributesMap() {
if (!testLogger.getMDCMap().isEmpty()) {
return testLogger.getMDCMap();
Expand Down
Loading