Skip to content
This repository was archived by the owner on Jul 5, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ffcff4c
Support W3C propagation in the process and http injection
Jun 26, 2018
c828db3
Set context by default
Jun 26, 2018
cfdc21f
fixes
Jun 27, 2018
bf8a8c4
Enable W3C on desktop: requests and dependencies
Jun 29, 2018
070fe7d
fix broken tests
Jun 29, 2018
57ef672
helpers and fixes
Jun 29, 2018
10dc70a
fix no w3c context in parent
Jun 29, 2018
f330c04
changelog and more tests
Jun 30, 2018
603e925
review comments, tracestate proper ordering and tracestate in the pro…
Jul 12, 2018
43a344a
0 default sampling
Jul 12, 2018
0cb176a
correlation backward compatibility via legacy ids in custom dimensions
Aug 2, 2018
fa60df5
Enable W3C via setting, not env var
Aug 2, 2018
a91c71a
minor fixes
Aug 2, 2018
2ebed64
fix stylecop
Aug 3, 2018
aec4fc4
limit visibility in common items
Aug 6, 2018
5c6b998
public w3c stuff
Aug 6, 2018
8d4f703
minor fixes
Aug 6, 2018
61aac47
minor fixes
Aug 6, 2018
f627813
some validation
Aug 9, 2018
34cdab0
more validations and code review
Aug 10, 2018
4f229b1
Tracestate and Traceparent
Aug 10, 2018
6bcad79
@ before msappid
Aug 13, 2018
78b4573
Merge branch 'develop' into lmolkova/W3CSupportOnCore
Aug 13, 2018
08ea8c5
rename GetTraceState to GetTracestate
Aug 13, 2018
7d67622
Merge branch 'lmolkova/W3CSupportOnCore' of https://github.com/Micros…
Aug 13, 2018
5735f66
traceid and spanid validation, az alias for all azure trace states
Aug 15, 2018
84a561c
fix separators
Aug 15, 2018
82faad3
Merge branch 'develop' into lmolkova/W3CSupportOnCore
Aug 15, 2018
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Author

Choose a reason for hiding this comment

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

why?

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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