diff --git a/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosException.cs b/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosException.cs index a319f3cc7e..440a608e20 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosException.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosException.cs @@ -17,6 +17,7 @@ namespace Microsoft.Azure.Cosmos public class CosmosException : Exception { private readonly string stackTrace; + private readonly Lazy lazyMessage; internal CosmosException( HttpStatusCode statusCode, @@ -26,10 +27,8 @@ internal CosmosException( ITrace trace, Error error, Exception innerException) - : base(CosmosException.GetMessageHelper( - statusCode, - headers, - message), innerException) + // The message is overridden. Base exception does not have CTOR for just innerException and the property is not virtual + : base(string.Empty, innerException) { this.ResponseBody = message; this.stackTrace = stackTrace; @@ -37,6 +36,12 @@ internal CosmosException( this.Headers = headers ?? new Headers(); this.Error = error; this.Trace = trace; + this.Diagnostics = new CosmosTraceDiagnostics(this.Trace ?? NoOpTrace.Singleton); + this.lazyMessage = new Lazy(() => CosmosException.GetMessageHelper( + statusCode, + this.Headers, + this.ResponseBody, + this.Diagnostics)); } /// @@ -59,6 +64,7 @@ public CosmosException( this.StatusCode = statusCode; this.ResponseBody = message; this.Trace = NoOpTrace.Singleton; + this.lazyMessage = new Lazy(() => message); this.Headers = new Headers() { SubStatusCode = (SubStatusCodes)subStatusCode, @@ -71,6 +77,9 @@ public CosmosException( } } + /// + public override string Message => this.lazyMessage.Value; + /// /// The body of the cosmos response message as a string /// @@ -117,7 +126,7 @@ public CosmosException( /// /// Gets the diagnostics for the request /// - public virtual CosmosDiagnostics Diagnostics => new CosmosTraceDiagnostics(this.Trace ?? NoOpTrace.Singleton); + public virtual CosmosDiagnostics Diagnostics { get; } /// public override string StackTrace @@ -189,32 +198,61 @@ internal ResponseMessage ToCosmosResponseMessage(RequestMessage request) private static string GetMessageHelper( HttpStatusCode statusCode, Headers headers, - string responseBody) + string responseBody, + CosmosDiagnostics diagnostics) { StringBuilder stringBuilder = new StringBuilder(); + CosmosException.AppendMessageWithoutDiagnostics( + stringBuilder, + statusCode, + headers, + responseBody); + + // Include the diagnostics for exceptions where it is critical + // to root cause failures. + if (statusCode == HttpStatusCode.RequestTimeout + || statusCode == HttpStatusCode.InternalServerError + || statusCode == HttpStatusCode.ServiceUnavailable + || (statusCode == HttpStatusCode.NotFound && headers.SubStatusCode == SubStatusCodes.ReadSessionNotAvailable)) + { + stringBuilder.Append("; Diagnostics:"); + stringBuilder.Append(diagnostics.ToString()); + } + + return stringBuilder.ToString(); + } + + private static void AppendMessageWithoutDiagnostics( + StringBuilder stringBuilder, + HttpStatusCode statusCode, + Headers headers, + string responseBody) + { stringBuilder.Append($"Response status code does not indicate success: "); stringBuilder.Append($"{statusCode} ({(int)statusCode})"); stringBuilder.Append("; Substatus: "); - stringBuilder.Append(headers?.SubStatusCodeLiteral ?? "0" ); + stringBuilder.Append(headers?.SubStatusCodeLiteral ?? "0"); stringBuilder.Append("; ActivityId: "); stringBuilder.Append(headers?.ActivityId ?? string.Empty); stringBuilder.Append("; Reason: ("); stringBuilder.Append(responseBody ?? string.Empty); stringBuilder.Append(");"); - - return stringBuilder.ToString(); } private string ToStringHelper( - StringBuilder stringBuilder) + StringBuilder stringBuilder) { if (stringBuilder == null) { throw new ArgumentNullException(nameof(stringBuilder)); } - stringBuilder.Append(this.Message); + CosmosException.AppendMessageWithoutDiagnostics( + stringBuilder, + this.StatusCode, + this.Headers, + this.ResponseBody); stringBuilder.AppendLine(); if (this.InnerException != null) diff --git a/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosOperationCanceledException.cs b/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosOperationCanceledException.cs index a863d17f07..54c74e15d2 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosOperationCanceledException.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosOperationCanceledException.cs @@ -17,6 +17,7 @@ namespace Microsoft.Azure.Cosmos public class CosmosOperationCanceledException : OperationCanceledException { private readonly OperationCanceledException originalException; + private readonly Lazy lazyMessage; /// /// Create an instance of CosmosOperationCanceledException @@ -30,6 +31,7 @@ public CosmosOperationCanceledException( { this.originalException = originalException ?? throw new ArgumentNullException(nameof(originalException)); this.Diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics)); + this.lazyMessage = this.CreateLazyMessage(); } internal CosmosOperationCanceledException( @@ -45,6 +47,7 @@ internal CosmosOperationCanceledException( trace.AddDatum("Operation Cancelled Exception", originalException); this.Diagnostics = new CosmosTraceDiagnostics(trace); + this.lazyMessage = this.CreateLazyMessage(); } /// @@ -55,7 +58,7 @@ public override string Source } /// - public override string Message => this.originalException.Message; + public override string Message => this.lazyMessage.Value; /// public override string StackTrace => this.originalException.StackTrace; @@ -86,5 +89,10 @@ public override string ToString() { return $"{this.originalException} {Environment.NewLine}CosmosDiagnostics: {this.Diagnostics}"; } + + private Lazy CreateLazyMessage() + { + return new Lazy(() => $"{this.originalException.Message} {Environment.NewLine}CosmosDiagnostics: {this.Diagnostics}"); + } } } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs index afe62f0317..80d5a83781 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs @@ -34,6 +34,7 @@ namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests using PartitionKey = Documents.PartitionKey; using static Microsoft.Azure.Cosmos.SDK.EmulatorTests.TransportClientHelper; using System.Reflection; + using System.Text.RegularExpressions; [TestClass] public class CosmosItemTests : BaseCosmosClientHelper @@ -88,6 +89,7 @@ public async Task CreateDropItemWithInvalidIdCharactersTest() { string message = ce.ToString(); Assert.IsNotNull(message); + CosmosItemTests.ValidateCosmosException(ce); } // Get a container reference that use RID values @@ -216,6 +218,7 @@ public async Task NegativeCreateItemTest() Assert.IsTrue(exception.StartsWith("Microsoft.Azure.Cosmos.CosmosException : Response status code does not indicate success: Forbidden (403); Substatus: 999999; ")); string diagnostics = ce.Diagnostics.ToString(); Assert.IsTrue(diagnostics.Contains("999999")); + CosmosItemTests.ValidateCosmosException(ce); } } @@ -351,6 +354,7 @@ public async Task CreateDropItemPartitionKeyNotInTypeTest() catch (CosmosException ex) { Assert.AreEqual(HttpStatusCode.NotFound, ex.StatusCode); + CosmosItemTests.ValidateCosmosException(ex); } } @@ -391,6 +395,7 @@ public async Task CreateDropItemMultiPartPartitionKeyTest() catch (CosmosException ex) { Assert.AreEqual(HttpStatusCode.NotFound, ex.StatusCode); + CosmosItemTests.ValidateCosmosException(ex); } } @@ -1710,7 +1715,7 @@ await containerInternal.PatchItemAsync( Assert.AreEqual(HttpStatusCode.NotFound, ex.StatusCode); Assert.IsTrue(ex.Message.Contains("Resource Not Found")); Assert.IsTrue(ex.Message.Contains("https://aka.ms/cosmosdb-tsg-not-found")); - + CosmosItemTests.ValidateCosmosException(ex); } // adding a child when parent / ancestor does not exist - 400 BadRequest response @@ -1727,6 +1732,7 @@ await containerInternal.PatchItemAsync( { Assert.AreEqual(HttpStatusCode.BadRequest, ex.StatusCode); Assert.IsTrue(ex.Message.Contains("Add Operation only support adding a leaf node of an existing node(array or object), no path found beyond: 'nonExistentParent'"), ex.Message); + CosmosItemTests.ValidateCosmosException(ex); } // precondition failure - 412 response @@ -1749,6 +1755,7 @@ await containerInternal.PatchItemAsync( { Assert.AreEqual(HttpStatusCode.PreconditionFailed, ex.StatusCode); Assert.IsTrue(ex.Message.Contains("One of the specified pre-condition is not met")); + CosmosItemTests.ValidateCosmosException(ex); } } @@ -2925,6 +2932,23 @@ private static async Task VerifyQueryToManyExceptionAsync( } } + private static void ValidateCosmosException(CosmosException exception) + { + if(exception.StatusCode == HttpStatusCode.RequestTimeout || + exception.StatusCode == HttpStatusCode.InternalServerError || + exception.StatusCode == HttpStatusCode.ServiceUnavailable) + { + Assert.IsTrue(exception.Message.Contains("Diagnostics")); + } + else + { + Assert.IsFalse(exception.Message.Contains("Diagnostics")); + } + + string toString = exception.ToString(); + Assert.AreEqual(1, Regex.Matches(toString, "Client Configuration").Count, $"The Cosmos Diagnostics does not exists or multiple instance are in the ToString(). {toString}"); + } + private static async Task ExecuteQueryAsync(Container container, HttpStatusCode expected) { FeedIterator iterator = container.GetItemQueryStreamIterator("select * from r"); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosOperationCanceledExceptionTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosOperationCanceledExceptionTests.cs index 831471e02d..200ed894ad 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosOperationCanceledExceptionTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosOperationCanceledExceptionTests.cs @@ -101,7 +101,9 @@ await container.CreateItemAsync( string diagnostics = ce.Diagnostics.ToString(); string toString = ce.ToString(); Assert.IsTrue(toString.Contains(diagnostics)); - Assert.IsTrue(toString.Contains(message)); + Assert.IsTrue(message.Contains(diagnostics)); + string messageWithoutDiagnostics = message.Substring(0, message.IndexOf(Environment.NewLine)).Trim(); + Assert.IsTrue(toString.Contains(messageWithoutDiagnostics)); } try @@ -115,13 +117,13 @@ await container.CreateItemAsync( } catch (CosmosOperationCanceledException ce) { - Assert.IsNotNull(ce); string message = ce.Message; string diagnostics = ce.Diagnostics.ToString(); - Assert.IsTrue(diagnostics.Contains("The operation was canceled.")); string toString = ce.ToString(); Assert.IsTrue(toString.Contains(diagnostics)); - Assert.IsTrue(toString.Contains(message)); + Assert.IsTrue(message.Contains(diagnostics)); + string messageWithoutDiagnostics = message.Substring(0, message.IndexOf(Environment.NewLine)).Trim(); + Assert.IsTrue(toString.Contains(messageWithoutDiagnostics)); } } } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Contracts/BenchmarkResults.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Contracts/BenchmarkResults.json index 65706d0d5f..f0ae1c463a 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Contracts/BenchmarkResults.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Contracts/BenchmarkResults.json @@ -11,11 +11,11 @@ "MockedItemBenchmark.DeleteItemExists;[Type=OfTWithClientTelemetryEnabled]": 39840.0, "MockedItemBenchmark.DeleteItemExists;[Type=OfTWithDiagnosticsToString]": 70924.0, "MockedItemBenchmark.DeleteItemExists;[Type=Stream]": 29872.0, - "MockedItemBenchmark.DeleteItemNotExists;[Type=OfT]": 48270.0, - "MockedItemBenchmark.DeleteItemNotExists;[Type=OfTCustom]": 48264.0, - "MockedItemBenchmark.DeleteItemNotExists;[Type=OfTWithClientTelemetryEnabled]": 51146.0, - "MockedItemBenchmark.DeleteItemNotExists;[Type=OfTWithDiagnosticsToString]": 82144.0, - "MockedItemBenchmark.DeleteItemNotExists;[Type=Stream]": 43180.0, + "MockedItemBenchmark.DeleteItemNotExists;[Type=OfT]": 42786.0, + "MockedItemBenchmark.DeleteItemNotExists;[Type=OfTCustom]": "43144.0", + "MockedItemBenchmark.DeleteItemNotExists;[Type=OfTWithClientTelemetryEnabled]": 44694.0, + "MockedItemBenchmark.DeleteItemNotExists;[Type=OfTWithDiagnosticsToString]": 77632.0, + "MockedItemBenchmark.DeleteItemNotExists;[Type=Stream]": 37610.0, "MockedItemBenchmark.ReadFeed;[Type=OfT]": 560692.0, "MockedItemBenchmark.ReadFeed;[Type=OfTCustom]": 571334.0, "MockedItemBenchmark.ReadFeed;[Type=OfTWithClientTelemetryEnabled]": 565196.0, @@ -26,11 +26,11 @@ "MockedItemBenchmark.ReadItemExists;[Type=OfTWithClientTelemetryEnabled]": 44440.0, "MockedItemBenchmark.ReadItemExists;[Type=OfTWithDiagnosticsToString]": 76298.0, "MockedItemBenchmark.ReadItemExists;[Type=Stream]": 34464.0, - "MockedItemBenchmark.ReadItemNotExists;[Type=OfT]": 52826.0, - "MockedItemBenchmark.ReadItemNotExists;[Type=OfTCustom]": 52864.0, - "MockedItemBenchmark.ReadItemNotExists;[Type=OfTWithClientTelemetryEnabled]": 55754.0, - "MockedItemBenchmark.ReadItemNotExists;[Type=OfTWithDiagnosticsToString]": 87640.0, - "MockedItemBenchmark.ReadItemNotExists;[Type=Stream]": 47994.0, + "MockedItemBenchmark.ReadItemNotExists;[Type=OfT]": 47408.0, + "MockedItemBenchmark.ReadItemNotExists;[Type=OfTCustom]": 47406.0, + "MockedItemBenchmark.ReadItemNotExists;[Type=OfTWithClientTelemetryEnabled]": 49298.0, + "MockedItemBenchmark.ReadItemNotExists;[Type=OfTWithDiagnosticsToString]": 81450.0, + "MockedItemBenchmark.ReadItemNotExists;[Type=Stream]": 42246.0, "MockedItemBenchmark.UpdateItem;[Type=OfT]": 45542.0, "MockedItemBenchmark.UpdateItem;[Type=OfTCustom]": 45532.0, "MockedItemBenchmark.UpdateItem;[Type=OfTWithClientTelemetryEnabled]": 48372.0, diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json index 4ff4c5da8f..74efdb4307 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json @@ -2446,9 +2446,11 @@ "Attributes": [], "MethodInfo": "Microsoft.Azure.Cosmos.CosmosDiagnostics Diagnostics;CanRead:True;CanWrite:False;Microsoft.Azure.Cosmos.CosmosDiagnostics get_Diagnostics();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, - "Microsoft.Azure.Cosmos.CosmosDiagnostics get_Diagnostics()": { + "Microsoft.Azure.Cosmos.CosmosDiagnostics get_Diagnostics()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { "Type": "Method", - "Attributes": [], + "Attributes": [ + "CompilerGeneratedAttribute" + ], "MethodInfo": "Microsoft.Azure.Cosmos.CosmosDiagnostics get_Diagnostics();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, "Microsoft.Azure.Cosmos.Headers get_Headers()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { @@ -2495,6 +2497,11 @@ "Attributes": [], "MethodInfo": "System.String get_ActivityId();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, + "System.String get_Message()": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "System.String get_Message();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "System.String get_ResponseBody()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { "Type": "Method", "Attributes": [ @@ -2507,6 +2514,11 @@ "Attributes": [], "MethodInfo": "System.String get_StackTrace();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, + "System.String Message": { + "Type": "Property", + "Attributes": [], + "MethodInfo": "System.String Message;CanRead:True;CanWrite:False;System.String get_Message();IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "System.String ResponseBody": { "Type": "Property", "Attributes": [], diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosExceptionTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosExceptionTests.cs index 1b38973208..7c51ddd3dd 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosExceptionTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosExceptionTests.cs @@ -42,6 +42,61 @@ public void VerifyHeaderAlwaysExists() Assert.IsNotNull(cosmosException.Headers, "Header should always be created to avoid null refs caused by users always expecting it to be there"); } + [TestMethod] + public void VerifyDiagnosticsInTimeoutAndServerError() + { + ITrace trace = NoOpTrace.Singleton; + string diagnosticString = new Diagnostics.CosmosTraceDiagnostics(trace).ToString(); + + CosmosException cosmosException = new CosmosException( + statusCode: HttpStatusCode.RequestTimeout, + message: "Test", + stackTrace: null, + headers: null, + trace: trace, + error: null, + innerException: null); + + Assert.IsTrue(cosmosException.Message.EndsWith(diagnosticString)); + Assert.IsTrue(cosmosException.ToString().Contains(diagnosticString)); + + cosmosException = new CosmosException( + statusCode: HttpStatusCode.InternalServerError, + message: "Test", + stackTrace: null, + headers: null, + trace: trace, + error: null, + innerException: null); + + Assert.IsTrue(cosmosException.Message.EndsWith(diagnosticString)); + Assert.IsTrue(cosmosException.ToString().Contains(diagnosticString)); + + cosmosException = new CosmosException( + statusCode: HttpStatusCode.ServiceUnavailable, + message: "Test", + stackTrace: null, + headers: null, + trace: trace, + error: null, + innerException: null); + + Assert.IsTrue(cosmosException.Message.EndsWith(diagnosticString)); + Assert.IsTrue(cosmosException.ToString().Contains(diagnosticString)); + + cosmosException = new CosmosException( + statusCode: HttpStatusCode.NotFound, + message: "Test", + stackTrace: null, + headers: null, + trace: trace, + error: null, + innerException: null); + + Assert.IsFalse(cosmosException.Message.Contains(diagnosticString)); + Assert.IsTrue(cosmosException.ToString().Contains(diagnosticString)); + } + [TestMethod] public void VerifyNullHeaderLogic() { @@ -205,7 +260,7 @@ public void VerifyDocumentClientExceptionWithNullHeader() public void VerifyDocumentClientExceptionToResponseMessage() { string errorMessage = "Test Exception!"; - DocumentClientException dce = null; + DocumentClientException dce; try { throw new DocumentClientException( @@ -230,7 +285,6 @@ public void VerifyDocumentClientExceptionToResponseMessage() public void VerifyTransportExceptionToResponseMessage() { string errorMessage = "Test Exception!"; - DocumentClientException dce = null; TransportException transportException = new TransportException( errorCode: TransportErrorCode.ConnectionBroken, innerException: null, @@ -240,6 +294,7 @@ public void VerifyTransportExceptionToResponseMessage() userPayload: true, payloadSent: true); + DocumentClientException dce; try { throw new ServiceUnavailableException( @@ -288,6 +343,16 @@ public void EnsureCorrectStatusCode() requestCharge, retryAfter); } + + CosmosException cosmosException = CosmosExceptionFactory.CreateNotFoundException(testMessage, new Headers() { SubStatusCodeLiteral = ((int)SubStatusCodes.ReadSessionNotAvailable).ToString(), ActivityId = activityId, RequestCharge = requestCharge, RetryAfterLiteral = retryAfterLiteral }); + this.ValidateExceptionInfo( + cosmosException, + HttpStatusCode.NotFound, + ((int)SubStatusCodes.ReadSessionNotAvailable).ToString(), + testMessage, + activityId, + requestCharge, + retryAfter); } [TestMethod] @@ -379,6 +444,14 @@ private void ValidateExceptionInfo( Assert.IsTrue(exception.ToString().Contains(message)); string expectedMessage = $"Response status code does not indicate success: {httpStatusCode} ({(int)httpStatusCode}); Substatus: {substatus}; ActivityId: {exception.ActivityId}; Reason: ({message});"; + if(httpStatusCode == HttpStatusCode.RequestTimeout + || httpStatusCode == HttpStatusCode.InternalServerError + || httpStatusCode == HttpStatusCode.ServiceUnavailable + || (httpStatusCode == HttpStatusCode.NotFound && exception.Headers.SubStatusCode == SubStatusCodes.ReadSessionNotAvailable)) + { + expectedMessage += "; Diagnostics:" + new Diagnostics.CosmosTraceDiagnostics(NoOpTrace.Singleton).ToString(); + } + Assert.AreEqual(expectedMessage, exception.Message); // Verify updating the header updates the exception info