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

Support W3C distributed tracing standard #945

Merged
merged 28 commits into from
Aug 15, 2018
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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.8.0-beta1
- [Adds opt-in support for W3C distributed tracing standard](https://github.com/Microsoft/ApplicationInsights-dotnet-server/pull/945)

## Version 2.7.2
- [Fix ServiceBus requests correlation](https://github.com/Microsoft/ApplicationInsights-dotnet-server/issues/970)

Expand Down
3 changes: 3 additions & 0 deletions Src/Common/Common.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
<Compile Include="$(MSBuildThisFileDirectory)RequestResponseHeaders.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SdkVersionUtils.cs" />
<Compile Include="$(MSBuildThisFileDirectory)StringUtilities.cs" />
<Compile Include="$(MSBuildThisFileDirectory)W3C\W3CActivityExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)W3C\W3CConstants.cs" />
<Compile Include="$(MSBuildThisFileDirectory)W3C\W3COperationCorrelationTelemetryInitializer.cs" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'netstandard1.6' ">
<Compile Include="$(MSBuildThisFileDirectory)WebHeaderCollectionExtensions.cs" />
Expand Down
34 changes: 32 additions & 2 deletions Src/Common/InjectionGuardConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,46 @@
/// These max limits are intentionally exaggerated to allow for unexpected responses, while still guarding against unreasonably large responses.
/// Example: While a 32 character response may be expected, 50 characters may be permitted while a 10,000 character response would be unreasonable and malicious.
/// </summary>
public static class InjectionGuardConstants
#if DEPENDENCY_COLLECTOR
public
#else
internal
#endif
static class InjectionGuardConstants
{
/// <summary>
/// Max length of AppId allowed in response from Breeze.
/// </summary>
public const int AppIdMaxLengeth = 50;
public const int AppIdMaxLength = 50;

/// <summary>
/// Max length of incoming Request Header value allowed.
/// </summary>
public const int RequestHeaderMaxLength = 1024;

/// <summary>
/// Max length of context header key.
/// </summary>
public const int ContextHeaderKeyMaxLength = 50;

/// <summary>
/// Max length of context header value.
/// </summary>
public const int ContextHeaderValueMaxLength = 1024;

/// <summary>
/// Max length of traceparent header value.
/// </summary>
public const int TraceParentHeaderMaxLength = 55;

/// <summary>
/// Max length of tracestate header value string.
/// </summary>
public const int TraceStateHeaderMaxLength = 512;

/// <summary>
/// Max number of key value pairs in the tracestate header.
/// </summary>
public const int TraceStateMaxPairs = 32;
}
}
61 changes: 56 additions & 5 deletions Src/Common/StringUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
/// <summary>
/// Generic functions to perform common operations on a string.
/// </summary>
public static class StringUtilities
#if DEPENDENCY_COLLECTOR
public
Copy link
Contributor

Choose a reason for hiding this comment

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

I like hiding this class from public surface. What's the reason to keep it public?

Copy link
Member Author

Choose a reason for hiding this comment

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

this class is used by AspNetCore SDK and should be visible to it. There are two alternatives:

  • copy paste (as it is done now and i'm getting read of it by this PR)
  • move common code to based SDK. I'm not fond of it because most of W3C code will go away from AI SDK eventually and I'd prefer to keep it all in one place. We can move other StringUtilities common methods in different PR

#else
internal
#endif
static class StringUtilities
{
private static readonly uint[] Lookup32 = CreateLookup32();

Expand Down Expand Up @@ -35,11 +40,57 @@ public static string EnforceMaxLength(string input, int maxLength)
/// <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();
return GenerateId(Guid.NewGuid().ToByteArray(), 0, 16);
}

/// <summary>
/// Generates random span Id as per W3C Distributed tracing specification.
/// https://github.com/w3c/distributed-tracing/blob/master/trace_context/HTTP_HEADER_FORMAT.md#span-id
/// </summary>
/// <returns>Random 8 bytes array encoded as hex string</returns>
public static string GenerateSpanId()
{
return GenerateId(Guid.NewGuid().ToByteArray(), 0, 8);
}

/// <summary>
/// Formats trace Id and span Id into valid Request-Id: |trace.span.
/// </summary>
/// <param name="traceId">Trace Id.</param>
/// <param name="spanId">Span id.</param>
/// <returns>valid Request-Id.</returns>
public static string FormatRequestId(string traceId, string spanId)
{
return String.Concat("|", traceId, ".", spanId, ".");
}

var result = new char[32];
for (int i = 0; i < 16; i++)
/// <summary>
/// Gets root id (string between '|' and the first dot) from the hierarchical Id.
/// </summary>
/// <param name="hierarchicalId">Id to extract root from.</param>
/// <returns>Root operation id.</returns>
internal static string GetRootId(string hierarchicalId)
{
// Returns the root Id from the '|' to the first '.' if any.
int rootEnd = hierarchicalId.IndexOf('.');
if (rootEnd < 0)
{
rootEnd = hierarchicalId.Length;
}

int rootStart = hierarchicalId[0] == '|' ? 1 : 0;
return hierarchicalId.Substring(rootStart, rootEnd - rootStart);
}

/// <summary>
/// Converts byte array to hex lower case string.
/// </summary>
/// <returns>Array encoded as hex string</returns>
private static string GenerateId(byte[] bytes, int start, int length)
{
// See https://stackoverflow.com/questions/311165/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-and-vice-versa/24343727#24343727
var result = new char[length * 2];
for (int i = start; i < start + length; i++)
{
var val = Lookup32[bytes[i]];
result[2 * i] = (char)val;
Expand Down
255 changes: 255 additions & 0 deletions Src/Common/W3C/W3CActivityExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
namespace Microsoft.ApplicationInsights.W3C
{
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Microsoft.ApplicationInsights.Common;

/// <summary>
/// Extends Activity to support W3C distributed tracing standard.
/// </summary>
[Obsolete("Not ready for public consumption.")]
Copy link
Contributor

Choose a reason for hiding this comment

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

will this Obsolete attribute apply to extension methods inside? I just don't know how it works for extensions...

[EditorBrowsable(EditorBrowsableState.Never)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Same question here - should it be applied to individual methods?

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

#if DEPENDENCY_COLLECTOR
public
#else
internal
#endif
static class W3CActivityExtensions
{
/// <summary>
/// Generate new W3C context.
/// </summary>
/// <param name="activity">Activity to generate W3C context on.</param>
/// <returns>The same Activity for chaining.</returns>
[Obsolete("Not ready for public consumption.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static Activity GenerateW3CContext(this Activity activity)
{
activity.SetVersion(W3CConstants.DefaultVersion);
activity.SetSampled(W3CConstants.TraceFlagRecordedAndNotRequested);
activity.SetSpanId(StringUtilities.GenerateSpanId());
activity.SetTraceId(StringUtilities.GenerateTraceId());
return activity;
}

/// <summary>
/// Checks if current Actuvuty has W3C properties on it.
/// </summary>
/// <param name="activity">Activity to check.</param>
/// <returns>True if Activity has W3C properties, false otherwise.</returns>
[Obsolete("Not ready for public consumption.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool IsW3CActivity(this Activity activity)
{
return activity != null && activity.Tags.Any(t => t.Key == W3CConstants.TraceIdTag);
}

/// <summary>
/// Updates context on the Activity based on the W3C Context in the parent Activity tree.
/// </summary>
/// <param name="activity">Activity to update W3C context on.</param>
/// <returns>The same Activity for chaining.</returns>
[Obsolete("Not ready for public consumption.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static Activity UpdateContextOnActivity(this Activity activity)
{
if (activity == null || activity.Tags.Any(t => t.Key == W3CConstants.TraceIdTag))
{
return activity;
}

// no w3c Tags on Activity
activity.Parent.UpdateContextOnActivity();

// at this point, Parent has W3C tags, but current activity does not - update it
return activity.UpdateContextFromParent();
}

/// <summary>
/// Gets traceparent header value for the Activity or null if there is no W3C context on it.
/// </summary>
/// <param name="activity">Activity to read W3C context from.</param>
/// <returns>traceparent header value.</returns>
[Obsolete("Not ready for public consumption.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static string GetTraceparent(this Activity activity)
{
string version = null, traceId = null, spanId = null, sampled = null;
foreach (var tag in activity.Tags)
{
switch (tag.Key)
{
case W3CConstants.TraceIdTag:
Copy link
Member

Choose a reason for hiding this comment

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

minor: sort the case statements either alphabetically or following the physical order of fields in traceparent.

Copy link
Member Author

Choose a reason for hiding this comment

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

why?

Copy link
Member

Choose a reason for hiding this comment

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

This would make it easier for people to read and maintain the code, similar like how we organize the using namespace statements - not a must have but definitely helpful.

Copy link
Member Author

Choose a reason for hiding this comment

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

while this makes sense, I do have them ordered based on the importance and think this is an appropriate order.

traceId = tag.Value;
break;
case W3CConstants.SpanIdTag:
spanId = tag.Value;
break;
case W3CConstants.VersionTag:
version = tag.Value;
break;
case W3CConstants.SampledTag:
sampled = tag.Value;
break;
}
}

if (traceId == null || spanId == null || version == null || sampled == null)
{
return null;
}

return string.Join("-", version, traceId, spanId, sampled);
}

/// <summary>
/// Initializes W3C context on the Activity from traceparent header value.
/// </summary>
/// <param name="activity">Activity to set W3C context on.</param>
/// <param name="value">Valid traceparent header like 00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01.</param>
[Obsolete("Not ready for public consumption.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static void SetTraceparent(this Activity activity, string value)
{
if (value != null)
{
var parts = value.Trim(' ', '-').Split('-');
if (parts.Length == 4 && !activity.IsW3CActivity())
{
string traceId = parts[1];
string parentSpanId = parts[2];

byte.TryParse(parts[3], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var sampled);

if (traceId.Length == 32 && parentSpanId.Length == 16)
Copy link
Contributor

Choose a reason for hiding this comment

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

we'll need to check for characters to be hex.

Copy link
Member Author

Choose a reason for hiding this comment

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

the spec now says that app MAY ignore the traceid/spanid if they are not valid. I remember we discussed that implementation will have to validate traceis/spanid and ignore invalid ones. I'll be happy to do this after spec will tell so :)

{
// we only support 00 version and ignore caller version
activity.SetVersion(W3CConstants.DefaultVersion);

// we always defer sampling
if ((sampled & W3CConstants.RequestedTraceFlag) == W3CConstants.RequestedTraceFlag)
{
activity.SetSampled(W3CConstants.TraceFlagRecordedAndRequested);
}
else
{
activity.SetSampled(W3CConstants.TraceFlagRecordedAndNotRequested);
}

activity.SetParentSpanId(parentSpanId);
activity.SetSpanId(StringUtilities.GenerateSpanId());
activity.SetTraceId(traceId);
}
}
}
}

/// <summary>
/// Gets tracestate header value from the Activity.
/// </summary>
/// <param name="activity">Activity to get tracestate from.</param>
/// <returns>tracestate header value.</returns>
[Obsolete("Not ready for public consumption.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static string GetTracestate(this Activity activity) =>
activity.Tags.FirstOrDefault(t => t.Key == W3CConstants.TracestateTag).Value;

/// <summary>
/// Sets tracestate header value on the Activity.
/// </summary>
/// <param name="activity">Activity to set tracestate on.</param>
/// <param name="value">tracestate header value.</param>
[Obsolete("Not ready for public consumption.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static void SetTraceState(this Activity activity, string value) =>
activity.AddTag(W3CConstants.TracestateTag, value);

/// <summary>
/// Gets TraceId from the Activity.
/// Use carefully: if may cause iteration over all tags!
/// </summary>
/// <param name="activity">Activity to get traceId from.</param>
/// <returns>TraceId value or null if it does not exist.</returns>
[Obsolete("Not ready for public consumption.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static string GetTraceId(this Activity activity) => activity.Tags.FirstOrDefault(t => t.Key == W3CConstants.TraceIdTag).Value;

/// <summary>
/// Gets SpanId from the Activity.
/// Use carefully: if may cause iteration over all tags!
/// </summary>
/// <param name="activity">Activity to get spanId from.</param>
/// <returns>SpanId value or null if it does not exist.</returns>
[Obsolete("Not ready for public consumption.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static string GetSpanId(this Activity activity) => activity.Tags.FirstOrDefault(t => t.Key == W3CConstants.SpanIdTag).Value;

/// <summary>
/// Gets ParentSpanId from the Activity.
/// Use carefully: if may cause iteration over all tags!
/// </summary>
/// <param name="activity">Activity to get ParentSpanId from.</param>
/// <returns>ParentSpanId value or null if it does not exist.</returns>
[Obsolete("Not ready for public consumption.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static string GetParentSpanId(this Activity activity) => activity.Tags.FirstOrDefault(t => t.Key == W3CConstants.ParentSpanIdTag).Value;

[Obsolete("Not ready for public consumption.")]
[EditorBrowsable(EditorBrowsableState.Never)]
internal static void SetParentSpanId(this Activity activity, string value) =>
activity.AddTag(W3CConstants.ParentSpanIdTag, value);

private static void SetTraceId(this Activity activity, string value) =>
activity.AddTag(W3CConstants.TraceIdTag, value);

private static void SetSpanId(this Activity activity, string value) =>
activity.AddTag(W3CConstants.SpanIdTag, value);

private static void SetVersion(this Activity activity, string value) =>
activity.AddTag(W3CConstants.VersionTag, value);

private static void SetSampled(this Activity activity, string value) =>
activity.AddTag(W3CConstants.SampledTag, value);

private static Activity UpdateContextFromParent(this Activity activity)
{
if (activity != null && activity.Tags.All(t => t.Key != W3CConstants.TraceIdTag))
{
if (activity.Parent == null)
{
activity.GenerateW3CContext();
}
else
{
foreach (var tag in activity.Parent.Tags)
{
switch (tag.Key)
{
case W3CConstants.TraceIdTag:
activity.SetTraceId(tag.Value);
break;
case W3CConstants.SpanIdTag:
activity.SetParentSpanId(tag.Value);
activity.SetSpanId(StringUtilities.GenerateSpanId());
break;
case W3CConstants.VersionTag:
activity.SetVersion(tag.Value);
break;
case W3CConstants.SampledTag:
activity.SetSampled(tag.Value);
break;
case W3CConstants.TracestateTag:
activity.SetTraceState(tag.Value);
break;
}
}
}
}

return activity;
}
}
}
Loading