diff --git a/src/Microsoft.CorrelationVector.UnitTests/CorrelationVectorTests.cs b/src/Microsoft.CorrelationVector.UnitTests/CorrelationVectorTests.cs index 8ecb698..a6a4e05 100644 --- a/src/Microsoft.CorrelationVector.UnitTests/CorrelationVectorTests.cs +++ b/src/Microsoft.CorrelationVector.UnitTests/CorrelationVectorTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Globalization; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -30,6 +31,18 @@ public void CreateV2CorrelationVectorTest() Assert.AreEqual(2, splitVector.Length, "Correlation Vector should be created with two components separated by a '.'"); Assert.AreEqual(22, splitVector[0].Length, "Correlation Vector base should be 22 characters long"); Assert.AreEqual("0", splitVector[1], "Correlation Vector extension should start with zero"); + } + + [TestMethod] + public void CreateV3CorrelationVectorTest() + { + var correlationVector = new CorrelationVectorV3(); + var splitVector = correlationVector.Value.Split('.'); + + Assert.AreEqual(3, splitVector.Length, "Correlation Vector should be created with three components separated by a '.'"); + Assert.AreEqual("A", splitVector[0], "Correlation Vector v3 should start with \"A\"."); + Assert.AreEqual(22, splitVector[1].Length, "Correlation Vector base should be 22 characters long"); + Assert.AreEqual("0", splitVector[2], "Correlation Vector extension should start with zero"); } [TestMethod] @@ -54,6 +67,19 @@ public void CreateCorrelationVectorFromGuidTestV2() Assert.AreEqual(2, splitVector.Length, "Correlation Vector should be created with two components separated by a '.'"); Assert.AreEqual(22, splitVector[0].Length, "Correlation Vector base should be 22 characters long"); Assert.AreEqual("0", splitVector[1], "Correlation Vector extension should start with zero"); + } + + [TestMethod] + public void CreateCorrelationVectorFromGuidTestV3() + { + var guid = System.Guid.NewGuid(); + var correlationVector = new CorrelationVectorV3(guid); + var splitVector = correlationVector.Value.Split('.'); + + Assert.AreEqual(3, splitVector.Length, "Correlation Vector should be created with three components separated by a '.'"); + Assert.AreEqual("A", splitVector[0], "Correlation Vector v3 should start with \"A\"."); + Assert.AreEqual(22, splitVector[1].Length, "Correlation Vector base should be 22 characters long"); + Assert.AreEqual("0", splitVector[2], "Correlation Vector extension should start with zero"); } [TestMethod] @@ -72,6 +98,16 @@ public void GetBaseAsGuidV2Test() var correlationVector = new CorrelationVectorV2(guid); Guid baseAsGuid = correlationVector.GetBaseAsGuid(); + Assert.AreEqual(guid, baseAsGuid, "Correlation Vector base as a guid should be the same as the initial guid"); + } + + [TestMethod] + public void GetBaseAsGuidV3Test() + { + var guid = System.Guid.NewGuid(); + var correlationVector = new CorrelationVectorV3(guid); + Guid baseAsGuid = correlationVector.GetBaseAsGuid(); + Assert.AreEqual(guid, baseAsGuid, "Correlation Vector base as a guid should be the same as the initial guid"); } @@ -153,6 +189,20 @@ public void ParseCorrelationVectorV2Test() Assert.AreEqual("3", splitVector[1], "Correlation Vector extension was not parsed properly"); Assert.AreEqual("4", splitVector[2], "Correlation Vector extension was not parsed properly"); Assert.AreEqual("5", splitVector[3], "Correlation Vector extension was not parsed properly"); + } + + [TestMethod] + public void ParseCorrelationVectorV3Test() + { + var correlationVector = CorrelationVector.Parse("A.Y58xO9ov0kmpPvkiuzMUVA.3.4.A"); + var splitVector = correlationVector.Value.Split('.'); + + Assert.AreEqual(CorrelationVectorVersion.V3, correlationVector.Version, "Correlation Vector version should be V3"); + Assert.AreEqual(5, splitVector.Length, "Correlation Vector was not parsed properly"); + Assert.AreEqual("Y58xO9ov0kmpPvkiuzMUVA", correlationVector.Base, "Correlation Vector base was not parsed properly"); + Assert.AreEqual("3", splitVector[2], "Correlation Vector extension was not parsed properly"); + Assert.AreEqual("4", splitVector[3], "Correlation Vector extension was not parsed properly"); + Assert.AreEqual(0xA, correlationVector.Extension, "Correlation Vector extension was not parsed properly"); } [TestMethod] @@ -254,6 +304,15 @@ public void ThrowWithTooBigCorrelationVectorValueV2() /* Bigger than 127 chars */ var vector = CorrelationVector.Extend("KZY+dsX2jEaZesgCPjJ2Ng.2147483647.2147483647.2147483647.2147483647.2147483647.2147483647.2147483647.2147483647.2147483647.2147483647"); }); + } + + [TestMethod] + public void ResetWithTooBigCorrelationVectorValueV3() + { + CorrelationVector.ValidateCorrelationVectorDuringCreation = true; + /* Bigger than 127 chars */ + var vector = CorrelationVector.Extend("A.KZY+dsX2jEaZesgCPjJ2Ng.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF"); + Assert.IsTrue(vector.Value.Contains("#"), "Reset vector must contain reset indicator"); } [TestMethod] @@ -317,6 +376,7 @@ public void SpinSortValidation() for (int i = 0; i < 100; i++) { // The cV after a Spin will look like .0..0, so the spinValue is at index = 2. + CorrelationVector newVector = CorrelationVector.Spin(vector.Value, spinParameters); var spinValue = uint.Parse(CorrelationVector.Spin(vector.Value, spinParameters).Value.Split('.')[2]); // Count the number of times the counter wraps. @@ -335,6 +395,54 @@ public void SpinSortValidation() Assert.IsTrue(wrappedCounter <= 1); } + [TestMethod] + public void SpinSortValidationV3() + { + var vector = new CorrelationVectorV3(); + var spinParameters = new SpinParameters + { + Entropy = SpinEntropy.Four, + Interval = SpinCounterInterval.Fine, + Periodicity = SpinCounterPeriodicity.Long + }; + + ulong lastSpinValue = 0; + var wrappedCounter = 0; + for (int i = 0; i < 100; i++) + { + // The cV after a Spin will look like .0_.0, so the spinValue is at index = 2. + CorrelationVector newVector = CorrelationVectorV3.Spin(vector.Value, spinParameters); + string hexValue = newVector.Value.Split('.', '_')[3]; + var spinValue = ulong.Parse(hexValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture); + + // Count the number of times the counter wraps. + if (spinValue <= lastSpinValue) + { + wrappedCounter++; + } + + lastSpinValue = spinValue; + + // Wait for 10ms. + Task.Delay(10).Wait(); + } + + // The counter should wrap at most 1 time. + Assert.IsTrue(wrappedCounter <= 1); + } + + [TestMethod] + public void TestResetV3() + { + CorrelationVector.ValidateCorrelationVectorDuringCreation = false; + const string baseVector = "A.KZY+dsX2jEaZesgCPjJ2Ng.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.FFF"; + + // we hit 127 chars limit, will reset vector + Tuple resetValues = CorrelationVector.Parse(baseVector).Reset(); + Assert.IsTrue(resetValues.Item1.Contains("#"), "Reset vector must contain reset indicator"); + Assert.AreEqual(baseVector, resetValues.Item2, "The stored vector is different from the base vector."); + } + [TestMethod] public void SpinPastMaxWithTerminationSignV2() { @@ -346,6 +454,31 @@ public void SpinPastMaxWithTerminationSignV2() Assert.AreEqual(string.Concat(baseVector, CorrelationVectorV2.TerminationSign), vector.Value); } + [TestMethod] + public void SpinPastMaxWithResetV3() + { + CorrelationVector.ValidateCorrelationVectorDuringCreation = false; + const string baseVector = "A.KZY+dsX2jEaZesgCPjJ2Ng.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF"; + + // we hit 127 chars limit, will reset vector and show value + var vector = CorrelationVector.Spin(baseVector); + Assert.IsTrue(vector.Value.Contains("#"), "Reset vector must contain reset indicator"); + } + + [TestMethod] + public void IncrementPastMaxWithResetV3() + { + CorrelationVector.ValidateCorrelationVectorDuringCreation = false; + const string baseVector = "A.PmvzQKgYek6Sdk/T5sWaqw.1.FA.A1.23_B6A5E62FC38E9974.1_B6A6A13E588CF82F.2A.AB.213_B6A92D24A00C0F9B.47.8B.12.34.A123.2B.23.41.FF"; + + var vectorToIncrement = CorrelationVector.Parse(baseVector); + // we hit 127 chars limit, will reset vector and show value + var incrementedString = vectorToIncrement.Increment(); + var newVector = CorrelationVector.Parse(incrementedString); + Assert.IsTrue(incrementedString.Contains("#"), "Reset vector must contain reset indicator"); + Assert.AreEqual(0x100, newVector.Extension, "Vector with extension FF should increment to 100"); + } + [TestMethod] public void ExtendPastMaxWithTerminationSign() { @@ -368,6 +501,39 @@ public void ExtendPastMaxWithTerminationSignV2() Assert.AreEqual(string.Concat(baseVector, CorrelationVectorV2.TerminationSign), vector.Value); } + [TestMethod] + public void ExtendPastMaxWithResetV3() + { + CorrelationVector.ValidateCorrelationVectorDuringCreation = false; + const string baseVector = "A.KZY+dsX2jEaZesgCPjJ2Ng.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.7FFFFFFF.FFF"; + + // we hit 127 chars limit, will append "!" to vector + var vector = CorrelationVector.Extend(baseVector); + Assert.IsTrue(vector.Value.Contains("#"), "Reset vector must contain reset indicator"); + } + + [TestMethod] + public void ConvertTraceparentV3() + { + const string traceparent = "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01"; + string[] traceSections = traceparent.Split('-'); + var vector = CorrelationVectorV3.Span(traceparent); + + // Convert trace ID to bytes + var traceIDBytes = new byte[traceSections[1].Length / 2]; + for (var i = 0; i < traceIDBytes.Length; i++) + { + traceIDBytes[i] = Convert.ToByte(traceSections[1].Substring(i * 2, 2), 16); + } + + // Convert 64-bit representation of base to bytes + string paddedBase = vector.Base.PadRight(24, '='); + byte[] cvBaseBytes = Convert.FromBase64String(paddedBase); + Assert.IsTrue(vector.Value.Contains("-"), "Span vector must contain span indicator"); + Assert.AreEqual(22, vector.Base.Length, "Correlation Vector base should be 22 characters long"); + CollectionAssert.AreEqual(traceIDBytes, cvBaseBytes, "Trace ID bytes and cV base bytes must be equal"); + } + [TestMethod] public void ImmutableWithTerminationSign() { diff --git a/src/Microsoft.CorrelationVector/CorrelationVector.cs b/src/Microsoft.CorrelationVector/CorrelationVector.cs index 377bea6..6d97817 100644 --- a/src/Microsoft.CorrelationVector/CorrelationVector.cs +++ b/src/Microsoft.CorrelationVector/CorrelationVector.cs @@ -74,6 +74,19 @@ private static CorrelationVectorVersion InferVersion(string correlationVector) { return CorrelationVectorVersion.V2; } + else if (index == 1) + { + // cV version indicated by starting single letter for V3 and after + if (correlationVector.Substring(0, index) == "A") + { + return CorrelationVectorVersion.V3; + } + else + { + //By default not reporting error, just return V1 + return CorrelationVectorVersion.V1; + } + } else { //By default not reporting error, just return V1 @@ -89,14 +102,14 @@ private static CorrelationVectorVersion InferVersion(string correlationVector) public static CorrelationVector Parse(string correlationVector) { CorrelationVectorVersion version = InferVersion(correlationVector); - return RunStaticMethod(correlationVector, version, CorrelationVectorV1.Parse, CorrelationVectorV2.Parse); + return RunStaticMethod(correlationVector, version, CorrelationVectorV1.Parse, CorrelationVectorV2.Parse, CorrelationVectorV3.Parse); } public static CorrelationVector Extend(string correlationVector) { CorrelationVectorVersion version = InferVersion(correlationVector); - return RunStaticMethod(correlationVector, version, CorrelationVectorV1.Extend, CorrelationVectorV2.Extend); + return RunStaticMethod(correlationVector, version, CorrelationVectorV1.Extend, CorrelationVectorV2.Extend, CorrelationVectorV3.Extend); } public static CorrelationVector Spin(string correlationVector) @@ -119,6 +132,8 @@ public static CorrelationVector Spin(string correlationVector, SpinParameters pa return CorrelationVectorV1.Spin(correlationVector, parameters); case CorrelationVectorVersion.V2: return CorrelationVectorV2.Spin(correlationVector, parameters); + case CorrelationVectorVersion.V3: + return CorrelationVectorV3.Spin(correlationVector, parameters); default: return null; } diff --git a/src/Microsoft.CorrelationVector/CorrelationVectorV3.cs b/src/Microsoft.CorrelationVector/CorrelationVectorV3.cs new file mode 100644 index 0000000..0c645b5 --- /dev/null +++ b/src/Microsoft.CorrelationVector/CorrelationVectorV3.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Threading; + +namespace Microsoft.CorrelationVector +{ + public sealed class CorrelationVectorV3 : CorrelationVector + { + internal new const byte MaxVectorLength = 127; + internal const byte BaseLength = 22; + + /// + /// Version character for Correlation Vector v3.0 + /// + public const char VersionChar = 'A'; + + /// + /// Standard delimiter used for suffix, version separation + /// + public const char StandardDelim = '.'; + + /// + /// Delimiter used for a cV that has had a reset operation + /// + public const char ResetDelim = '#'; + + /// + /// Delimiter used for a cV that is interoperable with a W3C traceparent + /// + public const char SpanDelim = '-'; + + /// + /// Delimiter used for a cV that uses a Spin operation + /// + public const char SpinDelim = '_'; + + /// + /// The vector stored in a Reset operation + /// + public string StoredVector = null; + + private static Random rng = new Random(); + + /// + /// Returns the full string representation of the Correlation Vector. + /// + public override string Value + { + get + { + // Convert extension to hex before returning + string hexExtension = extension.ToString("X"); + return string.Concat(this.BaseVector, ".", hexExtension); + } + } + + /// + /// The full correlation vector, excluding the suffix at the end. + /// This includes the "A." at the beginning. + /// + internal readonly string BaseVector; + + /// + /// The suffix at the end of the correlation vector, as an integer. + /// + private int extension = 0; + + /// + /// Returns the cV base of the correlation vector. + /// Example: cV with Value A.e8iECJiOvUGPvOVtchxG9g.F.A.23 returns e8iECJiOvUGPvOVtchxG9g + /// + public override string Base + { + get + { + // Search from first letter after "A." and stop at the first instance of a delimiter + return this.Value.Substring(2, this.Value.IndexOfAny(new char[] { StandardDelim, ResetDelim, SpanDelim, SpinDelim }, 2) - 2); + } + } + + public override int Extension + { + get + { + return this.extension; + } + } + + /// + /// Initializes a new instance of the class. + /// This should only be called if there is no correlation vector in the message header. + /// + public CorrelationVectorV3() + : this(GetUniqueValue(), 0, false) + { + } + + /// + /// Initializes a new instance of the class. + /// This should only be called if there is no correlation vector in the message header. + /// + public CorrelationVectorV3(Guid vectorBase) + : this(vectorBase.GetBaseFromGuid(BaseLength), 0, false) + { + } + + private static string GetUniqueValue() + { + return Guid.NewGuid().GetBaseFromGuid(BaseLength); + } + + private CorrelationVectorV3(string baseVector, int extension, bool immutable, bool appendVersion = true) + { + // first append the "A." unless it is not required to + string baseVectorWithVersion; + if (appendVersion) + { + baseVectorWithVersion = String.Concat(VersionChar, StandardDelim, baseVector); + } + else + { + baseVectorWithVersion = baseVector; + } + this.BaseVector = baseVectorWithVersion; + this.Version = CorrelationVectorVersion.V3; + this.extension = extension; + // this.immutable = immutable || CorrelationVector.IsOversized(baseVector, extension, version); + } + + /// + /// Creates a new correlation vector by extending an existing value. This should be + /// done at the entry point of an operation. + /// + /// + /// Taken from the message header indicated by . + /// + /// A new correlation vector extended from the current vector. + public new static CorrelationVectorV3 Extend(string correlationVector) + { + if (CorrelationVectorV3.ValidateCorrelationVectorDuringCreation) + { + } + + if (CorrelationVectorV3.IsOversized(correlationVector, 0)) + { + return CorrelationVectorV3.Parse(CorrelationVectorV3.Parse(correlationVector).Reset().Item1); + } + return new CorrelationVectorV3(correlationVector, 0, false, false); + } + + /// + /// Creates a new correlation vector by parsing its string representation. + /// + /// correlationVector. + /// Important: Make sure to include the "A." at the beginning! + /// CorrelationVector + public new static CorrelationVectorV3 Parse(string correlationVector) + { + if (!string.IsNullOrEmpty(correlationVector)) + { + int p = correlationVector.LastIndexOf('.'); + // bool oversized = CorrelationVectorV3.IsOversized(correlationVector, 0); + if (p > 0) + { + string extensionValue = correlationVector.Substring(p + 1); + int extension; + if (int.TryParse(extensionValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out extension) && extension >= 0) + { + return new CorrelationVectorV3(correlationVector.Substring(0, p), extension, false, false); + } + } + } + + return new CorrelationVectorV3(); + } + + /// + /// Creates a new correlation vector with a W3C traceparent. + /// + /// + /// CorrelationVector + public static CorrelationVectorV3 Span(string traceparent) + { + // Format: version_format-trace_id-parent_id-trace_flags + // We convert the trace_id into a cV base and append the parent_id to it. + string[] traceSections = traceparent.Split(SpanDelim); + var trace_id = traceSections[1]; + var parent_id = traceSections[2]; + var converted_base = ConvertTraceIdToCvBase(trace_id); + var converted_parent_id = parent_id.ToUpperInvariant(); + string newBaseVector = String.Concat(converted_base, SpanDelim, converted_parent_id); + return new CorrelationVectorV3(newBaseVector, 0, false, true); + } + + private static string ConvertTraceIdToCvBase(string trace_id) + { + var hexBytes = new byte[trace_id.Length / 2]; + for (var i = 0; i < hexBytes.Length; i++) + { + hexBytes[i] = Convert.ToByte(trace_id.Substring(i * 2, 2), 16); + } + return Convert.ToBase64String(hexBytes).Substring(0, BaseLength); + } + + /// + /// Creates a new correlation vector by applying the Spin operator to an existing value. + /// This should be done at the entry point of an operation. + /// + /// + /// Taken from the message header indicated by . + /// + /// A new correlation vector extended from the current vector. + public new static CorrelationVectorV3 Spin(string correlationVector) + { + SpinParameters defaultParameters = new SpinParameters + { + Interval = SpinCounterInterval.Coarse, + Periodicity = SpinCounterPeriodicity.Short, + Entropy = SpinEntropy.Two + }; + + return CorrelationVectorV3.Spin(correlationVector, defaultParameters); + } + + /// + /// Creates a new correlation vector by applying the Spin operator to an existing value. + /// This should be done at the entry point of an operation. + /// + /// + /// Taken from the message header indicated by . + /// + /// + /// The parameters to use when applying the Spin operator. + /// + /// A new correlation vector extended from the current vector. + public new static CorrelationVectorV3 Spin(string correlationVector, SpinParameters parameters) + { + if (CorrelationVectorV3.ValidateCorrelationVectorDuringCreation) + { + // CorrelationVectorV3.Validate(correlationVector); + } + + ulong value = GetTickValue(parameters); + + string s = unchecked((uint)value).ToString("X8"); + if (parameters.TotalBits > 32) + { + s = string.Concat((value >> 32).ToString("X8"), s); + } + + string baseVector = string.Concat(correlationVector, SpinDelim, s); + if (CorrelationVectorV3.IsOversized(baseVector, 0)) + { + string valueToResetFrom = string.Concat(baseVector, ".", 0); + var oversizedVector = Parse(valueToResetFrom); + Tuple resetValues = oversizedVector.Reset(); + return CorrelationVectorV3.Parse(resetValues.Item1); + } + + return new CorrelationVectorV3(baseVector, 0, false, false); + } + + public override Tuple Reset() + { + return Reset(this.Value); + } + + public Tuple Reset(string oversizedValue) + { + SpinParameters parameters = new SpinParameters + { + Interval = SpinCounterInterval.Coarse, + Periodicity = SpinCounterPeriodicity.Long, + Entropy = SpinEntropy.Four + }; + + string newExtension = oversizedValue.Substring(oversizedValue.LastIndexOf('.')+1); + + // Then get a sort/entropy value + string resetValue = GetTickValue(parameters).ToString("X16"); + + string newVector = String.Concat(VersionChar, StandardDelim, this.Base, ResetDelim, resetValue, StandardDelim, newExtension); + + // Store oversized vector in StoredVector for later use + StoredVector = oversizedValue; + return new Tuple(newVector, oversizedValue); + } + + private static ulong GetTickValue(SpinParameters parameters) + { + byte[] entropy = new byte[parameters.EntropyBytes]; + rng.NextBytes(entropy); + + ulong value = (ulong)(DateTime.UtcNow.Ticks >> parameters.TicksBitsToDrop); + for (int i = 0; i < parameters.EntropyBytes; i++) + { + value = (value << 8) | Convert.ToUInt64(entropy[i]); + } + + // Generate a bitmask and mask the lower TotalBits in the value. + // The mask is generated by (1 << TotalBits) - 1. We need to handle the edge case + // when shifting 64 bits, as it wraps around. + value &= (parameters.TotalBits == 64 ? 0 : (ulong)1 << parameters.TotalBits) - 1; + + return value; + } + + public override string Increment() + { + /* + if (this.immutable) + { + return this.Value; + } + */ + int snapshot = 0; + int next = 0; + do + { + snapshot = this.extension; + if (snapshot == int.MaxValue) // 7FFFFFFF + { + return this.Value; + } + next = snapshot + 1; + if (CorrelationVectorV3.IsOversized(this.BaseVector, next)) + { + string valueToResetFrom = string.Concat(this.BaseVector, ".", next.ToString("X")); + Tuple resetValues = Reset(valueToResetFrom); + // Reset this stuff + return resetValues.Item1; + } + } + while (snapshot != Interlocked.CompareExchange(ref this.extension, next, snapshot)); + return string.Concat(this.BaseVector, ".", next); + } + + private static bool IsOversized(string baseVector, int extension) + { + if (!string.IsNullOrEmpty(baseVector)) + { + int size = baseVector.Length + 1 + + (extension > 0 ? (int)Math.Log(extension, 16) : 0) + 1; + return size > MaxVectorLength; + } + return false; + } + } +}