diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bc1727a1..9340f44c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Version 2.8.0-beta1 +- [Adds opt-in support for W3C distributed tracing standard](https://github.com/Microsoft/ApplicationInsights-dotnet-server/pull/945) - Update Base SDK to version 2.8.0-beta1 ## Version 2.7.2 diff --git a/Src/Common/Common.projitems b/Src/Common/Common.projitems index 3843d6940..530dc1418 100644 --- a/Src/Common/Common.projitems +++ b/Src/Common/Common.projitems @@ -19,6 +19,9 @@ + + + diff --git a/Src/Common/InjectionGuardConstants.cs b/Src/Common/InjectionGuardConstants.cs index e9b1313bd..48f7fda63 100644 --- a/Src/Common/InjectionGuardConstants.cs +++ b/Src/Common/InjectionGuardConstants.cs @@ -5,16 +5,46 @@ /// These max limits are intentionally exaggerated to allow for unexpected responses, while still guarding against unreasonably large responses. /// Example: While a 32 character response may be expected, 50 characters may be permitted while a 10,000 character response would be unreasonable and malicious. /// - public static class InjectionGuardConstants +#if DEPENDENCY_COLLECTOR + public +#else + internal +#endif + static class InjectionGuardConstants { /// /// Max length of AppId allowed in response from Breeze. /// - public const int AppIdMaxLengeth = 50; + public const int AppIdMaxLength = 50; /// /// Max length of incoming Request Header value allowed. /// public const int RequestHeaderMaxLength = 1024; + + /// + /// Max length of context header key. + /// + public const int ContextHeaderKeyMaxLength = 50; + + /// + /// Max length of context header value. + /// + public const int ContextHeaderValueMaxLength = 1024; + + /// + /// Max length of traceparent header value. + /// + public const int TraceParentHeaderMaxLength = 55; + + /// + /// Max length of tracestate header value string. + /// + public const int TraceStateHeaderMaxLength = 512; + + /// + /// Max number of key value pairs in the tracestate header. + /// + public const int TraceStateMaxPairs = 32; } } diff --git a/Src/Common/StringUtilities.cs b/Src/Common/StringUtilities.cs index 662c29c62..078cd8803 100644 --- a/Src/Common/StringUtilities.cs +++ b/Src/Common/StringUtilities.cs @@ -3,11 +3,17 @@ using System; using System.Diagnostics; using System.Globalization; + using Microsoft.ApplicationInsights.W3C; /// /// Generic functions to perform common operations on a string. /// - public static class StringUtilities +#if DEPENDENCY_COLLECTOR + public +#else + internal +#endif + static class StringUtilities { private static readonly uint[] Lookup32 = CreateLookup32(); @@ -35,11 +41,64 @@ public static string EnforceMaxLength(string input, int maxLength) /// Random 16 bytes array encoded as hex string public static string GenerateTraceId() { - // See https://stackoverflow.com/questions/311165/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-and-vice-versa/24343727#24343727 - var bytes = Guid.NewGuid().ToByteArray(); + return GenerateId(Guid.NewGuid().ToByteArray(), 0, 16); + } + + /// + /// Generates random span Id as per W3C Distributed tracing specification. + /// https://github.com/w3c/distributed-tracing/blob/master/trace_context/HTTP_HEADER_FORMAT.md#span-id + /// + /// Random 8 bytes array encoded as hex string + public static string GenerateSpanId() + { + return GenerateId(Guid.NewGuid().ToByteArray(), 0, 8); + } - var result = new char[32]; - for (int i = 0; i < 16; i++) + /// + /// Formats trace Id and span Id into valid Request-Id: |trace.span. + /// + /// Trace Id. + /// Span id. + /// valid Request-Id. + public static string FormatRequestId(string traceId, string spanId) + { + return String.Concat("|", traceId, ".", spanId, "."); + } + + /// + /// Gets root id (string between '|' and the first dot) from the hierarchical Id. + /// + /// Id to extract root from. + /// Root operation id. + internal static string GetRootId(string hierarchicalId) + { + // Returns the root Id from the '|' to the first '.' if any. + int rootEnd = hierarchicalId.IndexOf('.'); + if (rootEnd < 0) + { + rootEnd = hierarchicalId.Length; + } + + int rootStart = hierarchicalId[0] == '|' ? 1 : 0; + return hierarchicalId.Substring(rootStart, rootEnd - rootStart); + } + +#pragma warning disable 612, 618 + internal static string FormatAzureTracestate(string appId) + { + return String.Concat(W3CConstants.AzureTracestateNamespace, "=", appId); + } +#pragma warning restore 612, 618 + + /// + /// Converts byte array to hex lower case string. + /// + /// Array encoded as hex string + private static string GenerateId(byte[] bytes, int start, int length) + { + // See https://stackoverflow.com/questions/311165/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-and-vice-versa/24343727#24343727 + var result = new char[length * 2]; + for (int i = start; i < start + length; i++) { var val = Lookup32[bytes[i]]; result[2 * i] = (char)val; diff --git a/Src/Common/W3C/W3CActivityExtensions.cs b/Src/Common/W3C/W3CActivityExtensions.cs new file mode 100644 index 000000000..45726516d --- /dev/null +++ b/Src/Common/W3C/W3CActivityExtensions.cs @@ -0,0 +1,272 @@ +namespace Microsoft.ApplicationInsights.W3C +{ + using System; + using System.ComponentModel; + using System.Diagnostics; + using System.Globalization; + using System.Linq; + using System.Text.RegularExpressions; + using Microsoft.ApplicationInsights.Common; + + /// + /// Extends Activity to support W3C distributed tracing standard. + /// + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] +#if DEPENDENCY_COLLECTOR + public +#else + internal +#endif + static class W3CActivityExtensions + { + private static readonly Regex TraceIdRegex = new Regex("^[a-f0-9]{32}$", RegexOptions.Compiled); + private static readonly Regex SpanIdRegex = new Regex("^[a-f0-9]{16}$", RegexOptions.Compiled); + + /// + /// Generate new W3C context. + /// + /// Activity to generate W3C context on. + /// The same Activity for chaining. + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static Activity GenerateW3CContext(this Activity activity) + { + activity.SetVersion(W3CConstants.DefaultVersion); + activity.SetSampled(W3CConstants.TraceFlagRecordedAndNotRequested); + activity.SetSpanId(StringUtilities.GenerateSpanId()); + activity.SetTraceId(StringUtilities.GenerateTraceId()); + return activity; + } + + /// + /// Checks if current Actuvuty has W3C properties on it. + /// + /// Activity to check. + /// True if Activity has W3C properties, false otherwise. + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static bool IsW3CActivity(this Activity activity) + { + return activity != null && activity.Tags.Any(t => t.Key == W3CConstants.TraceIdTag); + } + + /// + /// Updates context on the Activity based on the W3C Context in the parent Activity tree. + /// + /// Activity to update W3C context on. + /// The same Activity for chaining. + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static Activity UpdateContextOnActivity(this Activity activity) + { + if (activity == null || activity.Tags.Any(t => t.Key == W3CConstants.TraceIdTag)) + { + return activity; + } + + // no w3c Tags on Activity + activity.Parent.UpdateContextOnActivity(); + + // at this point, Parent has W3C tags, but current activity does not - update it + return activity.UpdateContextFromParent(); + } + + /// + /// Gets traceparent header value for the Activity or null if there is no W3C context on it. + /// + /// Activity to read W3C context from. + /// traceparent header value. + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static string GetTraceparent(this Activity activity) + { + string version = null, traceId = null, spanId = null, sampled = null; + foreach (var tag in activity.Tags) + { + switch (tag.Key) + { + case W3CConstants.TraceIdTag: + traceId = tag.Value; + break; + case W3CConstants.SpanIdTag: + spanId = tag.Value; + break; + case W3CConstants.VersionTag: + version = tag.Value; + break; + case W3CConstants.SampledTag: + sampled = tag.Value; + break; + } + } + + if (traceId == null || spanId == null || version == null || sampled == null) + { + return null; + } + + return string.Join("-", version, traceId, spanId, sampled); + } + + /// + /// Initializes W3C context on the Activity from traceparent header value. + /// + /// Activity to set W3C context on. + /// Valid traceparent header like 00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01. + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static void SetTraceparent(this Activity activity, string value) + { + if (activity.IsW3CActivity()) + { + return; + } + + // we only support 00 version and ignore caller version + activity.SetVersion(W3CConstants.DefaultVersion); + + string traceId = null, parentSpanId = null, sampledStr = null; + bool isValid = false; + + var parts = value?.Split('-'); + if (parts != null && parts.Length == 4) + { + traceId = parts[1]; + parentSpanId = parts[2]; + sampledStr = parts[3]; + isValid = TraceIdRegex.IsMatch(traceId) && SpanIdRegex.IsMatch(parentSpanId); + } + + if (isValid) + { + byte.TryParse(sampledStr, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var sampled); + + // we always defer sampling + if ((sampled & W3CConstants.RequestedTraceFlag) == W3CConstants.RequestedTraceFlag) + { + activity.SetSampled(W3CConstants.TraceFlagRecordedAndRequested); + } + else + { + activity.SetSampled(W3CConstants.TraceFlagRecordedAndNotRequested); + } + + activity.SetParentSpanId(parentSpanId); + activity.SetSpanId(StringUtilities.GenerateSpanId()); + activity.SetTraceId(traceId); + } + else + { + activity.SetSampled(W3CConstants.TraceFlagRecordedAndNotRequested); + activity.SetSpanId(StringUtilities.GenerateSpanId()); + activity.SetTraceId(StringUtilities.GenerateTraceId()); + } + } + + /// + /// Gets tracestate header value from the Activity. + /// + /// Activity to get tracestate from. + /// tracestate header value. + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static string GetTracestate(this Activity activity) => + activity.Tags.FirstOrDefault(t => t.Key == W3CConstants.TracestateTag).Value; + + /// + /// Sets tracestate header value on the Activity. + /// + /// Activity to set tracestate on. + /// tracestate header value. + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static void SetTracestate(this Activity activity, string value) => + activity.AddTag(W3CConstants.TracestateTag, value); + + /// + /// Gets TraceId from the Activity. + /// Use carefully: if may cause iteration over all tags! + /// + /// Activity to get traceId from. + /// TraceId value or null if it does not exist. + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static string GetTraceId(this Activity activity) => activity.Tags.FirstOrDefault(t => t.Key == W3CConstants.TraceIdTag).Value; + + /// + /// Gets SpanId from the Activity. + /// Use carefully: if may cause iteration over all tags! + /// + /// Activity to get spanId from. + /// SpanId value or null if it does not exist. + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static string GetSpanId(this Activity activity) => activity.Tags.FirstOrDefault(t => t.Key == W3CConstants.SpanIdTag).Value; + + /// + /// Gets ParentSpanId from the Activity. + /// Use carefully: if may cause iteration over all tags! + /// + /// Activity to get ParentSpanId from. + /// ParentSpanId value or null if it does not exist. + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static string GetParentSpanId(this Activity activity) => activity.Tags.FirstOrDefault(t => t.Key == W3CConstants.ParentSpanIdTag).Value; + + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] + internal static void SetParentSpanId(this Activity activity, string value) => + activity.AddTag(W3CConstants.ParentSpanIdTag, value); + + private static void SetTraceId(this Activity activity, string value) => + activity.AddTag(W3CConstants.TraceIdTag, value); + + private static void SetSpanId(this Activity activity, string value) => + activity.AddTag(W3CConstants.SpanIdTag, value); + + private static void SetVersion(this Activity activity, string value) => + activity.AddTag(W3CConstants.VersionTag, value); + + private static void SetSampled(this Activity activity, string value) => + activity.AddTag(W3CConstants.SampledTag, value); + + private static Activity UpdateContextFromParent(this Activity activity) + { + if (activity != null && activity.Tags.All(t => t.Key != W3CConstants.TraceIdTag)) + { + if (activity.Parent == null) + { + activity.GenerateW3CContext(); + } + else + { + foreach (var tag in activity.Parent.Tags) + { + switch (tag.Key) + { + case W3CConstants.TraceIdTag: + activity.SetTraceId(tag.Value); + break; + case W3CConstants.SpanIdTag: + activity.SetParentSpanId(tag.Value); + activity.SetSpanId(StringUtilities.GenerateSpanId()); + break; + case W3CConstants.VersionTag: + activity.SetVersion(tag.Value); + break; + case W3CConstants.SampledTag: + activity.SetSampled(tag.Value); + break; + case W3CConstants.TracestateTag: + activity.SetTracestate(tag.Value); + break; + } + } + } + } + + return activity; + } + } +} diff --git a/Src/Common/W3C/W3CConstants.cs b/Src/Common/W3C/W3CConstants.cs new file mode 100644 index 000000000..be319055a --- /dev/null +++ b/Src/Common/W3C/W3CConstants.cs @@ -0,0 +1,103 @@ +namespace Microsoft.ApplicationInsights.W3C +{ + using System; + using System.ComponentModel; + + /// + /// W3C constants. + /// + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] +#if DEPENDENCY_COLLECTOR + public +#else + internal +#endif + static class W3CConstants + { + /// + /// W3C traceparent header name. + /// + public const string TraceParentHeader = "traceparent"; + + /// + /// W3C tracestate header name. + /// + public const string TraceStateHeader = "tracestate"; + + /// + /// Name of the field that carry ApplicationInsights application Id in the tracestate header under az key. + /// + public const string ApplicationIdTraceStateField = "cid-v1"; + + /// + /// Name of the field that carry Azure-specific states in the tracestate header. + /// + public const string AzureTracestateNamespace = "az"; + + /// + /// Separator between Azure namespace values. + /// + public const char TracestateAzureSeparator = ';'; + + /// + /// Trace-Id tag name. + /// + internal const string TraceIdTag = "w3c_traceId"; + + /// + /// Span-Id tag name. + /// + internal const string SpanIdTag = "w3c_spanId"; + + /// + /// Parent span-Id tag name. + /// + internal const string ParentSpanIdTag = "w3c_parentSpanId"; + + /// + /// Version tag name. + /// + internal const string VersionTag = "w3c_version"; + + /// + /// Sampled tag name. + /// + internal const string SampledTag = "w3c_sampled"; + + /// + /// Tracestate tag name. + /// + internal const string TracestateTag = "w3c_tracestate"; + + /// + /// Default version value. + /// + internal const string DefaultVersion = "00"; + + /// + /// Default sampled flag value: may be recorded, not requested + /// + internal const string TraceFlagRecordedAndNotRequested = "02"; + + /// + /// Recorded and requested sampled flag value + /// + internal const string TraceFlagRecordedAndRequested = "03"; + + /// + /// Requested trace flag + /// + internal const byte RequestedTraceFlag = 1; + + /// + /// Legacy root Id tag name. + /// + internal const string LegacyRootIdProperty = "ai_legacyRootId"; + + /// + /// Legacy root Id tag name. + /// + internal const string LegacyRequestIdProperty = "ai_legacyRequestId"; + } +} diff --git a/Src/Common/W3C/W3COperationCorrelationTelemetryInitializer.cs b/Src/Common/W3C/W3COperationCorrelationTelemetryInitializer.cs new file mode 100644 index 000000000..615fa48db --- /dev/null +++ b/Src/Common/W3C/W3COperationCorrelationTelemetryInitializer.cs @@ -0,0 +1,124 @@ +namespace Microsoft.ApplicationInsights.W3C +{ + using System; + using System.ComponentModel; + using System.Diagnostics; + using System.Linq; + using Microsoft.ApplicationInsights.Channel; + using Microsoft.ApplicationInsights.Common; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Extensibility; + using Microsoft.ApplicationInsights.Extensibility.Implementation; + + /// + /// Telemetry Initializer that sets correlation ids for W3C. + /// + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] +#if DEPENDENCY_COLLECTOR + public +#else + internal +#endif + class W3COperationCorrelationTelemetryInitializer : ITelemetryInitializer + { + private const string RddDiagnosticSourcePrefix = "rdddsc"; + private const string SqlRemoteDependencyType = "SQL"; + + /// + /// Initializes telemety item. + /// + /// Telemetry item. + public void Initialize(ITelemetry telemetry) + { + Activity currentActivity = Activity.Current; + UpdateTelemetry(telemetry, currentActivity, false); + } + + internal static void UpdateTelemetry(ITelemetry telemetry, Activity activity, bool forceUpdate) + { + if (activity == null) + { + return; + } + + activity.UpdateContextOnActivity(); + + // Requests and dependnecies are initialized from the current Activity + // (i.e. telemetry.Id = current.Id). Activity is created for such requests specifically + // Traces, exceptions, events on the other side are children of current activity + // There is one exception - SQL DiagnosticSource where current Activity is a parent + // for dependency calls. + + OperationTelemetry opTelemetry = telemetry as OperationTelemetry; + bool initializeFromCurrent = opTelemetry != null; + + if (initializeFromCurrent) + { + initializeFromCurrent &= !(opTelemetry is DependencyTelemetry dependency && + dependency.Type == SqlRemoteDependencyType && + dependency.Context.GetInternalContext().SdkVersion + .StartsWith(RddDiagnosticSourcePrefix, StringComparison.Ordinal)); + } + + string spanId = null, parentSpanId = null; + foreach (var tag in activity.Tags) + { + switch (tag.Key) + { + case W3CConstants.TraceIdTag: +#if NET45 + // on .NET Fx Activities are not always reliable, this code prevents update + // of the telemetry that was forcibly updated during Activity lifetime + // ON .NET Core there is no such problem + if (telemetry.Context.Operation.Id == tag.Value && !forceUpdate) + { + return; + } +#endif + telemetry.Context.Operation.Id = tag.Value; + break; + case W3CConstants.SpanIdTag: + spanId = tag.Value; + break; + case W3CConstants.ParentSpanIdTag: + parentSpanId = tag.Value; + break; + case W3CConstants.TracestateTag: + if (telemetry is OperationTelemetry operation) + { + operation.Properties[W3CConstants.TracestateTag] = tag.Value; + } + + break; + } + } + + if (initializeFromCurrent) + { + opTelemetry.Id = StringUtilities.FormatRequestId(telemetry.Context.Operation.Id, spanId); + if (parentSpanId != null) + { + telemetry.Context.Operation.ParentId = StringUtilities.FormatRequestId(telemetry.Context.Operation.Id, parentSpanId); + } + } + else + { + telemetry.Context.Operation.ParentId = StringUtilities.FormatRequestId(telemetry.Context.Operation.Id, spanId); + } + + if (opTelemetry != null) + { + if (opTelemetry.Context.Operation.Id != activity.RootId) + { + opTelemetry.Properties[W3CConstants.LegacyRootIdProperty] = activity.RootId; + } + + if (opTelemetry.Id != activity.Id) + { + opTelemetry.Properties[W3CConstants.LegacyRequestIdProperty] = activity.Id; + } + } + } + } +} diff --git a/Src/Common/WebHeaderCollectionExtensions.cs b/Src/Common/WebHeaderCollectionExtensions.cs index abc35a4d2..7b9e06f7e 100644 --- a/Src/Common/WebHeaderCollectionExtensions.cs +++ b/Src/Common/WebHeaderCollectionExtensions.cs @@ -25,7 +25,7 @@ public static string GetNameValueHeaderValue(this NameValueCollection headers, s Debug.Assert(headerName != null, "headerName must not be null"); Debug.Assert(keyName != null, "keyName must not be null"); - IEnumerable headerValue = GetHeaderValue(headers, headerName); + IEnumerable headerValue = headers.GetHeaderValue(headerName); return HeadersUtilities.GetHeaderKeyValue(headerValue, keyName); } @@ -39,7 +39,7 @@ public static IDictionary GetNameValueCollectionFromHeader(this { Debug.Assert(headerName != null, "headerName must not be null"); - IEnumerable headerValue = GetHeaderValue(headers, headerName); + IEnumerable headerValue = headers.GetHeaderValue(headerName); return HeadersUtilities.GetHeaderDictionary(headerValue); } @@ -55,8 +55,8 @@ public static void SetNameValueHeaderValue(this NameValueCollection headers, str Debug.Assert(headerName != null, "headerName must not be null"); Debug.Assert(keyName != null, "keyName must not be null"); - IEnumerable headerValue = GetHeaderValue(headers, headerName); - headers[headerName] = string.Join(", ", HeadersUtilities.UpdateHeaderWithKeyValue(headerValue, keyName, value)); + IEnumerable headerValue = headers.GetHeaderValue(headerName); + headers[headerName] = string.Join(",", HeadersUtilities.UpdateHeaderWithKeyValue(headerValue, keyName, value)); } /// @@ -83,9 +83,45 @@ public static void SetHeaderFromNameValueCollection(this NameValueCollection hea } } - private static IEnumerable GetHeaderValue(NameValueCollection headers, string headerName) + /// + /// For the given header collection, for a given header name, returns collection of header values. + /// + /// Header collection. + /// Name of the header in the collection. + /// Maximum allowed header length + /// Maximum allowed number comma separated values in the header + /// List of comma separated values in the given header. + public static IEnumerable GetHeaderValue(this NameValueCollection headers, string headerName, int maxStringLength = -1, int maxItems = -1) { - return headers[headerName]?.Split(','); + var headerValueStr = headers[headerName]; + if (headerValueStr != null) + { + if (maxStringLength >= 0 && headerValueStr.Length > maxStringLength) + { + int lastValidComma = maxStringLength; + while (headerValueStr[lastValidComma] != ',' && lastValidComma > 0) + { + lastValidComma--; + } + + if (lastValidComma <= 0) + { + return null; + } + + headerValueStr = headerValueStr.Substring(0, lastValidComma); + } + + var items = headerValueStr.Split(','); + if (maxItems > 0 && items.Length > maxItems) + { + return items.Take(maxItems); + } + + return items; + } + + return null; } private static string FormatKeyValueHeader(string key, string value) diff --git a/Src/DependencyCollector/Net45.Tests/DependencyTrackingTelemetryModuleHttpTest.cs b/Src/DependencyCollector/Net45.Tests/DependencyTrackingTelemetryModuleHttpTest.cs index e7f0c6fbe..eb7e60832 100644 --- a/Src/DependencyCollector/Net45.Tests/DependencyTrackingTelemetryModuleHttpTest.cs +++ b/Src/DependencyCollector/Net45.Tests/DependencyTrackingTelemetryModuleHttpTest.cs @@ -11,13 +11,16 @@ using System.Threading; using System.Threading.Tasks; + using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.Common; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.DependencyCollector; using Microsoft.ApplicationInsights.DependencyCollector.Implementation; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation; + using Microsoft.ApplicationInsights.Extensibility.Implementation.ApplicationId; using Microsoft.ApplicationInsights.TestFramework; + using Microsoft.ApplicationInsights.W3C; using Microsoft.ApplicationInsights.Web.TestFramework; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -30,11 +33,14 @@ public class DependencyTrackingTelemetryModuleHttpTest private const string IKey = "F8474271-D231-45B6-8DD4-D344C309AE69"; private const string FakeProfileApiEndpoint = "https://dc.services.visualstudio.com/v2/track"; private const string LocalhostUrlDiagSource = "http://localhost:8088/"; - private const string LocalhostUrlEventSource = "http://localhost:8089/"; + private const string LocalhostUrlEventSource = "http://localhost:8090/"; + private const string expectedAppId = "someAppId"; + private readonly DictionaryApplicationIdProvider appIdProvider = new DictionaryApplicationIdProvider(); private StubTelemetryChannel channel; private TelemetryConfiguration config; - private List sentTelemetry; + private List sentTelemetry; + private object request; private object response; private object responseHeaders; @@ -43,7 +49,7 @@ public class DependencyTrackingTelemetryModuleHttpTest public void Initialize() { ServicePointManager.DefaultConnectionLimit = 1000; - this.sentTelemetry = new List(); + this.sentTelemetry = new List(); this.request = null; this.response = null; this.responseHeaders = null; @@ -52,11 +58,10 @@ public void Initialize() { OnSend = telemetry => { - // The correlation id lookup service also makes http call, just make sure we skip that - DependencyTelemetry depTelemetry = telemetry as DependencyTelemetry; - if (depTelemetry != null) + this.sentTelemetry.Add(telemetry); + + if (telemetry is DependencyTelemetry depTelemetry) { - this.sentTelemetry.Add(depTelemetry); depTelemetry.TryGetOperationDetail(RemoteDependencyConstants.HttpRequestOperationDetailName, out this.request); depTelemetry.TryGetOperationDetail(RemoteDependencyConstants.HttpResponseOperationDetailName, out this.response); depTelemetry.TryGetOperationDetail(RemoteDependencyConstants.HttpResponseOperationDetailName, out this.responseHeaders); @@ -65,10 +70,16 @@ public void Initialize() EndpointAddress = FakeProfileApiEndpoint }; + this.appIdProvider.Defined = new Dictionary + { + [IKey] = expectedAppId + }; + this.config = new TelemetryConfiguration { InstrumentationKey = IKey, - TelemetryChannel = this.channel + TelemetryChannel = this.channel, + ApplicationIdProvider = this.appIdProvider }; this.config.TelemetryInitializers.Add(new OperationCorrelationTelemetryInitializer()); @@ -299,6 +310,152 @@ public void TestDependencyCollectionEventSourceRedirect() this.TestCollectionResponseWithRedirects(false, LocalhostUrlEventSource); } +#pragma warning disable 612, 618 + /// + /// Tests that outgoing requests emit W3C headers and telemetry is initialized accordingly when configured so. + /// + [TestMethod] + [Timeout(5000)] + public void TestDependencyCollectionWithW3CHeadersDiagnosticSource() + { + using (var module = new DependencyTrackingTelemetryModule()) + { + module.EnableW3CHeadersInjection = true; + this.config.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); + module.Initialize(this.config); + + var parent = new Activity("parent") + .AddBaggage("k", "v") + .SetParentId("|guid.") + .Start() + .GenerateW3CContext(); + parent.SetTracestate("state=some"); + + var url = new Uri(LocalhostUrlDiagSource); + HttpWebRequest request = WebRequest.CreateHttp(LocalhostUrlDiagSource); + using (new LocalServer(LocalhostUrlDiagSource)) + { + using (request.GetResponse()) + { + } + } + + // DiagnosticSource Response event is fired after SendAsync returns on netcoreapp1.* + // let's wait until dependency is collected + Assert.IsTrue(SpinWait.SpinUntil(() => this.sentTelemetry != null, TimeSpan.FromSeconds(1))); + + parent.Stop(); + + string expectedTraceId = parent.GetTraceId(); + string expectedParentId = parent.GetSpanId(); + + DependencyTelemetry dependency = (DependencyTelemetry)this.sentTelemetry.Single(); + Assert.AreEqual(expectedTraceId, dependency.Context.Operation.Id); + Assert.AreEqual($"|{expectedTraceId}.{expectedParentId}.", dependency.Context.Operation.ParentId); + + var dependencyIdParts = dependency.Id.Split('.', '|'); + Assert.AreEqual(4, dependencyIdParts.Length); + Assert.AreEqual(expectedTraceId, dependencyIdParts[1]); + Assert.AreEqual($"00-{expectedTraceId}-{dependencyIdParts[2]}-02", request.Headers[W3CConstants.TraceParentHeader]); + + Assert.AreEqual($"{W3CConstants.AzureTracestateNamespace}={expectedAppId},state=some", request.Headers[W3CConstants.TraceStateHeader]); + + Assert.AreEqual("k=v", request.Headers[RequestResponseHeaders.CorrelationContextHeader]); + Assert.AreEqual("v", dependency.Properties["k"]); + Assert.AreEqual("state=some", dependency.Properties[W3CConstants.TracestateTag]); + + Assert.IsTrue(dependency.Properties.ContainsKey(W3CConstants.LegacyRequestIdProperty)); + Assert.IsTrue(dependency.Properties[W3CConstants.LegacyRequestIdProperty].StartsWith("|guid.")); + + Assert.IsTrue(dependency.Properties.ContainsKey(W3CConstants.LegacyRootIdProperty)); + Assert.AreEqual("guid", dependency.Properties[W3CConstants.LegacyRootIdProperty]); + } + } + + /// + /// Tests that outgoing requests emit W3C headers and telemetry is initialized accordingly when configured so. + /// + [TestMethod] + [Timeout(5000)] + public void TestDependencyCollectionWithW3CHeadersDiagnosticSourceAndStartParentOperation() + { + var telemetryClient = new TelemetryClient(this.config); + using (var module = new DependencyTrackingTelemetryModule()) + { + module.EnableW3CHeadersInjection = true; + this.config.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); + module.Initialize(this.config); + + Activity operationActvity; + using (telemetryClient.StartOperation("foo")) + { + operationActvity = Activity.Current; + HttpWebRequest httpRequest = WebRequest.CreateHttp(LocalhostUrlDiagSource); + using (new LocalServer(LocalhostUrlDiagSource)) + { + using (httpRequest.GetResponse()) + { + } + } + } + + // DiagnosticSource Response event is fired after SendAsync returns on netcoreapp1.* + // let's wait until dependency is collected + Assert.IsTrue(SpinWait.SpinUntil(() => this.sentTelemetry.Count >= 2, TimeSpan.FromSeconds(1))); + + RequestTelemetry requestTelemetry = this.sentTelemetry.OfType().Single(); + DependencyTelemetry dependencyTelemetry = this.sentTelemetry.OfType().Single(); + + Assert.AreEqual(requestTelemetry.Context.Operation.Id, dependencyTelemetry.Context.Operation.Id); + Assert.AreEqual(requestTelemetry.Id, dependencyTelemetry.Context.Operation.ParentId); + + Assert.AreEqual(operationActvity.RootId, dependencyTelemetry.Properties[W3CConstants.LegacyRootIdProperty]); + Assert.IsTrue(dependencyTelemetry.Properties[W3CConstants.LegacyRequestIdProperty].StartsWith(operationActvity.Id)); + } + } + + /// + /// Tests that outgoing requests emit W3C headers and telemetry is initialized accordingly when configured so. + /// + [TestMethod] + [Timeout(5000)] + public void TestDependencyCollectionWithW3CHeadersAndStateDiagnosticSource() + { + using (var module = new DependencyTrackingTelemetryModule()) + { + module.EnableW3CHeadersInjection = true; + this.config.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); + module.Initialize(this.config); + + var parent = new Activity("parent") + .Start() + .GenerateW3CContext(); + + parent.SetTracestate("some=state"); + + var url = new Uri(LocalhostUrlDiagSource); + HttpWebRequest request = WebRequest.CreateHttp(LocalhostUrlDiagSource); + using (new LocalServer(LocalhostUrlDiagSource)) + { + using (request.GetResponse()) + { + } + } + + // DiagnosticSource Response event is fired after SendAsync returns on netcoreapp1.* + // let's wait until dependency is collected + Assert.IsTrue(SpinWait.SpinUntil(() => this.sentTelemetry != null, TimeSpan.FromSeconds(1))); + + parent.Stop(); + + Assert.IsTrue(request.Headers[W3CConstants.TraceStateHeader].Contains($"{W3CConstants.AzureTracestateNamespace}={expectedAppId}")); + Assert.IsTrue(request.Headers[W3CConstants.TraceStateHeader].Contains("some=state")); + Assert.AreEqual(2, request.Headers[W3CConstants.TraceStateHeader].Split(',').Length); + } + } + +#pragma warning restore 612, 618 + private void TestCollectionPostRequests(bool enableDiagnosticSource, string url) { using (this.CreateDependencyTrackingModule(enableDiagnosticSource)) @@ -320,7 +477,7 @@ private void TestCollectionPostRequests(bool enableDiagnosticSource, string url) } } - this.ValidateTelemetry(enableDiagnosticSource, this.sentTelemetry.Single(), new Uri(url), request, true, "200"); + this.ValidateTelemetry(enableDiagnosticSource, (DependencyTelemetry)this.sentTelemetry.Single(), new Uri(url), request, true, "200"); } } @@ -353,7 +510,7 @@ private void TestCollectionResponseWithRedirects(bool enableDiagnosticSource, st } } - this.ValidateTelemetry(enableDiagnosticSource, this.sentTelemetry.Single(), new Uri(url), request, true, "200"); + this.ValidateTelemetry(enableDiagnosticSource, (DependencyTelemetry)this.sentTelemetry.Single(), new Uri(url), request, true, "200"); } } @@ -391,7 +548,7 @@ private void TestCollectionSuccessfulResponse(bool enableDiagnosticSource, strin } } - this.ValidateTelemetry(enableDiagnosticSource, this.sentTelemetry.Single(), new Uri(url), request, statusCode >= 200 && statusCode < 300, statusCode.ToString(CultureInfo.InvariantCulture), expectLegacyHeaders: injectLegacyHeaders); + this.ValidateTelemetry(enableDiagnosticSource, (DependencyTelemetry)this.sentTelemetry.Single(), new Uri(url), request, statusCode >= 200 && statusCode < 300, statusCode.ToString(CultureInfo.InvariantCulture), expectLegacyHeaders: injectLegacyHeaders); } } @@ -421,7 +578,7 @@ private async Task TestCollectionHttpClientSuccessfulResponse(string url, int st } } - this.ValidateTelemetry(true, this.sentTelemetry.Single(), new Uri(url), null, statusCode >= 200 && statusCode < 300, statusCode.ToString(CultureInfo.InvariantCulture), responseExpected: contentLength != 0); + this.ValidateTelemetry(true, (DependencyTelemetry)this.sentTelemetry.Single(), new Uri(url), null, statusCode >= 200 && statusCode < 300, statusCode.ToString(CultureInfo.InvariantCulture), responseExpected: contentLength != 0); } } @@ -477,7 +634,7 @@ private async Task TestZeroContentResponseAfterNonZeroResponse(string url, int s } Assert.AreEqual(2, this.sentTelemetry.Count); - this.ValidateTelemetry(true, this.sentTelemetry.Last(), new Uri(url), null, statusCode >= 200 && statusCode < 300, statusCode.ToString(CultureInfo.InvariantCulture), responseExpected: false); + this.ValidateTelemetry(true, (DependencyTelemetry)this.sentTelemetry.Last(), new Uri(url), null, statusCode >= 200 && statusCode < 300, statusCode.ToString(CultureInfo.InvariantCulture), responseExpected: false); } } @@ -507,7 +664,7 @@ private async Task TestCollectionCanceledRequest(bool enableDiagnosticSource, st await httpClient.GetAsync(url, cts.Token).ContinueWith(t => { }); } - this.ValidateTelemetry(enableDiagnosticSource, this.sentTelemetry.Single(), new Uri(url), null, false, string.Empty, responseExpected: false); + this.ValidateTelemetry(enableDiagnosticSource, (DependencyTelemetry)this.sentTelemetry.Single(), new Uri(url), null, false, string.Empty, responseExpected: false); } } @@ -524,7 +681,7 @@ private async Task TestCollectionDnsIssue(bool enableDiagnosticSource) // here the start of dependency is tracked with HttpDesktopDiagnosticSourceListener, // so the expected SDK version should have DiagnosticSource 'rdddsd' prefix. // however the end is tracked by FrameworkHttpEventListener - this.ValidateTelemetry(true, this.sentTelemetry.Single(), url, null, false, string.Empty, responseExpected: false); + this.ValidateTelemetry(true, (DependencyTelemetry)this.sentTelemetry.Single(), url, null, false, string.Empty, responseExpected: false); } else { diff --git a/Src/DependencyCollector/Net45/DependencyCollector.Net45.csproj b/Src/DependencyCollector/Net45/DependencyCollector.Net45.csproj index 5d9ec215e..3b332078c 100644 --- a/Src/DependencyCollector/Net45/DependencyCollector.Net45.csproj +++ b/Src/DependencyCollector/Net45/DependencyCollector.Net45.csproj @@ -1,4 +1,4 @@ - + @@ -13,13 +13,14 @@ false ..\..\ - $(DefineConstants);NET45;ALLOW_AGGRESSIVE_INLIGNING_ATTRIBUTE + $(DefineConstants);NET45;ALLOW_AGGRESSIVE_INLIGNING_ATTRIBUTE;DEPENDENCY_COLLECTOR ..\..\..\ApplicationInsightsSDKRules.ruleset true + TRACE;DEBUG;CODE_ANALYSIS;;NET45;ALLOW_AGGRESSIVE_INLIGNING_ATTRIBUTE @@ -68,4 +69,4 @@ - + \ No newline at end of file diff --git a/Src/DependencyCollector/NetCore.Tests/DependencyTrackingTelemetryModuleTestNetCore.cs b/Src/DependencyCollector/NetCore.Tests/DependencyTrackingTelemetryModuleTestNetCore.cs index 27dd9f8d0..6ef66b05d 100644 --- a/Src/DependencyCollector/NetCore.Tests/DependencyTrackingTelemetryModuleTestNetCore.cs +++ b/Src/DependencyCollector/NetCore.Tests/DependencyTrackingTelemetryModuleTestNetCore.cs @@ -14,13 +14,17 @@ using Microsoft.ApplicationInsights.DependencyCollector.Implementation; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation; + using Microsoft.ApplicationInsights.Extensibility.Implementation.ApplicationId; using Microsoft.ApplicationInsights.TestFramework; + using Microsoft.ApplicationInsights.W3C; using Microsoft.ApplicationInsights.Web.TestFramework; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.VisualStudio.TestTools.UnitTesting; +#pragma warning disable 612, 618 + /// /// .NET Core specific tests that verify Http Dependencies are collected for outgoing request /// @@ -30,10 +34,13 @@ public class DependencyTrackingTelemetryModuleTestNetCore private const string IKey = "F8474271-D231-45B6-8DD4-D344C309AE69"; private const string FakeProfileApiEndpoint = "https://dc.services.visualstudio.com/v2/track"; private const string localhostUrl = "http://localhost:5050"; + private const string expectedAppId = "cid-v1:someAppId"; + private readonly DictionaryApplicationIdProvider appIdProvider = new DictionaryApplicationIdProvider(); private StubTelemetryChannel channel; private TelemetryConfiguration config; private List sentTelemetry; + private object request; private object response; private object responseHeaders; @@ -66,10 +73,16 @@ public void Initialize() EndpointAddress = FakeProfileApiEndpoint }; + this.appIdProvider.Defined = new Dictionary + { + [IKey] = expectedAppId + }; + this.config = new TelemetryConfiguration { InstrumentationKey = IKey, - TelemetryChannel = this.channel + TelemetryChannel = this.channel, + ApplicationIdProvider = this.appIdProvider }; this.config.TelemetryInitializers.Add(new OperationCorrelationTelemetryInitializer()); @@ -194,6 +207,156 @@ public async Task TestDependencyCollectionDnsIssue() } } + /// + /// Tests that dependency is collected properly when there is parent activity. + /// + [TestMethod] + [Timeout(5000)] + public async Task TestDependencyCollectionWithW3CHeadersAndRequestId() + { + using (var module = new DependencyTrackingTelemetryModule()) + { + module.EnableW3CHeadersInjection = true; + this.config.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); + module.Initialize(this.config); + + var parent = new Activity("parent") + .AddBaggage("k", "v") + .SetParentId("|guid.") + .Start() + .GenerateW3CContext(); + parent.SetTracestate("state=some"); + var url = new Uri(localhostUrl); + var request = new HttpRequestMessage(HttpMethod.Get, url); + using (new LocalServer(localhostUrl)) + { + await new HttpClient().SendAsync(request); + } + + // DiagnosticSource Response event is fired after SendAsync returns on netcoreapp1.* + // let's wait until dependency is collected + Assert.IsTrue(SpinWait.SpinUntil(() => this.sentTelemetry.Count > 0, TimeSpan.FromSeconds(1))); + + parent.Stop(); + + string expectedTraceId = parent.GetTraceId(); + string expectedParentId = parent.GetSpanId(); + + DependencyTelemetry dependency = this.sentTelemetry.Single(); + Assert.AreEqual(expectedTraceId, dependency.Context.Operation.Id); + Assert.AreEqual($"|{expectedTraceId}.{expectedParentId}.", dependency.Context.Operation.ParentId); + + Assert.IsTrue(request.Headers.Contains(W3CConstants.TraceParentHeader)); + + var dependencyIdParts = dependency.Id.Split('.', '|'); + Assert.AreEqual(4, dependencyIdParts.Length); + + Assert.AreEqual(expectedTraceId, dependencyIdParts[1]); + Assert.AreEqual($"00-{expectedTraceId}-{dependencyIdParts[2]}-02", request.Headers.GetValues(W3CConstants.TraceParentHeader).Single()); + + Assert.IsTrue(request.Headers.Contains(W3CConstants.TraceStateHeader)); + Assert.AreEqual($"{W3CConstants.AzureTracestateNamespace}={expectedAppId},state=some", request.Headers.GetValues(W3CConstants.TraceStateHeader).Single()); + + Assert.IsTrue(request.Headers.Contains(RequestResponseHeaders.CorrelationContextHeader)); + Assert.AreEqual("k=v", request.Headers.GetValues(RequestResponseHeaders.CorrelationContextHeader).Single()); + + Assert.AreEqual("v", dependency.Properties["k"]); + Assert.AreEqual("state=some", dependency.Properties[W3CConstants.TracestateTag]); + + Assert.IsTrue(dependency.Properties.ContainsKey(W3CConstants.LegacyRequestIdProperty)); + Assert.IsTrue(dependency.Properties[W3CConstants.LegacyRequestIdProperty].StartsWith("|guid.")); + } + } + + /// + /// Tests that dependency is collected properly when there is parent activity. + /// + [TestMethod] + [Timeout(5000)] + public async Task TestDependencyCollectionWithW3CHeadersAndNoParentContext() + { + using (var module = new DependencyTrackingTelemetryModule()) + { + module.EnableW3CHeadersInjection = true; + this.config.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); + module.Initialize(this.config); + + var parent = new Activity("parent") + .Start(); + + var url = new Uri(localhostUrl); + var request = new HttpRequestMessage(HttpMethod.Get, url); + using (new LocalServer(localhostUrl)) + { + await new HttpClient().SendAsync(request); + } + + // DiagnosticSource Response event is fired after SendAsync returns on netcoreapp1.* + // let's wait until dependency is collected + Assert.IsTrue(SpinWait.SpinUntil(() => this.sentTelemetry != null, TimeSpan.FromSeconds(1))); + + parent.Stop(); + + string expectedTraceId = parent.GetTraceId(); + string expectedParentId = parent.GetSpanId(); + + DependencyTelemetry dependency = this.sentTelemetry.Single(); + Assert.AreEqual(expectedTraceId, dependency.Context.Operation.Id); + Assert.AreEqual($"|{expectedTraceId}.{expectedParentId}.", dependency.Context.Operation.ParentId); + + Assert.IsTrue(request.Headers.Contains(W3CConstants.TraceParentHeader)); + + var dependencyIdParts = dependency.Id.Split('.', '|'); + Assert.AreEqual(4, dependencyIdParts.Length); + + Assert.AreEqual(expectedTraceId, dependencyIdParts[1]); + Assert.AreEqual($"00-{expectedTraceId}-{dependencyIdParts[2]}-02", request.Headers.GetValues(W3CConstants.TraceParentHeader).Single()); + + Assert.IsTrue(request.Headers.Contains(W3CConstants.TraceStateHeader)); + Assert.AreEqual($"{W3CConstants.AzureTracestateNamespace}={expectedAppId}", request.Headers.GetValues(W3CConstants.TraceStateHeader).Single()); + + Assert.IsTrue(dependency.Properties.ContainsKey(W3CConstants.LegacyRequestIdProperty)); + Assert.IsTrue(dependency.Properties[W3CConstants.LegacyRequestIdProperty].StartsWith(parent.Id)); + } + } + + /// + /// Tests that dependency is collected properly when there is parent activity. + /// + [TestMethod] + [Timeout(5000)] + public async Task TestDependencyCollectionWithW3CHeadersWithState() + { + using (var module = new DependencyTrackingTelemetryModule()) + { + module.EnableW3CHeadersInjection = true; + this.config.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); + module.Initialize(this.config); + + var parent = new Activity("parent") + .Start() + .GenerateW3CContext(); + + parent.SetTracestate("some=state"); + + var url = new Uri(localhostUrl); + var request = new HttpRequestMessage(HttpMethod.Get, url); + using (new LocalServer(localhostUrl)) + { + await new HttpClient().SendAsync(request); + } + + // DiagnosticSource Response event is fired after SendAsync returns on netcoreapp1.* + // let's wait until dependency is collected + Assert.IsTrue(SpinWait.SpinUntil(() => this.sentTelemetry != null, TimeSpan.FromSeconds(1))); + + parent.Stop(); + + var traceState = HttpHeadersUtilities.GetHeaderValues(request.Headers, W3CConstants.TraceStateHeader).First(); + Assert.AreEqual($"{W3CConstants.AzureTracestateNamespace}={expectedAppId},some=state", traceState); + } + } + private void ValidateTelemetryForDiagnosticSource(DependencyTelemetry item, Uri url, HttpRequestMessage request, bool success, string resultCode, bool expectLegacyHeaders, Activity parent = null) { Assert.AreEqual(url, item.Data); @@ -301,4 +464,5 @@ public void Configure(IApplicationBuilder app) } } } +#pragma warning restore 612, 618 } diff --git a/Src/DependencyCollector/NetCore/DependencyCollector.NetCore.csproj b/Src/DependencyCollector/NetCore/DependencyCollector.NetCore.csproj index bbe06af17..1f5cd511d 100644 --- a/Src/DependencyCollector/NetCore/DependencyCollector.NetCore.csproj +++ b/Src/DependencyCollector/NetCore/DependencyCollector.NetCore.csproj @@ -15,6 +15,7 @@ 2.8.0-beta1 netstandard1.6 true + DEPENDENCY_COLLECTOR true Microsoft.AI.DependencyCollector ../../../Keys/35MSSharedLib1024.snk @@ -24,6 +25,10 @@ 1.6.1 + + TRACE;DEBUG;NETSTANDARD;NETSTANDARD1_6;DEPENDENCY_COLLECTOR + + diff --git a/Src/DependencyCollector/Shared.Tests/DependencyCollector.Shared.Tests.projitems b/Src/DependencyCollector/Shared.Tests/DependencyCollector.Shared.Tests.projitems index aa8c31627..2434f90f0 100644 --- a/Src/DependencyCollector/Shared.Tests/DependencyCollector.Shared.Tests.projitems +++ b/Src/DependencyCollector/Shared.Tests/DependencyCollector.Shared.Tests.projitems @@ -27,6 +27,8 @@ + + diff --git a/Src/DependencyCollector/Shared.Tests/HeaderCollectionManipulationTests.cs b/Src/DependencyCollector/Shared.Tests/HeaderCollectionManipulationTests.cs index 2bedc0ab9..fbb539f10 100644 --- a/Src/DependencyCollector/Shared.Tests/HeaderCollectionManipulationTests.cs +++ b/Src/DependencyCollector/Shared.Tests/HeaderCollectionManipulationTests.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using Microsoft.ApplicationInsights.Common; + using Microsoft.ApplicationInsights.W3C; using VisualStudio.TestTools.UnitTesting; [TestClass] @@ -58,13 +59,13 @@ public void SetNameValueHeaderWorksCorrectly() // Non empty collection - adding new key headers.SetNameValueHeaderValue("Request-Context", "roleName", "workerRole"); Assert.AreEqual(1, headers.Keys.Count); - Assert.AreEqual("appId=appIdValue, roleName=workerRole", headers["Request-Context"]); + Assert.AreEqual("appId=appIdValue,roleName=workerRole", headers["Request-Context"]); // overwritting existing key headers.SetNameValueHeaderValue("Request-Context", "roleName", "webRole"); headers.SetNameValueHeaderValue("Request-Context", "appId", "udpatedAppId"); Assert.AreEqual(1, headers.Keys.Count); - Assert.AreEqual("roleName=webRole, appId=udpatedAppId", headers["Request-Context"]); + Assert.AreEqual("roleName=webRole,appId=udpatedAppId", headers["Request-Context"]); } /// @@ -161,5 +162,55 @@ public void SetNameValueHeaderWithNonEmptyCollectionSetsHeader() Assert.IsNotNull(headers["Correlation-Context"]); Assert.AreEqual("k1=v1,k2=v2,k1=v3", headers["Correlation-Context"]); } + +#pragma warning disable 612, 618 + [TestMethod] + public void GetHeaderValueNoMax() + { + WebHeaderCollection headers = new WebHeaderCollection { [W3CConstants.TraceStateHeader] = "k1=v1,k2=v2" }; + var values = headers.GetHeaderValue(W3CConstants.TraceStateHeader)?.ToList(); + Assert.IsNotNull(values); + Assert.AreEqual(2, values.Count); + Assert.AreEqual("k1=v1", values.First()); + Assert.AreEqual("k2=v2", values.Last()); + } + + [Xunit.Theory] + [Xunit.InlineData(12)] // k1=v1,k2=v2,".Length + [Xunit.InlineData(11)] // k1=v1,k2=v2".Length + [Xunit.InlineData(15)] // k1=v1,k2=v2,k3=".Length + [Xunit.InlineData(13)] // k1=v1,k2=v2,k".Length + public void GetHeaderValueMaxLenTruncatesEnd(int maxLength) + { + WebHeaderCollection headers = new WebHeaderCollection { [W3CConstants.TraceStateHeader] = "k1=v1,k2=v2,k3=v3,k4=v4" }; + var values = headers.GetHeaderValue(W3CConstants.TraceStateHeader, maxLength)?.ToList(); + Assert.IsNotNull(values); + Assert.AreEqual(2, values.Count); + Assert.AreEqual("k1=v1", values.First()); + Assert.AreEqual("k2=v2", values.Last()); + } + + [Xunit.Theory] + [Xunit.InlineData(0)] + [Xunit.InlineData(3)] + public void GetHeaderValueMaxLenTruncatesEndInvalid(int maxLength) + { + WebHeaderCollection headers = new WebHeaderCollection { [W3CConstants.TraceStateHeader] = "k1=v1,k2=v2" }; + var values = headers.GetHeaderValue(W3CConstants.TraceStateHeader, maxLength)?.ToList(); + Assert.IsNull(values); + } + + [TestMethod] + public void GetHeaderValueMaxItemsTruncatesEnd() + { + WebHeaderCollection headers = new WebHeaderCollection { [W3CConstants.TraceStateHeader] = "k1=v1,k2=v2,k3=v3,k4=v4" }; + var values = headers.GetHeaderValue(W3CConstants.TraceStateHeader, 100500, 2)?.ToList(); + Assert.IsNotNull(values); + Assert.AreEqual(2, values.Count); + Assert.AreEqual("k1=v1", values.First()); + Assert.AreEqual("k2=v2", values.Last()); + } + +#pragma warning restore 612, 618 } } diff --git a/Src/DependencyCollector/Shared.Tests/Implementation/DependencyCollectorDiagnosticListenerTests.Netstandard16.cs b/Src/DependencyCollector/Shared.Tests/Implementation/DependencyCollectorDiagnosticListenerTests.Netstandard16.cs index e9d7ad62f..87c37999a 100644 --- a/Src/DependencyCollector/Shared.Tests/Implementation/DependencyCollectorDiagnosticListenerTests.Netstandard16.cs +++ b/Src/DependencyCollector/Shared.Tests/Implementation/DependencyCollectorDiagnosticListenerTests.Netstandard16.cs @@ -36,8 +36,8 @@ public partial class DependencyCollectorDiagnosticListenerTests private TelemetryConfiguration configuration; private string testInstrumentationKey1 = nameof(testInstrumentationKey1); - private string testApplicationId1 = nameof(testApplicationId1); - private string testApplicationId2 = nameof(testApplicationId2); + private string testApplicationId1 = "cid-v1:" + nameof(testApplicationId1); + private string testApplicationId2 = "cid-v1:" + nameof(testApplicationId2); private StubTelemetryChannel telemetryChannel; private HttpCoreDiagnosticSourceListener listener; @@ -84,7 +84,8 @@ public void Initialize() this.configuration, setComponentCorrelationHttpHeaders: true, correlationDomainExclusionList: new string[] { "excluded.host.com" }, - injectLegacyHeaders: false); + injectLegacyHeaders: false, + injectW3CHeaders: false); } /// @@ -216,22 +217,29 @@ public void OnRequestInjectsLegacyHeaders() this.configuration, setComponentCorrelationHttpHeaders: true, correlationDomainExclusionList: new[] { "excluded.host.com" }, - injectLegacyHeaders: true); + injectLegacyHeaders: true, + injectW3CHeaders: false); - Guid loggingRequestId = Guid.NewGuid(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUrlWithScheme); - listenerWithLegacyHeaders.OnRequest(request, loggingRequestId); - - IOperationHolder dependency; - Assert.IsTrue(listenerWithLegacyHeaders.PendingDependencyTelemetry.TryGetValue(request, out dependency)); - Assert.AreEqual(0, this.sentTelemetry.Count); - - var legacyRootIdHeader = GetRequestHeaderValues(request, RequestResponseHeaders.StandardRootIdHeader).Single(); - var legacyParentIdHeader = GetRequestHeaderValues(request, RequestResponseHeaders.StandardParentIdHeader).Single(); - var requestIdHeader = GetRequestHeaderValues(request, RequestResponseHeaders.RequestIdHeader).Single(); - Assert.AreEqual(dependency.Telemetry.Id, legacyParentIdHeader); - Assert.AreEqual(dependency.Telemetry.Context.Operation.Id, legacyRootIdHeader); - Assert.AreEqual(dependency.Telemetry.Id, requestIdHeader); + using (listenerWithLegacyHeaders) + { + Guid loggingRequestId = Guid.NewGuid(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUrlWithScheme); + listenerWithLegacyHeaders.OnRequest(request, loggingRequestId); + + IOperationHolder dependency; + Assert.IsTrue( + listenerWithLegacyHeaders.PendingDependencyTelemetry.TryGetValue(request, out dependency)); + Assert.AreEqual(0, this.sentTelemetry.Count); + + var legacyRootIdHeader = GetRequestHeaderValues(request, RequestResponseHeaders.StandardRootIdHeader) + .Single(); + var legacyParentIdHeader = + GetRequestHeaderValues(request, RequestResponseHeaders.StandardParentIdHeader).Single(); + var requestIdHeader = GetRequestHeaderValues(request, RequestResponseHeaders.RequestIdHeader).Single(); + Assert.AreEqual(dependency.Telemetry.Id, legacyParentIdHeader); + Assert.AreEqual(dependency.Telemetry.Context.Operation.Id, legacyRootIdHeader); + Assert.AreEqual(dependency.Telemetry.Id, requestIdHeader); + } } /// diff --git a/Src/DependencyCollector/Shared.Tests/Implementation/DependencyCollectorDiagnosticListenerTests.Netstandard20.cs b/Src/DependencyCollector/Shared.Tests/Implementation/DependencyCollectorDiagnosticListenerTests.Netstandard20.cs index 3575a042d..076705a8d 100644 --- a/Src/DependencyCollector/Shared.Tests/Implementation/DependencyCollectorDiagnosticListenerTests.Netstandard20.cs +++ b/Src/DependencyCollector/Shared.Tests/Implementation/DependencyCollectorDiagnosticListenerTests.Netstandard20.cs @@ -15,6 +15,7 @@ namespace Microsoft.ApplicationInsights.Tests using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation; using Microsoft.ApplicationInsights.TestFramework; + using Microsoft.ApplicationInsights.W3C; using Microsoft.VisualStudio.TestTools.UnitTesting; /// @@ -52,22 +53,97 @@ public void OnActivityStartInjectsLegacyHeaders() this.configuration, setComponentCorrelationHttpHeaders: true, correlationDomainExclusionList: new[] { "excluded.host.com" }, - injectLegacyHeaders: true); + injectLegacyHeaders: true, + injectW3CHeaders: false); + + using (listenerWithLegacyHeaders) + { + var activity = new Activity("System.Net.Http.HttpRequestOut"); + activity.AddBaggage("k", "v"); + activity.Start(); + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUrlWithScheme); + listenerWithLegacyHeaders.OnActivityStart(request); + + // Request-Id and Correlation-Context are injected by HttpClient + // check only legacy headers here + Assert.AreEqual(Activity.Current.RootId, + request.Headers.GetValues(RequestResponseHeaders.StandardRootIdHeader).Single()); + Assert.AreEqual(Activity.Current.Id, + request.Headers.GetValues(RequestResponseHeaders.StandardParentIdHeader).Single()); + Assert.AreEqual(this.testApplicationId1, + GetRequestContextKeyValue(request, RequestResponseHeaders.RequestContextCorrelationSourceKey)); + } + } - var activity = new Activity("System.Net.Http.HttpRequestOut"); - activity.AddBaggage("k", "v"); - activity.Start(); +#pragma warning disable 612, 618 + /// + /// Tests that OnStartActivity injects W3C headers. + /// + [TestMethod] + public void OnActivityStartInjectsW3CHeaders() + { + var listenerWithW3CHeaders = new HttpCoreDiagnosticSourceListener( + this.configuration, + setComponentCorrelationHttpHeaders: true, + correlationDomainExclusionList: new[] { "excluded.host.com" }, + injectLegacyHeaders: false, + injectW3CHeaders: true); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUrlWithScheme); - listenerWithLegacyHeaders.OnActivityStart(request); + this.configuration.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); - // Request-Id and Correlation-Context are injected by HttpClient - // check only legacy headers here - Assert.AreEqual(Activity.Current.RootId, request.Headers.GetValues(RequestResponseHeaders.StandardRootIdHeader).Single()); - Assert.AreEqual(Activity.Current.Id, request.Headers.GetValues(RequestResponseHeaders.StandardParentIdHeader).Single()); - Assert.AreEqual(this.testApplicationId1, GetRequestContextKeyValue(request, RequestResponseHeaders.RequestContextCorrelationSourceKey)); + using (listenerWithW3CHeaders) + { + var activity = new Activity("System.Net.Http.HttpRequestOut").SetParentId("|guid.").Start(); + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUrlWithScheme); + listenerWithW3CHeaders.OnActivityStart(request); + + // Request-Id and Correlation-Context are injected by HttpClient + // check only W3C headers here + Assert.AreEqual(this.testApplicationId1, GetRequestContextKeyValue(request, RequestResponseHeaders.RequestContextCorrelationSourceKey)); + Assert.AreEqual($"00-{activity.GetTraceId()}-{activity.GetSpanId()}-02", request.Headers.GetValues(W3CConstants.TraceParentHeader).Single()); + Assert.AreEqual($"{W3CConstants.AzureTracestateNamespace}={this.testApplicationId1}", request.Headers.GetValues(W3CConstants.TraceStateHeader).Single()); + } + } + + /// + /// Tests that OnStartActivity injects W3C headers. + /// + [TestMethod] + public void OnActivityStartInjectsW3CHeadersAndTracksLegacyId() + { + var listenerWithW3CHeaders = new HttpCoreDiagnosticSourceListener( + this.configuration, + setComponentCorrelationHttpHeaders: true, + correlationDomainExclusionList: new string[0], + injectLegacyHeaders: false, + injectW3CHeaders: true); + + this.configuration.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); + using (listenerWithW3CHeaders) + { + var activity = new Activity("System.Net.Http.HttpRequestOut").SetParentId("foo").Start(); + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUrlWithScheme); + listenerWithW3CHeaders.OnActivityStart(request); + + // simulate Request-Id injection by .NET + request.Headers.Add(RequestResponseHeaders.RequestIdHeader, activity.Id); + + listenerWithW3CHeaders.OnActivityStop(new HttpResponseMessage(HttpStatusCode.OK), request, TaskStatus.RanToCompletion); + + var telemetry = this.sentTelemetry.Single() as DependencyTelemetry; + Assert.IsTrue(telemetry.Properties.ContainsKey(W3CConstants.LegacyRequestIdProperty)); + Assert.AreEqual(activity.Id, telemetry.Properties[W3CConstants.LegacyRequestIdProperty]); + + Assert.IsTrue(telemetry.Properties.ContainsKey(W3CConstants.LegacyRootIdProperty)); + Assert.AreEqual(activity.RootId, telemetry.Properties[W3CConstants.LegacyRootIdProperty]); + } } +#pragma warning restore 612, 618 + /// /// Tests that OnStopActivity tracks telemetry. /// diff --git a/Src/DependencyCollector/Shared.Tests/Implementation/DesktopDiagnosticSourceHttpProcessingTests.cs b/Src/DependencyCollector/Shared.Tests/Implementation/DesktopDiagnosticSourceHttpProcessingTests.cs index a151a6c7d..91972f499 100644 --- a/Src/DependencyCollector/Shared.Tests/Implementation/DesktopDiagnosticSourceHttpProcessingTests.cs +++ b/Src/DependencyCollector/Shared.Tests/Implementation/DesktopDiagnosticSourceHttpProcessingTests.cs @@ -69,7 +69,13 @@ public void TestInitialize() ApplicationIdProvider = new MockApplicationIdProvider(TestInstrumentationKey, TestApplicationId) }; - this.httpDesktopProcessingFramework = new DesktopDiagnosticSourceHttpProcessing(this.configuration, new CacheBasedOperationHolder("testCache", 100 * 1000), /*setCorrelationHeaders*/ true, new List(), false); + this.httpDesktopProcessingFramework = new DesktopDiagnosticSourceHttpProcessing( + this.configuration, + new CacheBasedOperationHolder("testCache", 100 * 1000), + setCorrelationHeaders: true, + correlationDomainExclusionList: new List(), + injectLegacyHeaders: false, + enableW3CHeaders: false); DependencyTableStore.IsDesktopHttpDiagnosticSourceActivated = false; } @@ -185,7 +191,9 @@ public void RddTestHttpDesktopProcessingFrameworkOnBeginAddsSourceHeader() Assert.IsNull(request.Headers[RequestResponseHeaders.RequestContextHeader]); this.httpDesktopProcessingFramework.OnBegin(request); - Assert.IsNotNull(request.Headers.GetNameValueHeaderValue(RequestResponseHeaders.RequestContextHeader, RequestResponseHeaders.RequestContextCorrelationSourceKey)); + Assert.IsNotNull(request.Headers.GetNameValueHeaderValue( + RequestResponseHeaders.RequestContextHeader, + RequestResponseHeaders.RequestContextCorrelationSourceKey)); } /// @@ -194,7 +202,13 @@ public void RddTestHttpDesktopProcessingFrameworkOnBeginAddsSourceHeader() [TestMethod] public void RddTestHttpDesktopProcessingFrameworkOnBeginAddsLegacyHeaders() { - var httpProcessingLegacyHeaders = new DesktopDiagnosticSourceHttpProcessing(this.configuration, new CacheBasedOperationHolder("testCache", 100 * 1000), /*setCorrelationHeaders*/ true, new List(), true); + var httpProcessingLegacyHeaders = new DesktopDiagnosticSourceHttpProcessing( + this.configuration, + new CacheBasedOperationHolder("testCache", 100 * 1000), + setCorrelationHeaders: true, + correlationDomainExclusionList: new List(), + injectLegacyHeaders: true, + enableW3CHeaders: false); var request = WebRequest.Create(this.testUrl); Assert.IsNull(request.Headers[RequestResponseHeaders.StandardParentIdHeader]); @@ -276,10 +290,11 @@ public void RddTestHttpDesktopProcessingFrameworkOnBeginSkipsAddingSourceHeaderP var localHttpProcessingFramework = new DesktopDiagnosticSourceHttpProcessing( this.configuration, - new CacheBasedOperationHolder("testCache", 100 * 1000), - false, - new List(), - false); + new CacheBasedOperationHolder("testCache", 100 * 1000), + setCorrelationHeaders: false, + correlationDomainExclusionList: new List(), + injectLegacyHeaders: false, + enableW3CHeaders: false); localHttpProcessingFramework.OnBegin(request); Assert.IsNull(request.Headers[RequestResponseHeaders.RequestContextHeader]); @@ -288,10 +303,11 @@ public void RddTestHttpDesktopProcessingFrameworkOnBeginSkipsAddingSourceHeaderP ICollection exclusionList = new SanitizedHostList() { "randomstringtoexclude", hostnamepart }; localHttpProcessingFramework = new DesktopDiagnosticSourceHttpProcessing( this.configuration, - new CacheBasedOperationHolder("testCache", 100 * 1000), - true, - exclusionList, - false); + new CacheBasedOperationHolder("testCache", 100 * 1000), + setCorrelationHeaders: true, + correlationDomainExclusionList: exclusionList, + injectLegacyHeaders: false, + enableW3CHeaders: false); localHttpProcessingFramework.OnBegin(request); Assert.IsNull(request.Headers[RequestResponseHeaders.RequestContextHeader]); diff --git a/Src/DependencyCollector/Shared.Tests/Implementation/FrameworkSqlProcessingTest.cs b/Src/DependencyCollector/Shared.Tests/Implementation/FrameworkSqlProcessingTest.cs index 9be54ab73..18af87956 100644 --- a/Src/DependencyCollector/Shared.Tests/Implementation/FrameworkSqlProcessingTest.cs +++ b/Src/DependencyCollector/Shared.Tests/Implementation/FrameworkSqlProcessingTest.cs @@ -41,6 +41,10 @@ public void TestInitialize() [TestCleanup] public void Cleanup() { + while (Activity.Current != null) + { + Activity.Current.Stop(); + } } #region ExecuteReader diff --git a/Src/DependencyCollector/Shared.Tests/Implementation/ProfilerHttpProcessingTest.cs b/Src/DependencyCollector/Shared.Tests/Implementation/ProfilerHttpProcessingTest.cs index ec12a2332..d1b18e08c 100644 --- a/Src/DependencyCollector/Shared.Tests/Implementation/ProfilerHttpProcessingTest.cs +++ b/Src/DependencyCollector/Shared.Tests/Implementation/ProfilerHttpProcessingTest.cs @@ -20,6 +20,7 @@ using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation; using Microsoft.ApplicationInsights.TestFramework; + using Microsoft.ApplicationInsights.W3C; using Microsoft.ApplicationInsights.Web.TestFramework; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -31,7 +32,7 @@ public sealed class ProfilerHttpProcessingTest : IDisposable #region Fields private const int TimeAccuracyMilliseconds = 150; // this may be big number when under debugger private const string TestInstrumentationKey = nameof(TestInstrumentationKey); - private const string TestApplicationId = nameof(TestApplicationId); + private const string TestApplicationId = "cid-v1:" + nameof(TestApplicationId); private TelemetryConfiguration configuration; private Uri testUrl = new Uri("http://www.microsoft.com/"); private Uri testUrlNonStandardPort = new Uri("http://www.microsoft.com:911/"); @@ -83,7 +84,8 @@ public void TestInitialize() new ObjectInstanceBasedOperationHolder(), setCorrelationHeaders: true, correlationDomainExclusionList: new List(), - injectLegacyHeaders: false); + injectLegacyHeaders: false, + injectW3CHeaders: false); } [TestCleanup] @@ -182,7 +184,9 @@ public void RddTestHttpProcessingProfilerOnBeginAddsSourceHeader() Assert.IsNull(request.Headers[RequestResponseHeaders.RequestContextHeader]); this.httpProcessingProfiler.OnBeginForGetResponse(request); - Assert.IsNotNull(request.Headers.GetNameValueHeaderValue(RequestResponseHeaders.RequestContextHeader, RequestResponseHeaders.RequestContextCorrelationSourceKey)); + Assert.IsNotNull(request.Headers.GetNameValueHeaderValue( + RequestResponseHeaders.RequestContextHeader, + RequestResponseHeaders.RequestContextCorrelationSourceKey)); } /// @@ -200,7 +204,8 @@ public void RddTestHttpProcessingProfilerOnBeginAddsLegacyHeadersAreEnabled() new ObjectInstanceBasedOperationHolder(), setCorrelationHeaders: true, correlationDomainExclusionList: new List(), - injectLegacyHeaders: true); + injectLegacyHeaders: true, + injectW3CHeaders: false); var client = new TelemetryClient(this.configuration); using (var op = client.StartOperation("request")) @@ -278,6 +283,63 @@ public void RddTestHttpProcessingProfilerOnBeginAddsCorrelationContextHeader() Assert.IsTrue(actualCorrelationContextHeader == "Key2=Value2,Key1=Value1" || actualCorrelationContextHeader == "Key1=Value1,Key2=Value2"); } +#pragma warning disable 612, 618 + /// + /// Ensures that the source request header is added when request is sent. + /// + [TestMethod] + public void RddTestHttpProcessingProfilerOnBeginAddsW3CHeadersWhenEnabled() + { + var request = WebRequest.Create(this.testUrl); + + this.configuration.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); + var httpProcessingW3C = new ProfilerHttpProcessing( + this.configuration, + null, + new ObjectInstanceBasedOperationHolder(), + setCorrelationHeaders: true, + correlationDomainExclusionList: new List(), + injectLegacyHeaders: true, + injectW3CHeaders: true); + ClientServerDependencyTracker.IsW3CEnabled = true; + + var client = new TelemetryClient(this.configuration); + RequestTelemetry requestTelemetry; + + Activity dependencyActivity; + using (var op = client.StartOperation("request")) + { + Activity.Current.AddBaggage("k", "v"); + Activity.Current.AddTag(W3CConstants.TracestateTag, "some=state"); + httpProcessingW3C.OnBeginForGetResponse(request); + + Assert.AreEqual("k=v", request.Headers[RequestResponseHeaders.CorrelationContextHeader]); + Assert.AreEqual($"{W3CConstants.AzureTracestateNamespace}={TestApplicationId},some=state", request.Headers[W3CConstants.TraceStateHeader]); + + requestTelemetry = op.Telemetry; + + dependencyActivity = Activity.Current; + + var returnObjectPassed = TestUtils.GenerateHttpWebResponse(HttpStatusCode.OK); + httpProcessingW3C.OnEndForEndGetResponse(null, returnObjectPassed, request, null); + } + + Assert.AreEqual(2, this.sendItems.Count); + var dependencies = this.sendItems.OfType().ToArray(); + + Assert.AreEqual(1, dependencies.Length); + var dependency = dependencies.Single(); + Assert.IsNotNull(dependency); + + var dependencyIdParts = dependency.Id.Split('.', '|'); + Assert.AreEqual(4, dependencyIdParts.Length); + + var traceParent = request.Headers[W3CConstants.TraceParentHeader]; + Assert.AreEqual($"{W3CConstants.DefaultVersion}-{dependencyIdParts[1]}-{dependencyIdParts[2]}-{W3CConstants.TraceFlagRecordedAndNotRequested}", + traceParent); + } +#pragma warning restore 612, 618 + /// /// Ensures that the source request header is not added, as per the config, when request is sent. /// @@ -292,13 +354,27 @@ public void RddTestHttpProcessingProfilerOnBeginSkipsAddingSourceHeaderPerConfig Assert.IsNull(request.Headers[RequestResponseHeaders.RequestContextHeader]); Assert.AreEqual(0, request.Headers.Keys.Cast().Where((x) => { return x.StartsWith("x-ms-", StringComparison.OrdinalIgnoreCase); }).Count()); - var httpProcessingProfiler = new ProfilerHttpProcessing(this.configuration, null, new ObjectInstanceBasedOperationHolder(), /*setCorrelationHeaders*/ false, new List(), true); + var httpProcessingProfiler = new ProfilerHttpProcessing( + this.configuration, + null, + new ObjectInstanceBasedOperationHolder(), + setCorrelationHeaders: false, + correlationDomainExclusionList: new List(), + injectLegacyHeaders: true, + injectW3CHeaders: false); httpProcessingProfiler.OnBeginForGetResponse(request); Assert.IsNull(request.Headers[RequestResponseHeaders.RequestContextHeader]); Assert.AreEqual(0, request.Headers.Keys.Cast().Where((x) => { return x.StartsWith("x-ms-", StringComparison.OrdinalIgnoreCase); }).Count()); ICollection exclusionList = new SanitizedHostList() { "randomstringtoexclude", hostnamepart }; - httpProcessingProfiler = new ProfilerHttpProcessing(this.configuration, null, new ObjectInstanceBasedOperationHolder(), /*setCorrelationHeaders*/ true, exclusionList, true); + httpProcessingProfiler = new ProfilerHttpProcessing( + this.configuration, + null, + new ObjectInstanceBasedOperationHolder(), + setCorrelationHeaders: true, + correlationDomainExclusionList: exclusionList, + injectLegacyHeaders: true, + injectW3CHeaders: false); httpProcessingProfiler.OnBeginForGetResponse(request); Assert.IsNull(request.Headers[RequestResponseHeaders.RequestContextHeader]); Assert.AreEqual(0, request.Headers.Keys.Cast().Where((x) => { return x.StartsWith("x-ms-", StringComparison.OrdinalIgnoreCase); }).Count()); diff --git a/Src/DependencyCollector/Shared.Tests/W3C/W3CActiviityExtentionsTests.cs b/Src/DependencyCollector/Shared.Tests/W3C/W3CActiviityExtentionsTests.cs new file mode 100644 index 000000000..93a9f745c --- /dev/null +++ b/Src/DependencyCollector/Shared.Tests/W3C/W3CActiviityExtentionsTests.cs @@ -0,0 +1,219 @@ +namespace Microsoft.ApplicationInsights.DependencyCollector.W3C +{ + using System.Diagnostics; + using System.Linq; + using Microsoft.ApplicationInsights.W3C; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class W3CActiviityExtentionsTests + { +#pragma warning disable 612, 618 + + private const string TraceId = "01010101010101010101010101010101"; + private const string ParenSpanId = "0202020202020202"; + + [TestCleanup] + public void Cleanup() + { + while (Activity.Current != null) + { + Activity.Current.Stop(); + } + } + + [TestMethod] + public void SetInvalidTraceParent() + { + var invalidTraceParents = new[] + { + "123", string.Empty, null, "00-00", "00-00-00", "00-00-00-", "-00-00-00", "00-00-00-00-00", + "00-00-00- ", " -00-00-00", "---", "00---", "00-00--", "00--00-", "00---00" + }; + foreach (var traceparent in invalidTraceParents) + { + var a = new Activity("foo"); + a.SetTraceparent(traceparent); + + Assert.IsFalse(a.Tags.Any(t => t.Key == W3CConstants.ParentSpanIdTag), traceparent); + Assert.IsNull(a.GetParentSpanId()); + Assert.IsNull(a.GetTracestate()); + + Assert.AreEqual(W3CConstants.DefaultVersion, a.Tags.Single(t => t.Key == W3CConstants.VersionTag).Value, traceparent); + Assert.AreEqual(W3CConstants.TraceFlagRecordedAndNotRequested, a.Tags.Single(t => t.Key == W3CConstants.SampledTag).Value, traceparent); + + Assert.IsTrue(a.IsW3CActivity(), traceparent); + Assert.AreEqual(32, a.GetTraceId().Length, traceparent); + Assert.AreEqual(16, a.GetSpanId().Length, traceparent); + + Assert.AreEqual($"{W3CConstants.DefaultVersion}-{a.GetTraceId()}-{a.GetSpanId()}-{W3CConstants.TraceFlagRecordedAndNotRequested}", a.GetTraceparent(), traceparent); + } + } + + [TestMethod] + public void InvalidTraceIdAllTraceparentIsIgnored() + { + var invalidTraceIds = new[] + { + "123", + "000102030405060708090a0b0c0d0f", // 30 chars + "000102030405060708090a0b0c0d0f0", // 31 char + "000102030405060708090a0b0c0d0f0g", // 32 char non-hex + "000102030405060708090a0b0c0d0f0A", // 32 char upper case + "000102030405060708090a0b0c0d0f000" // 33 chars + }; + foreach (var traceId in invalidTraceIds) + { + var a = new Activity("foo"); + + a.SetTraceparent($"00-{traceId}-{ParenSpanId}-00"); + + Assert.IsFalse(a.Tags.Any(t => t.Key == W3CConstants.ParentSpanIdTag), traceId); + Assert.IsNull(a.GetParentSpanId()); + Assert.IsNull(a.GetTracestate()); + + Assert.AreEqual(W3CConstants.DefaultVersion, a.Tags.Single(t => t.Key == W3CConstants.VersionTag).Value, traceId); + Assert.AreEqual(W3CConstants.TraceFlagRecordedAndNotRequested, a.Tags.Single(t => t.Key == W3CConstants.SampledTag).Value, traceId); + + Assert.IsTrue(a.IsW3CActivity(), traceId); + Assert.AreEqual(32, a.GetTraceId().Length, traceId); + Assert.AreEqual(16, a.GetSpanId().Length, traceId); + + Assert.AreEqual($"{W3CConstants.DefaultVersion}-{a.GetTraceId()}-{a.GetSpanId()}-{W3CConstants.TraceFlagRecordedAndNotRequested}", a.GetTraceparent(), traceId); + } + } + + [TestMethod] + public void InvalidSapnIdAllTraceparentIsIgnored() + { + var invalidSpanIds = new[] + { + "123", + "00010203040506", // 14 chars + "000102030405060", // 15 char + "000102030405060g", // 16 char non-hex + "000102030405060A", // 16 char upper case + "00010203040506070" // 15 chars + }; + foreach (var parentSpanId in invalidSpanIds) + { + var a = new Activity("foo"); + + a.SetTraceparent($"00-{TraceId}-{parentSpanId}-00"); + + Assert.IsFalse(a.Tags.Any(t => t.Key == W3CConstants.ParentSpanIdTag), parentSpanId); + Assert.IsNull(a.GetParentSpanId()); + Assert.IsNull(a.GetTracestate()); + + Assert.AreEqual(W3CConstants.DefaultVersion, a.Tags.Single(t => t.Key == W3CConstants.VersionTag).Value, parentSpanId); + Assert.AreEqual(W3CConstants.TraceFlagRecordedAndNotRequested, a.Tags.Single(t => t.Key == W3CConstants.SampledTag).Value, parentSpanId); + + Assert.IsTrue(a.IsW3CActivity(), parentSpanId); + Assert.AreEqual(32, a.GetTraceId().Length, parentSpanId); + Assert.AreEqual(16, a.GetSpanId().Length, parentSpanId); + + Assert.AreEqual($"{W3CConstants.DefaultVersion}-{a.GetTraceId()}-{a.GetSpanId()}-{W3CConstants.TraceFlagRecordedAndNotRequested}", a.GetTraceparent(), parentSpanId); + } + } + + [TestMethod] + public void SetValidTraceParent() + { + var a = new Activity("foo"); + a.SetTraceparent($"00-{TraceId}-{ParenSpanId}-00"); + + Assert.IsTrue(a.IsW3CActivity()); + Assert.AreEqual(TraceId, a.Tags.SingleOrDefault(t => t.Key == W3CConstants.TraceIdTag).Value); + Assert.AreEqual(ParenSpanId, a.Tags.SingleOrDefault(t => t.Key == W3CConstants.ParentSpanIdTag).Value); + Assert.IsNotNull(a.Tags.SingleOrDefault(t => t.Key == W3CConstants.SpanIdTag)); + Assert.AreEqual(16, a.Tags.Single(t => t.Key == W3CConstants.SpanIdTag).Value.Length); + Assert.AreEqual(W3CConstants.TraceFlagRecordedAndNotRequested, a.Tags.SingleOrDefault(t => t.Key == W3CConstants.SampledTag).Value); + Assert.AreEqual(W3CConstants.DefaultVersion, a.Tags.SingleOrDefault(t => t.Key == W3CConstants.VersionTag).Value); + + Assert.AreEqual(TraceId, a.GetTraceId()); + Assert.AreEqual(ParenSpanId, a.GetParentSpanId()); + Assert.IsNotNull(a.GetSpanId()); + Assert.AreEqual(a.Tags.Single(t => t.Key == W3CConstants.SpanIdTag).Value, a.GetSpanId()); + Assert.AreEqual($"{W3CConstants.DefaultVersion}-{TraceId}-{a.GetSpanId()}-{W3CConstants.TraceFlagRecordedAndNotRequested}", a.GetTraceparent()); + Assert.IsNull(a.GetTracestate()); + } + + [TestMethod] + public void UpdateContextWithoutParent() + { + var a = new Activity("foo"); + + Assert.IsFalse(a.IsW3CActivity()); + + a.UpdateContextOnActivity(); + Assert.IsTrue(a.IsW3CActivity()); + Assert.IsNotNull(a.GetTraceId()); + Assert.IsNotNull(a.GetSpanId()); + Assert.IsNull(a.GetParentSpanId()); + Assert.IsNotNull(a.GetSpanId()); + + Assert.AreEqual($"00-{a.GetTraceId()}-{a.GetSpanId()}-02", a.GetTraceparent()); + Assert.IsNull(a.GetTracestate()); + } + + [TestMethod] + public void UpdateContextWithParent() + { + var parent = new Activity("foo").Start(); + parent.SetTraceparent($"00-{TraceId}-{ParenSpanId}-01"); + parent.SetTracestate("some=state"); + var child = new Activity("bar").Start(); + child.UpdateContextOnActivity(); + + Assert.IsTrue(child.IsW3CActivity()); + Assert.AreEqual(TraceId, child.GetTraceId()); + Assert.AreEqual(parent.GetSpanId(), child.GetParentSpanId()); + Assert.AreEqual($"{W3CConstants.DefaultVersion}-{TraceId}-{child.GetSpanId()}-{W3CConstants.TraceFlagRecordedAndRequested}", child.GetTraceparent()); + Assert.AreEqual(parent.GetTracestate(), child.GetTracestate()); + } + + [TestMethod] + public void SetTraceState() + { + var a = new Activity("foo").Start(); + a.SetTracestate("some=state"); + Assert.AreEqual("some=state", a.GetTracestate()); + } + + [TestMethod] + public void UnsupportedVersionsAreIgnored() + { + var a = new Activity("foo").Start(); + a.SetTraceparent($"12-{TraceId}-{ParenSpanId}-00"); + + var b = new Activity("bar").Start(); + b.SetTraceparent($"ff-{TraceId}-{ParenSpanId}-00"); + + Assert.AreEqual($"00-{TraceId}-{a.GetSpanId()}-02", a.GetTraceparent()); + Assert.AreEqual($"00-{TraceId}-{b.GetSpanId()}-02", b.GetTraceparent()); + } + + [TestMethod] + public void RequestedFlagIsRespected() + { + var requestedParents = new[] { "01", "03", "05", "ff" }; + var notRequestedParents = new[] { "00", "02", "04", "fe" }; + + foreach (var req in requestedParents) + { + var a = new Activity("foo").Start(); + a.SetTraceparent($"00-{TraceId}-{ParenSpanId}-{req}"); + Assert.AreEqual($"00-{TraceId}-{a.GetSpanId()}-03", a.GetTraceparent(), req); + } + + foreach (var notReq in notRequestedParents) + { + var a = new Activity("foo").Start(); + a.SetTraceparent($"00-{TraceId}-{ParenSpanId}-{notReq}"); + Assert.AreEqual($"00-{TraceId}-{a.GetSpanId()}-02", a.GetTraceparent(), notReq); + } + } + +#pragma warning restore 612, 618 + } +} diff --git a/Src/DependencyCollector/Shared.Tests/W3C/W3COperationCorrelationTelemetryInitializerTests.cs b/Src/DependencyCollector/Shared.Tests/W3C/W3COperationCorrelationTelemetryInitializerTests.cs new file mode 100644 index 000000000..a4d5cd2a2 --- /dev/null +++ b/Src/DependencyCollector/Shared.Tests/W3C/W3COperationCorrelationTelemetryInitializerTests.cs @@ -0,0 +1,261 @@ +namespace Microsoft.ApplicationInsights.DependencyCollector.W3C +{ + using System.Diagnostics; + using System.Linq; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Extensibility.Implementation; + using Microsoft.ApplicationInsights.W3C; + using Microsoft.VisualStudio.TestTools.UnitTesting; + +#pragma warning disable 612, 618 + [TestClass] + public class W3COperationCorrelationTelemetryInitializerTests + { + [TestCleanup] + public void Cleanup() + { + while (Activity.Current != null) + { + Activity.Current.Stop(); + } + } + + [TestMethod] + public void InitializerCreatesNewW3CContext() + { + Activity a = new Activity("dummy") + .Start(); + + RequestTelemetry request = new RequestTelemetry(); + + new W3COperationCorrelationTelemetryInitializer().Initialize(request); + + Assert.IsNotNull(request.Context.Operation.Id); + Assert.IsNull(request.Context.Operation.ParentId); + Assert.AreEqual($"|{a.GetTraceId()}.{a.GetSpanId()}.", request.Id); + + Assert.AreEqual(2, request.Properties.Count); + + Assert.IsTrue(request.Properties.ContainsKey(W3CConstants.LegacyRequestIdProperty)); + Assert.AreEqual(a.Id, request.Properties[W3CConstants.LegacyRequestIdProperty]); + + Assert.IsTrue(request.Properties.ContainsKey(W3CConstants.LegacyRootIdProperty)); + Assert.AreEqual(a.RootId, request.Properties[W3CConstants.LegacyRootIdProperty]); + } + + [TestMethod] + public void InitializerSetsCorrelationIdsOnTraceTelemetry() + { + Activity a = new Activity("dummy") + .Start() + .GenerateW3CContext(); + + string expectedTrace = a.GetTraceId(); + string expectedParent = a.GetSpanId(); + + TraceTelemetry trace = new TraceTelemetry(); + new W3COperationCorrelationTelemetryInitializer().Initialize(trace); + + Assert.AreEqual(expectedTrace, trace.Context.Operation.Id); + Assert.AreEqual($"|{expectedTrace}.{expectedParent}.", trace.Context.Operation.ParentId); + + Assert.IsFalse(trace.Properties.Any()); + } + + [TestMethod] + public void InitializerSetsCorrelationIdsOnRequestTelemetry() + { + Activity a = new Activity("dummy") + .Start() + .GenerateW3CContext(); + + string expectedTrace = a.GetTraceId(); + string expectedSpanId = a.GetSpanId(); + + string expectedParent = "0123456789abcdef"; + a.AddTag(W3CConstants.ParentSpanIdTag, expectedParent); + + RequestTelemetry request = new RequestTelemetry(); + new W3COperationCorrelationTelemetryInitializer().Initialize(request); + + Assert.AreEqual(expectedTrace, request.Context.Operation.Id); + Assert.AreEqual($"|{expectedTrace}.{expectedParent}.", request.Context.Operation.ParentId); + Assert.AreEqual($"|{expectedTrace}.{expectedSpanId}.", request.Id); + + Assert.AreEqual(2, request.Properties.Count); + + Assert.IsTrue(request.Properties.ContainsKey(W3CConstants.LegacyRequestIdProperty)); + Assert.AreEqual(a.Id, request.Properties[W3CConstants.LegacyRequestIdProperty]); + + Assert.IsTrue(request.Properties.ContainsKey(W3CConstants.LegacyRootIdProperty)); + Assert.AreEqual(a.RootId, request.Properties[W3CConstants.LegacyRootIdProperty]); + } + + [TestMethod] + public void InitializerSetsCorrelationIdsOnRequestTelemetryNoParent() + { + Activity a = new Activity("dummy") + .Start() + .GenerateW3CContext(); + + string expectedTrace = a.GetTraceId(); + string expectedSpanId = a.GetSpanId(); + + RequestTelemetry request = new RequestTelemetry(); + new W3COperationCorrelationTelemetryInitializer().Initialize(request); + + Assert.AreEqual(expectedTrace, request.Context.Operation.Id); + Assert.IsNull(request.Context.Operation.ParentId); + Assert.AreEqual($"|{expectedTrace}.{expectedSpanId}.", request.Id); + + Assert.AreEqual(2, request.Properties.Count); + + Assert.IsTrue(request.Properties.ContainsKey(W3CConstants.LegacyRequestIdProperty)); + Assert.AreEqual(a.Id, request.Properties[W3CConstants.LegacyRequestIdProperty]); + + Assert.IsTrue(request.Properties.ContainsKey(W3CConstants.LegacyRootIdProperty)); + Assert.AreEqual(a.RootId, request.Properties[W3CConstants.LegacyRootIdProperty]); + } + + [TestMethod] + public void InitializerNoopWithoutActivity() + { + RequestTelemetry request = new RequestTelemetry(); + new W3COperationCorrelationTelemetryInitializer().Initialize(request); + + Assert.IsNull(request.Context.Operation.Id); + Assert.IsNull(request.Context.Operation.ParentId); + + Assert.IsFalse(request.Properties.Any()); + } + + [TestMethod] + public void InitializerIgnoresExistingValues() + { + Activity a = new Activity("dummy") + .Start() + .GenerateW3CContext(); + + string expectedTrace = a.GetTraceId(); + string expectedSpanId = a.GetSpanId(); + + string expectedParent = "0123456789abcdef"; + a.AddTag(W3CConstants.ParentSpanIdTag, expectedParent); + + RequestTelemetry request = new RequestTelemetry(); + + request.Context.Operation.Id = "operation id"; + request.Context.Operation.ParentId = "parent id"; + request.Id = "id"; + + new W3COperationCorrelationTelemetryInitializer().Initialize(request); + + Assert.AreEqual(expectedTrace, request.Context.Operation.Id); + Assert.AreEqual($"|{expectedTrace}.{expectedParent}.", request.Context.Operation.ParentId); + Assert.AreEqual($"|{expectedTrace}.{expectedSpanId}.", request.Id); + } + + [TestMethod] + public void InitializerPopulatesTraceStateOnRequestAndDependencyTelemetry() + { + Activity a = new Activity("dummy") + .Start() + .GenerateW3CContext(); + + a.SetTracestate("key=value"); + + string expectedTrace = a.GetTraceId(); + string expectedSpanId = a.GetSpanId(); + + RequestTelemetry request = new RequestTelemetry(); + DependencyTelemetry dependency = new DependencyTelemetry(); + TraceTelemetry trace = new TraceTelemetry(); + var initializer = new W3COperationCorrelationTelemetryInitializer(); + initializer.Initialize(request); + initializer.Initialize(dependency); + initializer.Initialize(trace); + + Assert.AreEqual(expectedTrace, request.Context.Operation.Id); + Assert.AreEqual($"|{expectedTrace}.{expectedSpanId}.", request.Id); + + Assert.AreEqual("key=value", request.Properties[W3CConstants.TracestateTag]); + Assert.AreEqual("key=value", dependency.Properties[W3CConstants.TracestateTag]); + Assert.IsFalse(trace.Properties.Any()); + } + + [TestMethod] + public void InitializerOnNestedActivitities() + { + Activity requestActivity = new Activity("request") + .Start(); + + RequestTelemetry request = new RequestTelemetry(); + new W3COperationCorrelationTelemetryInitializer().Initialize(request); + + Activity nested1 = new Activity("nested1").Start(); + Activity nested2 = new Activity("nested1").Start(); + + DependencyTelemetry dependency2 = new DependencyTelemetry(); + new W3COperationCorrelationTelemetryInitializer().Initialize(dependency2); + + Assert.AreEqual(request.Context.Operation.Id, nested2.GetTraceId()); + Assert.AreEqual(request.Context.Operation.Id, nested1.GetTraceId()); + + Assert.AreEqual(request.Id, $"|{nested1.GetTraceId()}.{nested1.GetParentSpanId()}."); + Assert.AreEqual(nested1.GetSpanId(), nested2.GetParentSpanId()); + + Assert.AreEqual(request.Context.Operation.Id, dependency2.Context.Operation.Id); + + nested2.Stop(); + + DependencyTelemetry dependency1 = new DependencyTelemetry(); + new W3COperationCorrelationTelemetryInitializer().Initialize(dependency1); + + Assert.AreEqual(request.Id, $"|{nested1.GetTraceId()}.{nested1.GetParentSpanId()}."); + Assert.AreEqual(dependency2.Context.Operation.ParentId, dependency1.Id); + Assert.AreEqual(request.Context.Operation.Id, dependency1.Context.Operation.Id); + Assert.AreEqual(request.Id, dependency1.Context.Operation.ParentId); + } + + [TestMethod] + public void InitializerOnSqlDepenedency() + { + Activity requestActivity = new Activity("request") + .Start() + .GenerateW3CContext(); + + RequestTelemetry request = new RequestTelemetry(); + DependencyTelemetry sqlDependency = new DependencyTelemetry() + { + Type = "SQL" + }; + sqlDependency.Context.GetInternalContext().SdkVersion = "rdddsc:12345"; + string expectedId = sqlDependency.Id; + + new W3COperationCorrelationTelemetryInitializer().Initialize(sqlDependency); + new W3COperationCorrelationTelemetryInitializer().Initialize(request); + + Assert.AreEqual(request.Context.Operation.Id, sqlDependency.Context.Operation.Id); + Assert.AreEqual(request.Id, sqlDependency.Context.Operation.ParentId); + Assert.AreEqual(expectedId, sqlDependency.Id); + } + + [TestMethod] + public void InitializerOnActivityWithParentWithoutW3CTags() + { + Activity parentActivity = new Activity("parent") + .Start(); + Activity childActivity = new Activity("child") + .Start(); + + RequestTelemetry request = new RequestTelemetry(); + new W3COperationCorrelationTelemetryInitializer().Initialize(request); + + Assert.AreEqual(request.Context.Operation.Id, parentActivity.GetTraceId()); + Assert.AreEqual(request.Context.Operation.Id, childActivity.GetTraceId()); + Assert.AreEqual(request.Id, $"|{childActivity.GetTraceId()}.{childActivity.GetSpanId()}."); + Assert.AreEqual(request.Context.Operation.ParentId, $"|{childActivity.GetTraceId()}.{parentActivity.GetSpanId()}."); + } + } +#pragma warning restore 612, 618 +} diff --git a/Src/DependencyCollector/Shared/DependencyTrackingTelemetryModule.cs b/Src/DependencyCollector/Shared/DependencyTrackingTelemetryModule.cs index 6653b7749..5ddf2446e 100644 --- a/Src/DependencyCollector/Shared/DependencyTrackingTelemetryModule.cs +++ b/Src/DependencyCollector/Shared/DependencyTrackingTelemetryModule.cs @@ -9,6 +9,7 @@ using Microsoft.ApplicationInsights.DependencyCollector.Implementation.SqlClientDiagnostics; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing; + using Microsoft.ApplicationInsights.W3C; #if NETSTANDARD1_6 using System.Reflection; using System.Runtime.Versioning; @@ -42,9 +43,6 @@ public class DependencyTrackingTelemetryModule : ITelemetryModule, IDisposable private TelemetryConfiguration telemetryConfiguration; private bool isInitialized = false; private bool disposed = false; - private bool correlationHeadersEnabled = true; - private ICollection excludedCorrelationDomains = new SanitizedHostList(); - private ICollection includeDiagnosticSourceActivities = new List(); /// /// Gets or sets a value indicating whether to disable runtime instrumentation. @@ -61,43 +59,25 @@ public class DependencyTrackingTelemetryModule : ITelemetryModule, IDisposable /// public bool EnableLegacyCorrelationHeadersInjection { get; set; } + /// + /// Gets or sets a value indicating whether to enable W3C distributed tracing headers injection. + /// + public bool EnableW3CHeadersInjection { get; set; } = false; + /// /// Gets the component correlation configuration. /// - public ICollection ExcludeComponentCorrelationHttpHeadersOnDomains - { - get - { - return this.excludedCorrelationDomains; - } - } + public ICollection ExcludeComponentCorrelationHttpHeadersOnDomains { get; } = new SanitizedHostList(); /// /// Gets the list of diagnostic sources and activities to exclude from collection. /// - public ICollection IncludeDiagnosticSourceActivities - { - get - { - return this.includeDiagnosticSourceActivities; - } - } + public ICollection IncludeDiagnosticSourceActivities { get; } = new List(); /// /// Gets or sets a value indicating whether the correlation headers would be set on outgoing http requests. /// - public bool SetComponentCorrelationHttpHeaders - { - get - { - return this.correlationHeadersEnabled; - } - - set - { - this.correlationHeadersEnabled = value; - } - } + public bool SetComponentCorrelationHttpHeaders { get; set; } = true; /// /// Gets or sets the endpoint that is to be used to get the application insights resource's profile (appId etc.). @@ -130,9 +110,13 @@ public void Initialize(TelemetryConfiguration configuration) if (!this.isInitialized) { try - { + { this.telemetryConfiguration = configuration; +#if NET45 + ClientServerDependencyTracker.IsW3CEnabled = this.EnableW3CHeadersInjection; +#endif + #if !NETSTANDARD1_6 // Net40 only supports runtime instrumentation // Net45 supports either but not both to avoid duplication @@ -144,7 +128,8 @@ public void Initialize(TelemetryConfiguration configuration) configuration, this.SetComponentCorrelationHttpHeaders, this.ExcludeComponentCorrelationHttpHeadersOnDomains, - this.EnableLegacyCorrelationHeadersInjection); + this.EnableLegacyCorrelationHeadersInjection, + this.EnableW3CHeadersInjection); if (this.IncludeDiagnosticSourceActivities != null && this.IncludeDiagnosticSourceActivities.Count > 0) { @@ -198,7 +183,8 @@ internal virtual void InitializeForRuntimeProfiler() DependencyTableStore.Instance.WebRequestConditionalHolder, this.SetComponentCorrelationHttpHeaders, this.ExcludeComponentCorrelationHttpHeadersOnDomains, - this.EnableLegacyCorrelationHeadersInjection); + this.EnableLegacyCorrelationHeadersInjection, + this.EnableW3CHeadersInjection); this.sqlCommandProcessing = new ProfilerSqlCommandProcessing(this.telemetryConfiguration, agentVersion, DependencyTableStore.Instance.SqlRequestConditionalHolder); this.sqlConnectionProcessing = new ProfilerSqlConnectionProcessing(this.telemetryConfiguration, agentVersion, DependencyTableStore.Instance.SqlRequestConditionalHolder); @@ -274,7 +260,8 @@ private void InitializeForDiagnosticAndFrameworkEventSource() DependencyTableStore.Instance.WebRequestCacheHolder, this.SetComponentCorrelationHttpHeaders, this.ExcludeComponentCorrelationHttpHeadersOnDomains, - this.EnableLegacyCorrelationHeadersInjection); + this.EnableLegacyCorrelationHeadersInjection, + this.EnableW3CHeadersInjection); this.httpDesktopDiagnosticSourceListener = new HttpDesktopDiagnosticSourceListener(desktopHttpProcessing, new ApplicationInsightsUrlFilter(this.telemetryConfiguration)); } diff --git a/Src/DependencyCollector/Shared/HttpCoreDiagnosticSourceListener.cs b/Src/DependencyCollector/Shared/HttpCoreDiagnosticSourceListener.cs index 59ce645f8..7c21ed256 100644 --- a/Src/DependencyCollector/Shared/HttpCoreDiagnosticSourceListener.cs +++ b/Src/DependencyCollector/Shared/HttpCoreDiagnosticSourceListener.cs @@ -16,6 +16,7 @@ namespace Microsoft.ApplicationInsights.DependencyCollector.Implementation using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation; using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing; + using Microsoft.ApplicationInsights.W3C; internal class HttpCoreDiagnosticSourceListener : IObserver>, IDisposable { @@ -56,8 +57,14 @@ internal class HttpCoreDiagnosticSourceListener : IObserver correlationDomainExclusionList, bool injectLegacyHeaders) + private readonly bool injectW3CHeaders = false; + + public HttpCoreDiagnosticSourceListener( + TelemetryConfiguration configuration, + bool setComponentCorrelationHttpHeaders, + IEnumerable correlationDomainExclusionList, + bool injectLegacyHeaders, + bool injectW3CHeaders) { this.client = new TelemetryClient(configuration); this.client.Context.GetInternalContext().SdkVersion = SdkVersionUtils.GetSdkVersion("rdd" + RddSource.DiagnosticSourceCore + ":"); @@ -70,6 +77,7 @@ public HttpCoreDiagnosticSourceListener(TelemetryConfiguration configuration, bo this.setComponentCorrelationHttpHeaders = setComponentCorrelationHttpHeaders; this.correlationDomainExclusionList = correlationDomainExclusionList ?? Enumerable.Empty(); this.injectLegacyHeaders = injectLegacyHeaders; + this.injectW3CHeaders = injectW3CHeaders; this.subscriber = new HttpCoreDiagnosticSourceSubscriber(this, this.applicationInsightsUrlFilter, this.isNetCore20HttpClient); } @@ -336,12 +344,6 @@ internal void OnActivityStop(HttpResponseMessage response, HttpRequestMessage re return; } - // If we started auxiliary Activity before to override the Id with W3C compatible one, now it's time to stop it - if (currentActivity.Duration == TimeSpan.Zero) - { - currentActivity.Stop(); - } - DependencyCollectorEventSource.Log.HttpCoreDiagnosticSourceListenerStop(currentActivity.Id); Uri requestUri = request.RequestUri; @@ -364,6 +366,12 @@ internal void OnActivityStop(HttpResponseMessage response, HttpRequestMessage re this.client.Initialize(telemetry); + // If we started auxiliary Activity before to override the Id with W3C compatible one, now it's time to stop it + if (currentActivity.Duration == TimeSpan.Zero) + { + currentActivity.Stop(); + } + telemetry.Timestamp = currentActivity.StartTimeUtc; telemetry.Name = resourceName; telemetry.Target = requestUri.Host; @@ -386,6 +394,11 @@ internal void OnActivityStop(HttpResponseMessage response, HttpRequestMessage re telemetry.Success = false; } + if (this.injectW3CHeaders) + { + // this.SetLegacyId(telemetry, request); + } + this.client.TrackDependency(telemetry); } @@ -444,18 +457,17 @@ internal void OnResponse(HttpResponseMessage response, Guid loggingRequestId) } } +#pragma warning disable 612, 618 private void InjectRequestHeaders(HttpRequestMessage request, string instrumentationKey, bool isLegacyEvent = false) { try { - var currentActivity = Activity.Current; - HttpRequestHeaders requestHeaders = request.Headers; if (requestHeaders != null && this.setComponentCorrelationHttpHeaders && !this.correlationDomainExclusionList.Contains(request.RequestUri.Host)) { + string sourceApplicationId = null; try { - string sourceApplicationId = null; if (!string.IsNullOrEmpty(instrumentationKey) && !HttpHeadersUtilities.ContainsRequestContextKeyValue(requestHeaders, RequestResponseHeaders.RequestContextCorrelationSourceKey) && (this.configuration.ApplicationIdProvider?.TryGetApplicationId(instrumentationKey, out sourceApplicationId) ?? false)) @@ -468,6 +480,7 @@ private void InjectRequestHeaders(HttpRequestMessage request, string instrumenta AppMapCorrelationEventSource.Log.UnknownError(ExceptionUtilities.GetExceptionDetailString(e)); } + var currentActivity = Activity.Current; if (isLegacyEvent) { if (!requestHeaders.Contains(RequestResponseHeaders.RequestIdHeader)) @@ -475,25 +488,7 @@ private void InjectRequestHeaders(HttpRequestMessage request, string instrumenta requestHeaders.Add(RequestResponseHeaders.RequestIdHeader, currentActivity.Id); } - if (!requestHeaders.Contains(RequestResponseHeaders.CorrelationContextHeader)) - { - // we expect baggage to be empty or contain a few items - using (IEnumerator> e = currentActivity.Baggage.GetEnumerator()) - { - if (e.MoveNext()) - { - var baggage = new List(); - do - { - KeyValuePair item = e.Current; - baggage.Add(new NameValueHeaderValue(item.Key, item.Value).ToString()); - } - while (e.MoveNext()); - - requestHeaders.Add(RequestResponseHeaders.CorrelationContextHeader, baggage); - } - } - } + this.InjectCorrelationContext(requestHeaders, currentActivity); } if (this.injectLegacyHeaders) @@ -512,6 +507,40 @@ private void InjectRequestHeaders(HttpRequestMessage request, string instrumenta requestHeaders.Add(RequestResponseHeaders.StandardParentIdHeader, parentId); } } + + if (this.injectW3CHeaders) + { + currentActivity.UpdateContextOnActivity(); + string traceParent = currentActivity.GetTraceparent(); + if (traceParent != null && !requestHeaders.Contains(W3CConstants.TraceParentHeader)) + { + requestHeaders.Add(W3CConstants.TraceParentHeader, traceParent); + } + + string traceState = currentActivity.GetTracestate(); + if (!requestHeaders.Contains(W3CConstants.TraceStateHeader)) + { + if (sourceApplicationId != null) + { + // TODO: there could be another az in the state. + // last updated state should appear first in the tracestate + string appIdPair = StringUtilities.FormatAzureTracestate(sourceApplicationId); + if (traceState == null) + { + traceState = appIdPair; + } + else + { + traceState = appIdPair + "," + traceState; + } + } + + if (traceState != null) + { + requestHeaders.Add(W3CConstants.TraceStateHeader, traceState); + } + } + } } } catch (Exception e) @@ -519,6 +548,7 @@ private void InjectRequestHeaders(HttpRequestMessage request, string instrumenta AppMapCorrelationEventSource.Log.UnknownError(ExceptionUtilities.GetExceptionDetailString(e)); } } +#pragma warning restore 612, 618 private void ParseResponse(HttpResponseMessage response, DependencyTelemetry telemetry) { @@ -549,6 +579,29 @@ private void ParseResponse(HttpResponseMessage response, DependencyTelemetry tel telemetry.Success = (statusCode > 0) && (statusCode < 400); } + private void InjectCorrelationContext(HttpRequestHeaders requestHeaders, Activity currentActivity) + { + if (!requestHeaders.Contains(RequestResponseHeaders.CorrelationContextHeader)) + { + // we expect baggage to be empty or contain a few items + using (IEnumerator> e = currentActivity.Baggage.GetEnumerator()) + { + if (e.MoveNext()) + { + var baggage = new List(); + do + { + KeyValuePair item = e.Current; + baggage.Add(new NameValueHeaderValue(item.Key, item.Value).ToString()); + } + while (e.MoveNext()); + + requestHeaders.Add(RequestResponseHeaders.CorrelationContextHeader, baggage); + } + } + } + } + /// /// Diagnostic listener implementation that listens for events specific to outgoing dependency requests. /// diff --git a/Src/DependencyCollector/Shared/Implementation/ClientServerDependencyTracker.cs b/Src/DependencyCollector/Shared/Implementation/ClientServerDependencyTracker.cs index c476a49a3..f7524a16c 100644 --- a/Src/DependencyCollector/Shared/Implementation/ClientServerDependencyTracker.cs +++ b/Src/DependencyCollector/Shared/Implementation/ClientServerDependencyTracker.cs @@ -2,16 +2,17 @@ { using System; using System.Data.SqlClient; -#if NET45 using System.Diagnostics; -#endif using System.Net; using Microsoft.ApplicationInsights.Common; using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.W3C; internal static class ClientServerDependencyTracker { - private const string DependencyActivityName = "Microsoft.AppInsights.Web.Dependency"; + internal const string DependencyActivityName = "Microsoft.ApplivationInsights.Web.Dependency"; + + internal static bool IsW3CEnabled { get; set; } = false; /// /// Gets or sets a value indicating whether pretending the profiler is attached or not. @@ -57,7 +58,7 @@ internal static DependencyTelemetry BeginTracking(TelemetryClient telemetryClien telemetryClient.Initialize(telemetry); // Every operation must have its own Activity - // if dependency is tracked with profiler of event source, we need to generate a proper hierarchical Id for it + // if dependency is tracked with profiler of event source, we need to generate a proper Id for it // in case of HTTP it will be propagated into the requert header. // So, we will create a new Activity for the dependency, just to generate an Id. activity = new Activity(DependencyActivityName); @@ -77,7 +78,6 @@ internal static DependencyTelemetry BeginTracking(TelemetryClient telemetryClien // end of workaround activity.Start(); - activity.Stop(); } // telemetry is initialized from current Activity (root and parent Id, but not the Id) @@ -89,6 +89,13 @@ internal static DependencyTelemetry BeginTracking(TelemetryClient telemetryClien telemetry.Context.Operation.Id = activity.RootId; } +#pragma warning disable 612, 618 + if (IsW3CEnabled) + { + W3COperationCorrelationTelemetryInitializer.UpdateTelemetry(telemetry, activity, true); + } +#pragma warning restore 612, 618 + PretendProfilerIsAttached = false; return telemetry; } diff --git a/Src/DependencyCollector/Shared/Implementation/DesktopDiagnosticSourceHttpProcessing.cs b/Src/DependencyCollector/Shared/Implementation/DesktopDiagnosticSourceHttpProcessing.cs index c867ca883..31b065d35 100644 --- a/Src/DependencyCollector/Shared/Implementation/DesktopDiagnosticSourceHttpProcessing.cs +++ b/Src/DependencyCollector/Shared/Implementation/DesktopDiagnosticSourceHttpProcessing.cs @@ -16,8 +16,8 @@ internal sealed class DesktopDiagnosticSourceHttpProcessing : HttpProcessing { private readonly CacheBasedOperationHolder telemetryTable; - internal DesktopDiagnosticSourceHttpProcessing(TelemetryConfiguration configuration, CacheBasedOperationHolder telemetryTupleHolder, bool setCorrelationHeaders, ICollection correlationDomainExclusionList, bool injectLegacyHeaders) - : base(configuration, SdkVersionUtils.GetSdkVersion("rdd" + RddSource.DiagnosticSourceDesktop + ":"), null, setCorrelationHeaders, correlationDomainExclusionList, injectLegacyHeaders) + internal DesktopDiagnosticSourceHttpProcessing(TelemetryConfiguration configuration, CacheBasedOperationHolder telemetryTupleHolder, bool setCorrelationHeaders, ICollection correlationDomainExclusionList, bool injectLegacyHeaders, bool enableW3CHeaders) + : base(configuration, SdkVersionUtils.GetSdkVersion("rdd" + RddSource.DiagnosticSourceDesktop + ":"), null, setCorrelationHeaders, correlationDomainExclusionList, injectLegacyHeaders, enableW3CHeaders) { if (telemetryTupleHolder == null) { diff --git a/Src/DependencyCollector/Shared/Implementation/FrameworkHttpProcessing.cs b/Src/DependencyCollector/Shared/Implementation/FrameworkHttpProcessing.cs index f65dacd9e..e803d74f8 100644 --- a/Src/DependencyCollector/Shared/Implementation/FrameworkHttpProcessing.cs +++ b/Src/DependencyCollector/Shared/Implementation/FrameworkHttpProcessing.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Diagnostics; using System.Globalization; using System.Net; using Microsoft.ApplicationInsights.Common; @@ -19,7 +20,7 @@ internal sealed class FrameworkHttpProcessing : HttpProcessing private readonly ApplicationInsightsUrlFilter applicationInsightsUrlFilter; internal FrameworkHttpProcessing(TelemetryConfiguration configuration, CacheBasedOperationHolder telemetryTupleHolder, bool setCorrelationHeaders, ICollection correlationDomainExclusionList, bool injectLegacyHeaders) - : base(configuration, SdkVersionUtils.GetSdkVersion("rdd" + RddSource.Framework + ":"), null, setCorrelationHeaders, correlationDomainExclusionList, injectLegacyHeaders) + : base(configuration, SdkVersionUtils.GetSdkVersion("rdd" + RddSource.Framework + ":"), null, setCorrelationHeaders, correlationDomainExclusionList, injectLegacyHeaders, false) { if (telemetryTupleHolder == null) { @@ -59,7 +60,8 @@ public void OnBeginHttpCallback(long id, string resourceName) } catch (UriFormatException) { - DependencyCollectorEventSource.Log.NotExpectedCallback(id, "OnBeginHttp", "resourceName is not a URL " + resourceName); + DependencyCollectorEventSource.Log.NotExpectedCallback(id, "OnBeginHttp", + "resourceName is not a URL " + resourceName); return; } @@ -89,6 +91,14 @@ public void OnBeginHttpCallback(long id, string resourceName) { DependencyCollectorEventSource.Log.CallbackError(id, "OnBeginHttp", exception); } + finally + { + Activity current = Activity.Current; + if (current?.OperationName == ClientServerDependencyTracker.DependencyActivityName) + { + current.Stop(); + } + } } /// diff --git a/Src/DependencyCollector/Shared/Implementation/HttpProcessing.cs b/Src/DependencyCollector/Shared/Implementation/HttpProcessing.cs index 7e2df2a98..2e7fc0608 100644 --- a/Src/DependencyCollector/Shared/Implementation/HttpProcessing.cs +++ b/Src/DependencyCollector/Shared/Implementation/HttpProcessing.cs @@ -11,6 +11,7 @@ namespace Microsoft.ApplicationInsights.DependencyCollector.Implementation using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation; + using Microsoft.ApplicationInsights.W3C; /// /// Concrete class with all processing logic to generate RDD data from the callbacks @@ -24,11 +25,12 @@ internal abstract class HttpProcessing private readonly ICollection correlationDomainExclusionList; private readonly bool setCorrelationHeaders; private readonly bool injectLegacyHeaders; + private readonly bool injectW3CHeaders; /// /// Initializes a new instance of the class. /// - public HttpProcessing(TelemetryConfiguration configuration, string sdkVersion, string agentVersion, bool setCorrelationHeaders, ICollection correlationDomainExclusionList, bool injectLegacyHeaders) + protected HttpProcessing(TelemetryConfiguration configuration, string sdkVersion, string agentVersion, bool setCorrelationHeaders, ICollection correlationDomainExclusionList, bool injectLegacyHeaders, bool injectW3CHeaders) { this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); this.applicationInsightsUrlFilter = new ApplicationInsightsUrlFilter(configuration); @@ -44,6 +46,7 @@ public HttpProcessing(TelemetryConfiguration configuration, string sdkVersion, s } this.injectLegacyHeaders = injectLegacyHeaders; + this.injectW3CHeaders = injectW3CHeaders; } /// @@ -89,7 +92,8 @@ internal object OnBegin(object thisObj, bool injectCorrelationHeaders = true) if (url == null) { - DependencyCollectorEventSource.Log.NotExpectedCallback(thisObj.GetHashCode(), "OnBeginHttp", "resourceName is empty"); + DependencyCollectorEventSource.Log.NotExpectedCallback(thisObj.GetHashCode(), "OnBeginHttp", + "resourceName is empty"); return null; } @@ -149,19 +153,23 @@ internal object OnBegin(object thisObj, bool injectCorrelationHeaders = true) // Add the source instrumentation key header if collection is enabled, the request host is not in the excluded list and the same header doesn't already exist if (this.setCorrelationHeaders && !this.correlationDomainExclusionList.Contains(url.Host)) { + string applicationId = null; try { - string applicationId = null; if (!string.IsNullOrEmpty(telemetry.Context.InstrumentationKey) - && webRequest.Headers.GetNameValueHeaderValue(RequestResponseHeaders.RequestContextHeader, RequestResponseHeaders.RequestContextCorrelationSourceKey) == null - && (this.configuration.ApplicationIdProvider?.TryGetApplicationId(telemetry.Context.InstrumentationKey, out applicationId) ?? false)) + && webRequest.Headers.GetNameValueHeaderValue(RequestResponseHeaders.RequestContextHeader, + RequestResponseHeaders.RequestContextCorrelationSourceKey) == null + && (this.configuration.ApplicationIdProvider?.TryGetApplicationId( + telemetry.Context.InstrumentationKey, out applicationId) ?? false)) { - webRequest.Headers.SetNameValueHeaderValue(RequestResponseHeaders.RequestContextHeader, RequestResponseHeaders.RequestContextCorrelationSourceKey, applicationId); + webRequest.Headers.SetNameValueHeaderValue(RequestResponseHeaders.RequestContextHeader, + RequestResponseHeaders.RequestContextCorrelationSourceKey, applicationId); } } catch (Exception ex) { - AppMapCorrelationEventSource.Log.SetCrossComponentCorrelationHeaderFailed(ex.ToInvariantString()); + AppMapCorrelationEventSource.Log.SetCrossComponentCorrelationHeaderFailed( + ex.ToInvariantString()); } if (this.injectLegacyHeaders) @@ -185,6 +193,8 @@ internal object OnBegin(object thisObj, bool injectCorrelationHeaders = true) } } + Activity currentActivity = Activity.Current; + // ApplicationInsights only need to inject Request-Id and Correlation-Context headers // for profiler instrumentation, in case of Http Desktop DiagnosticSourceListener // they are injected in DiagnosticSource (with the System.Net.Http.Desktop.HttpRequestOut.Start event) @@ -195,20 +205,59 @@ internal object OnBegin(object thisObj, bool injectCorrelationHeaders = true) webRequest.Headers.Add(RequestResponseHeaders.RequestIdHeader, telemetry.Id); } - if (webRequest.Headers[RequestResponseHeaders.CorrelationContextHeader] == null) + if (currentActivity != null) + { + this.InjectCorrelationContext(webRequest.Headers, currentActivity); + } + } + +#pragma warning disable 612, 618 + if (this.injectW3CHeaders && currentActivity != null) + { + string traceParent = currentActivity.GetTraceparent(); + if (traceParent != null && webRequest.Headers[W3CConstants.TraceParentHeader] == null) + { + webRequest.Headers.Add(W3CConstants.TraceParentHeader, traceParent); + } + + string traceState = currentActivity.GetTracestate(); + if (webRequest.Headers[W3CConstants.TraceStateHeader] == null) { - var currentActivity = Activity.Current; - if (currentActivity != null && currentActivity.Baggage.Any()) + if (applicationId != null) { - webRequest.Headers.SetHeaderFromNameValueCollection(RequestResponseHeaders.CorrelationContextHeader, currentActivity.Baggage); + // TODO: there could be another az in the state. + string appIdPair = StringUtilities.FormatAzureTracestate(applicationId); + if (traceState == null) + { + traceState = appIdPair; + } + else + { + traceState = appIdPair + "," + traceState; + } + } + + if (traceState != null) + { + webRequest.Headers.Add(W3CConstants.TraceStateHeader, traceState); } } } +#pragma warning restore 612, 618 } } catch (Exception exception) { - DependencyCollectorEventSource.Log.CallbackError(thisObj == null ? 0 : thisObj.GetHashCode(), "OnBeginHttp", exception); + DependencyCollectorEventSource.Log.CallbackError(thisObj == null ? 0 : thisObj.GetHashCode(), + "OnBeginHttp", exception); + } + finally + { + Activity current = Activity.Current; + if (current?.OperationName == ClientServerDependencyTracker.DependencyActivityName) + { + current.Stop(); + } } return null; @@ -233,6 +282,10 @@ internal void OnEndResponse(object request, object response) { statusCode = (int)responseObj.StatusCode; this.SetTarget(telemetry, responseObj.Headers); + if (this.injectW3CHeaders && request is HttpWebRequest httpRequest) + { + // this.SetLegacyId(telemetry, httpRequest.Headers); + } // Set the operation details for the response telemetry.SetOperationDetail(RemoteDependencyConstants.HttpResponseOperationDetailName, responseObj); @@ -276,6 +329,10 @@ internal void OnEndException(object exception, object request) { statusCode = (int)responseObj.StatusCode; this.SetTarget(telemetry, responseObj.Headers); + if (this.injectW3CHeaders && request is HttpWebRequest httpRequest) + { + // this.SetLegacyId(telemetry, httpRequest.Headers); + } // Set the operation details for the response telemetry.SetOperationDetail(RemoteDependencyConstants.HttpResponseOperationDetailName, responseObj); @@ -325,6 +382,11 @@ internal void OnEndResponse(object request, object statusCode, object responseHe } this.SetTarget(telemetry, (WebHeaderCollection)responseHeaders); + if (this.injectW3CHeaders && request is HttpWebRequest httpRequest) + { + // this.SetLegacyId(telemetry, httpRequest.Headers); + } + telemetry.SetOperationDetail(RemoteDependencyConstants.HttpResponseHeadersOperationDetailName, responseHeaders); ClientServerDependencyTracker.EndTracking(this.telemetryClient, telemetry); @@ -410,7 +472,9 @@ private void SetTarget(DependencyTelemetry telemetry, WebHeaderCollection respon try { - targetAppId = responseHeaders.GetNameValueHeaderValue(RequestResponseHeaders.RequestContextHeader, RequestResponseHeaders.RequestContextCorrelationTargetKey); + targetAppId = responseHeaders.GetNameValueHeaderValue( + RequestResponseHeaders.RequestContextHeader, + RequestResponseHeaders.RequestContextCorrelationTargetKey); } catch (Exception ex) { @@ -435,5 +499,13 @@ private void SetStatusCode(DependencyTelemetry telemetry, int statusCode) telemetry.ResultCode = statusCode > 0 ? statusCode.ToString(CultureInfo.InvariantCulture) : string.Empty; telemetry.Success = (statusCode > 0) && (statusCode < 400); } + + private void InjectCorrelationContext(WebHeaderCollection requestHeaders, Activity activity) + { + if (requestHeaders[RequestResponseHeaders.CorrelationContextHeader] == null && activity.Baggage.Any()) + { + requestHeaders.SetHeaderFromNameValueCollection(RequestResponseHeaders.CorrelationContextHeader, activity.Baggage); + } + } } } \ No newline at end of file diff --git a/Src/DependencyCollector/Shared/Implementation/ProfilerHttpProcessing.cs b/Src/DependencyCollector/Shared/Implementation/ProfilerHttpProcessing.cs index bddda0b72..c6b2c363b 100644 --- a/Src/DependencyCollector/Shared/Implementation/ProfilerHttpProcessing.cs +++ b/Src/DependencyCollector/Shared/Implementation/ProfilerHttpProcessing.cs @@ -20,8 +20,8 @@ internal sealed class ProfilerHttpProcessing : HttpProcessing /// /// Initializes a new instance of the class. /// - public ProfilerHttpProcessing(TelemetryConfiguration configuration, string agentVersion, ObjectInstanceBasedOperationHolder telemetryTupleHolder, bool setCorrelationHeaders, ICollection correlationDomainExclusionList, bool injectLegacyHeaders) - : base(configuration, SdkVersionUtils.GetSdkVersion("rdd" + RddSource.Profiler + ":"), agentVersion, setCorrelationHeaders, correlationDomainExclusionList, injectLegacyHeaders) + public ProfilerHttpProcessing(TelemetryConfiguration configuration, string agentVersion, ObjectInstanceBasedOperationHolder telemetryTupleHolder, bool setCorrelationHeaders, ICollection correlationDomainExclusionList, bool injectLegacyHeaders, bool injectW3CHeaders) + : base(configuration, SdkVersionUtils.GetSdkVersion("rdd" + RddSource.Profiler + ":"), agentVersion, setCorrelationHeaders, correlationDomainExclusionList, injectLegacyHeaders, injectW3CHeaders) { if (telemetryTupleHolder == null) { diff --git a/Src/DependencyCollector/Shared/Implementation/ProfilerSqlProcessingBase.cs b/Src/DependencyCollector/Shared/Implementation/ProfilerSqlProcessingBase.cs index caded0809..85463bbcd 100644 --- a/Src/DependencyCollector/Shared/Implementation/ProfilerSqlProcessingBase.cs +++ b/Src/DependencyCollector/Shared/Implementation/ProfilerSqlProcessingBase.cs @@ -2,6 +2,7 @@ { using System; using System.Data.SqlClient; + using System.Diagnostics; using System.Globalization; using System.Threading.Tasks; using Microsoft.ApplicationInsights.Common; @@ -255,6 +256,14 @@ private object OnBegin(object thisObj) { DependencyCollectorEventSource.Log.CallbackError(thisObj == null ? 0 : thisObj.GetHashCode(), "OnBeginSql", exception); } + finally + { + Activity current = Activity.Current; + if (current?.OperationName == ClientServerDependencyTracker.DependencyActivityName) + { + current.Stop(); + } + } return null; } diff --git a/Src/PerformanceCollector/Net45/Perf.Net45.csproj b/Src/PerformanceCollector/Net45/Perf.Net45.csproj index c6341d305..05e7618dc 100644 --- a/Src/PerformanceCollector/Net45/Perf.Net45.csproj +++ b/Src/PerformanceCollector/Net45/Perf.Net45.csproj @@ -49,6 +49,7 @@ + diff --git a/Src/Web/Web.Net45.Tests/AspNetDiagnosticTelemetryModuleTest.cs b/Src/Web/Web.Net45.Tests/AspNetDiagnosticTelemetryModuleTest.cs index de8e64778..e2adb27f3 100644 --- a/Src/Web/Web.Net45.Tests/AspNetDiagnosticTelemetryModuleTest.cs +++ b/Src/Web/Web.Net45.Tests/AspNetDiagnosticTelemetryModuleTest.cs @@ -10,11 +10,14 @@ using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation; + using Microsoft.ApplicationInsights.W3C; using Microsoft.ApplicationInsights.Web; using Microsoft.ApplicationInsights.Web.Helpers; using Microsoft.ApplicationInsights.Web.TestFramework; + using Microsoft.AspNet.TelemetryCorrelation; using Microsoft.VisualStudio.TestTools.UnitTesting; +#pragma warning disable 612, 618 [TestClass] public class AspNetDiagnosticTelemetryModuleTest : IDisposable { @@ -291,7 +294,7 @@ public void StandardHeadersWinOverLegacyHeaders() } [TestMethod] - public void TestActivityIdGeneratioWithEmptyHeaders() + public void TestActivityIdGenerationWithEmptyHeaders() { this.module = this.CreateModule(); @@ -324,13 +327,178 @@ public void TestActivityIdGeneratioWithEmptyHeaders() //// end of workaround test } + [TestMethod] + public void TestActivityIdGenerationWithW3CEnabled() + { + this.module = this.CreateModule(enableW3cSupport: true); + + this.aspNetDiagnosticsSource.StartActivity(); + Activity activity = Activity.Current; + + this.aspNetDiagnosticsSource.StopActivity(); + + var request = this.sendItems.OfType().Single(); + + Assert.AreEqual(32, request.Context.Operation.Id.Length); + Assert.IsTrue(Regex.Match(request.Context.Operation.Id, @"[a-z][0-9]").Success); + + Assert.AreEqual(request.Context.Operation.Id, activity.RootId); + Assert.AreEqual(request.Context.Operation.ParentId, activity.GetParentSpanId()); + Assert.AreEqual(request.Id, $"|{activity.GetTraceId()}.{activity.GetSpanId()}."); + + Assert.IsFalse(request.Properties.ContainsKey(W3CConstants.LegacyRootIdProperty)); + } + + [TestMethod] + public void W3CHeadersWinOverLegacyWhenEnabled() + { + FakeAspNetDiagnosticSource.FakeContext = + HttpModuleHelper.GetFakeHttpContext(new Dictionary + { + ["traceparent"] = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + ["x-ms-request-id"] = "legacy-id", + ["x-ms-request-rooit-id"] = "legacy-root-id" + }); + + this.module = this.CreateModule("x-ms-request-root-id", "x-ms-request-id", enableW3cSupport: true); + + var activity = new Activity(FakeAspNetDiagnosticSource.IncomingRequestEventName); + Assert.IsTrue(this.aspNetDiagnosticsSource.IsEnabled(FakeAspNetDiagnosticSource.IncomingRequestEventName, activity)); + this.aspNetDiagnosticsSource.StartActivityWithoutChecks(activity); + this.aspNetDiagnosticsSource.StopActivity(); + + Assert.AreEqual("4bf92f3577b34da6a3ce929d0e0e4736", activity.RootId); + Assert.AreEqual("00f067aa0ba902b7", activity.GetParentSpanId()); + + Assert.AreEqual(1, this.sendItems.Count); + + var requestTelemetry = this.sendItems[0] as RequestTelemetry; + Assert.IsNotNull(requestTelemetry); + Assert.AreEqual("4bf92f3577b34da6a3ce929d0e0e4736", requestTelemetry.Context.Operation.Id); + Assert.AreEqual("4bf92f3577b34da6a3ce929d0e0e4736", activity.GetTraceId()); + Assert.AreEqual("|4bf92f3577b34da6a3ce929d0e0e4736.00f067aa0ba902b7.", requestTelemetry.Context.Operation.ParentId); + Assert.AreEqual($"|4bf92f3577b34da6a3ce929d0e0e4736.{activity.GetSpanId()}.", requestTelemetry.Id); + + Assert.IsFalse(requestTelemetry.Properties.ContainsKey(W3CConstants.LegacyRootIdProperty)); + } + + [TestMethod] + public void W3CHeadersWinOverRequestIdWhenEnabled() + { + FakeAspNetDiagnosticSource.FakeContext = + HttpModuleHelper.GetFakeHttpContext(new Dictionary + { + ["traceparent"] = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + }); + + this.module = this.CreateModule(enableW3cSupport: true); + + var activity = new Activity(FakeAspNetDiagnosticSource.IncomingRequestEventName); + activity.SetParentId("|requestId."); + Assert.IsTrue(this.aspNetDiagnosticsSource.IsEnabled(FakeAspNetDiagnosticSource.IncomingRequestEventName, activity)); + this.aspNetDiagnosticsSource.StartActivityWithoutChecks(activity); + this.aspNetDiagnosticsSource.StopActivity(); + + Assert.AreEqual("4bf92f3577b34da6a3ce929d0e0e4736", activity.GetTraceId()); + Assert.AreEqual("00f067aa0ba902b7", activity.GetParentSpanId()); + + Assert.AreEqual(1, this.sendItems.Count); + + var requestTelemetry = this.sendItems[0] as RequestTelemetry; + Assert.IsNotNull(requestTelemetry); + Assert.AreEqual("4bf92f3577b34da6a3ce929d0e0e4736", requestTelemetry.Context.Operation.Id); + Assert.AreEqual("|4bf92f3577b34da6a3ce929d0e0e4736.00f067aa0ba902b7.", requestTelemetry.Context.Operation.ParentId); + Assert.AreEqual($"|4bf92f3577b34da6a3ce929d0e0e4736.{activity.GetSpanId()}.", requestTelemetry.Id); + + Assert.IsTrue(requestTelemetry.Properties.ContainsKey(W3CConstants.LegacyRootIdProperty)); + Assert.AreEqual("requestId", requestTelemetry.Properties[W3CConstants.LegacyRootIdProperty]); + + Assert.IsTrue(requestTelemetry.Properties.ContainsKey(W3CConstants.LegacyRequestIdProperty)); + Assert.IsTrue(requestTelemetry.Properties[W3CConstants.LegacyRequestIdProperty].StartsWith("|requestId.")); + } + + [TestMethod] + public void RequestIdBecomesParentWhenThereAreNoW3CHeaders() + { + FakeAspNetDiagnosticSource.FakeContext = + HttpModuleHelper.GetFakeHttpContext(new Dictionary + { + ["Request-Id"] = "|requestId." + }); + this.module = this.CreateModule(enableW3cSupport: true); + + var activity = new Activity(FakeAspNetDiagnosticSource.IncomingRequestEventName); + + activity.Extract(HttpContext.Current.Request.Headers); + + Assert.IsTrue(this.aspNetDiagnosticsSource.IsEnabled(FakeAspNetDiagnosticSource.IncomingRequestEventName, activity)); + this.aspNetDiagnosticsSource.StartActivityWithoutChecks(activity); + this.aspNetDiagnosticsSource.StopActivity(); + + Assert.AreEqual(32, activity.GetTraceId().Length); + Assert.AreEqual(16, activity.GetSpanId().Length); + Assert.IsNull(activity.GetParentSpanId()); + + Assert.AreEqual(1, this.sendItems.Count); + + var requestTelemetry = this.sendItems[0] as RequestTelemetry; + Assert.IsNotNull(requestTelemetry); + Assert.AreEqual(activity.GetTraceId(), requestTelemetry.Context.Operation.Id); + Assert.AreEqual("|requestId.", requestTelemetry.Context.Operation.ParentId); + Assert.AreEqual($"|{activity.GetTraceId()}.{activity.GetSpanId()}.", requestTelemetry.Id); + + Assert.IsTrue(requestTelemetry.Properties.ContainsKey(W3CConstants.LegacyRootIdProperty)); + Assert.AreEqual("requestId", requestTelemetry.Properties[W3CConstants.LegacyRootIdProperty]); + + Assert.IsTrue(requestTelemetry.Properties.ContainsKey(W3CConstants.LegacyRequestIdProperty)); + Assert.IsTrue(requestTelemetry.Properties[W3CConstants.LegacyRequestIdProperty].StartsWith("|requestId.")); + } + + [TestMethod] + public void CustomHeadersBecomeParentWhenThereAreNoW3CHeaders() + { + FakeAspNetDiagnosticSource.FakeContext = + HttpModuleHelper.GetFakeHttpContext(new Dictionary + { + ["rootHeaderName"] = "root", + ["parentHeaderName"] = "parent" + }); + this.module = this.CreateModule("rootHeaderName", "parentHeaderName", enableW3cSupport: true); + + var activity = new Activity(FakeAspNetDiagnosticSource.IncomingRequestEventName); + + activity.Extract(HttpContext.Current.Request.Headers); + + Assert.IsTrue(this.aspNetDiagnosticsSource.IsEnabled(FakeAspNetDiagnosticSource.IncomingRequestEventName, activity)); + this.aspNetDiagnosticsSource.StartActivityWithoutChecks(activity); + this.aspNetDiagnosticsSource.StopActivity(); + + Assert.AreEqual(32, activity.GetTraceId().Length); + Assert.AreEqual(16, activity.GetSpanId().Length); + Assert.IsNull(activity.GetParentSpanId()); + + Assert.AreEqual(1, this.sendItems.Count); + + var requestTelemetry = this.sendItems[0] as RequestTelemetry; + Assert.IsNotNull(requestTelemetry); + Assert.AreEqual(activity.GetTraceId(), requestTelemetry.Context.Operation.Id); + Assert.AreEqual("parent", requestTelemetry.Context.Operation.ParentId); + Assert.AreEqual($"|{activity.GetTraceId()}.{activity.GetSpanId()}.", requestTelemetry.Id); + + Assert.IsTrue(requestTelemetry.Properties.ContainsKey(W3CConstants.LegacyRootIdProperty)); + Assert.AreEqual("root", requestTelemetry.Properties[W3CConstants.LegacyRootIdProperty]); + + Assert.IsTrue(requestTelemetry.Properties.ContainsKey(W3CConstants.LegacyRequestIdProperty)); + Assert.IsTrue(requestTelemetry.Properties[W3CConstants.LegacyRequestIdProperty].StartsWith("|root.")); + } + public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } - private AspNetDiagnosticTelemetryModule CreateModule(string rootIdHeaderName = null, string parentIdHeaderName = null) + private AspNetDiagnosticTelemetryModule CreateModule(string rootIdHeaderName = null, string parentIdHeaderName = null, bool enableW3cSupport = false) { var initializer = new Web.OperationCorrelationTelemetryInitializer(); if (rootIdHeaderName != null) @@ -349,8 +517,10 @@ private AspNetDiagnosticTelemetryModule CreateModule(string rootIdHeaderName = n var requestModule = new RequestTrackingTelemetryModule() { - EnableChildRequestTrackingSuppression = false + EnableChildRequestTrackingSuppression = false, + EnableW3CHeadersExtraction = enableW3cSupport }; + var exceptionModule = new ExceptionTrackingTelemetryModule(); requestModule.Initialize(this.configuration); exceptionModule.Initialize(this.configuration); @@ -489,4 +659,5 @@ private void Dispose(bool dispose) } } } +#pragma warning restore 612, 618 } \ No newline at end of file diff --git a/Src/Web/Web.Net45.Tests/RequestTrackingTelemetryModuleTest.Net45.cs b/Src/Web/Web.Net45.Tests/RequestTrackingTelemetryModuleTest.Net45.cs index 029962ca5..48920b85f 100644 --- a/Src/Web/Web.Net45.Tests/RequestTrackingTelemetryModuleTest.Net45.cs +++ b/Src/Web/Web.Net45.Tests/RequestTrackingTelemetryModuleTest.Net45.cs @@ -3,16 +3,20 @@ using System; using System.Collections.Generic; using System.Diagnostics; + using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; + using Microsoft.ApplicationInsights.Common; using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.W3C; using Microsoft.ApplicationInsights.Web.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; using Assert = Xunit.Assert; +#pragma warning disable 612, 618 /// /// NET 4.5 specific tests for RequestTrackingTelemetryModule. /// @@ -135,6 +139,117 @@ public void OnBeginSetsOperationContextWithEnabledLegacyHeaders() Assert.True(requestTelemetry.Id.StartsWith("|guid2.", StringComparison.Ordinal)); } + [TestMethod] + public void TrackRequestWithW3CHeaders() + { + this.TestRequestTrackingWithW3CSupportEnabled( + startActivity: true, + addRequestId: false); + } + + [TestMethod] + public void TrackRequestWithW3CHeadersAndNoParentActivity() + { + this.TestRequestTrackingWithW3CSupportEnabled( + startActivity: false, + addRequestId: false); + } + + [TestMethod] + public void TrackRequestWithW3CAndRequestIdHeaders() + { + this.TestRequestTrackingWithW3CSupportEnabled( + startActivity: true, + addRequestId: true); + } + + [TestMethod] + public void TrackRequestWithW3CAndRequestIdHeadersAndNoParentActivity() + { + this.TestRequestTrackingWithW3CSupportEnabled( + startActivity: false, + addRequestId: true); + } + + [TestMethod] + public void TrackRequestWithW3CEnabledAndNoHeaders() + { + this.TestRequestTrackingWithW3CSupportEnabledAndNoW3CHeaders( + startActivity: true, + addRequestId: false); + } + + [TestMethod] + public void TrackRequestWithW3CEnabledAndNoHeadersAndNoParentActivity() + { + this.TestRequestTrackingWithW3CSupportEnabledAndNoW3CHeaders( + startActivity: false, + addRequestId: false); + } + + [TestMethod] + public void TrackRequestWithW3CEnabledAndRequestIdHeader() + { + this.TestRequestTrackingWithW3CSupportEnabledAndNoW3CHeaders( + startActivity: true, + addRequestId: true); + } + + [TestMethod] + public void TrackRequestWithW3CEnabledAndRequestIdHeaderAndNoParentActivity() + { + this.TestRequestTrackingWithW3CSupportEnabledAndNoW3CHeaders( + startActivity: false, + addRequestId: true); + } + + [TestMethod] + public void TrackRequestWithW3CEnabledAndAppIdInState() + { + string expectedAppId = "cid-v1:some-app-id"; + var headers = new Dictionary + { + ["traceparent"] = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + ["tracestate"] = $"state=some,{W3CConstants.AzureTracestateNamespace}={expectedAppId}", + }; + + var context = HttpModuleHelper.GetFakeHttpContext(headers); + var module = this.RequestTrackingTelemetryModuleFactory(this.CreateDefaultConfig(context), enableW3CTracing: true); + + module.OnBeginRequest(context); + var activityInitializedByW3CHeader = Activity.Current; + Assert.Equal("state=some", activityInitializedByW3CHeader.GetTracestate()); + + var requestTelemetry = context.GetRequestTelemetry(); + module.OnEndRequest(context); + + Assert.Equal(expectedAppId, requestTelemetry.Source); + } + + [TestMethod] + public void TrackRequestWithW3CEnabledAndRequestContextAndAppIdInState() + { + string expectedAppId = "cid-v1:some-app-id"; + var headers = new Dictionary + { + ["traceparent"] = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + ["tracestate"] = $"state=some,{W3CConstants.AzureTracestateNamespace}={expectedAppId}", + ["Request-Context"] = "cid-v1:dummy" + }; + + var context = HttpModuleHelper.GetFakeHttpContext(headers); + var module = this.RequestTrackingTelemetryModuleFactory(this.CreateDefaultConfig(context), enableW3CTracing: true); + + module.OnBeginRequest(context); + var activityInitializedByW3CHeader = Activity.Current; + Assert.Equal("state=some", activityInitializedByW3CHeader.GetTracestate()); + + var requestTelemetry = context.GetRequestTelemetry(); + module.OnEndRequest(context); + + Assert.Equal(expectedAppId, requestTelemetry.Source); + } + [TestMethod] public void OnBeginSetsOperationContextWithDisabledLegacyHeaders() { @@ -280,5 +395,93 @@ public void TelemetryCreatedWithinRequestScopeIsRequestChildWhenActivityIsLost() Assert.True(trace.Context.Operation.ParentId.StartsWith(requestTelemetry.Id, StringComparison.Ordinal)); Assert.Equal("v", trace.Properties["k"]); } + + private void TestRequestTrackingWithW3CSupportEnabled(bool startActivity, bool addRequestId) + { + var headers = new Dictionary + { + ["traceparent"] = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + ["tracestate"] = "state=some", + ["Correlation-Context"] = "k=v" + }; + + if (addRequestId) + { + headers.Add("Request-Id", "|abc.1.2.3."); + } + + var context = HttpModuleHelper.GetFakeHttpContext(headers); + var module = this.RequestTrackingTelemetryModuleFactory(this.CreateDefaultConfig(context), enableW3CTracing: true); + + if (startActivity) + { + var activity = new Activity("operation"); + activity.Start(); + } + + module.OnBeginRequest(context); + var activityInitializedByW3CHeader = Activity.Current; + + Assert.Equal("4bf92f3577b34da6a3ce929d0e0e4736", activityInitializedByW3CHeader.GetTraceId()); + Assert.Equal("00f067aa0ba902b7", activityInitializedByW3CHeader.GetParentSpanId()); + Assert.Equal(16, activityInitializedByW3CHeader.GetSpanId().Length); + Assert.Equal("state=some", activityInitializedByW3CHeader.GetTracestate()); + Assert.Equal("v", activityInitializedByW3CHeader.Baggage.Single(t => t.Key == "k").Value); + + var requestTelemetry = context.GetRequestTelemetry(); + module.OnEndRequest(context); + + Assert.Equal($"|4bf92f3577b34da6a3ce929d0e0e4736.{activityInitializedByW3CHeader.GetSpanId()}.", requestTelemetry.Id); + Assert.Equal("4bf92f3577b34da6a3ce929d0e0e4736", requestTelemetry.Context.Operation.Id); + Assert.Equal("|4bf92f3577b34da6a3ce929d0e0e4736.00f067aa0ba902b7.", requestTelemetry.Context.Operation.ParentId); + + Assert.Equal("state=some", requestTelemetry.Properties[W3CConstants.TracestateTag]); + } + + private void TestRequestTrackingWithW3CSupportEnabledAndNoW3CHeaders(bool startActivity, bool addRequestId) + { + var headers = new Dictionary(); + + if (addRequestId) + { + headers.Add("Request-Id", "|abc.1.2.3."); + } + + var context = HttpModuleHelper.GetFakeHttpContext(headers); + + var module = this.RequestTrackingTelemetryModuleFactory(this.CreateDefaultConfig(context), enableW3CTracing: true); + + if (startActivity) + { + var activity = new Activity("operation"); + activity.Start(); + } + + module.OnBeginRequest(context); + var activityInitializedByW3CHeader = Activity.Current; + + Assert.Equal(32, activityInitializedByW3CHeader.GetTraceId().Length); + Assert.Equal(16, activityInitializedByW3CHeader.GetSpanId().Length); + Assert.Null(activityInitializedByW3CHeader.GetParentSpanId()); + + Assert.Null(activityInitializedByW3CHeader.GetTracestate()); + Assert.False(activityInitializedByW3CHeader.Baggage.Any()); + + var requestTelemetry = context.GetRequestTelemetry(); + module.OnEndRequest(context); + + Assert.Equal($"|{activityInitializedByW3CHeader.GetTraceId()}.{activityInitializedByW3CHeader.GetSpanId()}.", requestTelemetry.Id); + Assert.Equal(activityInitializedByW3CHeader.GetTraceId(), requestTelemetry.Context.Operation.Id); + + if (addRequestId) + { + Assert.Equal("|abc.1.2.3.", requestTelemetry.Context.Operation.ParentId); + } + else + { + Assert.Null(requestTelemetry.Context.Operation.ParentId); + } + } } +#pragma warning restore 612, 618 } \ No newline at end of file diff --git a/Src/Web/Web.Net45.Tests/Web.Net45.Tests.csproj b/Src/Web/Web.Net45.Tests/Web.Net45.Tests.csproj index 78921b827..429ed37c7 100644 --- a/Src/Web/Web.Net45.Tests/Web.Net45.Tests.csproj +++ b/Src/Web/Web.Net45.Tests/Web.Net45.Tests.csproj @@ -31,6 +31,9 @@ ..\..\..\..\packages\Microsoft.ApplicationInsights.2.8.0-beta1\lib\net45\Microsoft.ApplicationInsights.dll + + ..\..\..\..\packages\Microsoft.AspNet.TelemetryCorrelation.1.0.3\lib\net45\Microsoft.AspNet.TelemetryCorrelation.dll + ..\..\..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll True diff --git a/Src/Web/Web.Net45.Tests/packages.config b/Src/Web/Web.Net45.Tests/packages.config index 780902417..aeb5f164d 100644 --- a/Src/Web/Web.Net45.Tests/packages.config +++ b/Src/Web/Web.Net45.Tests/packages.config @@ -4,6 +4,7 @@ + diff --git a/Src/Web/Web.Net45/AspNetDiagnosticTelemetryModule.cs b/Src/Web/Web.Net45/AspNetDiagnosticTelemetryModule.cs index be0475990..e66b83d44 100644 --- a/Src/Web/Web.Net45/AspNetDiagnosticTelemetryModule.cs +++ b/Src/Web/Web.Net45/AspNetDiagnosticTelemetryModule.cs @@ -5,12 +5,13 @@ using System.Diagnostics; using System.Web; using Microsoft.ApplicationInsights.Common; - using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation; using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing; + using Microsoft.ApplicationInsights.W3C; using Microsoft.ApplicationInsights.Web.Implementation; +#pragma warning disable 612, 618 /// /// Listens to ASP.NET DiagnosticSource and enables instrumentation with Activity: let ASP.NET create root Activity for the request. /// @@ -142,30 +143,36 @@ public AspNetEventObserver(RequestTrackingTelemetryModule requestModule, Excepti { var context = HttpContext.Current; var request = context.Request; - string rootId = null; - if (ActivityHelpers.RootOperationIdHeaderName != null) - { - rootId = request.UnvalidatedGetHeader(ActivityHelpers.RootOperationIdHeaderName); - } - if (!string.IsNullOrEmpty(rootId)) + if (ActivityHelpers.IsW3CTracingEnabled) { - // Got legacy headers from older AppInsights version or some custom header. - // Let's set activity ParentId with custom root id - activity.SetParentId(rootId); + ActivityHelpers.ExtractW3CContext(request, activity); } - else + + if (activity.ParentId == null) { + string rootId = null; + if (ActivityHelpers.RootOperationIdHeaderName != null) + { + rootId = request.UnvalidatedGetHeader(ActivityHelpers.RootOperationIdHeaderName); + } + + string traceId = ActivityHelpers.IsW3CTracingEnabled + ? activity.GetTraceId() + : StringUtilities.GenerateTraceId(); + // As a first step in supporting W3C protocol in ApplicationInsights, // we want to generate Activity Ids in the W3C compatible format. // While .NET changes to Activity are pending, we want to ensure trace starts with W3C compatible Id // as early as possible, so that everyone has a chance to upgrade and have compatibility with W3C systems once they arrive. // So if there is no current Activity (i.e. there were no Request-Id header in the incoming request), we'll override ParentId on // the current Activity by the properly formatted one. This workaround should go away - // with W3C support on .NET https://github.com/dotnet/corefx/issues/30331 (TODO) - activity.SetParentId(StringUtilities.GenerateTraceId()); - - // end of workaround + // with W3C support on .NET https://github.com/dotnet/corefx/issues/30331 + // So, if there were no headers we generate W3C compatible Id, + // otherwise use legacy/custom headers that were provided + activity.SetParentId(!string.IsNullOrEmpty(rootId) + ? rootId // legacy or custom headers + : traceId); } } } @@ -240,4 +247,5 @@ private bool IsFirstRequest(HttpContext context) } } } +#pragma warning restore 612, 618 } diff --git a/Src/Web/Web.Shared.Net.Tests/RequestTrackingTelemetryModuleTest.cs b/Src/Web/Web.Shared.Net.Tests/RequestTrackingTelemetryModuleTest.cs index d69ca11ba..0fed3f2f5 100644 --- a/Src/Web/Web.Shared.Net.Tests/RequestTrackingTelemetryModuleTest.cs +++ b/Src/Web/Web.Shared.Net.Tests/RequestTrackingTelemetryModuleTest.cs @@ -484,11 +484,12 @@ private string GetActivityRootId(string telemetryId) return telemetryId.Substring(1, telemetryId.IndexOf('.') - 1); } - private RequestTrackingTelemetryModule RequestTrackingTelemetryModuleFactory(TelemetryConfiguration config = null) + private RequestTrackingTelemetryModule RequestTrackingTelemetryModuleFactory(TelemetryConfiguration config = null, bool enableW3CTracing = false) { var module = new RequestTrackingTelemetryModule() { - EnableChildRequestTrackingSuppression = false + EnableChildRequestTrackingSuppression = false, + EnableW3CHeadersExtraction = enableW3CTracing }; module.Initialize(config ?? this.CreateDefaultConfig(HttpModuleHelper.GetFakeHttpContext())); diff --git a/Src/Web/Web.Shared.Net/ApplicationInsightsHttpModule.cs b/Src/Web/Web.Shared.Net/ApplicationInsightsHttpModule.cs index f3b5f8ed0..a94d039f2 100644 --- a/Src/Web/Web.Shared.Net/ApplicationInsightsHttpModule.cs +++ b/Src/Web/Web.Shared.Net/ApplicationInsightsHttpModule.cs @@ -127,7 +127,7 @@ private void OnBeginRequest(object sender, EventArgs eventArgs) { if (this.requestModule.SetComponentCorrelationHttpHeaders) { - this.AddCorreleationHeaderOnSendRequestHeaders(httpApplication); + this.AddCorrelationHeaderOnSendRequestHeaders(httpApplication); } } } @@ -137,7 +137,7 @@ private void OnBeginRequest(object sender, EventArgs eventArgs) /// When sending the response headers, allow request module to add the IKey's target hash. /// /// HttpApplication instance. - private void AddCorreleationHeaderOnSendRequestHeaders(HttpApplication httpApplication) + private void AddCorrelationHeaderOnSendRequestHeaders(HttpApplication httpApplication) { try { diff --git a/Src/Web/Web.Shared.Net/Implementation/ActivityHelpers.cs b/Src/Web/Web.Shared.Net/Implementation/ActivityHelpers.cs index 24b241641..9a733d0fe 100644 --- a/Src/Web/Web.Shared.Net/Implementation/ActivityHelpers.cs +++ b/Src/Web/Web.Shared.Net/Implementation/ActivityHelpers.cs @@ -1,8 +1,16 @@ namespace Microsoft.ApplicationInsights.Common { + using System; + using System.Diagnostics; + using System.Linq; + using System.Text; using System.Web; + + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.W3C; using Microsoft.ApplicationInsights.Web.Implementation; +#pragma warning disable 612, 618 internal class ActivityHelpers { internal const string RequestActivityItemName = "Microsoft.ApplicationInsights.Web.Activity"; @@ -11,6 +19,8 @@ internal class ActivityHelpers internal static string ParentOperationIdHeaderName { get; set; } + internal static bool IsW3CTracingEnabled { get; set; } = false; + /// /// Checks if given RequestId is hierarchical. /// @@ -42,5 +52,91 @@ internal static bool TryParseCustomHeaders(HttpRequest request, out string rootI return rootId != null || parentId != null; } + + internal static void ExtractW3CContext(HttpRequest request, Activity activity) + { + var traceParent = request.UnvalidatedGetHeader(W3CConstants.TraceParentHeader); + if (traceParent != null) + { + var traceParentStr = StringUtilities.EnforceMaxLength(traceParent, InjectionGuardConstants.TraceParentHeaderMaxLength); + activity.SetTraceparent(traceParentStr); + + if (activity.ParentId == null) + { + activity.SetParentId(activity.GetTraceId()); + } + } + else + { + activity.GenerateW3CContext(); + } + + if (!activity.Baggage.Any()) + { + var baggage = request.Headers.GetNameValueCollectionFromHeader(RequestResponseHeaders.CorrelationContextHeader); + + if (baggage != null && baggage.Any()) + { + foreach (var item in baggage) + { + var itemName = StringUtilities.EnforceMaxLength(item.Key, InjectionGuardConstants.ContextHeaderKeyMaxLength); + var itemValue = StringUtilities.EnforceMaxLength(item.Value, InjectionGuardConstants.ContextHeaderValueMaxLength); + activity.AddBaggage(itemName, itemValue); + } + } + } + } + + internal static void ExtractTracestate(HttpRequest request, Activity activity, RequestTelemetry requestTelemetry) + { + var tracestate = request.UnvalidatedGetHeaders().GetHeaderValue( + W3CConstants.TraceStateHeader, + InjectionGuardConstants.TraceStateHeaderMaxLength, + InjectionGuardConstants.TraceStateMaxPairs)?.ToList(); + if (tracestate != null && tracestate.Any()) + { + // it's likely there are a few and string builder is not beneficial in this case + var pairsExceptAz = new StringBuilder(); + for (int i = 0; i < tracestate.Count; i++) + { + if (tracestate[i].StartsWith(W3CConstants.AzureTracestateNamespace + "=", StringComparison.Ordinal)) + { + // start after 'az=' + if (TryExtractAppIdFromAzureTracestate(tracestate[i].Substring(3), out var appId)) + { + requestTelemetry.Source = appId; + } + } + else + { + pairsExceptAz.Append(tracestate[i]).Append(','); + } + } + + if (pairsExceptAz.Length > 0) + { + // remove last comma + var tracestateStr = pairsExceptAz.ToString(0, pairsExceptAz.Length - 1); + activity.SetTracestate(StringUtilities.EnforceMaxLength(tracestateStr, InjectionGuardConstants.TraceStateHeaderMaxLength)); + } + } + } + + private static bool TryExtractAppIdFromAzureTracestate(string azTracestate, out string appId) + { + appId = null; + var parts = azTracestate.Split(W3CConstants.TracestateAzureSeparator); + + var appIds = parts.Where(p => p.StartsWith(W3CConstants.ApplicationIdTraceStateField, StringComparison.Ordinal)).ToArray(); + + if (appIds.Length != 1) + { + return false; + } + + appId = appIds[0]; + return true; + } } +#pragma warning restore 612, 618 } \ No newline at end of file diff --git a/Src/Web/Web.Shared.Net/Implementation/RequestTrackingExtensions.cs b/Src/Web/Web.Shared.Net/Implementation/RequestTrackingExtensions.cs index 5bc883b1c..efb6e6f2f 100644 --- a/Src/Web/Web.Shared.Net/Implementation/RequestTrackingExtensions.cs +++ b/Src/Web/Web.Shared.Net/Implementation/RequestTrackingExtensions.cs @@ -6,8 +6,10 @@ using System.Web; using Microsoft.ApplicationInsights.Common; using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.W3C; using Microsoft.AspNet.TelemetryCorrelation; +#pragma warning disable 612, 618 internal static class RequestTrackingExtensions { internal static RequestTelemetry CreateRequestTelemetryPrivate( @@ -22,17 +24,29 @@ internal static RequestTelemetry CreateRequestTelemetryPrivate( var currentActivity = Activity.Current; var requestContext = result.Context.Operation; - if (currentActivity == null) + if (currentActivity == null) { // if there was no BeginRequest, ASP.NET HttpModule did not have a chance to set current activity (and will never do it). currentActivity = new Activity(ActivityHelpers.RequestActivityItemName); - if (currentActivity.Extract(platformContext.Request.Headers)) + + if (ActivityHelpers.IsW3CTracingEnabled) + { + ActivityHelpers.ExtractW3CContext(platformContext.Request, currentActivity); + ActivityHelpers.ExtractTracestate(platformContext.Request, currentActivity, result); + // length enforced in SetW3CContext + currentActivity.SetParentId(currentActivity.GetTraceId()); + W3COperationCorrelationTelemetryInitializer.UpdateTelemetry(result, currentActivity, true); + + SetLegacyContextIds(platformContext.Request, currentActivity, result); + } + else if (currentActivity.Extract(platformContext.Request.Headers)) { requestContext.ParentId = currentActivity.ParentId; } - else + else if (ActivityHelpers.TryParseCustomHeaders(platformContext.Request, out var rootId, out var parentId)) { - if (ActivityHelpers.TryParseCustomHeaders(platformContext.Request, out var rootId, out var parentId)) + currentActivity.SetParentId(rootId); + if (!string.IsNullOrEmpty(parentId)) { currentActivity.SetParentId(rootId); if (!string.IsNullOrEmpty(parentId)) @@ -40,53 +54,65 @@ internal static RequestTelemetry CreateRequestTelemetryPrivate( requestContext.ParentId = parentId; } } - else - { - // As a first step in supporting W3C protocol in ApplicationInsights, - // we want to generate Activity Ids in the W3C compatible format. - // While .NET changes to Activity are pending, we want to ensure trace starts with W3C compatible Id - // as early as possible, so that everyone has a chance to upgrade and have compatibility with W3C systems once they arrive. - // So if there is no current Activity (i.e. there were no Request-Id header in the incoming request), we'll override ParentId on - // the current Activity by the properly formatted one. This workaround should go away - // with W3C support on .NET https://github.com/dotnet/corefx/issues/30331 (TODO) - currentActivity.SetParentId(StringUtilities.GenerateTraceId()); - - // end of workaround - } + } + else + { + // As a first step in supporting W3C protocol in ApplicationInsights, + // we want to generate Activity Ids in the W3C compatible format. + // While .NET changes to Activity are pending, we want to ensure trace starts with W3C compatible Id + // as early as possible, so that everyone has a chance to upgrade and have compatibility with W3C systems once they arrive. + // So if there is no current Activity (i.e. there were no Request-Id header in the incoming request), we'll override ParentId on + // the current Activity by the properly formatted one. This workaround should go away + // with W3C support on .NET https://github.com/dotnet/corefx/issues/30331 + currentActivity.SetParentId(StringUtilities.GenerateTraceId()); + // end of workaround } currentActivity.Start(); } else { - if (ActivityHelpers.IsHierarchicalRequestId(currentActivity.ParentId)) + // currentActivity != null + if (ActivityHelpers.IsW3CTracingEnabled) + { + if (!currentActivity.IsW3CActivity()) + { + ActivityHelpers.ExtractW3CContext(platformContext.Request, currentActivity); + } + + ActivityHelpers.ExtractTracestate(platformContext.Request, currentActivity, result); + + W3COperationCorrelationTelemetryInitializer.UpdateTelemetry(result, currentActivity, true); + SetLegacyContextIds(platformContext.Request, currentActivity, result); + } + else if (ActivityHelpers.IsHierarchicalRequestId(currentActivity.ParentId)) { requestContext.ParentId = currentActivity.ParentId; } - else + else if (ActivityHelpers.ParentOperationIdHeaderName != null) { - if (ActivityHelpers.ParentOperationIdHeaderName != null) + var parentId = platformContext.Request.UnvalidatedGetHeader(ActivityHelpers.ParentOperationIdHeaderName); + if (!string.IsNullOrEmpty(parentId)) { - var parentId = platformContext.Request.UnvalidatedGetHeader(ActivityHelpers.ParentOperationIdHeaderName); - if (!string.IsNullOrEmpty(parentId)) - { - requestContext.ParentId = parentId; - } + requestContext.ParentId = parentId; } } } - // we have Activity.Current, we need to properly initialize request telemetry and store it in HttpContext - if (string.IsNullOrEmpty(requestContext.Id)) + if (!ActivityHelpers.IsW3CTracingEnabled) { - requestContext.Id = currentActivity.RootId; - foreach (var item in currentActivity.Baggage) - { - result.Properties[item.Key] = item.Value; + // we have Activity.Current, we need to properly initialize request telemetry and store it in HttpContext + if (string.IsNullOrEmpty(requestContext.Id)) + { + requestContext.Id = currentActivity.RootId; + foreach (var item in currentActivity.Baggage) + { + result.Properties[item.Key] = item.Value; + } } - } - result.Id = currentActivity.Id; + result.Id = currentActivity.Id; + } // save current activity in case it will be lost - we will use it in Web.OperationCorrelationTelemetryIntitalizer platformContext.Items[ActivityHelpers.RequestActivityItemName] = currentActivity; @@ -170,5 +196,26 @@ internal static string CreateRequestNamePrivate(this HttpContext platformContext return name; } + + private static void SetLegacyContextIds(HttpRequest request, Activity activity, RequestTelemetry requestTelemetry) + { + if (request.UnvalidatedGetHeader(W3CConstants.TraceParentHeader) != null) + { + return; + } + + var requestId = request.UnvalidatedGetHeader(RequestResponseHeaders.RequestIdHeader); + if (requestId != null) + { + var parentId = StringUtilities.EnforceMaxLength(requestId, InjectionGuardConstants.RequestHeaderMaxLength); + requestTelemetry.Context.Operation.ParentId = parentId; + } + else if (ActivityHelpers.TryParseCustomHeaders(request, out var _, out var parentId)) + { + parentId = StringUtilities.EnforceMaxLength(parentId, InjectionGuardConstants.RequestHeaderMaxLength); + requestTelemetry.Context.Operation.ParentId = parentId; + } + } } +#pragma warning restore 612, 618 } \ No newline at end of file diff --git a/Src/Web/Web.Shared.Net/RequestTrackingTelemetryModule.cs b/Src/Web/Web.Shared.Net/RequestTrackingTelemetryModule.cs index 1a0f6b64d..93e676bd1 100644 --- a/Src/Web/Web.Shared.Net/RequestTrackingTelemetryModule.cs +++ b/Src/Web/Web.Shared.Net/RequestTrackingTelemetryModule.cs @@ -12,8 +12,9 @@ using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation; + using Microsoft.ApplicationInsights.W3C; using Microsoft.ApplicationInsights.Web.Implementation; - + /// /// Telemetry module tracking requests using http module. /// @@ -66,6 +67,11 @@ public class RequestTrackingTelemetryModule : ITelemetryModule /// public bool SetComponentCorrelationHttpHeaders { get; set; } = true; + /// + /// Gets or sets a value indicating whether to enable W3C distributed tracing headers support. + /// + public bool EnableW3CHeadersExtraction { get; set; } = false; + /// /// Gets or sets the endpoint that is to be used to get the application insights resource's profile (appId etc.). /// @@ -173,7 +179,9 @@ public void OnEndRequest(HttpContext context) try { - sourceAppId = context.Request.UnvalidatedGetHeaders().GetNameValueHeaderValue(RequestResponseHeaders.RequestContextHeader, RequestResponseHeaders.RequestContextCorrelationSourceKey); + sourceAppId = context.Request.UnvalidatedGetHeaders().GetNameValueHeaderValue( + RequestResponseHeaders.RequestContextHeader, + RequestResponseHeaders.RequestContextCorrelationSourceKey); } catch (Exception ex) { @@ -238,7 +246,9 @@ public void AddTargetHashForResponseHeader(HttpContext context) try { if (!string.IsNullOrEmpty(requestTelemetry.Context.InstrumentationKey) - && context.Response.Headers.GetNameValueHeaderValue(RequestResponseHeaders.RequestContextHeader, RequestResponseHeaders.RequestContextCorrelationTargetKey) == null) + && context.Response.Headers.GetNameValueHeaderValue( + RequestResponseHeaders.RequestContextHeader, + RequestResponseHeaders.RequestContextCorrelationTargetKey) == null) { string applicationId = null; if (this.telemetryConfiguration.ApplicationIdProvider?.TryGetApplicationId(requestTelemetry.Context.InstrumentationKey, out applicationId) ?? false) @@ -276,6 +286,8 @@ public void Initialize(TelemetryConfiguration configuration) { this.childRequestTrackingSuppressionModule = new ChildRequestTrackingSuppressionModule(maxRequestsTracked: this.ChildRequestTrackingInternalDictionarySize); } + + ActivityHelpers.IsW3CTracingEnabled = this.EnableW3CHeadersExtraction; } ///