diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a01f8d6..76d8e702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## Version 2.5.0-beta1 +- [Adds opt-in support for W3C distributed tracing standard](https://github.com/Microsoft/ApplicationInsights-aspnetcore/pull/735) + ## Version 2.4.1 - Patch release to update Web/Base SDK version dependency to 2.7.2 which fixed a bug (https://github.com/Microsoft/ApplicationInsights-dotnet-server/issues/970) diff --git a/src/Microsoft.ApplicationInsights.AspNetCore/Common/InjectionGuardConstants.cs b/src/Microsoft.ApplicationInsights.AspNetCore/Common/InjectionGuardConstants.cs deleted file mode 100644 index e00e14da..00000000 --- a/src/Microsoft.ApplicationInsights.AspNetCore/Common/InjectionGuardConstants.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Microsoft.ApplicationInsights.AspNetCore.Common -{ - /// - /// These values are listed to guard against malicious injections by limiting the max size allowed in an HTTP Response. - /// 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 - { - /// - /// Max length of AppId allowed in response from Breeze. - /// - public const int AppIdMaxLengeth = 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; - } -} \ No newline at end of file diff --git a/src/Microsoft.ApplicationInsights.AspNetCore/Common/StringUtilities.cs b/src/Microsoft.ApplicationInsights.AspNetCore/Common/StringUtilities.cs deleted file mode 100644 index 1bda44ea..00000000 --- a/src/Microsoft.ApplicationInsights.AspNetCore/Common/StringUtilities.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Globalization; - -namespace Microsoft.ApplicationInsights.AspNetCore.Common -{ - using System.Diagnostics; - - /// - /// Generic functions to perform common operations on a string. - /// - public static class StringUtilities - { - private static readonly uint[] Lookup32 = CreateLookup32(); - - /// - /// Check a strings length and trim to a max length if needed. - /// - public static string EnforceMaxLength(string input, int maxLength) - { - // TODO: remove/obsolete and use StringUtilities from Web SDK - Debug.Assert(input != null, $"{nameof(input)} must not be null"); - Debug.Assert(maxLength > 0, $"{nameof(maxLength)} must be greater than 0"); - - if (input != null && input.Length > maxLength) - { - input = input.Substring(0, maxLength); - } - - return input; - } - - /// - /// Generates random trace Id as per W3C Distributed tracing specification. - /// https://github.com/w3c/distributed-tracing/blob/master/trace_context/HTTP_HEADER_FORMAT.md#trace-id - /// - /// Random 16 bytes array encoded as hex string - internal 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(); - - var result = new char[32]; - for (int i = 0; i < 16; i++) - { - var val = Lookup32[bytes[i]]; - result[2 * i] = (char)val; - result[(2 * i) + 1] = (char)(val >> 16); - } - - return new string(result); - } - - private static uint[] CreateLookup32() - { - // 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 uint[256]; - for (int i = 0; i < 256; i++) - { - string s = i.ToString("x2", CultureInfo.InvariantCulture); - result[i] = ((uint)s[0]) + ((uint)s[1] << 16); - } - - return result; - } - } -} diff --git a/src/Microsoft.ApplicationInsights.AspNetCore/DiagnosticListeners/Implementation/HeadersUtilities.cs b/src/Microsoft.ApplicationInsights.AspNetCore/DiagnosticListeners/Implementation/HeadersUtilities.cs index ce688cd9..a10865af 100644 --- a/src/Microsoft.ApplicationInsights.AspNetCore/DiagnosticListeners/Implementation/HeadersUtilities.cs +++ b/src/Microsoft.ApplicationInsights.AspNetCore/DiagnosticListeners/Implementation/HeadersUtilities.cs @@ -1,10 +1,10 @@ namespace Microsoft.ApplicationInsights.AspNetCore.DiagnosticListeners { - using Microsoft.ApplicationInsights.AspNetCore.Common; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; + using Microsoft.ApplicationInsights.Common; /// /// Generic functions that can be used to get and set Http headers. diff --git a/src/Microsoft.ApplicationInsights.AspNetCore/DiagnosticListeners/Implementation/HostingDiagnosticListener.cs b/src/Microsoft.ApplicationInsights.AspNetCore/DiagnosticListeners/Implementation/HostingDiagnosticListener.cs index d19c5e3c..82a585ca 100644 --- a/src/Microsoft.ApplicationInsights.AspNetCore/DiagnosticListeners/Implementation/HostingDiagnosticListener.cs +++ b/src/Microsoft.ApplicationInsights.AspNetCore/DiagnosticListeners/Implementation/HostingDiagnosticListener.cs @@ -3,19 +3,23 @@ using System; using System.Diagnostics; using System.Globalization; + using System.Linq; using System.Net.Http.Headers; using System.Reflection; + using System.Text; using Extensibility.Implementation.Tracing; - using Microsoft.ApplicationInsights.AspNetCore.Common; using Microsoft.ApplicationInsights.AspNetCore.Extensions; + using Microsoft.ApplicationInsights.Common; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation; + using Microsoft.ApplicationInsights.W3C; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DiagnosticAdapter; using Microsoft.Extensions.Primitives; +#pragma warning disable 612, 618 /// /// implementation that listens for events specific to AspNetCore hosting layer. /// @@ -33,6 +37,8 @@ internal class HostingDiagnosticListener : IApplicationInsightDiagnosticListener private readonly string sdkVersion = SdkVersionUtils.GetVersion(); private readonly bool injectResponseHeaders; private readonly bool trackExceptions; + private readonly bool enableW3CHeaders; + private const string ActivityCreatedByHostingDiagnosticListener = "ActivityCreatedByHostingDiagnosticListener"; /// @@ -42,12 +48,14 @@ internal class HostingDiagnosticListener : IApplicationInsightDiagnosticListener /// Provider for resolving application Id to be used in multiple instruemntation keys scenarios. /// Flag that indicates that response headers should be injected. /// Flag that indicates that exceptions should be tracked. - public HostingDiagnosticListener(TelemetryClient client, IApplicationIdProvider applicationIdProvider, bool injectResponseHeaders, bool trackExceptions) + /// Flag that indicates that W3C header parsing should be enabled. + public HostingDiagnosticListener(TelemetryClient client, IApplicationIdProvider applicationIdProvider, bool injectResponseHeaders, bool trackExceptions, bool enableW3CHeaders) { this.client = client ?? throw new ArgumentNullException(nameof(client)); this.applicationIdProvider = applicationIdProvider; this.injectResponseHeaders = injectResponseHeaders; this.trackExceptions = trackExceptions; + this.enableW3CHeaders = enableW3CHeaders; } /// @@ -77,23 +85,43 @@ public void OnHttpRequestInStart(HttpContext httpContext) } var currentActivity = Activity.Current; - var isActivityCreatedFromRequestIdHeader = false; + var isActivityCreatedFromRequestIdHeader = true; + string sourceAppId = null; + string originalParentId = currentActivity.ParentId; - if (currentActivity.ParentId != null) + Activity newActivity = null; + // W3C + if (this.enableW3CHeaders) { - isActivityCreatedFromRequestIdHeader = true; + isActivityCreatedFromRequestIdHeader = false; + SetW3CContext(httpContext.Request.Headers, currentActivity, out sourceAppId); + + var parentSpanId = currentActivity.GetParentSpanId(); + if (parentSpanId != null) + { + originalParentId = $"|{currentActivity.GetTraceId()}.{parentSpanId}."; + } } - else if (httpContext.Request.Headers.TryGetValue(RequestResponseHeaders.StandardRootIdHeader, out var xmsRequestRootId)) + + // x-ms-* + if (originalParentId == null && + httpContext.Request.Headers.TryGetValue(RequestResponseHeaders.StandardRootIdHeader, out StringValues alternativeRootIdValues) && + alternativeRootIdValues != StringValues.Empty) { - xmsRequestRootId = StringUtilities.EnforceMaxLength(xmsRequestRootId, InjectionGuardConstants.RequestHeaderMaxLength); - var activity = new Activity(ActivityCreatedByHostingDiagnosticListener); - activity.SetParentId(xmsRequestRootId); - activity.Start(); - httpContext.Features.Set(activity); + isActivityCreatedFromRequestIdHeader = false; + newActivity = new Activity(ActivityCreatedByHostingDiagnosticListener) + .SetParentId(StringUtilities.EnforceMaxLength(alternativeRootIdValues.First(), + InjectionGuardConstants.RequestHeaderMaxLength)); - currentActivity = activity; + if (httpContext.Request.Headers.TryGetValue(RequestResponseHeaders.StandardParentIdHeader, out StringValues parentId)) + { + originalParentId = StringUtilities.EnforceMaxLength(parentId.First(), + InjectionGuardConstants.RequestHeaderMaxLength); + } } - else + + // no headers + else if (originalParentId == null) { // As a first step in supporting W3C protocol in ApplicationInsights, // we want to generate Activity Ids in the W3C compatible format. @@ -103,17 +131,24 @@ public void OnHttpRequestInStart(HttpContext httpContext) // 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 - var activity = new Activity(ActivityCreatedByHostingDiagnosticListener); - - activity.SetParentId(StringUtilities.GenerateTraceId()); - activity.Start(); - httpContext.Features.Set(activity); - currentActivity = activity; - + newActivity = new Activity(ActivityCreatedByHostingDiagnosticListener); + newActivity.SetParentId(StringUtilities.GenerateTraceId()); // end of workaround } + if (newActivity != null) + { + newActivity.Start(); + currentActivity = newActivity; + } + var requestTelemetry = InitializeRequestTelemetry(httpContext, currentActivity, isActivityCreatedFromRequestIdHeader, Stopwatch.GetTimestamp()); + if (this.enableW3CHeaders && sourceAppId != null) + { + requestTelemetry.Source = sourceAppId; + } + + requestTelemetry.Context.Operation.ParentId = originalParentId; SetAppIdInResponseHeader(httpContext, requestTelemetry); } } @@ -138,36 +173,54 @@ public void OnBeginRequest(HttpContext httpContext, long timestamp) var activity = new Activity(ActivityCreatedByHostingDiagnosticListener); var isActivityCreatedFromRequestIdHeader = false; - StringValues requestId; - StringValues standardRootId; + string sourceAppId = null; + IHeaderDictionary requestHeaders = httpContext.Request.Headers; - if (requestHeaders.TryGetValue(RequestResponseHeaders.RequestIdHeader, out requestId)) + + string originalParentId = null; + // W3C + if (this.enableW3CHeaders) + { + SetW3CContext(httpContext.Request.Headers, activity, out sourceAppId); + var parentSpanId = activity.GetParentSpanId(); + if (parentSpanId != null) + { + originalParentId = $"|{activity.GetTraceId()}.{parentSpanId}."; + } + + // length enforced in SetW3CContext + } + + // Request-Id + if (requestHeaders.TryGetValue(RequestResponseHeaders.RequestIdHeader, out StringValues requestIdValues) && + requestIdValues != StringValues.Empty) { - requestId = StringUtilities.EnforceMaxLength(requestId, InjectionGuardConstants.RequestHeaderMaxLength); + var requestId = StringUtilities.EnforceMaxLength(requestIdValues.First(), InjectionGuardConstants.RequestHeaderMaxLength); isActivityCreatedFromRequestIdHeader = true; activity.SetParentId(requestId); - string[] baggage = requestHeaders.GetCommaSeparatedValues(RequestResponseHeaders.CorrelationContextHeader); - if (baggage != StringValues.Empty) + ReadCorrelationContext(requestHeaders, activity); + + if (originalParentId == null) { - foreach (var item in baggage) - { - NameValueHeaderValue baggageItem; - if (NameValueHeaderValue.TryParse(item, out baggageItem)) - { - var itemName = StringUtilities.EnforceMaxLength(baggageItem.Name, InjectionGuardConstants.ContextHeaderKeyMaxLength); - var itemValue = StringUtilities.EnforceMaxLength(baggageItem.Value, InjectionGuardConstants.ContextHeaderValueMaxLength); - activity.AddBaggage(baggageItem.Name, baggageItem.Value); - } - } + originalParentId = requestId; } } - else if (requestHeaders.TryGetValue(RequestResponseHeaders.StandardRootIdHeader, out standardRootId)) + + // x-ms-request-id + else if (requestHeaders.TryGetValue(RequestResponseHeaders.StandardRootIdHeader, out StringValues alternativeRootIdValues) && + alternativeRootIdValues != StringValues.Empty) { - standardRootId = StringUtilities.EnforceMaxLength(standardRootId, InjectionGuardConstants.RequestHeaderMaxLength); - activity.SetParentId(standardRootId); + string alternativeRootId = StringUtilities.EnforceMaxLength(alternativeRootIdValues.First(), InjectionGuardConstants.RequestHeaderMaxLength); + activity.SetParentId(alternativeRootId); + + if (originalParentId == null && requestHeaders.TryGetValue(RequestResponseHeaders.StandardParentIdHeader, out StringValues parentId)) + { + originalParentId = StringUtilities.EnforceMaxLength(parentId.First(), + InjectionGuardConstants.RequestHeaderMaxLength); + } } - else + else if(!activity.IsW3CActivity()) { // As a first step in supporting W3C protocol in ApplicationInsights, // we want to generate Activity Ids in the W3C compatible format. @@ -176,16 +229,22 @@ public void OnBeginRequest(HttpContext httpContext, long timestamp) // 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 - + activity.SetParentId(StringUtilities.GenerateTraceId()); // end of workaround } activity.Start(); - httpContext.Features.Set(activity); var requestTelemetry = InitializeRequestTelemetry(httpContext, activity, isActivityCreatedFromRequestIdHeader, timestamp); + if (this.enableW3CHeaders && sourceAppId != null) + { + requestTelemetry.Source = sourceAppId; + } + + // fix parent that may be modified by non-W3C operation correlation + requestTelemetry.Context.Operation.ParentId = originalParentId; SetAppIdInResponseHeader(httpContext, requestTelemetry); } } @@ -240,54 +299,53 @@ private RequestTelemetry InitializeRequestTelemetry(HttpContext httpContext, Act { var requestTelemetry = new RequestTelemetry(); - StringValues standardParentId; - if (isActivityCreatedFromRequestIdHeader) + if (!this.enableW3CHeaders) { - requestTelemetry.Context.Operation.ParentId = activity.ParentId; + requestTelemetry.Context.Operation.Id = activity.RootId; + requestTelemetry.Id = activity.Id; + } - foreach (var prop in activity.Baggage) + foreach (var prop in activity.Baggage) + { + if (!requestTelemetry.Properties.ContainsKey(prop.Key)) { - if (!requestTelemetry.Context.Properties.ContainsKey(prop.Key)) - { - requestTelemetry.Context.Properties[prop.Key] = prop.Value; - } + requestTelemetry.Properties[prop.Key] = prop.Value; } } - else if (httpContext.Request.Headers.TryGetValue(RequestResponseHeaders.StandardParentIdHeader, out standardParentId)) - { - standardParentId = StringUtilities.EnforceMaxLength(standardParentId, InjectionGuardConstants.RequestHeaderMaxLength); - requestTelemetry.Context.Operation.ParentId = standardParentId; - } - - requestTelemetry.Id = activity.Id; - requestTelemetry.Context.Operation.Id = activity.RootId; this.client.Initialize(requestTelemetry); + requestTelemetry.Source = GetAppIdFromRequestHeader(httpContext.Request.Headers, requestTelemetry.Context.InstrumentationKey); + + requestTelemetry.Start(timestamp); + httpContext.Features.Set(requestTelemetry); + + return requestTelemetry; + } + + private string GetAppIdFromRequestHeader(IHeaderDictionary requestHeaders, string instrumentationKey) + { // set Source - string headerCorrelationId = HttpHeadersUtilities.GetRequestContextKeyValue(httpContext.Request.Headers, RequestResponseHeaders.RequestContextSourceKey); + string headerCorrelationId = HttpHeadersUtilities.GetRequestContextKeyValue(requestHeaders, RequestResponseHeaders.RequestContextSourceKey); - string applicationId = null; // If the source header is present on the incoming request, and it is an external component (not the same ikey as the one used by the current component), populate the source field. if (!string.IsNullOrEmpty(headerCorrelationId)) { - headerCorrelationId = StringUtilities.EnforceMaxLength(headerCorrelationId, InjectionGuardConstants.AppIdMaxLengeth); - if (string.IsNullOrEmpty(requestTelemetry.Context.InstrumentationKey)) + headerCorrelationId = StringUtilities.EnforceMaxLength(headerCorrelationId, InjectionGuardConstants.AppIdMaxLength); + if (string.IsNullOrEmpty(instrumentationKey)) { - requestTelemetry.Source = headerCorrelationId; + return headerCorrelationId; } - else if ((this.applicationIdProvider?.TryGetApplicationId(requestTelemetry.Context.InstrumentationKey, out applicationId) ?? false) - && applicationId != headerCorrelationId) + string applicationId = null; + if ((this.applicationIdProvider?.TryGetApplicationId(instrumentationKey, out applicationId) ?? false) + && applicationId != headerCorrelationId) { - requestTelemetry.Source = headerCorrelationId; + return headerCorrelationId; } } - requestTelemetry.Start(timestamp); - httpContext.Features.Set(requestTelemetry); - - return requestTelemetry; + return null; } private void SetAppIdInResponseHeader(HttpContext httpContext, RequestTelemetry requestTelemetry) @@ -369,5 +427,83 @@ private void OnException(HttpContext httpContext, Exception exception) this.client.Track(exceptionTelemetry); } } + + private void SetW3CContext(IHeaderDictionary requestHeaders, Activity activity, out string sourceAppId) + { + sourceAppId = null; + if (requestHeaders.TryGetValue(W3CConstants.TraceParentHeader, out StringValues traceParentValues)) + { + var parentTraceParent = StringUtilities.EnforceMaxLength(traceParentValues.First(), + InjectionGuardConstants.TraceParentHeaderMaxLength); + activity.SetTraceparent(parentTraceParent); + } + else + { + activity.GenerateW3CContext(); + } + + string[] traceStateValues = HttpHeadersUtilities.SafeGetCommaSeparatedHeaderValues(requestHeaders, W3CConstants.TraceStateHeader, + InjectionGuardConstants.TraceStateHeaderMaxLength, InjectionGuardConstants.TraceStateMaxPairs); + + if (traceStateValues != null && traceStateValues.Any()) + { + var pairsExceptAz = new StringBuilder(); + foreach (var t in traceStateValues) + { + if (t.StartsWith(W3CConstants.AzureTracestateNamespace + "=", StringComparison.Ordinal)) + { + // start after 'az=' + TryExtractAppIdFromAzureTracestate(t.Substring(3), out sourceAppId); + } + else + { + pairsExceptAz.Append(t).Append(','); + } + } + + if (pairsExceptAz.Length > 0) + { + // remove last comma + var tracestateStr = pairsExceptAz.ToString(0, pairsExceptAz.Length - 1); + activity.SetTracestate(StringUtilities.EnforceMaxLength(tracestateStr, InjectionGuardConstants.TraceStateHeaderMaxLength)); + } + } + + ReadCorrelationContext(requestHeaders, activity); + } + + private void ReadCorrelationContext(IHeaderDictionary requestHeaders, Activity activity) + { + string[] baggage = requestHeaders.GetCommaSeparatedValues(RequestResponseHeaders.CorrelationContextHeader); + if (baggage != StringValues.Empty && !activity.Baggage.Any()) + { + foreach (var item in baggage) + { + if (NameValueHeaderValue.TryParse(item, out var baggageItem)) + { + var itemName = StringUtilities.EnforceMaxLength(baggageItem.Name, InjectionGuardConstants.ContextHeaderKeyMaxLength); + var itemValue = StringUtilities.EnforceMaxLength(baggageItem.Value, InjectionGuardConstants.ContextHeaderValueMaxLength); + activity.AddBaggage(itemName, itemValue); + } + } + } + } + + 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/Microsoft.ApplicationInsights.AspNetCore/DiagnosticListeners/Implementation/HttpHeadersUtilities.cs b/src/Microsoft.ApplicationInsights.AspNetCore/DiagnosticListeners/Implementation/HttpHeadersUtilities.cs index a276ca14..4a8bc3ea 100644 --- a/src/Microsoft.ApplicationInsights.AspNetCore/DiagnosticListeners/Implementation/HttpHeadersUtilities.cs +++ b/src/Microsoft.ApplicationInsights.AspNetCore/DiagnosticListeners/Implementation/HttpHeadersUtilities.cs @@ -96,5 +96,50 @@ internal static void SetHeaderKeyValue(IHeaderDictionary headers, string headerN headers[headerName] = new StringValues(HeadersUtilities.SetHeaderKeyValue(headers[headerName].AsEnumerable(), keyName, keyValue).ToArray()); } + + internal static string[] SafeGetCommaSeparatedHeaderValues(IHeaderDictionary headers, string headerName, int maxLength, int maxItems) + { + string[] traceStateValues = headers.GetCommaSeparatedValues(headerName); + + if (traceStateValues == null) + { + return null; + } + + int length = traceStateValues.Sum(p => p.Length) + traceStateValues.Length - 1; // all values and commas + if (length <= maxLength && traceStateValues.Length <= maxItems) + { + return traceStateValues; + } + + List truncated; + if (length > maxLength) + { + int currentLength = 0; + + truncated = traceStateValues.TakeWhile(kvp => + { + if (currentLength + kvp.Length > maxLength) + { + return false; + } + + currentLength += kvp.Length + 1; // pair and comma + return true; + }).ToList(); + } + else + { + truncated = traceStateValues.ToList(); + } + + // if there are more than maxItems - truncate the end + if (truncated.Count > maxItems) + { + return truncated.Take(maxItems).ToArray(); + } + + return truncated.ToArray(); + } } } diff --git a/src/Microsoft.ApplicationInsights.AspNetCore/Extensions/ApplicationInsightsExtensions.cs b/src/Microsoft.ApplicationInsights.AspNetCore/Extensions/ApplicationInsightsExtensions.cs index 3bb21c09..5764e199 100644 --- a/src/Microsoft.ApplicationInsights.AspNetCore/Extensions/ApplicationInsightsExtensions.cs +++ b/src/Microsoft.ApplicationInsights.AspNetCore/Extensions/ApplicationInsightsExtensions.cs @@ -162,7 +162,10 @@ public static IServiceCollection AddApplicationInsightsTelemetry(this IServiceCo var includedActivities = module.IncludeDiagnosticSourceActivities; includedActivities.Add("Microsoft.Azure.EventHubs"); includedActivities.Add("Microsoft.Azure.ServiceBus"); + + module.EnableW3CHeadersInjection = o.RequestCollectionOptions.EnableW3CDistributedTracing; }); + services.ConfigureTelemetryModule((module, options) => { module.CollectionOptions = options.RequestCollectionOptions; diff --git a/src/Microsoft.ApplicationInsights.AspNetCore/Extensions/RequestCollectionOptions.cs b/src/Microsoft.ApplicationInsights.AspNetCore/Extensions/RequestCollectionOptions.cs index d2637cb5..b6610f9d 100644 --- a/src/Microsoft.ApplicationInsights.AspNetCore/Extensions/RequestCollectionOptions.cs +++ b/src/Microsoft.ApplicationInsights.AspNetCore/Extensions/RequestCollectionOptions.cs @@ -9,6 +9,7 @@ public RequestCollectionOptions() { this.InjectResponseHeaders = true; this.TrackExceptions = true; + this.EnableW3CDistributedTracing = false; } /// @@ -20,5 +21,10 @@ public RequestCollectionOptions() /// Get or sets value indicating whether exceptions are be tracked. /// public bool TrackExceptions { get; set; } + + /// + /// Get or sets value indicating whether W3C distributed tracing standard is enabled. + /// + public bool EnableW3CDistributedTracing { get; set; } } } diff --git a/src/Microsoft.ApplicationInsights.AspNetCore/Implementation/TelemetryConfigurationOptionsSetup.cs b/src/Microsoft.ApplicationInsights.AspNetCore/Implementation/TelemetryConfigurationOptionsSetup.cs index a71cdca0..616e3bd0 100644 --- a/src/Microsoft.ApplicationInsights.AspNetCore/Implementation/TelemetryConfigurationOptionsSetup.cs +++ b/src/Microsoft.ApplicationInsights.AspNetCore/Implementation/TelemetryConfigurationOptionsSetup.cs @@ -13,6 +13,7 @@ namespace Microsoft.Extensions.DependencyInjection using Microsoft.Extensions.Options; using Microsoft.ApplicationInsights.Extensibility.Implementation; using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing; + using Microsoft.ApplicationInsights.W3C; /// /// Initializes TelemetryConfiguration based on values in @@ -88,7 +89,12 @@ public void Configure(TelemetryConfiguration configuration) this.AddQuickPulse(configuration); this.AddSampling(configuration); this.DisableHeartBeatIfConfigured(); - + + if (applicationInsightsServiceOptions.RequestCollectionOptions.EnableW3CDistributedTracing) + { + this.EnableW3CHeaders(configuration); + } + configuration.TelemetryProcessorChainBuilder.Build(); if (this.applicationInsightsServiceOptions.DeveloperMode != null) @@ -177,5 +183,12 @@ private void DisableHeartBeatIfConfigured() } } } + +#pragma warning disable 612, 618 + private void EnableW3CHeaders(TelemetryConfiguration configuration) + { + configuration.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); + } +#pragma warning restore 612, 618 } } \ No newline at end of file diff --git a/src/Microsoft.ApplicationInsights.AspNetCore/Microsoft.ApplicationInsights.AspNetCore.csproj b/src/Microsoft.ApplicationInsights.AspNetCore/Microsoft.ApplicationInsights.AspNetCore.csproj index b7262923..c2ec52cf 100644 --- a/src/Microsoft.ApplicationInsights.AspNetCore/Microsoft.ApplicationInsights.AspNetCore.csproj +++ b/src/Microsoft.ApplicationInsights.AspNetCore/Microsoft.ApplicationInsights.AspNetCore.csproj @@ -1,7 +1,7 @@  Microsoft.ApplicationInsights.AspNetCore - 2.4.1 + 2.5.0-beta1 Microsoft © Microsoft Corporation. All rights reserved. Application Insights for ASP.NET Core Web Applications @@ -65,6 +65,7 @@ All + All @@ -77,11 +78,11 @@ - - - - - + + + + + diff --git a/src/Microsoft.ApplicationInsights.AspNetCore/RequestTrackingTelemetryModule.cs b/src/Microsoft.ApplicationInsights.AspNetCore/RequestTrackingTelemetryModule.cs index 78f2ce68..f56aae74 100644 --- a/src/Microsoft.ApplicationInsights.AspNetCore/RequestTrackingTelemetryModule.cs +++ b/src/Microsoft.ApplicationInsights.AspNetCore/RequestTrackingTelemetryModule.cs @@ -1,5 +1,3 @@ -using Microsoft.ApplicationInsights.AspNetCore.Extensions; - namespace Microsoft.ApplicationInsights.AspNetCore { using System; @@ -8,6 +6,7 @@ namespace Microsoft.ApplicationInsights.AspNetCore using System.Diagnostics; using System.Threading; using Microsoft.ApplicationInsights.AspNetCore.DiagnosticListeners; + using Microsoft.ApplicationInsights.AspNetCore.Extensions; using Microsoft.ApplicationInsights.Extensibility; /// @@ -16,9 +15,9 @@ namespace Microsoft.ApplicationInsights.AspNetCore public class RequestTrackingTelemetryModule : ITelemetryModule, IObserver, IDisposable { private TelemetryClient telemetryClient; - private IApplicationIdProvider applicationIdProvider; + private readonly IApplicationIdProvider applicationIdProvider; private ConcurrentBag subscriptions; - private List diagnosticListeners; + private readonly List diagnosticListeners; private bool isInitialized = false; private readonly object lockObject = new object(); @@ -30,6 +29,10 @@ public RequestTrackingTelemetryModule() : this(null) this.CollectionOptions = new RequestCollectionOptions(); } + /// + /// Creates RequestTrackingTelemetryModule. + /// + /// public RequestTrackingTelemetryModule(IApplicationIdProvider applicationIdProvider) { this.applicationIdProvider = applicationIdProvider; @@ -37,6 +40,9 @@ public RequestTrackingTelemetryModule(IApplicationIdProvider applicationIdProvid this.diagnosticListeners = new List(); } + /// + /// Gets or sets request collection options. + /// public RequestCollectionOptions CollectionOptions { get; set; } /// @@ -56,9 +62,10 @@ public void Initialize(TelemetryConfiguration configuration) this.diagnosticListeners.Add (new HostingDiagnosticListener( this.telemetryClient, - applicationIdProvider, + this.applicationIdProvider, this.CollectionOptions.InjectResponseHeaders, - this.CollectionOptions.TrackExceptions)); + this.CollectionOptions.TrackExceptions, + this.CollectionOptions.EnableW3CDistributedTracing)); this.diagnosticListeners.Add (new MvcDiagnosticsListener()); diff --git a/test/EmptyApp.FunctionalTests/App.config b/test/EmptyApp.FunctionalTests/App.config index 380c07bc..aba20670 100644 --- a/test/EmptyApp.FunctionalTests/App.config +++ b/test/EmptyApp.FunctionalTests/App.config @@ -4,7 +4,7 @@ - + diff --git a/test/EmptyApp20.FunctionalTests/App.config b/test/EmptyApp20.FunctionalTests/App.config index 380c07bc..aba20670 100644 --- a/test/EmptyApp20.FunctionalTests/App.config +++ b/test/EmptyApp20.FunctionalTests/App.config @@ -4,7 +4,7 @@ - + diff --git a/test/FunctionalTestUtils20/InProcessServer.cs b/test/FunctionalTestUtils20/InProcessServer.cs index 9e2749bb..f2e0e15a 100644 --- a/test/FunctionalTestUtils20/InProcessServer.cs +++ b/test/FunctionalTestUtils20/InProcessServer.cs @@ -99,10 +99,11 @@ private bool StartListener(string listenerConnectionString) return true; } + private string StartApplication(string assemblyName) { output.WriteLine(string.Format("{0}: Launching application at: {1}", DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt"), this.url)); - return this.Start(assemblyName); ; + return this.Start(assemblyName); } private void StopApplication() @@ -149,11 +150,12 @@ private string Start(string assemblyName) Defined = new Dictionary {[IKey] = AppId} }); }); - if (configureHost != null) + + if (this.configureHost != null) { - builder = configureHost(builder); + builder = this.configureHost(builder); } - + this.hostingEngine = builder.Build(); this.hostingEngine.Start(); diff --git a/test/FunctionalTestUtils20/TelemetryTestsBase.cs b/test/FunctionalTestUtils20/TelemetryTestsBase.cs index de0e109a..c27acb83 100644 --- a/test/FunctionalTestUtils20/TelemetryTestsBase.cs +++ b/test/FunctionalTestUtils20/TelemetryTestsBase.cs @@ -2,6 +2,7 @@ { using System; using System.Diagnostics; + using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Reflection; @@ -12,14 +13,12 @@ using System.Threading.Tasks; using AI; using Microsoft.ApplicationInsights.DataContracts; - using Microsoft.AspNetCore.Hosting; - using Microsoft.ApplicationInsights.Channel; using Microsoft.Extensions.DependencyInjection; using Xunit; using Xunit.Abstractions; + using Microsoft.ApplicationInsights.Extensibility; #if NET451 || NET461 using Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector; - using Microsoft.ApplicationInsights.Extensibility; #endif public abstract class TelemetryTestsBase @@ -34,28 +33,40 @@ public TelemetryTestsBase(ITestOutputHelper output) [MethodImpl(MethodImplOptions.NoOptimization)] public TelemetryItem ValidateBasicRequest(InProcessServer server, string requestPath, RequestTelemetry expected, bool expectRequestContextInResponse = true) + { + return ValidateRequestWithHeaders(server, requestPath, null, expected, expectRequestContextInResponse); + } + + [MethodImpl(MethodImplOptions.NoOptimization)] + public TelemetryItem ValidateRequestWithHeaders(InProcessServer server, string requestPath, Dictionary requestHeaders, RequestTelemetry expected, bool expectRequestContextInResponse = true) { // Subtract 50 milliseconds to hack around strange behavior on build server where the RequestTelemetry.Timestamp is somehow sometimes earlier than now by a few milliseconds. expected.Timestamp = DateTimeOffset.Now.Subtract(TimeSpan.FromMilliseconds(50)); Stopwatch timer = Stopwatch.StartNew(); - var response = this.ExecuteRequest(server.BaseHost + requestPath); + var response = this.ExecuteRequest(server.BaseHost + requestPath, requestHeaders); var actual = server.Listener.ReceiveItemsOfType>(1, TestListenerTimeoutInMs); timer.Stop(); this.DebugTelemetryItems(actual); - this.output.WriteLine("Response headers: " + string.Join(",", response.Headers.Select(kvp => $"{kvp.Key} = {kvp.Value}"))); + this.output.WriteLine("Response headers: " + string.Join(",", response.Headers.Select(kvp => $"{kvp.Key} = {kvp.Value.First()}"))); var item = actual.OfType>().FirstOrDefault(); Assert.NotNull(item); var data = ((TelemetryItem)item).data.baseData; - + Assert.Equal(expected.ResponseCode, data.responseCode); Assert.Equal(expected.Name, data.name); Assert.Equal(expected.Success, data.success); Assert.Equal(expected.Url, new Uri(data.url)); Assert.Equal(expectRequestContextInResponse, response.Headers.Contains("Request-Context")); + if (expectRequestContextInResponse) + { + Assert.True(response.Headers.TryGetValues("Request-Context", out var appIds)); + Assert.Equal($"appId={InProcessServer.AppId}", appIds.Single()); + } + output.WriteLine("actual.Duration: " + data.duration); output.WriteLine("timer.Elapsed: " + timer.Elapsed); Assert.True(TimeSpan.Parse(data.duration) < timer.Elapsed.Add(TimeSpan.FromMilliseconds(20)), "duration"); @@ -78,9 +89,9 @@ public void ValidateBasicException(InProcessServer server, string requestPath, E Assert.NotEmpty(data.exceptions[0].parsedStack); } - public void ValidateBasicDependency(InProcessServer server, string requestPath, DependencyTelemetry expected) + public (TelemetryItem, TelemetryItem) ValidateBasicDependency(InProcessServer server, string requestPath, DependencyTelemetry expected) { - this.ExecuteRequest(server.BaseHost + requestPath); + var response = this.ExecuteRequest(server.BaseHost + requestPath); var actual = server.Listener.ReceiveItems(TestListenerTimeoutInMs); this.DebugTelemetryItems(actual); @@ -92,13 +103,13 @@ public void ValidateBasicDependency(InProcessServer server, string requestPath, Assert.Equal(expected.ResultCode, dependencyData.resultCode); Assert.Equal(expected.Success, dependencyData.success); -#if !NET461 var requestTelemetry = actual.OfType>().FirstOrDefault(); Assert.NotNull(requestTelemetry); - Assert.Contains(dependencyTelemetry.tags["ai.operation.id"], requestTelemetry.tags["ai.operation.parentId"]); Assert.Equal(requestTelemetry.tags["ai.operation.id"], dependencyTelemetry.tags["ai.operation.id"]); -#endif + Assert.Contains(dependencyTelemetry.data.baseData.id, requestTelemetry.tags["ai.operation.parentId"]); + + return (requestTelemetry, dependencyTelemetry); } #if NET451 || NET461 @@ -120,7 +131,7 @@ public void ValidatePerformanceCountersAreCollected(string assemblyName) } #endif - protected HttpResponseMessage ExecuteRequest(string requestPath) + protected HttpResponseMessage ExecuteRequest(string requestPath, Dictionary headers = null) { var httpClientHandler = new HttpClientHandler(); httpClientHandler.UseDefaultCredentials = true; @@ -128,9 +139,18 @@ protected HttpResponseMessage ExecuteRequest(string requestPath) using (HttpClient httpClient = new HttpClient(httpClientHandler, true)) { this.output.WriteLine(string.Format("{0}: Executing request: {1}", DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt"), requestPath)); - var task = httpClient.GetAsync(requestPath); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestPath); + if (headers != null) + { + foreach (var h in headers) + { + request.Headers.Add(h.Key, h.Value); + } + } + + var task = httpClient.SendAsync(request); task.Wait(TestListenerTimeoutInMs); - this.output.WriteLine(string.Format("{0}: Ended request: {1}", DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt"), requestPath)); + this.output.WriteLine(string.Format("{0:MM/dd/yyyy hh:mm:ss.fff tt}: Ended request: {1}", DateTime.Now, requestPath)); return task.Result; } @@ -145,7 +165,7 @@ protected void DebugTelemetryItems(Envelope[] telemetries) if (dependency != null) { var data = ((TelemetryItem)dependency).data.baseData; - builder.AppendLine($"{dependency.ToString()} - {data.data} - {((TelemetryItem)dependency).time} - {data.duration} - {data.id} - {data.name} - {data.resultCode} - {data.success} - {data.target} - {data.type}"); + builder.AppendLine($"{dependency} - {data.data} - {((TelemetryItem)dependency).time} - {data.duration} - {data.id} - {data.name} - {data.resultCode} - {data.success} - {data.target} - {data.type}"); } else { @@ -153,7 +173,7 @@ protected void DebugTelemetryItems(Envelope[] telemetries) if (request != null) { var data = ((TelemetryItem)request).data.baseData; - builder.AppendLine($"{request.ToString()} - {data.url} - {((TelemetryItem)request).time} - {data.duration} - {data.id} - {data.name} - {data.success} - {data.responseCode}"); + builder.AppendLine($"{request} - {data.url} - {((TelemetryItem)request).time} - {data.duration} - {data.id} - {data.name} - {data.success} - {data.responseCode}"); } else { @@ -161,7 +181,7 @@ protected void DebugTelemetryItems(Envelope[] telemetries) if (exception != null) { var data = ((TelemetryItem)exception).data.baseData; - builder.AppendLine($"{exception.ToString()} - {data.exceptions[0].message} - {data.exceptions[0].stack} - {data.exceptions[0].typeName} - {data.severityLevel}"); + builder.AppendLine($"{exception} - {data.exceptions[0].message} - {data.exceptions[0].stack} - {data.exceptions[0].typeName} - {data.severityLevel}"); } else { @@ -169,7 +189,7 @@ protected void DebugTelemetryItems(Envelope[] telemetries) if (message != null) { var data = ((TelemetryItem)message).data.baseData; - builder.AppendLine($"{message.ToString()} - {data.message} - {data.severityLevel}"); + builder.AppendLine($"{message} - {data.message} - {data.severityLevel}"); } else { diff --git a/test/MVCFramework.FunctionalTests/App.config b/test/MVCFramework.FunctionalTests/App.config index 380c07bc..aba20670 100644 --- a/test/MVCFramework.FunctionalTests/App.config +++ b/test/MVCFramework.FunctionalTests/App.config @@ -4,7 +4,7 @@ - + diff --git a/test/Microsoft.ApplicationInsights.AspNetCore.Tests/App.config b/test/Microsoft.ApplicationInsights.AspNetCore.Tests/App.config index 380c07bc..aba20670 100644 --- a/test/Microsoft.ApplicationInsights.AspNetCore.Tests/App.config +++ b/test/Microsoft.ApplicationInsights.AspNetCore.Tests/App.config @@ -4,7 +4,7 @@ - + diff --git a/test/Microsoft.ApplicationInsights.AspNetCore.Tests/ExceptionTrackingMiddlewareTest.cs b/test/Microsoft.ApplicationInsights.AspNetCore.Tests/ExceptionTrackingMiddlewareTest.cs index 430147bf..8f083a10 100644 --- a/test/Microsoft.ApplicationInsights.AspNetCore.Tests/ExceptionTrackingMiddlewareTest.cs +++ b/test/Microsoft.ApplicationInsights.AspNetCore.Tests/ExceptionTrackingMiddlewareTest.cs @@ -20,7 +20,8 @@ public void InvokeTracksExceptionThrownByNextMiddlewareAsHandledByPlatform() CommonMocks.MockTelemetryClient(telemetry => this.sentTelemetry = telemetry), CommonMocks.GetMockApplicationIdProvider(), injectResponseHeaders: true, - trackExceptions: true); + trackExceptions: true, + enableW3CHeaders:false); middleware.OnHostingException(null, null); @@ -36,7 +37,8 @@ public void SdkVersionIsPopulatedByMiddleware() CommonMocks.MockTelemetryClient(telemetry => this.sentTelemetry = telemetry), CommonMocks.GetMockApplicationIdProvider(), injectResponseHeaders: true, - trackExceptions: true); + trackExceptions: true, + enableW3CHeaders: false); middleware.OnHostingException(null, null); diff --git a/test/Microsoft.ApplicationInsights.AspNetCore.Tests/Extensions/ApplicationInsightsExtensionsTests.cs b/test/Microsoft.ApplicationInsights.AspNetCore.Tests/Extensions/ApplicationInsightsExtensionsTests.cs index d84a1fa7..884957bb 100644 --- a/test/Microsoft.ApplicationInsights.AspNetCore.Tests/Extensions/ApplicationInsightsExtensionsTests.cs +++ b/test/Microsoft.ApplicationInsights.AspNetCore.Tests/Extensions/ApplicationInsightsExtensionsTests.cs @@ -25,6 +25,7 @@ namespace Microsoft.Extensions.DependencyInjection.Test using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing; using Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector; using Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector.QuickPulse; + using Microsoft.ApplicationInsights.W3C; using Microsoft.ApplicationInsights.WindowsServer; using Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel; using Microsoft.AspNetCore.Hosting; @@ -944,6 +945,51 @@ public static void HeartbeatIsDisabledWithServiceOptions() Assert.False(heartbeatModule.IsHeartbeatEnabled); } +#pragma warning disable 612, 618 + [Fact] + public static void W3CIsDisabledByDefault() + { + var services = CreateServicesAndAddApplicationinsightsTelemetry(null, "http://localhost:1234/v2/track/"); + IServiceProvider serviceProvider = services.BuildServiceProvider(); + var telemetryConfiguration = serviceProvider.GetTelemetryConfiguration(); + + Assert.DoesNotContain(telemetryConfiguration.TelemetryInitializers, t => t is W3COperationCorrelationTelemetryInitializer); + Assert.DoesNotContain(TelemetryConfiguration.Active.TelemetryInitializers, t => t is W3COperationCorrelationTelemetryInitializer); + + var modules = serviceProvider.GetServices().ToList(); + + var requestTracking = modules.OfType().ToList(); + var dependencyTracking = modules.OfType().ToList(); + Assert.Single(requestTracking); + Assert.Single(dependencyTracking); + + Assert.False(requestTracking.Single().CollectionOptions.EnableW3CDistributedTracing); + Assert.False(dependencyTracking.Single().EnableW3CHeadersInjection); + } + + [Fact] + public static void W3CIsEnabledWhenConfiguredInOptions() + { + var services = CreateServicesAndAddApplicationinsightsTelemetry(null, + "http://localhost:1234/v2/track/", + o => o.RequestCollectionOptions.EnableW3CDistributedTracing = true); + IServiceProvider serviceProvider = services.BuildServiceProvider(); + var telemetryConfiguration = serviceProvider.GetTelemetryConfiguration(); + + Assert.Contains(telemetryConfiguration.TelemetryInitializers, t => t is W3COperationCorrelationTelemetryInitializer); + + var modules = serviceProvider.GetServices().ToList(); + + var requestTracking = modules.OfType().ToList(); + var dependencyTracking = modules.OfType().ToList(); + Assert.Single(requestTracking); + Assert.Single(dependencyTracking); + + Assert.True(requestTracking.Single().CollectionOptions.EnableW3CDistributedTracing); + Assert.True(dependencyTracking.Single().EnableW3CHeadersInjection); + } +#pragma warning restore 612, 618 + private static int GetTelemetryProcessorsCountInConfiguration(TelemetryConfiguration telemetryConfiguration) { return telemetryConfiguration.TelemetryProcessors.Where(processor => processor.GetType() == typeof(T)).Count(); diff --git a/test/Microsoft.ApplicationInsights.AspNetCore.Tests/Helpers/CommonMocks.cs b/test/Microsoft.ApplicationInsights.AspNetCore.Tests/Helpers/CommonMocks.cs index 61187432..0bf963cd 100644 --- a/test/Microsoft.ApplicationInsights.AspNetCore.Tests/Helpers/CommonMocks.cs +++ b/test/Microsoft.ApplicationInsights.AspNetCore.Tests/Helpers/CommonMocks.cs @@ -19,6 +19,13 @@ public static TelemetryClient MockTelemetryClient(Action onSendCallb }); } + public static TelemetryClient MockTelemetryClient(Action onSendCallback, TelemetryConfiguration configuration) + { + configuration.InstrumentationKey = InstrumentationKey; + configuration.TelemetryChannel = new FakeTelemetryChannel {OnSend = onSendCallback}; + return new TelemetryClient(configuration); + } + internal static IApplicationIdProvider GetMockApplicationIdProvider() { return new MockApplicationIdProvider(InstrumentationKey, TestApplicationId); diff --git a/test/Microsoft.ApplicationInsights.AspNetCore.Tests/HttpHeadersUtilitiesTest.cs b/test/Microsoft.ApplicationInsights.AspNetCore.Tests/HttpHeadersUtilitiesTest.cs index dd333a54..3dfcc027 100644 --- a/test/Microsoft.ApplicationInsights.AspNetCore.Tests/HttpHeadersUtilitiesTest.cs +++ b/test/Microsoft.ApplicationInsights.AspNetCore.Tests/HttpHeadersUtilitiesTest.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using DiagnosticListeners; + using Microsoft.ApplicationInsights.W3C; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Xunit; @@ -120,10 +121,107 @@ public void SetRequestContextKeyValueShouldSetTheHeaders() public void ContainsRequestContextKeyValueShouldReturnTrueWhenExists() { IHeaderDictionary headers = new HeaderDictionary( - new Dictionary() { { RequestResponseHeaders.RequestContextHeader, new StringValues("app=id,other=otherValue") } }); + new Dictionary { { RequestResponseHeaders.RequestContextHeader, new StringValues("app=id,other=otherValue") } }); Assert.True(HttpHeadersUtilities.ContainsRequestContextKeyValue(headers, "app")); Assert.True(HttpHeadersUtilities.ContainsRequestContextKeyValue(headers, "other")); Assert.False(HttpHeadersUtilities.ContainsRequestContextKeyValue(headers, "Non-exists")); } + +#pragma warning disable 612, 618 + [Fact] + public void GetHeaderValueEmpty() + { + IHeaderDictionary headers = new HeaderDictionary(); + var values = HttpHeadersUtilities.SafeGetCommaSeparatedHeaderValues(headers, W3CConstants.TraceStateHeader, 100500, 100500)?.ToList(); + Assert.NotNull(values); + Assert.Empty(values); + } + + [Fact] + public void GetHeaderValueNoMax1() + { + IHeaderDictionary headers = new HeaderDictionary(new Dictionary { [W3CConstants.TraceStateHeader] = "k1=v1,k2=v2" }); + var values = HttpHeadersUtilities.SafeGetCommaSeparatedHeaderValues(headers, W3CConstants.TraceStateHeader, 100500, 100500)?.ToList(); + Assert.NotNull(values); + Assert.Equal(2, values.Count); + Assert.Equal("k1=v1", values.First()); + Assert.Equal("k2=v2", values.Last()); + } + + [Fact] + public void GetHeaderValueNoMax2() + { + IHeaderDictionary headers = new HeaderDictionary(new Dictionary { [W3CConstants.TraceStateHeader] = new []{"k1=v1,k2=v2", "k3=v3,k4=v4" }}); + var values = HttpHeadersUtilities.SafeGetCommaSeparatedHeaderValues(headers, W3CConstants.TraceStateHeader, 100500, 100500)?.ToList(); + Assert.NotNull(values); + Assert.Equal(4, values.Count); + Assert.Equal("k1=v1", values[0]); + Assert.Equal("k2=v2", values[1]); + Assert.Equal("k3=v3", values[2]); + Assert.Equal("k4=v4", values[3]); + } + + [Theory] + [InlineData(12)] // k1=v1,k2=v2,".Length + [InlineData(11)] // k1=v1,k2=v2".Length + [InlineData(15)] // k1=v1,k2=v2,k3=".Length + [InlineData(13)] // k1=v1,k2=v2,k".Length + public void GetHeaderValueMaxLenTruncatesEnd(int maxLength) + { + IHeaderDictionary headers = new HeaderDictionary(new Dictionary { [W3CConstants.TraceStateHeader] = "k1=v1,k2=v2,k3=v3,k4=v4" }); + var values = HttpHeadersUtilities.SafeGetCommaSeparatedHeaderValues(headers, W3CConstants.TraceStateHeader, maxLength, 100500)?.ToList(); + Assert.NotNull(values); + Assert.Equal(2, values.Count); + Assert.Equal("k1=v1", values.First()); + Assert.Equal("k2=v2", values.Last()); + } + + [Theory] + [InlineData(12)] // k1=v1,k2=v2,".Length + [InlineData(11)] // k1=v1,k2=v2".Length + [InlineData(15)] // k1=v1,k2=v2,k3=".Length + [InlineData(13)] // k1=v1,k2=v2,k".Length + public void GetHeaderValueMaxLenTruncatesEnd2(int maxLength) + { + IHeaderDictionary headers = new HeaderDictionary(new Dictionary { [W3CConstants.TraceStateHeader] = new[] { "k1=v1,k2=v2", "k3=v3,k4=v4" } }); + var values = HttpHeadersUtilities.SafeGetCommaSeparatedHeaderValues(headers, W3CConstants.TraceStateHeader, maxLength, 100500)?.ToList(); + Assert.NotNull(values); + Assert.Equal(2, values.Count); + Assert.Equal("k1=v1", values.First()); + Assert.Equal("k2=v2", values.Last()); + } + + [Theory] + [InlineData(0)] + [InlineData(3)] + public void GetHeaderValueMaxLenTruncatesEndInvalid(int maxLength) + { + IHeaderDictionary headers = new HeaderDictionary(new Dictionary { [W3CConstants.TraceStateHeader] = "k1=v1,k2=v2" }); + var values = HttpHeadersUtilities.SafeGetCommaSeparatedHeaderValues(headers, W3CConstants.TraceStateHeader, maxLength, 100500)?.ToList(); + Assert.NotNull(values); + Assert.Empty(values); + } + + [Fact] + public void GetHeaderValueMaxItemsTruncatesEndInvalid() + { + IHeaderDictionary headers = new HeaderDictionary(new Dictionary { [W3CConstants.TraceStateHeader] = "k1=v1,k2=v2" }); + var values = HttpHeadersUtilities.SafeGetCommaSeparatedHeaderValues(headers, W3CConstants.TraceStateHeader, 100500, 0)?.ToList(); + Assert.NotNull(values); + Assert.Empty(values); + } + + [Fact] + public void GetHeaderValueMaxItemsTruncatesEnd() + { + IHeaderDictionary headers = new HeaderDictionary(new Dictionary { [W3CConstants.TraceStateHeader] = "k1=v1,k2=v2,k3=v3,k4=v4" }); + var values = HttpHeadersUtilities.SafeGetCommaSeparatedHeaderValues(headers, W3CConstants.TraceStateHeader, 100500, 2)?.ToList(); + Assert.NotNull(values); + Assert.Equal(2, values.Count); + Assert.Equal("k1=v1", values.First()); + Assert.Equal("k2=v2", values.Last()); + } +#pragma warning restore 612, 618 + } } diff --git a/test/Microsoft.ApplicationInsights.AspNetCore.Tests/RequestTrackingMiddlewareTest.cs b/test/Microsoft.ApplicationInsights.AspNetCore.Tests/RequestTrackingMiddlewareTest.cs index f8dc893c..b243e964 100644 --- a/test/Microsoft.ApplicationInsights.AspNetCore.Tests/RequestTrackingMiddlewareTest.cs +++ b/test/Microsoft.ApplicationInsights.AspNetCore.Tests/RequestTrackingMiddlewareTest.cs @@ -11,13 +11,17 @@ using Microsoft.ApplicationInsights.AspNetCore.Tests.Helpers; using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation; + using Microsoft.ApplicationInsights.W3C; using Microsoft.AspNetCore.Http; using Xunit; public class RequestTrackingMiddlewareTest : IDisposable { private const string HttpRequestScheme = "http"; + private const string ExpectedAppId = "cid-v1:some-app-id"; + private static readonly HostString HttpRequestHost = new HostString("testHost"); private static readonly PathString HttpRequestPath = new PathString("/path/path"); private static readonly QueryString HttpRequestQueryString = new QueryString("?query=1"); @@ -64,7 +68,7 @@ private HttpContext CreateContext(string scheme, HostString host, PathString? pa private ConcurrentQueue sentTelemetry = new ConcurrentQueue(); - private readonly HostingDiagnosticListener middleware; + private HostingDiagnosticListener middleware; public RequestTrackingMiddlewareTest() { @@ -72,7 +76,8 @@ public RequestTrackingMiddlewareTest() CommonMocks.MockTelemetryClient(telemetry => this.sentTelemetry.Enqueue(telemetry)), CommonMocks.GetMockApplicationIdProvider(), injectResponseHeaders: true, - trackExceptions: true); + trackExceptions: true, + enableW3CHeaders: false); } [Fact] @@ -95,7 +100,7 @@ public void TestSdkVersionIsPopulatedByMiddleware() Assert.True(requestTelemetry.Duration.TotalMilliseconds >= 0); Assert.True(requestTelemetry.Success); Assert.Equal(CommonMocks.InstrumentationKey, requestTelemetry.Context.InstrumentationKey); - Assert.Equal("", requestTelemetry.Source); + Assert.True(string.IsNullOrEmpty(requestTelemetry.Source)); Assert.Equal(CreateUri(HttpRequestScheme, HttpRequestHost), requestTelemetry.Url); Assert.NotEmpty(requestTelemetry.Context.GetInternalContext().SdkVersion); Assert.Contains(SdkVersionTestUtils.VersionPrefix, requestTelemetry.Context.GetInternalContext().SdkVersion); @@ -120,7 +125,7 @@ public void TestRequestUriIsPopulatedByMiddleware() Assert.True(requestTelemetry.Duration.TotalMilliseconds >= 0); Assert.True(requestTelemetry.Success); Assert.Equal(CommonMocks.InstrumentationKey, requestTelemetry.Context.InstrumentationKey); - Assert.Equal("", requestTelemetry.Source); + Assert.True(string.IsNullOrEmpty(requestTelemetry.Source)); Assert.Equal(CreateUri(HttpRequestScheme, HttpRequestHost, HttpRequestPath, HttpRequestQueryString), requestTelemetry.Url); Assert.NotEmpty(requestTelemetry.Context.GetInternalContext().SdkVersion); Assert.Contains(SdkVersionTestUtils.VersionPrefix, requestTelemetry.Context.GetInternalContext().SdkVersion); @@ -148,7 +153,7 @@ public void RequestWillBeMarkedAsFailedForRunawayException() Assert.True(requestTelemetry.Duration.TotalMilliseconds >= 0); Assert.False(requestTelemetry.Success); Assert.Equal(CommonMocks.InstrumentationKey, requestTelemetry.Context.InstrumentationKey); - Assert.Equal("", requestTelemetry.Source); + Assert.True(string.IsNullOrEmpty(requestTelemetry.Source)); Assert.Equal(CreateUri(HttpRequestScheme, HttpRequestHost), requestTelemetry.Url); Assert.NotEmpty(requestTelemetry.Context.GetInternalContext().SdkVersion); Assert.Contains(SdkVersionTestUtils.VersionPrefix, requestTelemetry.Context.GetInternalContext().SdkVersion); @@ -324,7 +329,7 @@ public void OnEndRequestSetsRequestNameToMethodAndPathForPostRequest() Assert.True(requestTelemetry.Duration.TotalMilliseconds >= 0); Assert.True(requestTelemetry.Success); Assert.Equal(CommonMocks.InstrumentationKey, requestTelemetry.Context.InstrumentationKey); - Assert.Equal("", requestTelemetry.Source); + Assert.True(string.IsNullOrEmpty(requestTelemetry.Source)); Assert.Equal(CreateUri(HttpRequestScheme, HttpRequestHost, "/Test"), requestTelemetry.Url); Assert.NotEmpty(requestTelemetry.Context.GetInternalContext().SdkVersion); Assert.Contains(SdkVersionTestUtils.VersionPrefix, requestTelemetry.Context.GetInternalContext().SdkVersion); @@ -349,7 +354,7 @@ public void OnEndRequestSetsRequestNameToMethodAndPath() Assert.True(requestTelemetry.Duration.TotalMilliseconds >= 0); Assert.True(requestTelemetry.Success); Assert.Equal(CommonMocks.InstrumentationKey, requestTelemetry.Context.InstrumentationKey); - Assert.Equal("", requestTelemetry.Source); + Assert.True(string.IsNullOrEmpty(requestTelemetry.Source)); Assert.Equal(CreateUri(HttpRequestScheme, HttpRequestHost, "/Test"), requestTelemetry.Url); Assert.NotEmpty(requestTelemetry.Context.GetInternalContext().SdkVersion); Assert.Contains(SdkVersionTestUtils.VersionPrefix, requestTelemetry.Context.GetInternalContext().SdkVersion); @@ -376,7 +381,7 @@ public void OnEndRequestFromSameInstrumentationKey() Assert.True(requestTelemetry.Duration.TotalMilliseconds >= 0); Assert.True(requestTelemetry.Success); Assert.Equal(CommonMocks.InstrumentationKey, requestTelemetry.Context.InstrumentationKey); - Assert.Equal("", requestTelemetry.Source); + Assert.True(string.IsNullOrEmpty(requestTelemetry.Source)); Assert.Equal(CreateUri(HttpRequestScheme, HttpRequestHost, "/Test"), requestTelemetry.Url); Assert.NotEmpty(requestTelemetry.Context.GetInternalContext().SdkVersion); Assert.Contains(SdkVersionTestUtils.VersionPrefix, requestTelemetry.Context.GetInternalContext().SdkVersion); @@ -537,7 +542,8 @@ public void ResponseHeadersAreNotInjectedWhenDisabled() CommonMocks.MockTelemetryClient(telemetry => this.sentTelemetry.Enqueue(telemetry)), CommonMocks.GetMockApplicationIdProvider(), injectResponseHeaders: false, - trackExceptions: true); + trackExceptions: true, + enableW3CHeaders: false); noHeadersMiddleware.OnBeginRequest(context, 0); Assert.False(context.Response.Headers.ContainsKey(RequestResponseHeaders.RequestContextHeader)); @@ -557,7 +563,8 @@ public void ExceptionsAreNotTrackedInjectedWhenDisabled() CommonMocks.MockTelemetryClient(telemetry => this.sentTelemetry.Enqueue(telemetry)), CommonMocks.GetMockApplicationIdProvider(), injectResponseHeaders: true, - trackExceptions: false); + trackExceptions: false, + enableW3CHeaders: false); noExceptionsMiddleware.OnHostingException(context, new Exception("HostingException")); noExceptionsMiddleware.OnDiagnosticsHandledException(context, new Exception("DiagnosticsHandledException")); @@ -581,6 +588,237 @@ public void DoesntAddSourceIfRequestHeadersDontHaveSource() Assert.True(string.IsNullOrEmpty(requestTelemetry.Source)); } + #pragma warning disable 612, 618 + [Fact] + public void OnBeginRequestWithW3CHeadersIsTrackedCorrectly() + { + var configuration = TelemetryConfiguration.CreateDefault(); + configuration.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); + this.middleware = new HostingDiagnosticListener( + CommonMocks.MockTelemetryClient(telemetry => this.sentTelemetry.Enqueue(telemetry), configuration), + CommonMocks.GetMockApplicationIdProvider(), + injectResponseHeaders: true, + trackExceptions: true, + enableW3CHeaders: true); + + var context = CreateContext(HttpRequestScheme, HttpRequestHost, "/Test", method: "POST"); + + context.Request.Headers[W3CConstants.TraceParentHeader] = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + context.Request.Headers[W3CConstants.TraceStateHeader] = "state=some"; + context.Request.Headers[RequestResponseHeaders.CorrelationContextHeader] = "k=v"; + + if (HostingDiagnosticListener.IsAspNetCore20) + { + var activity = new Activity("operation"); + activity.Start(); + + middleware.OnHttpRequestInStart(context); + + Assert.NotEqual(Activity.Current, activity); + } + else + { + middleware.OnBeginRequest(context, Stopwatch.GetTimestamp()); + } + + 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); + + if (HostingDiagnosticListener.IsAspNetCore20) + { + middleware.OnHttpRequestInStop(context); + } + else + { + middleware.OnEndRequest(context, Stopwatch.GetTimestamp()); + } + + Assert.Single(sentTelemetry); + var requestTelemetry = (RequestTelemetry)this.sentTelemetry.Single(); + + Assert.Equal($"|4bf92f3577b34da6a3ce929d0e0e4736.{activityInitializedByW3CHeader.GetSpanId()}.", requestTelemetry.Id); + Assert.Equal("4bf92f3577b34da6a3ce929d0e0e4736", requestTelemetry.Context.Operation.Id); + Assert.Equal("|4bf92f3577b34da6a3ce929d0e0e4736.00f067aa0ba902b7.", requestTelemetry.Context.Operation.ParentId); + + Assert.True(context.Response.Headers.TryGetValue(RequestResponseHeaders.RequestContextHeader, out var appId)); + Assert.Equal($"appId={CommonMocks.TestApplicationId}", appId); + } + + [Fact] + public void OnBeginRequestWithW3CHeadersAndRequestIdIsTrackedCorrectly() + { + var configuration = TelemetryConfiguration.CreateDefault(); + configuration.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); + this.middleware = new HostingDiagnosticListener( + CommonMocks.MockTelemetryClient(telemetry => this.sentTelemetry.Enqueue(telemetry), configuration), + CommonMocks.GetMockApplicationIdProvider(), + injectResponseHeaders: true, + trackExceptions: true, + enableW3CHeaders: true); + + var context = CreateContext(HttpRequestScheme, HttpRequestHost, "/Test", method: "POST"); + + context.Request.Headers[RequestResponseHeaders.RequestIdHeader] = "|abc.1.2.3."; + context.Request.Headers[W3CConstants.TraceParentHeader] = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + context.Request.Headers[W3CConstants.TraceStateHeader] = "state=some"; + context.Request.Headers[RequestResponseHeaders.CorrelationContextHeader] = "k=v"; + + middleware.OnBeginRequest(context, Stopwatch.GetTimestamp()); + var activityInitializedByW3CHeader = Activity.Current; + + Assert.Equal("|abc.1.2.3.", activityInitializedByW3CHeader.ParentId); + 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); + + middleware.OnEndRequest(context, Stopwatch.GetTimestamp()); + + Assert.Single(sentTelemetry); + var requestTelemetry = (RequestTelemetry)this.sentTelemetry.Single(); + + Assert.Equal($"|4bf92f3577b34da6a3ce929d0e0e4736.{activityInitializedByW3CHeader.GetSpanId()}.", requestTelemetry.Id); + Assert.Equal("4bf92f3577b34da6a3ce929d0e0e4736", requestTelemetry.Context.Operation.Id); + Assert.Equal("|4bf92f3577b34da6a3ce929d0e0e4736.00f067aa0ba902b7.", requestTelemetry.Context.Operation.ParentId); + + Assert.True(context.Response.Headers.TryGetValue(RequestResponseHeaders.RequestContextHeader, out var appId)); + Assert.Equal($"appId={CommonMocks.TestApplicationId}", appId); + + Assert.Equal("abc", requestTelemetry.Properties["ai_legacyRootId"]); + Assert.StartsWith("|abc.1.2.3.", requestTelemetry.Properties["ai_legacyRequestId"]); + } + + [Fact] + public void OnBeginRequestWithNoW3CHeadersAndRequestIdIsTrackedCorrectly() + { + var configuration = TelemetryConfiguration.CreateDefault(); + configuration.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); + this.middleware = new HostingDiagnosticListener( + CommonMocks.MockTelemetryClient(telemetry => this.sentTelemetry.Enqueue(telemetry), configuration), + CommonMocks.GetMockApplicationIdProvider(), + injectResponseHeaders: true, + trackExceptions: true, + enableW3CHeaders: true); + + var context = CreateContext(HttpRequestScheme, HttpRequestHost, "/Test", method: "POST"); + + context.Request.Headers[RequestResponseHeaders.RequestIdHeader] = "|abc.1.2.3."; + context.Request.Headers[RequestResponseHeaders.CorrelationContextHeader] = "k=v"; + + middleware.OnBeginRequest(context, Stopwatch.GetTimestamp()); + var activityInitializedByW3CHeader = Activity.Current; + + Assert.Equal("|abc.1.2.3.", activityInitializedByW3CHeader.ParentId); + middleware.OnEndRequest(context, Stopwatch.GetTimestamp()); + + Assert.Single(sentTelemetry); + var requestTelemetry = (RequestTelemetry)this.sentTelemetry.Single(); + + Assert.Equal($"|{activityInitializedByW3CHeader.GetTraceId()}.{activityInitializedByW3CHeader.GetSpanId()}.", requestTelemetry.Id); + Assert.Equal(activityInitializedByW3CHeader.GetTraceId(), requestTelemetry.Context.Operation.Id); + Assert.Equal("|abc.1.2.3.", requestTelemetry.Context.Operation.ParentId); + + Assert.Equal("abc", requestTelemetry.Properties["ai_legacyRootId"]); + Assert.StartsWith("|abc.1.2.3.", requestTelemetry.Properties["ai_legacyRequestId"]); + } + + [Fact] + public void OnBeginRequestWithW3CSupportAndNoHeadersIsTrackedCorrectly() + { + var configuration = TelemetryConfiguration.CreateDefault(); + configuration.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); + this.middleware = new HostingDiagnosticListener( + CommonMocks.MockTelemetryClient(telemetry => this.sentTelemetry.Enqueue(telemetry), configuration), + CommonMocks.GetMockApplicationIdProvider(), + injectResponseHeaders: true, + trackExceptions: true, + enableW3CHeaders: true); + + var context = CreateContext(HttpRequestScheme, HttpRequestHost, "/Test", method: "POST"); + + middleware.OnBeginRequest(context, Stopwatch.GetTimestamp()); + + var activityInitializedByW3CHeader = Activity.Current; + Assert.Null(activityInitializedByW3CHeader.ParentId); + Assert.NotNull(activityInitializedByW3CHeader.GetTraceId()); + Assert.Equal(32, activityInitializedByW3CHeader.GetTraceId().Length); + Assert.Equal(16, activityInitializedByW3CHeader.GetSpanId().Length); + Assert.Equal($"00-{activityInitializedByW3CHeader.GetTraceId()}-{activityInitializedByW3CHeader.GetSpanId()}-02", + activityInitializedByW3CHeader.GetTraceparent()); + Assert.Null(activityInitializedByW3CHeader.GetTracestate()); + Assert.Empty(activityInitializedByW3CHeader.Baggage); + + middleware.OnEndRequest(context, Stopwatch.GetTimestamp()); + + Assert.Single(sentTelemetry); + var requestTelemetry = (RequestTelemetry)this.sentTelemetry.Single(); + + Assert.Equal($"|{activityInitializedByW3CHeader.GetTraceId()}.{activityInitializedByW3CHeader.GetSpanId()}.", requestTelemetry.Id); + Assert.Equal(activityInitializedByW3CHeader.GetTraceId(), requestTelemetry.Context.Operation.Id); + Assert.Null(requestTelemetry.Context.Operation.ParentId); + + Assert.True(context.Response.Headers.TryGetValue(RequestResponseHeaders.RequestContextHeader, out var appId)); + Assert.Equal($"appId={CommonMocks.TestApplicationId}", appId); + } + + [Fact] + public void OnBeginRequestWithW3CHeadersAndAppIdInState() + { + var configuration = TelemetryConfiguration.CreateDefault(); + configuration.TelemetryInitializers.Add(new W3COperationCorrelationTelemetryInitializer()); + this.middleware = new HostingDiagnosticListener( + CommonMocks.MockTelemetryClient(telemetry => this.sentTelemetry.Enqueue(telemetry), configuration), + CommonMocks.GetMockApplicationIdProvider(), + injectResponseHeaders: true, + trackExceptions: true, + enableW3CHeaders: true); + + var context = CreateContext(HttpRequestScheme, HttpRequestHost, "/Test", method: "POST"); + + context.Request.Headers[W3CConstants.TraceParentHeader] = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00"; + context.Request.Headers[W3CConstants.TraceStateHeader] = $"state=some,{W3CConstants.AzureTracestateNamespace}={ExpectedAppId}"; + + if (HostingDiagnosticListener.IsAspNetCore20) + { + var activity = new Activity("operation"); + activity.Start(); + + middleware.OnHttpRequestInStart(context); + Assert.NotEqual(Activity.Current, activity); + } + else + { + middleware.OnBeginRequest(context, Stopwatch.GetTimestamp()); + } + + var activityInitializedByW3CHeader = Activity.Current; + + Assert.Equal("state=some", activityInitializedByW3CHeader.GetTracestate()); + + if (HostingDiagnosticListener.IsAspNetCore20) + { + middleware.OnHttpRequestInStop(context); + } + else + { + middleware.OnEndRequest(context, Stopwatch.GetTimestamp()); + } + + Assert.Single(sentTelemetry); + var requestTelemetry = (RequestTelemetry)this.sentTelemetry.Single(); + + Assert.Equal(ExpectedAppId, requestTelemetry.Source); + + Assert.True(context.Response.Headers.TryGetValue(RequestResponseHeaders.RequestContextHeader, out var appId)); + Assert.Equal($"appId={CommonMocks.TestApplicationId}", appId); + } +#pragma warning restore 612, 618 + private void HandleRequestBegin(HttpContext context, long timestamp) { if (HostingDiagnosticListener.IsAspNetCore20) diff --git a/test/WebApi.FunctionalTests/App.config b/test/WebApi.FunctionalTests/App.config index 380c07bc..aba20670 100644 --- a/test/WebApi.FunctionalTests/App.config +++ b/test/WebApi.FunctionalTests/App.config @@ -4,7 +4,7 @@ - + diff --git a/test/WebApi20.FunctionalTests/FunctionalTest/MultipleWebHostsTests.cs b/test/WebApi20.FunctionalTests/FunctionalTest/MultipleWebHostsTests.cs index b4234cff..9fd65865 100644 --- a/test/WebApi20.FunctionalTests/FunctionalTest/MultipleWebHostsTests.cs +++ b/test/WebApi20.FunctionalTests/FunctionalTest/MultipleWebHostsTests.cs @@ -147,6 +147,8 @@ public void ActiveConfigurationIsNotCorruptedAfterWebHostIsDisposed() var message = listener.ReceiveItemsOfType>(1, TestListenerTimeoutInMs); Assert.Single(message); + + this.output.WriteLine(((TelemetryItem)message.Single()).data.baseData.message); Assert.Equal("some message after web host is disposed", ((TelemetryItem)message.Single()).data.baseData.message); } } diff --git a/test/WebApi20.FunctionalTests/FunctionalTest/RequestTelemetryWebApiTests.cs b/test/WebApi20.FunctionalTests/FunctionalTest/RequestTelemetryWebApiTests.cs index 41864103..2d4f8eb1 100644 --- a/test/WebApi20.FunctionalTests/FunctionalTest/RequestTelemetryWebApiTests.cs +++ b/test/WebApi20.FunctionalTests/FunctionalTest/RequestTelemetryWebApiTests.cs @@ -1,17 +1,20 @@ namespace WebApi20.FunctionalTests.FunctionalTest { + using System; + using System.Collections.Generic; + using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; using FunctionalTestUtils; - using Microsoft.ApplicationInsights.DataContracts; - using Microsoft.ApplicationInsights.DependencyCollector; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.DependencyCollector; using Xunit; using Xunit.Abstractions; - public class RequestTelemetryWebApiTests : TelemetryTestsBase + public class RequestTelemetryWebApiTests : TelemetryTestsBase, IDisposable { private const string assemblyName = "WebApi20.FunctionalTests20"; public RequestTelemetryWebApiTests(ITestOutputHelper output) : base (output) @@ -126,6 +129,210 @@ IWebHostBuilder Config(IWebHostBuilder builder) // end of workaround test } } + + [Fact] + public void TestW3CHeadersAreNotEnabledByDefault() + { + using (var server = new InProcessServer(assemblyName, this.output)) + { + const string RequestPath = "/api/values"; + + var expectedRequestTelemetry = new RequestTelemetry(); + expectedRequestTelemetry.Name = "GET Values/Get"; + expectedRequestTelemetry.ResponseCode = "200"; + expectedRequestTelemetry.Success = true; + expectedRequestTelemetry.Url = new Uri(server.BaseHost + RequestPath); + + var activity = new Activity("dummy").SetParentId("|abc.123.").Start(); + var headers = new Dictionary + { + ["traceparent"] = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + ["tracestate"] = "some=state" + }; + + var actualRequest = this.ValidateRequestWithHeaders(server, RequestPath, headers, expectedRequestTelemetry); + + Assert.Equal(activity.RootId, actualRequest.tags["ai.operation.id"]); + Assert.Contains(activity.Id, actualRequest.tags["ai.operation.parentId"]); + } + } + + [Fact] + public void TestW3CHeadersAreParsedWhenEnabledInConfig() + { + using (var server = new InProcessServer(assemblyName, this.output, builder => + { + return builder.ConfigureServices( services => + { + services.AddApplicationInsightsTelemetry(o => o.RequestCollectionOptions.EnableW3CDistributedTracing = true); + }); + })) + { + const string RequestPath = "/api/values"; + + var expectedRequestTelemetry = new RequestTelemetry(); + expectedRequestTelemetry.Name = "GET Values/Get"; + expectedRequestTelemetry.ResponseCode = "200"; + expectedRequestTelemetry.Success = true; + expectedRequestTelemetry.Url = new Uri(server.BaseHost + RequestPath); + + var activity = new Activity("dummy").SetParentId("|abc.123.").Start(); + var headers = new Dictionary + { + ["traceparent"] = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + ["tracestate"] = "some=state", + ["Correlation-Context"] = "k1=v1,k2=v2" + }; + + var actualRequest = this.ValidateRequestWithHeaders(server, RequestPath, headers, expectedRequestTelemetry); + + Assert.Equal("4bf92f3577b34da6a3ce929d0e0e4736", actualRequest.tags["ai.operation.id"]); + Assert.Equal("|4bf92f3577b34da6a3ce929d0e0e4736.00f067aa0ba902b7.", actualRequest.tags["ai.operation.parentId"]); + Assert.Equal("v1", actualRequest.data.baseData.properties["k1"]); + Assert.Equal("v2", actualRequest.data.baseData.properties["k2"]); + } + } + + [Fact] + public void TestW3CEnabledW3CHeadersOnly() + { + using (var server = new InProcessServer(assemblyName, this.output, builder => + { + return builder.ConfigureServices(services => + { + services.AddApplicationInsightsTelemetry(o => o.RequestCollectionOptions.EnableW3CDistributedTracing = true); + var depCollectorSd = services.Single(sd => + sd.ImplementationType == typeof(DependencyTrackingTelemetryModule)); + services.Remove(depCollectorSd); + }); + })) + { + const string RequestPath = "/api/values"; + + var expectedRequestTelemetry = new RequestTelemetry(); + expectedRequestTelemetry.Name = "GET Values/Get"; + expectedRequestTelemetry.ResponseCode = "200"; + expectedRequestTelemetry.Success = true; + expectedRequestTelemetry.Url = new Uri(server.BaseHost + RequestPath); + + var headers = new Dictionary + { + ["traceparent"] = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + ["tracestate"] = "some=state,az=cid-v1:xyz", + ["Correlation-Context"] = "k1=v1,k2=v2" + }; + + var actualRequest = this.ValidateRequestWithHeaders(server, RequestPath, headers, expectedRequestTelemetry); + + Assert.Equal("4bf92f3577b34da6a3ce929d0e0e4736", actualRequest.tags["ai.operation.id"]); + Assert.StartsWith("|4bf92f3577b34da6a3ce929d0e0e4736.00f067aa0ba902b7.", actualRequest.tags["ai.operation.parentId"]); + Assert.Equal("v1", actualRequest.data.baseData.properties["k1"]); + Assert.Equal("v2", actualRequest.data.baseData.properties["k2"]); + } + } + + [Fact] + public void TestW3CEnabledRequestIdAndW3CHeaders() + { + using (var server = new InProcessServer(assemblyName, this.output, builder => + { + return builder.ConfigureServices(services => + { + services.AddApplicationInsightsTelemetry(o => o.RequestCollectionOptions.EnableW3CDistributedTracing = true); + }); + })) + { + const string RequestPath = "/api/values"; + + var expectedRequestTelemetry = new RequestTelemetry(); + expectedRequestTelemetry.Name = "GET Values/Get"; + expectedRequestTelemetry.ResponseCode = "200"; + expectedRequestTelemetry.Success = true; + expectedRequestTelemetry.Url = new Uri(server.BaseHost + RequestPath); + + // this will force Request-Id header injection, it will start with |abc.123. + var activity = new Activity("dummy").SetParentId("|abc.123.").Start(); + var headers = new Dictionary + { + ["traceparent"] = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + ["tracestate"] = "some=state,az=cid-v1:xyz", + ["Correlation-Context"] = "k1=v1,k2=v2" + }; + + var actualRequest = this.ValidateRequestWithHeaders(server, RequestPath, headers, expectedRequestTelemetry); + + Assert.Equal("4bf92f3577b34da6a3ce929d0e0e4736", actualRequest.tags["ai.operation.id"]); + Assert.StartsWith("|4bf92f3577b34da6a3ce929d0e0e4736.00f067aa0ba902b7.", actualRequest.tags["ai.operation.parentId"]); + Assert.Equal("v1", actualRequest.data.baseData.properties["k1"]); + Assert.Equal("v2", actualRequest.data.baseData.properties["k2"]); + Assert.Equal("abc", actualRequest.data.baseData.properties["ai_legacyRootId"]); + Assert.StartsWith("|abc.123", actualRequest.data.baseData.properties["ai_legacyRequestId"]); + } + } + + [Fact] + public void TestW3CEnabledRequestIdAndNoW3CHeaders() + { + using (var server = new InProcessServer(assemblyName, this.output, + builder => + { + return builder.ConfigureServices(services => + { + services.AddApplicationInsightsTelemetry(o => o.RequestCollectionOptions.EnableW3CDistributedTracing = true); + }); + })) + { + const string RequestPath = "/api/values"; + + var expectedRequestTelemetry = new RequestTelemetry(); + expectedRequestTelemetry.Name = "GET Values/Get"; + expectedRequestTelemetry.ResponseCode = "200"; + expectedRequestTelemetry.Success = true; + expectedRequestTelemetry.Url = new Uri(server.BaseHost + RequestPath); + + // this will force Request-Id header injection, it will start with |abc.123. + var activity = new Activity("dummy").SetParentId("|abc.123.").Start(); + var actualRequest = this.ValidateBasicRequest(server, RequestPath, expectedRequestTelemetry); + + Assert.Equal(32, actualRequest.tags["ai.operation.id"].Length); + Assert.StartsWith("|abc.123.", actualRequest.tags["ai.operation.parentId"]); + } + } + + [Fact] + public void TestW3CIsUsedWithoutHeadersWhenEnabledInConfig() + { + using (var server = new InProcessServer(assemblyName, this.output, + builder => + { + return builder.ConfigureServices(services => + { + services.AddApplicationInsightsTelemetry(o => o.RequestCollectionOptions.EnableW3CDistributedTracing = true); + }); + })) + { + const string RequestPath = "/api/values"; + + var expectedRequestTelemetry = new RequestTelemetry(); + expectedRequestTelemetry.Name = "GET Values/Get"; + expectedRequestTelemetry.ResponseCode = "200"; + expectedRequestTelemetry.Success = true; + expectedRequestTelemetry.Url = new Uri(server.BaseHost + RequestPath); + + var actualRequest = this.ValidateBasicRequest(server, RequestPath, expectedRequestTelemetry); + + Assert.Equal(32, actualRequest.tags["ai.operation.id"].Length); + Assert.Equal(1 + 32 + 1 + 16 + 1, actualRequest.data.baseData.id.Length); + } + } + + public void Dispose() + { + while (Activity.Current != null) + { + Activity.Current.Stop(); + } + } } } diff --git a/test/WebApi20.FunctionalTests/FunctionalTest/TelemetryModuleWorkingWebApiTests.cs b/test/WebApi20.FunctionalTests/FunctionalTest/TelemetryModuleWorkingWebApiTests.cs index 3cdc07d5..5f7ab6c9 100644 --- a/test/WebApi20.FunctionalTests/FunctionalTest/TelemetryModuleWorkingWebApiTests.cs +++ b/test/WebApi20.FunctionalTests/FunctionalTest/TelemetryModuleWorkingWebApiTests.cs @@ -1,11 +1,19 @@ namespace WebApi20.FunctionalTests.FunctionalTest { using FunctionalTestUtils; + using System; + using System.Diagnostics; + using System.Linq; + using Microsoft.ApplicationInsights.AspNetCore; + using Microsoft.ApplicationInsights.DependencyCollector; using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.W3C; + using Microsoft.Extensions.DependencyInjection; using Xunit; using Xunit.Abstractions; - public class TelemetryModuleWorkingWebApiTests : TelemetryTestsBase +#pragma warning disable 612, 618 + public class TelemetryModuleWorkingWebApiTests : TelemetryTestsBase, IDisposable { private const string assemblyName = "WebApi20.FunctionalTests20"; public TelemetryModuleWorkingWebApiTests(ITestOutputHelper output) : base (output) @@ -31,6 +39,51 @@ public void TestBasicDependencyPropertiesAfterRequestingBasicPage() } } + [Fact] + public void TestDependencyAndRequestWithW3CStandard() + { + const string RequestPath = "/api/values"; + + using (var server = new InProcessServer(assemblyName, this.output, builder => + { + return builder.ConfigureServices( + services => + { + services.AddApplicationInsightsTelemetry( + o => o.RequestCollectionOptions.EnableW3CDistributedTracing = true); + + // enable headers injection on localhost + var dependencyModuleConfigFactoryDescriptor = services.Where(sd => sd.ServiceType == typeof(ITelemetryModuleConfigurator)); + services.Remove(dependencyModuleConfigFactoryDescriptor.First()); + + services.ConfigureTelemetryModule((module, o) => + { + module.EnableW3CHeadersInjection = true; + }); + }); + })) + { + DependencyTelemetry expected = new DependencyTelemetry + { + ResultCode = "200", + Success = true, + Name = "GET " + RequestPath, + Data = server.BaseHost + RequestPath + }; + + var activity = new Activity("dummy") + .Start(); + + var (request, dependency) = this.ValidateBasicDependency(server, RequestPath, expected); + string expectedTraceId = activity.GetTraceId(); + string expectedParentSpanId = activity.GetSpanId(); + + Assert.Equal(expectedTraceId, request.tags["ai.operation.id"]); + Assert.Equal(expectedTraceId, dependency.tags["ai.operation.id"]); + Assert.Equal($"|{expectedTraceId}.{expectedParentSpanId}.", dependency.tags["ai.operation.parentId"]); + } + } + [Fact] public void TestIfPerformanceCountersAreCollected() { @@ -39,5 +92,14 @@ public void TestIfPerformanceCountersAreCollected() ValidatePerformanceCountersAreCollected(assemblyName); #endif } + + public void Dispose() + { + while (Activity.Current != null) + { + Activity.Current.Stop(); + } + } } +#pragma warning restore 612, 618 } diff --git a/test/WebApi20.FunctionalTests/Startup.cs b/test/WebApi20.FunctionalTests/Startup.cs index bd464605..2838a258 100644 --- a/test/WebApi20.FunctionalTests/Startup.cs +++ b/test/WebApi20.FunctionalTests/Startup.cs @@ -1,16 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.ApplicationInsights.Channel; using FunctionalTestUtils; -using System.IO; + namespace WebApi20.FunctionalTests { diff --git a/test/WebApi20.FunctionalTests/WebApi20.FunctionalTests20.csproj b/test/WebApi20.FunctionalTests/WebApi20.FunctionalTests20.csproj index 11643633..39978720 100644 --- a/test/WebApi20.FunctionalTests/WebApi20.FunctionalTests20.csproj +++ b/test/WebApi20.FunctionalTests/WebApi20.FunctionalTests20.csproj @@ -1,4 +1,4 @@ - + 2.0.0 @@ -14,9 +14,6 @@ ..\..\artifacts\obj\test\$(MSBuildProjectName) pdbonly true - - Library -