diff --git a/Microsoft.Azure.Cosmos/src/Diagnostics/CosmosDiagnostics.cs b/Microsoft.Azure.Cosmos/src/Diagnostics/CosmosDiagnostics.cs index 8e8749374f..5734723c4d 100644 --- a/Microsoft.Azure.Cosmos/src/Diagnostics/CosmosDiagnostics.cs +++ b/Microsoft.Azure.Cosmos/src/Diagnostics/CosmosDiagnostics.cs @@ -4,6 +4,7 @@ namespace Microsoft.Azure.Cosmos { using System; + using System.Collections.Generic; /// /// Contains the cosmos diagnostic information for the current request to Azure Cosmos DB service. @@ -27,5 +28,11 @@ public virtual TimeSpan GetClientElapsedTime() /// /// The string field instance in the Azure CosmosDB database service. public abstract override string ToString(); + + /// + /// Gets the list of all regions that were contacted for a request + /// + /// The list of tuples containing the Region name and the URI + public abstract IReadOnlyList<(string regionName, Uri uri)> GetContactedRegions(); } } diff --git a/Microsoft.Azure.Cosmos/src/Diagnostics/CosmosTraceDiagnostics.cs b/Microsoft.Azure.Cosmos/src/Diagnostics/CosmosTraceDiagnostics.cs index cb2f5b6b2a..ae7245d0eb 100644 --- a/Microsoft.Azure.Cosmos/src/Diagnostics/CosmosTraceDiagnostics.cs +++ b/Microsoft.Azure.Cosmos/src/Diagnostics/CosmosTraceDiagnostics.cs @@ -5,9 +5,12 @@ namespace Microsoft.Azure.Cosmos.Diagnostics { using System; + using System.Collections.Generic; + using System.Linq; using System.Text; using Microsoft.Azure.Cosmos.Json; using Microsoft.Azure.Cosmos.Tracing; + using Microsoft.Azure.Cosmos.Tracing.TraceData; internal sealed class CosmosTraceDiagnostics : CosmosDiagnostics { @@ -40,6 +43,31 @@ public override TimeSpan GetClientElapsedTime() return this.Value.Duration; } + public override IReadOnlyList<(string regionName, Uri uri)> GetContactedRegions() + { + HashSet<(string, Uri)> regionsContacted = new HashSet<(string, Uri)>(); + ITrace rootTrace = this.Value; + this.WalkTraceTreeForRegionsContated(rootTrace, regionsContacted); + return regionsContacted.ToList(); + } + + private void WalkTraceTreeForRegionsContated(ITrace currentTrace, HashSet<(string, Uri)> regionsContacted) + { + foreach (object datums in currentTrace.Data.Values) + { + if (datums is ClientSideRequestStatisticsTraceDatum clientSideRequestStatisticsTraceDatum) + { + regionsContacted.UnionWith(clientSideRequestStatisticsTraceDatum.RegionsContactedWithName); + return; + } + } + + foreach (ITrace childTrace in currentTrace.Children) + { + this.WalkTraceTreeForRegionsContated(childTrace, regionsContacted); + } + } + private string ToJsonString() { ReadOnlyMemory utf8String = this.WriteTraceToJsonWriter(JsonSerializationFormat.Text); diff --git a/Microsoft.Azure.Cosmos/src/Tracing/TraceData/ClientSideRequestStatisticsTraceDatum.cs b/Microsoft.Azure.Cosmos/src/Tracing/TraceData/ClientSideRequestStatisticsTraceDatum.cs index 1515069006..2c95b4d2c3 100644 --- a/Microsoft.Azure.Cosmos/src/Tracing/TraceData/ClientSideRequestStatisticsTraceDatum.cs +++ b/Microsoft.Azure.Cosmos/src/Tracing/TraceData/ClientSideRequestStatisticsTraceDatum.cs @@ -29,7 +29,7 @@ public ClientSideRequestStatisticsTraceDatum(DateTime startTime) this.ContactedReplicas = new List(); this.StoreResponseStatisticsList = new List(); this.FailedReplicas = new HashSet(); - this.RegionsContacted = new HashSet(); + this.RegionsContactedWithName = new HashSet<(string, Uri)>(); this.clientSideRequestStatisticsCreateTime = Stopwatch.GetTimestamp(); } @@ -49,6 +49,8 @@ public ClientSideRequestStatisticsTraceDatum(DateTime startTime) public HashSet RegionsContacted { get; } + public HashSet<(string, Uri)> RegionsContactedWithName { get; } + public TimeSpan RequestLatency { get @@ -119,6 +121,7 @@ public void RecordResponse(DocumentServiceRequest request, StoreResult storeResu DateTime responseTime = DateTime.UtcNow; Uri locationEndpoint = request.RequestContext.LocationEndpointToRoute; + string regionName = request.RequestContext.RegionName; StoreResponseStatistics responseStatistics = new StoreResponseStatistics( startDateTime, responseTime, @@ -141,7 +144,7 @@ public void RecordResponse(DocumentServiceRequest request, StoreResult storeResu if (locationEndpoint != null) { - this.RegionsContacted.Add(locationEndpoint); + this.RegionsContactedWithName.Add((regionName, locationEndpoint)); } this.StoreResponseStatisticsList.Add(responseStatistics); diff --git a/Microsoft.Azure.Cosmos/src/Tracing/TraceWriter.TraceJsonWriter.cs b/Microsoft.Azure.Cosmos/src/Tracing/TraceWriter.TraceJsonWriter.cs index 144b5fee5b..6a06f354ae 100644 --- a/Microsoft.Azure.Cosmos/src/Tracing/TraceWriter.TraceJsonWriter.cs +++ b/Microsoft.Azure.Cosmos/src/Tracing/TraceWriter.TraceJsonWriter.cs @@ -193,7 +193,7 @@ public void Visit(ClientSideRequestStatisticsTraceDatum clientSideRequestStatist this.WriteJsonUriArrayWithDuplicatesCounted("ContactedReplicas", clientSideRequestStatisticsTraceDatum.ContactedReplicas); - this.WriteJsonUriArray("RegionsContacted", clientSideRequestStatisticsTraceDatum.RegionsContacted); + this.WriteRegionsContactedArray("RegionsContacted", clientSideRequestStatisticsTraceDatum.RegionsContactedWithName); this.WriteJsonUriArray("FailedReplicas", clientSideRequestStatisticsTraceDatum.FailedReplicas); this.jsonWriter.WriteFieldName("AddressResolutionStatistics"); @@ -321,6 +321,22 @@ private void WriteJsonUriArray(string propertyName, IEnumerable uris) this.jsonWriter.WriteArrayEnd(); } + private void WriteRegionsContactedArray(string propertyName, IEnumerable<(string, Uri)> uris) + { + this.jsonWriter.WriteFieldName(propertyName); + this.jsonWriter.WriteArrayStart(); + + if (uris != null) + { + foreach ((string _, Uri uri) contactedRegion in uris) + { + this.WriteStringValueOrNull(contactedRegion.uri?.ToString()); + } + } + + this.jsonWriter.WriteArrayEnd(); + } + /// /// Writes the list of URIs to JSON. /// Sequential duplicates are counted and written as a single object to prevent 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 38ddcf1da2..297420a729 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs @@ -2433,6 +2433,18 @@ public async Task CustomPropertiesItemRequestOptionsTest() Assert.AreEqual(HttpStatusCode.Created, responseAstype.StatusCode); } + [TestMethod] + public async Task RegionsContactedTest() + { + ToDoActivity item = ToDoActivity.CreateRandomToDoActivity(); + ItemResponse response = await this.Container.CreateItemAsync(item, new Cosmos.PartitionKey(item.pk)); + Assert.IsNotNull(response.Diagnostics); + IReadOnlyList<(string region, Uri uri)> regionsContacted = response.Diagnostics.GetContactedRegions(); + Assert.AreEqual(regionsContacted.Count, 1); + Assert.AreEqual(regionsContacted[0].region, Regions.SouthCentralUS); + Assert.IsNotNull(regionsContacted[0].uri); + } + #if PREVIEW [TestMethod] public async Task VerifyDocumentCrudWithMultiHashKind() diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/BaselineTest/TestBaseline/TraceWriterBaselineTests.TraceData.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/BaselineTest/TestBaseline/TraceWriterBaselineTests.TraceData.xml index a6c4cd071e..e2e2c85b9d 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/BaselineTest/TestBaseline/TraceWriterBaselineTests.TraceData.xml +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/BaselineTest/TestBaseline/TraceWriterBaselineTests.TraceData.xml @@ -222,8 +222,8 @@ datum.FailedReplicas.Add(uri1); datum.FailedReplicas.Add(uri2); - datum.RegionsContacted.Add(uri1); - datum.RegionsContacted.Add(uri2); + datum.RegionsContactedWithName.Add(("local", uri1)); + datum.RegionsContactedWithName.Add(("local", uri2)); datum.RequestEndTimeUtc = DateTime.MaxValue; @@ -377,7 +377,7 @@ datum.FailedReplicas.Add(default); - datum.RegionsContacted.Add(default); + datum.RegionsContactedWithName.Add(default); datum.RequestEndTimeUtc = default; 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 a6544b2654..31507703ae 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 @@ -1936,6 +1936,11 @@ "Microsoft.Azure.Cosmos.CosmosDiagnostics;System.Object;IsAbstract:True;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { "Subclasses": {}, "Members": { + "System.Collections.Generic.IReadOnlyList`1[System.ValueTuple`2[System.String,System.Uri]] GetContactedRegions()": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "System.Collections.Generic.IReadOnlyList`1[System.ValueTuple`2[System.String,System.Uri]] GetContactedRegions();IsAbstract:True;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "System.String ToString()": { "Type": "Method", "Attributes": [], diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Tracing/ContactedRegionsTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Tracing/ContactedRegionsTests.cs new file mode 100644 index 0000000000..34d8469829 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Tracing/ContactedRegionsTests.cs @@ -0,0 +1,67 @@ +namespace Microsoft.Azure.Cosmos.Tests.Tracing +{ + using System; + using System.Collections.Generic; + using System.Text; + using Microsoft.Azure.Cosmos.Diagnostics; + using Microsoft.Azure.Cosmos.Tracing; + using Microsoft.Azure.Cosmos.Tracing.TraceData; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class ContactedRegionsTests + { + [TestMethod] + public void ContactedRegionsTest() + { + CosmosDiagnostics diagnostics = new CosmosTraceDiagnostics(this.CreateTestTraceTree()); + IReadOnlyList<(string, Uri)> regionsContacted = diagnostics.GetContactedRegions(); + Assert.IsNotNull(regionsContacted); + Assert.AreEqual(regionsContacted.Count, 4); + } + + private ITrace CreateTestTraceTree() + { + ITrace trace; + using (trace = Trace.GetRootTrace("Root Trace", TraceComponent.Unknown, TraceLevel.Info)) + { + using (ITrace firstLevel = trace.StartChild("First level Node", TraceComponent.Unknown, TraceLevel.Info)) + { + using (ITrace secondLevel = trace.StartChild("Second level Node", TraceComponent.Unknown, TraceLevel.Info)) + { + using (ITrace thirdLevel = trace.StartChild("Third level Node", TraceComponent.Unknown, TraceLevel.Info)) + { + thirdLevel.AddDatum("Client Side Request Stats", this.GetDatumObject(Regions.CentralUS)); + } + } + + using (ITrace secondLevel = trace.StartChild("Second level Node", TraceComponent.Unknown, TraceLevel.Info)) + { + secondLevel.AddDatum("Client Side Request Stats", this.GetDatumObject(Regions.CentralIndia, Regions.EastUS2)); + } + } + + using (ITrace firstLevel = trace.StartChild("First level Node", TraceComponent.Unknown, TraceLevel.Info)) + { + firstLevel.AddDatum("Client Side Request Stats", this.GetDatumObject(Regions.FranceCentral)); + } + } + + return trace; + } + + private TraceDatum GetDatumObject(string regionName1, string regionName2 = null) + { + ClientSideRequestStatisticsTraceDatum datum = new ClientSideRequestStatisticsTraceDatum(DateTime.UtcNow); + Uri uri1 = new Uri("http://someUri1.com"); + datum.RegionsContactedWithName.Add((regionName1, uri1)); + if (regionName2 != null) + { + Uri uri2 = new Uri("http://someUri2.com"); + datum.RegionsContactedWithName.Add((regionName2, uri2)); + } + + return datum; + } + } +} diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Tracing/TraceWriterBaselineTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Tracing/TraceWriterBaselineTests.cs index dfda2d8ef7..c2a5f0285d 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Tracing/TraceWriterBaselineTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Tracing/TraceWriterBaselineTests.cs @@ -373,8 +373,8 @@ public void TraceData() datum.FailedReplicas.Add(uri1); datum.FailedReplicas.Add(uri2); - datum.RegionsContacted.Add(uri1); - datum.RegionsContacted.Add(uri2); + datum.RegionsContactedWithName.Add(("local", uri1)); + datum.RegionsContactedWithName.Add(("local", uri2)); datum.RequestEndTimeUtc = DateTime.MaxValue; @@ -426,7 +426,7 @@ public void TraceData() datum.FailedReplicas.Add(default); - datum.RegionsContacted.Add(default); + datum.RegionsContactedWithName.Add(default); datum.RequestEndTimeUtc = default;