diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosDiagnostics.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosDiagnostics.java index 8eff237e0a42..09ee3932efbc 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosDiagnostics.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosDiagnostics.java @@ -8,6 +8,7 @@ import com.azure.cosmos.util.Beta; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,11 +22,13 @@ public final class CosmosDiagnostics { private static final Logger LOGGER = LoggerFactory.getLogger(CosmosDiagnostics.class); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final String COSMOS_DIAGNOSTICS_KEY = "cosmosDiagnostics"; private ClientSideRequestStatistics clientSideRequestStatistics; private FeedResponseDiagnostics feedResponseDiagnostics; static final String USER_AGENT = Utils.getUserAgent(); + static final String USER_AGENT_KEY = "userAgent"; CosmosDiagnostics(DiagnosticsClientContext diagnosticsClientContext) { this.clientSideRequestStatistics = new ClientSideRequestStatistics(diagnosticsClientContext); @@ -52,16 +55,7 @@ CosmosDiagnostics clientSideRequestStatistics(ClientSideRequestStatistics client @Override public String toString() { StringBuilder stringBuilder = new StringBuilder(); - if (this.feedResponseDiagnostics != null) { - stringBuilder.append("userAgent=").append(USER_AGENT).append(System.lineSeparator()); - stringBuilder.append(feedResponseDiagnostics); - } else { - try { - stringBuilder.append(OBJECT_MAPPER.writeValueAsString(this.clientSideRequestStatistics)); - } catch (JsonProcessingException e) { - LOGGER.error("Error while parsing diagnostics " + e); - } - } + fillCosmosDiagnostics(null, stringBuilder); return stringBuilder.toString(); } @@ -92,4 +86,30 @@ public Set getRegionsContacted() { FeedResponseDiagnostics getFeedResponseDiagnostics() { return feedResponseDiagnostics; } + + void fillCosmosDiagnostics(ObjectNode parentNode, StringBuilder stringBuilder) { + if (this.feedResponseDiagnostics != null) { + if (parentNode != null) { + parentNode.put(USER_AGENT_KEY, USER_AGENT); + parentNode.putPOJO(COSMOS_DIAGNOSTICS_KEY, feedResponseDiagnostics); + } + + if (stringBuilder != null) { + stringBuilder.append(USER_AGENT_KEY +"=").append(USER_AGENT).append(System.lineSeparator()); + stringBuilder.append(feedResponseDiagnostics); + } + } else { + if (parentNode != null) { + parentNode.putPOJO(COSMOS_DIAGNOSTICS_KEY, clientSideRequestStatistics); + } + + if (stringBuilder != null) { + try { + stringBuilder.append(OBJECT_MAPPER.writeValueAsString(this.clientSideRequestStatistics)); + } catch (JsonProcessingException e) { + LOGGER.error("Error while parsing diagnostics ", e); + } + } + } + } } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosException.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosException.java index 4f92e55bc8ef..174442983e22 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosException.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosException.java @@ -5,14 +5,17 @@ import com.azure.core.exception.AzureException; import com.azure.cosmos.implementation.Constants; +import com.azure.cosmos.implementation.CosmosError; import com.azure.cosmos.implementation.HttpConstants; import com.azure.cosmos.implementation.RequestTimeline; import com.azure.cosmos.implementation.Utils; +import com.azure.cosmos.implementation.apachecommons.lang.StringUtils; import com.azure.cosmos.implementation.directconnectivity.Uri; -import com.azure.cosmos.implementation.CosmosError; import com.azure.cosmos.implementation.directconnectivity.rntbd.RntbdEndpointStatistics; import com.azure.cosmos.models.ModelBridgeInternal; -import com.azure.cosmos.implementation.apachecommons.lang.StringUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import java.time.Duration; import java.util.HashMap; @@ -20,6 +23,8 @@ import java.util.Map; import java.util.stream.Collectors; +import static com.azure.cosmos.CosmosDiagnostics.USER_AGENT_KEY; + /** * This class defines a custom exception type for all operations on * CosmosClient in the Azure Cosmos DB database service. Applications are @@ -39,6 +44,7 @@ public class CosmosException extends AzureException { private static final long serialVersionUID = 1L; + private static final ObjectMapper mapper = Utils.getSimpleObjectMapper(); private final static String USER_AGENT = Utils.getUserAgent(); private final int statusCode; private final Map responseHeaders; @@ -155,10 +161,19 @@ protected CosmosException(String message, Exception exception, Map> filterRequestHeaders = filterSensitiveData(requestHeaders); + if (filterRequestHeaders != null) { + exceptionMessageNode.put("requestHeaders", filterRequestHeaders.toString()); + } + + if(this.cosmosDiagnostics != null) { + cosmosDiagnostics.fillCosmosDiagnostics(exceptionMessageNode, null); + } + + return mapper.writeValueAsString(exceptionMessageNode); + } catch (JsonProcessingException ex) { + return getClass().getSimpleName() + "{" + USER_AGENT_KEY +"=" + USER_AGENT + ", error=" + cosmosError + ", " + + "resourceAddress='" + + resourceAddress + ", statusCode=" + statusCode + ", message=" + getMessage() + + ", causeInfo=" + causeInfo() + ", responseHeaders=" + responseHeaders + ", requestHeaders=" + + filterSensitiveData(requestHeaders) + '}'; + } } String innerErrorMessage() { diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosDiagnosticsTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosDiagnosticsTest.java index d18817dd4c97..b6916ed094ef 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosDiagnosticsTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosDiagnosticsTest.java @@ -28,7 +28,7 @@ import com.azure.cosmos.models.ModelBridgeInternal; import com.azure.cosmos.models.PartitionKey; import com.azure.cosmos.rx.TestSuiteBase; -import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -39,6 +39,7 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.io.IOException; import java.lang.reflect.Field; import java.net.InetSocketAddress; import java.time.Instant; @@ -63,7 +64,7 @@ public class CosmosDiagnosticsTest extends TestSuiteBase { private CosmosAsyncContainer cosmosAsyncContainer; @BeforeClass(groups = {"simple"}, timeOut = SETUP_TIMEOUT) - public void beforeClass() throws Exception { + public void beforeClass() { assertThat(this.gatewayClient).isNull(); gatewayClient = new CosmosClientBuilder() .endpoint(TestConfigurations.HOST) @@ -150,7 +151,7 @@ public void gatewayDiagnostics() { // TODO: (nakumars) - Uncomment the following line after your client telemetry fix // assertThat(createResponse.getDiagnostics().getRegionsContacted()).isNotEmpty(); validateTransportRequestTimelineGateway(diagnostics); - validateJson(diagnostics); + isValidJSON(diagnostics); } finally { if (testGatewayClient != null) { testGatewayClient.close(); @@ -172,6 +173,8 @@ public void gatewayDiagnosticsOnException() { InternalObjectNode.class); fail("request should fail as partition key is wrong"); } catch (CosmosException exception) { + isValidJSON(exception.toString()); + isValidJSON(exception.getMessage()); String diagnostics = exception.getDiagnostics().toString(); assertThat(exception.getStatusCode()).isEqualTo(HttpConstants.StatusCodes.NOTFOUND); assertThat(diagnostics).contains("\"connectionMode\":\"GATEWAY\""); @@ -185,7 +188,7 @@ public void gatewayDiagnosticsOnException() { // assertThat(createResponse.getDiagnostics().getRegionsContacted()).isNotEmpty(); assertThat(exception.getDiagnostics().getDuration()).isNotNull(); validateTransportRequestTimelineGateway(diagnostics); - validateJson(diagnostics); + isValidJSON(diagnostics); } } @@ -229,7 +232,7 @@ public void directDiagnostics() { assertThat(createResponse.getDiagnostics().getRegionsContacted()).isNotEmpty(); assertThat(createResponse.getDiagnostics().getDuration()).isNotNull(); validateTransportRequestTimelineDirect(diagnostics); - validateJson(diagnostics); + isValidJSON(diagnostics); // validate that on failed operation request timeline is populated try { @@ -422,13 +425,15 @@ public void directDiagnosticsOnException() { InternalObjectNode.class); fail("request should fail as partition key is wrong"); } catch (CosmosException exception) { + isValidJSON(exception.toString()); + isValidJSON(exception.getMessage()); String diagnostics = exception.getDiagnostics().toString(); assertThat(exception.getStatusCode()).isEqualTo(HttpConstants.StatusCodes.NOTFOUND); assertThat(diagnostics).contains("\"connectionMode\":\"DIRECT\""); assertThat(diagnostics).doesNotContain(("\"resourceAddress\":null")); assertThat(exception.getDiagnostics().getRegionsContacted()).isNotEmpty(); assertThat(exception.getDiagnostics().getDuration()).isNotNull(); - validateJson(diagnostics); + isValidJSON(diagnostics); // TODO https://github.com/Azure/azure-sdk-for-java/issues/8035 // uncomment below if above issue is fixed //validateTransportRequestTimelineDirect(diagnostics); @@ -821,11 +826,13 @@ private void validateTransportRequestTimelineDirect(String diagnostics) { assertThat(diagnostics).contains("\"durationInMicroSec\""); } - private void validateJson(String jsonInString) { + public void isValidJSON(final String json) { try { - OBJECT_MAPPER.readTree(jsonInString); - } catch(JsonProcessingException ex) { - fail("Diagnostic string is not in json format"); + final JsonParser parser = new ObjectMapper().createParser(json); + while (parser.nextToken() != null) { + } + } catch (IOException ex) { + fail("Diagnostic string is not in json format ", ex); } } diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosExceptionTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosExceptionTest.java index df92bffce61f..650525a8bc69 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosExceptionTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosExceptionTest.java @@ -58,7 +58,7 @@ public class CosmosExceptionTest { @Test(groups = { "unit" }) public void sdkVersionPresent() { CosmosException dce = BridgeInternal.createCosmosException(0); - assertThat(dce.toString()).contains("userAgent=" + Utils.getUserAgent()); + assertThat(dce.toString()).contains("\"userAgent\":\"" + Utils.getUserAgent()); } @Test(groups = { "unit" }) @@ -118,7 +118,7 @@ public void statusCodeIsCorrect(Class type, int expectedStatusC constructor.setAccessible(true); final CosmosException instance = constructor.newInstance("some-message", null, "some-uri"); assertEquals(instance.getStatusCode(), expectedStatusCode); - assertThat(instance.toString()).contains("userAgent=" + Utils.getUserAgent()); + assertThat(instance.toString()).contains("\"userAgent\":\"" + Utils.getUserAgent()); } catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException error) { String message = lenientFormat("could not create instance of %s due to %s", type, error); throw new AssertionError(message, error); diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/AddressSelectorTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/AddressSelectorTest.java index f714c26042ef..d598816095ae 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/AddressSelectorTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/AddressSelectorTest.java @@ -33,7 +33,7 @@ public void getPrimaryUri_NoAddress() throws Exception { } @Test(groups = "unit", expectedExceptions = GoneException.class, expectedExceptionsMessageRegExp = - "The requested resource is no longer available at the server. Returned addresses are \\{https://cosmos1/,https://cosmos2/\\}") + ".*\"innerErrorMessage\":\"The requested resource is no longer available at the server. Returned addresses are .*https://cosmos1/,https://cosmos2/}.*") public void getPrimaryUri_NoPrimaryAddress() throws Exception { RxDocumentServiceRequest request = mockDocumentServiceRequest(clientContext); Mockito.doReturn(null).when(request).getDefaultReplicaIndex(); diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/rx/MultiMasterConflictResolutionTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/rx/MultiMasterConflictResolutionTest.java index c074e97b1bf3..5a83db12ad00 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/rx/MultiMasterConflictResolutionTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/rx/MultiMasterConflictResolutionTest.java @@ -82,7 +82,8 @@ public void conflictResolutionPolicyCRUD() { // when (e.StatusCode == HttpStatusCode.BadRequest) CosmosException dce = Utils.as(e, CosmosException.class); if (dce != null && dce.getStatusCode() == 400) { - assertThat(dce.getMessage()).contains("Invalid path '\\\\\\/a\\\\\\/b' for last writer wins conflict resolution"); + assertThat(dce.getMessage()).contains("Invalid path '\\\\\\\\\\\\/a\\\\\\\\\\\\/b' for last writer " + + "wins conflict resolution"); } else { throw e; }