Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
17492eb
Removed auto-instrumentation suppression if request contains traceparent
denisivan0v Dec 6, 2021
06e16de
Typo
denisivan0v Dec 6, 2021
a72d148
net461 build fix
denisivan0v Dec 6, 2021
c0081fa
Added request retry instrumentation
denisivan0v Dec 6, 2021
51f9c7d
net461 test asserts adjusted
denisivan0v Dec 6, 2021
e525d4e
Merge branch 'main' into denisivan0v/http-retries-redirects
denisivan0v Dec 16, 2021
ee7a37f
Added HttpWebRequest instrumentation
denisivan0v Dec 17, 2021
ae35eea
Code style fix
denisivan0v Dec 17, 2021
5cf93b4
Code fixes
denisivan0v Dec 17, 2021
0a6bf0b
Two tests adjusted to the new behavior
denisivan0v Dec 17, 2021
a477f2a
Code style fixes
denisivan0v Dec 17, 2021
58e92f5
Code and tests fixes
denisivan0v Dec 17, 2021
b961b78
HttpWebRequestInstrumentationWorksIfAlreadyInstrumented test adjusted
denisivan0v Dec 17, 2021
5980e60
Test fix
denisivan0v Dec 17, 2021
d669628
More test adjustments
denisivan0v Dec 17, 2021
1a20acc
More test adjustments
denisivan0v Dec 17, 2021
138120e
Minors
denisivan0v Dec 18, 2021
3cec856
Test fixes
denisivan0v Dec 18, 2021
203df89
Experiment with AsyncLocal
denisivan0v Dec 18, 2021
770907c
AsyncLocal adjustments
denisivan0v Dec 20, 2021
54859f9
TestRedirectedRequest test adjusted
denisivan0v Dec 20, 2021
ce1a7a9
Naming
denisivan0v Dec 20, 2021
a771451
Improved implementation and added tests
denisivan0v Dec 21, 2021
2adc1af
Code style fix
denisivan0v Dec 21, 2021
abed0c1
Code style fix
denisivan0v Dec 21, 2021
f7fef66
Trigger Build
denisivan0v Dec 21, 2021
63358b3
Update test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestAc…
denisivan0v Dec 30, 2021
2a693e1
Update src/OpenTelemetry.Instrumentation.Http/HttpRequestMessageConte…
denisivan0v Jan 27, 2022
c8e2fd0
Merge branch 'main' into denisivan0v/http-retries-redirects
denisivan0v Jan 27, 2022
1145ef7
Indentation fixed
denisivan0v Jan 27, 2022
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
2 changes: 2 additions & 0 deletions src/OpenTelemetry.Api/Internal/SpanAttributeConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ internal static class SpanAttributeConstants
{
public const string StatusCodeKey = "otel.status_code";
public const string StatusDescriptionKey = "otel.status_description";
public const string PreviousTryContextKey = "otel.previous_try_context";
public const string RetryCountKey = "http.retry_count";
public const string DatabaseStatementTypeKey = "db.statement_type";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ internal static class HttpRequestMessageContextPropagation

internal static Action<HttpRequestMessage, string, string> HeaderValueSetter => (request, name, value) =>
{
request.Headers.Remove(name);
request.Headers.Add(name, value);
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ internal sealed class HttpHandlerDiagnosticListener : ListenerHandler
internal static readonly Version Version = AssemblyName.Version;
internal static readonly ActivitySource ActivitySource = new ActivitySource(ActivitySourceName, Version.ToString());

private const string RestartedActivityKey = "dotnet.restarted_activity";

private static readonly Regex CoreAppMajorVersionCheckRegex = new Regex("^\\.NETCoreApp,Version=v(\\d+)\\.", RegexOptions.Compiled | RegexOptions.IgnoreCase);

private readonly PropertyFetcher<HttpRequestMessage> startRequestFetcher = new PropertyFetcher<HttpRequestMessage>("Request");
Expand Down Expand Up @@ -88,14 +90,32 @@ public override void OnStartActivity(Activity activity, object payload)
return;
}

// TODO: Investigate why this check is needed.
if (Propagators.DefaultTextMapPropagator.Extract(default, request, HttpRequestMessageContextPropagation.HeaderValuesGetter) != default)
var context = Propagators.DefaultTextMapPropagator.Extract(default, request, HttpRequestMessageContextPropagation.HeaderValuesGetter);
if (context != default && request.Properties.TryGetValue(SpanAttributeConstants.PreviousTryContextKey, out var previousContext))
{
// this request is already instrumented, we should back off
// Handling request retry or redirect.
var retryCount = 1;
if (request.Properties.TryGetValue(SpanAttributeConstants.RetryCountKey, out var previousRetryCount))
{
retryCount = (int)previousRetryCount + 1;
}

// Suppressing activity started by HttpClient DiagnosticsHandler.
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 section is a bit unclear without some examples and explanation.
some questions:

  1. Does httpClient diagnostichandler created a single Activity only, or does it create one for every retry?
  2. Which one are we stopping here.
  3. We are creating a new activity here - is it supposed to be the child of the original http client activity or is it expected to be its sibling (i.e same parent, likely that of the Asp.Net Core Request)
  4. Why do we explicitly set Activity.Current in line 108?

activity.IsAllDataRequested = false;
return;
activity.Stop();

activity = ActivitySource.StartActivity(activity.Kind, context.ActivityContext, links: new[] { new ActivityLink((ActivityContext)previousContext) });
Activity.Current = activity;

request.Properties[RestartedActivityKey] = activity;

activity.SetTag(SpanAttributeConstants.RetryCountKey, retryCount);
request.Properties[SpanAttributeConstants.RetryCountKey] = retryCount;
}

// Store activity context for the next possible try.
request.Properties[SpanAttributeConstants.PreviousTryContextKey] = activity.Context;

// Propagate context irrespective of sampling decision
var textMapPropagator = Propagators.DefaultTextMapPropagator;
if (!(this.httpClientSupportsW3C && textMapPropagator is TraceContextPropagator))
Expand Down Expand Up @@ -195,6 +215,15 @@ public override void OnStopActivity(Activity activity, object payload)
HttpInstrumentationEventSource.Log.EnrichmentException(ex);
}
}

if (this.startRequestFetcher.TryFetch(payload, out HttpRequestMessage request) && request != null)
{
if (request.Properties.TryGetValue(RestartedActivityKey, out object restartedActivityInstance))
{
var restartedActivity = (Activity)restartedActivityInstance;
restartedActivity.Stop();
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Threading;
using OpenTelemetry.Context.Propagation;
using OpenTelemetry.Trace;

Expand All @@ -40,7 +41,7 @@ internal static class HttpWebRequestActivitySource
public const string ActivityName = ActivitySourceName + ".HttpRequestOut";

internal static readonly Func<HttpWebRequest, string, IEnumerable<string>> HttpWebRequestHeaderValuesGetter = (request, name) => request.Headers.GetValues(name);
internal static readonly Action<HttpWebRequest, string, string> HttpWebRequestHeaderValuesSetter = (request, name, value) => request.Headers.Add(name, value);
internal static readonly Action<HttpWebRequest, string, string> HttpWebRequestHeaderValuesSetter = (request, name, value) => request.Headers.Set(name, value);

internal static HttpWebRequestInstrumentationOptions Options = new HttpWebRequestInstrumentationOptions();

Expand Down Expand Up @@ -199,10 +200,6 @@ private static void AddExceptionTags(Exception exception, Activity activity)
private static void InstrumentRequest(HttpWebRequest request, ActivityContext activityContext)
=> Propagators.DefaultTextMapPropagator.Inject(new PropagationContext(activityContext, Baggage.Current), request, HttpWebRequestHeaderValuesSetter);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsRequestInstrumented(HttpWebRequest request)
=> Propagators.DefaultTextMapPropagator.Extract(default, request, HttpWebRequestHeaderValuesGetter) != default;

private static void ProcessRequest(HttpWebRequest request)
{
if (!WebRequestActivitySource.HasListeners() || !Options.EventFilter(request))
Expand All @@ -216,15 +213,40 @@ private static void ProcessRequest(HttpWebRequest request)
return;
}

if (IsRequestInstrumented(request))
if (RequestProperties.Instance == null)
{
RequestProperties.Instance = new Dictionary<string, object>();
}

Activity activity;
var context = Propagators.DefaultTextMapPropagator.Extract(default, request, HttpWebRequestHeaderValuesGetter);
if (context != default && RequestProperties.Instance.TryGetValue(SpanAttributeConstants.PreviousTryContextKey, out var previousContext))
{
// This request was instrumented by previous
// ProcessRequest, such is the case with redirect responses where the same request is sent again.
return;
// ProcessRequest, such is the case with retries or redirect responses where the same request is sent again.

var retryCount = 1;
if (RequestProperties.Instance.TryGetValue(SpanAttributeConstants.RetryCountKey, out var previousRetryCount))
{
retryCount = (int)previousRetryCount + 1;
}

activity = WebRequestActivitySource.StartActivity(ActivityName, ActivityKind.Client, context.ActivityContext, links: new[] { new ActivityLink((ActivityContext)previousContext) });

activity?.SetTag(SpanAttributeConstants.RetryCountKey, retryCount);
RequestProperties.Instance[SpanAttributeConstants.RetryCountKey] = retryCount;
}
else
{
activity = WebRequestActivitySource.StartActivity(ActivityName, ActivityKind.Client);
}

var activity = WebRequestActivitySource.StartActivity(ActivityName, ActivityKind.Client);
var activityContext = Activity.Current?.Context ?? default;
if (activityContext != default)
{
// Store activity context for the next possible try.
RequestProperties.Instance[SpanAttributeConstants.PreviousTryContextKey] = activityContext;
}

// Propagation must still be done in all cases, to allow
// downstream services to continue from parent context, if any.
Expand Down Expand Up @@ -636,6 +658,17 @@ private static Func<object[], T> CreateTypeInstance<T>(ConstructorInfo construct
return (Func<object[], T>)setterMethod.CreateDelegate(typeof(Func<object[], T>));
}

private static class RequestProperties
{
private static readonly AsyncLocal<Dictionary<string, object>> Properties = new AsyncLocal<Dictionary<string, object>>();

public static Dictionary<string, object> Instance
{
get => Properties.Value;
set => Properties.Value = value;
}
}

private class HashtableWrapper : Hashtable, IEnumerable
{
private readonly Hashtable table;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,10 @@ public async Task HttpClientInstrumentation_AddViaFactory_HttpInstrumentation_Co
}

[Fact]
public async Task HttpClientInstrumentationBacksOffIfAlreadyInstrumented()
public async Task HttpClientInstrumentationWorksIfAlreadyInstrumented()
{
// TODO: Investigate why this feature is required.
const string traceId = "0123456789abcdef0123456789abcdef";
const string parentSpanId = "0123456789abcdef";
var processor = new Mock<BaseProcessor<Activity>>();

var request = new HttpRequestMessage
Expand All @@ -283,7 +284,7 @@ public async Task HttpClientInstrumentationBacksOffIfAlreadyInstrumented()
Method = new HttpMethod("GET"),
};

request.Headers.Add("traceparent", "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01");
request.Headers.Add("traceparent", $"00-{traceId}-{parentSpanId}-01");

using (Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
Expand All @@ -294,7 +295,63 @@ public async Task HttpClientInstrumentationBacksOffIfAlreadyInstrumented()
await c.SendAsync(request);
}

Assert.Equal(4, processor.Invocations.Count); // SetParentProvider/OnShutdown/Dispose/OnStart called.
Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called.
var activity = (Activity)processor.Invocations[2].Arguments[0];

Assert.Equal(ActivityKind.Client, activity.Kind);
Assert.NotEqual(default, activity.Context.SpanId);
Assert.NotEqual(traceId, activity.TraceId.ToString());
Assert.NotEqual(parentSpanId, activity.SpanId.ToString());
Assert.NotEqual(parentSpanId, activity.ParentSpanId.ToString());
}

/// <summary>
/// Test request connection retry: reflection hook does not throw.
/// </summary>
[Theory]
[InlineData("GET")]
[InlineData("POST")]
public async Task TestSecureTransportRetryFailureRequest(string method)
{
var processor = new Mock<BaseProcessor<Activity>>();

var uriBuilder = new UriBuilder(this.url) { Scheme = "https" };

using (Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.AddProcessor(processor.Object)
.Build())
{
using (var client = new HttpClient(new SimpleRetryHandler(new HttpClientHandler(), maxRetryCount: 1)))
{
var ex = await Assert.ThrowsAnyAsync<Exception>(() =>
{
return method == "GET"
? client.GetAsync(uriBuilder.Uri)
: client.PostAsync(uriBuilder.Uri, new StringContent("hello world"));
});
Assert.True(ex is InvalidOperationException);
}
}

Assert.Equal(8, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnStart/OnStart/OnEnd/OnShutdown/Dispose called.
var firstTry = (Activity)processor.Invocations[2].Arguments[0];
var secondTry = (Activity)processor.Invocations[5].Arguments[0];

Assert.Equal(ActivityKind.Client, firstTry.Kind);
Assert.NotEqual(default, firstTry.Context.SpanId);

Assert.Equal(ActivityKind.Client, secondTry.Kind);
Assert.NotEqual(default, secondTry.Context.SpanId);

ActivityLink activityLink = secondTry.Links.FirstOrDefault();
Assert.NotEqual(default, activityLink);
Assert.Equal(firstTry.Context.TraceId, activityLink.Context.TraceId);
Assert.Equal(firstTry.Context.SpanId, activityLink.Context.SpanId);

var retryCount = secondTry.GetTagItem("http.retry_count");
Assert.NotNull(retryCount);
Assert.Equal(1, (int)retryCount);
}

[Fact]
Expand Down Expand Up @@ -438,6 +495,39 @@ private static void ActivityEnrichment(Activity activity, string method, object
break;
}
}

private sealed class SimpleRetryHandler : DelegatingHandler
{
private readonly int maxRetryCount;

public SimpleRetryHandler(HttpMessageHandler innerHandler, int maxRetryCount = 3)
: base(innerHandler)
{
this.maxRetryCount = maxRetryCount;
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage response = null;
for (int i = 0; i < this.maxRetryCount + 1; i++)
{
try
{
response = await base.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
return response;
}
}
catch
{
// ignored
}
}

return response;
}
}
}
}
#endif
Loading