Skip to content
This repository has been archived by the owner on Jul 5, 2020. It is now read-only.

Use W3C trace Id format for Activities that do not have parents #952

Merged
merged 5 commits into from
Jul 18, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## Version 2.7.0-beta4
- [When there is no parent operation, generate W3C compatible operation Id](https://github.com/Microsoft/ApplicationInsights-dotnet-server/pull/952)

## Version 2.7.0-beta3
- [Fix: SerializationException resolving Activity in cross app-domain calls](https://github.com/Microsoft/ApplicationInsights-dotnet-server/issues/613)
- [Fix: Race condition in generic diagnostic source listener](https://github.com/Microsoft/ApplicationInsights-dotnet-server/pull/948)
Expand Down
31 changes: 31 additions & 0 deletions Src/Common/ActivityExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Microsoft.ApplicationInsights.Common
{
using System.Diagnostics;

internal static class ActivityExtensions
{
public static Activity UpdateParent(this Activity original, string newParentId)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unittest this method

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

{
if (original == null || original.Parent != null)
{
return original;
}

var auxActivity = new Activity(original.OperationName)
.SetParentId(newParentId)
.SetStartTime(original.StartTimeUtc);

foreach (var baggageItem in original.Baggage)
{
auxActivity.AddBaggage(baggageItem.Key, baggageItem.Value);
}

foreach (var tag in original.Tags)
{
auxActivity.AddTag(tag.Key, tag.Value);
}

return auxActivity.Start();
}
}
}
1 change: 1 addition & 0 deletions Src/Common/Common.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<Import_RootNamespace>Microsoft.ApplicationInsights.Common</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)ActivityExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)AppMapCorrelationEventSource.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConditionalWeakTableExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExceptionUtilities.cs" />
Expand Down
37 changes: 37 additions & 0 deletions Src/Common/StringUtilities.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Microsoft.ApplicationInsights.Common
{
using System;
using System.Diagnostics;
using System.Globalization;

Expand All @@ -8,6 +9,8 @@
/// </summary>
public static class StringUtilities
{
private static readonly uint[] Lookup32 = CreateLookup32();

/// <summary>
/// Check a strings length and trim to a max length if needed.
/// </summary>
Expand All @@ -24,5 +27,39 @@ public static string EnforceMaxLength(string input, int maxLength)

return input;
}

/// <summary>
/// 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
/// </summary>
/// <returns>Random 16 bytes array encoded as hex string</returns>
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();

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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.ApplicationInsights.Common;
Expand Down Expand Up @@ -167,7 +168,7 @@ public async Task TestDependencyCollectionWithParentActivity()

parent.Stop();

this.ValidateTelemetryForDiagnosticSource(this.sentTelemetry.Single(), url, request, true, "200", false);
this.ValidateTelemetryForDiagnosticSource(this.sentTelemetry.Single(), url, request, true, "200", false, parent);

Assert.AreEqual("k=v", request.Headers.GetValues(RequestResponseHeaders.CorrelationContextHeader).Single());
}
Expand All @@ -193,7 +194,7 @@ public async Task TestDependencyCollectionDnsIssue()
}
}

private void ValidateTelemetryForDiagnosticSource(DependencyTelemetry item, Uri url, HttpRequestMessage request, bool success, string resultCode, bool expectLegacyHeaders)
private void ValidateTelemetryForDiagnosticSource(DependencyTelemetry item, Uri url, HttpRequestMessage request, bool success, string resultCode, bool expectLegacyHeaders, Activity parent = null)
{
Assert.AreEqual(url, item.Data);
Assert.AreEqual(url.Host, item.Target);
Expand All @@ -215,6 +216,19 @@ private void ValidateTelemetryForDiagnosticSource(DependencyTelemetry item, Uri

var requestId = item.Id;
Assert.IsTrue(requestId.StartsWith('|' + item.Context.Operation.Id + '.'));

if (parent == null)
{
// W3C compatible-Id ( should go away when W3C is implemented in .NET https://github.com/dotnet/corefx/issues/30331)
Assert.AreEqual(32, item.Context.Operation.Id.Length);
Assert.IsTrue(Regex.Match(item.Context.Operation.Id, @"[a-z][0-9]").Success);
// end of workaround test
}
else
{
Assert.AreEqual(parent.RootId, item.Context.Operation.Id);
}

if (request != null)
{
Assert.AreEqual(requestId, request.Headers.GetValues(RequestResponseHeaders.RequestIdHeader).Single());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ public void OnActivityStartInjectsLegacyHeaders()

// Request-Id and Correlation-Context are injected by HttpClient
// check only legacy headers here
Assert.AreEqual(activity.RootId, request.Headers.GetValues(RequestResponseHeaders.StandardRootIdHeader).Single());
Assert.AreEqual(activity.Id, request.Headers.GetValues(RequestResponseHeaders.StandardParentIdHeader).Single());
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));
}

Expand All @@ -82,8 +82,8 @@ public void OnActivityStopTracksTelemetry()
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUrlWithScheme);
this.listener.OnActivityStart(request);

activity = Activity.Current;
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
activity.SetEndTime(startTime.AddSeconds(1));
this.listener.OnActivityStop(response, request, TaskStatus.RanToCompletion);

var telemetry = this.sentTelemetry.Single() as DependencyTelemetry;
Expand All @@ -96,7 +96,8 @@ public void OnActivityStopTracksTelemetry()
Assert.AreEqual(true, telemetry.Success);

Assert.AreEqual(activity.StartTimeUtc, telemetry.Timestamp);
Assert.AreEqual(1, telemetry.Duration.TotalSeconds);
Assert.IsTrue(1 <= telemetry.Duration.TotalSeconds);
Assert.IsTrue(2 > telemetry.Duration.TotalSeconds);

Assert.AreEqual(activity.RootId, telemetry.Context.Operation.Id);
Assert.AreEqual(activity.ParentId, telemetry.Context.Operation.ParentId);
Expand All @@ -111,6 +112,39 @@ public void OnActivityStopTracksTelemetry()
this.ValidateOperationDetails(telemetry);
}

/// <summary>
/// Tests that OnStopActivity tracks telemetry.
/// </summary>
[TestMethod]
public void OnActivityStopWithParentTracksTelemetry()
{
var parent = new Activity("parent")
.AddBaggage("k", "v")
.Start();

var activity = new Activity("System.Net.Http.HttpRequestOut").Start();

HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUrlWithScheme);
this.listener.OnActivityStart(request);

HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
this.listener.OnActivityStop(response, request, TaskStatus.RanToCompletion);

var telemetry = this.sentTelemetry.Single() as DependencyTelemetry;

Assert.AreEqual(parent.RootId, telemetry.Context.Operation.Id);
Assert.AreEqual(parent.Id, telemetry.Context.Operation.ParentId);
Assert.AreEqual(activity.Id, telemetry.Id);
Assert.AreEqual("v", telemetry.Context.Properties["k"]);

string expectedVersion =
SdkVersionHelper.GetExpectedSdkVersion(typeof(DependencyTrackingTelemetryModule), prefix: "rdddsc:");
Assert.AreEqual(expectedVersion, telemetry.Context.GetInternalContext().SdkVersion);

// Check the operation details
this.ValidateOperationDetails(telemetry);
}

/// <summary>
/// Tests that OnStopActivity tracks cancelled request.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
Expand Down Expand Up @@ -94,6 +95,36 @@ public void EventHubsSuccessfulSendIsHandled()
}
}

[TestMethod]
public void EventHubsSuccessfulSendIsHandledWithoutParent()
{
using (var module = new DependencyTrackingTelemetryModule())
{
module.IncludeDiagnosticSourceActivities.Add("Microsoft.Azure.EventHubs");
module.Initialize(this.configuration);

DiagnosticListener listener = new DiagnosticListener("Microsoft.Azure.EventHubs");

var telemetry = this.TrackOperation<DependencyTelemetry>(listener, "Microsoft.Azure.EventHubs.Send", TaskStatus.RanToCompletion);

Assert.IsNotNull(telemetry);
Assert.AreEqual("Send", telemetry.Name);
Assert.AreEqual(RemoteDependencyConstants.AzureEventHubs, telemetry.Type);
Assert.AreEqual("sb://eventhubname.servicebus.windows.net/ | ehname", telemetry.Target);
Assert.IsTrue(telemetry.Success.Value);

// W3C compatible-Id ( should go away when W3C is implemented in .NET https://github.com/dotnet/corefx/issues/30331)
Assert.AreEqual(32, telemetry.Context.Operation.Id.Length);
Assert.IsTrue(Regex.Match(telemetry.Context.Operation.Id, @"[a-z][0-9]").Success);
// end of workaround test

Assert.AreEqual("eventhubname.servicebus.windows.net", telemetry.Properties["peer.hostname"]);
Assert.AreEqual("ehname", telemetry.Properties["eh.event_hub_name"]);
Assert.AreEqual("SomePartitionKeyHere", telemetry.Properties["eh.partition_key"]);
Assert.AreEqual("EventHubClient1(ehname)", telemetry.Properties["eh.client_id"]);
}
}

[TestMethod]
public void EventHubsFailedSendIsHandled()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
Expand Down Expand Up @@ -85,6 +86,33 @@ public void ServiceBusSendHanding()
}
}

[TestMethod]
public void ServiceBusSendHandingWithoutParent()
{
using (var module = new DependencyTrackingTelemetryModule())
{
module.IncludeDiagnosticSourceActivities.Add("Microsoft.Azure.ServiceBus");
module.Initialize(this.configuration);

DiagnosticListener listener = new DiagnosticListener("Microsoft.Azure.ServiceBus");

var telemetry = this.TrackOperation<DependencyTelemetry>(listener, "Microsoft.Azure.ServiceBus.Send", TaskStatus.RanToCompletion);

Assert.IsNotNull(telemetry);
Assert.AreEqual("Send", telemetry.Name);
Assert.AreEqual(RemoteDependencyConstants.AzureServiceBus, telemetry.Type);
Assert.AreEqual("sb://queuename.myservicebus.com/ | queueName", telemetry.Target);
Assert.IsTrue(telemetry.Success.Value);

// W3C compatible-Id ( should go away when W3C is implemented in .NET https://github.com/dotnet/corefx/issues/30331)
Assert.AreEqual(32, telemetry.Context.Operation.Id.Length);
Assert.IsTrue(Regex.Match(telemetry.Context.Operation.Id, @"[a-z][0-9]").Success);
// end of workaround test

Assert.AreEqual("messageId", telemetry.Properties["MessageId"]);
}
}

[TestMethod]
public void ServiceBusBadStatusHanding()
{
Expand Down Expand Up @@ -136,6 +164,32 @@ public void ServiceBusProcessHanding()
}
}

[TestMethod]
public void ServiceBusProcessHandingWithoutParent()
{
using (var module = new DependencyTrackingTelemetryModule())
{
module.IncludeDiagnosticSourceActivities.Add("Microsoft.Azure.ServiceBus");
module.Initialize(this.configuration);

DiagnosticListener listener = new DiagnosticListener("Microsoft.Azure.ServiceBus");

var telemetry = this.TrackOperation<RequestTelemetry>(listener, "Microsoft.Azure.ServiceBus.Process", TaskStatus.RanToCompletion);

Assert.IsNotNull(telemetry);
Assert.AreEqual("Process", telemetry.Name);
Assert.AreEqual($"type:{RemoteDependencyConstants.AzureServiceBus} | name:queueName | endpoint:sb://queuename.myservicebus.com/", telemetry.Source);
Assert.IsTrue(telemetry.Success.Value);

// W3C compatible-Id ( should go away when W3C is implemented in .NET https://github.com/dotnet/corefx/issues/30331)
Assert.AreEqual(32, telemetry.Context.Operation.Id.Length);
Assert.IsTrue(Regex.Match(telemetry.Context.Operation.Id, @"[a-z][0-9]").Success);
// end of workaround test

Assert.AreEqual("messageId", telemetry.Properties["MessageId"]);
}
}

[TestMethod]
public void ServiceBusExceptionsAreIgnored()
{
Expand Down
27 changes: 25 additions & 2 deletions Src/DependencyCollector/Shared/HttpCoreDiagnosticSourceListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ internal void OnException(Exception exception, HttpRequestMessage request)
DependencyCollectorEventSource.Log.CurrentActivityIsNull(HttpExceptionEventName);
return;
}

DependencyCollectorEventSource.Log.HttpCoreDiagnosticSourceListenerException(currentActivity.Id);

this.pendingExceptions.TryAdd(currentActivity.Id, exception);
Expand Down Expand Up @@ -296,6 +296,20 @@ internal void OnActivityStart(HttpRequestMessage request)
return;
}

// 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 parent Activity (i.e. this request has happened in the background, without parent scope), we'll override
// the current Activity with the one with properly formatted Id. This workaround should go away
// with W3C support on .NET https://github.com/dotnet/corefx/issues/30331
if (currentActivity.Parent == null)
{
currentActivity.UpdateParent(StringUtilities.GenerateTraceId());
}

// end of workaround

DependencyCollectorEventSource.Log.HttpCoreDiagnosticSourceListenerStart(currentActivity.Id);

this.InjectRequestHeaders(request, this.configuration.InstrumentationKey);
Expand All @@ -322,6 +336,12 @@ 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;
Expand Down Expand Up @@ -384,7 +404,10 @@ internal void OnRequest(HttpRequestMessage request, Guid loggingRequestId)
Uri requestUri = request.RequestUri;
var resourceName = request.Method.Method + " " + requestUri.AbsolutePath;

var dependency = this.client.StartOperation<DependencyTelemetry>(resourceName);
var dependency = Activity.Current != null ?
this.client.StartOperation<DependencyTelemetry>(resourceName) :
this.client.StartOperation<DependencyTelemetry>(resourceName, StringUtilities.GenerateTraceId());

dependency.Telemetry.Target = requestUri.Host;
dependency.Telemetry.Type = RemoteDependencyConstants.HTTP;
dependency.Telemetry.Data = requestUri.OriginalString;
Expand Down
Loading