963 lines
41 KiB
C#
963 lines
41 KiB
C#
// <copyright file="HttpWebRequestActivitySourceTests.netfx.cs" company="OpenTelemetry Authors">
|
|
// Copyright The OpenTelemetry Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
// </copyright>
|
|
#if NETFRAMEWORK
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using OpenTelemetry.Instrumentation.Dependencies.Implementation;
|
|
using OpenTelemetry.Internal.Test;
|
|
using OpenTelemetry.Trace;
|
|
using Xunit;
|
|
|
|
namespace OpenTelemetry.Instrumentation.Dependencies.Tests
|
|
{
|
|
public class HttpWebRequestActivitySourceTests : IDisposable
|
|
{
|
|
private readonly IDisposable testServer;
|
|
private readonly string testServerHost;
|
|
private readonly int testServerPort;
|
|
private readonly string hostNameAndPort;
|
|
|
|
static HttpWebRequestActivitySourceTests()
|
|
{
|
|
// Need to touch something in HttpWebRequestActivitySource to do the static injection.
|
|
GC.KeepAlive(HttpWebRequestActivitySource.Options);
|
|
}
|
|
|
|
public HttpWebRequestActivitySourceTests()
|
|
{
|
|
Assert.Null(Activity.Current);
|
|
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
|
|
Activity.ForceDefaultIdFormat = false;
|
|
|
|
this.testServer = TestHttpServer.RunServer(
|
|
ctx => ProcessServerRequest(ctx),
|
|
out this.testServerHost,
|
|
out this.testServerPort);
|
|
|
|
this.hostNameAndPort = $"{this.testServerHost}:{this.testServerPort}";
|
|
|
|
void ProcessServerRequest(HttpListenerContext context)
|
|
{
|
|
string redirects = context.Request.QueryString["redirects"];
|
|
if (!string.IsNullOrWhiteSpace(redirects) && int.TryParse(redirects, out int parsedRedirects) && parsedRedirects > 0)
|
|
{
|
|
context.Response.Redirect(this.BuildRequestUrl(queryString: $"redirects={--parsedRedirects}"));
|
|
context.Response.OutputStream.Close();
|
|
return;
|
|
}
|
|
|
|
string responseContent;
|
|
if (context.Request.QueryString["skipRequestContent"] == null)
|
|
{
|
|
using StreamReader readStream = new StreamReader(context.Request.InputStream);
|
|
|
|
responseContent = readStream.ReadToEnd();
|
|
}
|
|
else
|
|
{
|
|
responseContent = $"{{\"Id\":\"{Guid.NewGuid()}\"}}";
|
|
}
|
|
|
|
string responseCode = context.Request.QueryString["responseCode"];
|
|
if (!string.IsNullOrWhiteSpace(responseCode))
|
|
{
|
|
context.Response.StatusCode = int.Parse(responseCode);
|
|
}
|
|
else
|
|
{
|
|
context.Response.StatusCode = 200;
|
|
}
|
|
|
|
if (context.Response.StatusCode != 204)
|
|
{
|
|
using StreamWriter writeStream = new StreamWriter(context.Response.OutputStream);
|
|
|
|
writeStream.Write(responseContent);
|
|
}
|
|
else
|
|
{
|
|
context.Response.OutputStream.Close();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
this.testServer.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// A simple test to make sure the Http Diagnostic Source is added into the list of DiagnosticListeners.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TestHttpDiagnosticListenerIsRegistered()
|
|
{
|
|
bool listenerFound = false;
|
|
using ActivityListener activityListener = new ActivityListener
|
|
{
|
|
ShouldListenTo = activitySource =>
|
|
{
|
|
if (activitySource.Name == HttpWebRequestActivitySource.ActivitySourceName)
|
|
{
|
|
listenerFound = true;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
};
|
|
ActivitySource.AddActivityListener(activityListener);
|
|
Assert.True(listenerFound, "The Http Diagnostic Listener didn't get added to the AllListeners list.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// A simple test to make sure the Http Diagnostic Source is initialized properly after we subscribed to it, using
|
|
/// the subscribe overload with just the observer argument.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task TestReflectInitializationViaSubscription()
|
|
{
|
|
using var eventRecords = new ActivitySourceRecorder();
|
|
|
|
// Send a random Http request to generate some events
|
|
using (var client = new HttpClient())
|
|
{
|
|
(await client.GetAsync(this.BuildRequestUrl())).Dispose();
|
|
}
|
|
|
|
// Just make sure some events are written, to confirm we successfully subscribed to it.
|
|
// We should have exactly one Start and one Stop event
|
|
Assert.Equal(2, eventRecords.Records.Count);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test to make sure we get both request and response events.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("GET")]
|
|
[InlineData("POST")]
|
|
[InlineData("POST", "skipRequestContent=1")]
|
|
public async Task TestBasicReceiveAndResponseEvents(string method, string queryString = null)
|
|
{
|
|
var url = this.BuildRequestUrl(queryString: queryString);
|
|
|
|
using var eventRecords = new ActivitySourceRecorder();
|
|
|
|
// Send a random Http request to generate some events
|
|
using (var client = new HttpClient())
|
|
{
|
|
(method == "GET"
|
|
? await client.GetAsync(url)
|
|
: await client.PostAsync(url, new StringContent("hello world"))).Dispose();
|
|
}
|
|
|
|
// We should have exactly one Start and one Stop event
|
|
Assert.Equal(2, eventRecords.Records.Count);
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
|
|
|
|
// Check to make sure: The first record must be a request, the next record must be a response.
|
|
(Activity activity, HttpWebRequest startRequest) = AssertFirstEventWasStart(eventRecords);
|
|
|
|
VerifyHeaders(startRequest);
|
|
VerifyActivityStartTags(this.hostNameAndPort, method, url, activity);
|
|
|
|
Assert.True(eventRecords.Records.TryDequeue(out var stopEvent));
|
|
Assert.Equal("Stop", stopEvent.Key);
|
|
HttpWebResponse response = (HttpWebResponse)stopEvent.Value.GetCustomProperty("HttpWebRequest.Response");
|
|
Assert.NotNull(response);
|
|
|
|
VerifyActivityStopTags("200", "OK", activity);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("GET")]
|
|
[InlineData("POST")]
|
|
public async Task TestBasicReceiveAndResponseEventsWithoutSampling(string method)
|
|
{
|
|
using var eventRecords = new ActivitySourceRecorder(activityDataRequest: ActivityDataRequest.None);
|
|
|
|
// Send a random Http request to generate some events
|
|
using (var client = new HttpClient())
|
|
{
|
|
(method == "GET"
|
|
? await client.GetAsync(this.BuildRequestUrl())
|
|
: await client.PostAsync(this.BuildRequestUrl(), new StringContent("hello world"))).Dispose();
|
|
}
|
|
|
|
// There should be no events because we turned off sampling.
|
|
Assert.Empty(eventRecords.Records);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("GET")]
|
|
[InlineData("POST")]
|
|
public async Task TestBasicReceiveAndResponseEventsWitPassThroughSampling(string method)
|
|
{
|
|
using var eventRecords = new ActivitySourceRecorder(activityDataRequest: ActivityDataRequest.PropagationData);
|
|
|
|
// Send a random Http request to generate some events
|
|
using (var client = new HttpClient())
|
|
{
|
|
(method == "GET"
|
|
? await client.GetAsync(this.BuildRequestUrl())
|
|
: await client.PostAsync(this.BuildRequestUrl(), new StringContent("hello world"))).Dispose();
|
|
}
|
|
|
|
// We should have exactly one Start and one Stop event
|
|
Assert.Equal(2, eventRecords.Records.Count);
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
|
|
|
|
// Check to make sure: The first record must be a request, the next record must be a response.
|
|
(Activity activity, HttpWebRequest startRequest) = AssertFirstEventWasStart(eventRecords);
|
|
|
|
VerifyHeaders(startRequest);
|
|
|
|
Assert.True(eventRecords.Records.TryDequeue(out var stopEvent));
|
|
Assert.Equal("Stop", stopEvent.Key);
|
|
HttpWebResponse response = (HttpWebResponse)stopEvent.Value.GetCustomProperty("HttpWebRequest.Response");
|
|
Assert.NotNull(response);
|
|
|
|
Assert.Empty(activity.Tags);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("GET", 0)]
|
|
[InlineData("GET", 1)]
|
|
[InlineData("GET", 2)]
|
|
[InlineData("GET", 3)]
|
|
[InlineData("POST", 0)]
|
|
[InlineData("POST", 1)]
|
|
[InlineData("POST", 2)]
|
|
[InlineData("POST", 3)]
|
|
public async Task TestBasicReceiveAndResponseWebRequestEvents(string method, int mode)
|
|
{
|
|
string url = this.BuildRequestUrl();
|
|
|
|
using var eventRecords = new ActivitySourceRecorder();
|
|
|
|
// Send a random Http request to generate some events
|
|
var webRequest = (HttpWebRequest)WebRequest.Create(url);
|
|
|
|
if (method == "POST")
|
|
{
|
|
webRequest.Method = method;
|
|
|
|
Stream stream = null;
|
|
switch (mode)
|
|
{
|
|
case 0:
|
|
stream = webRequest.GetRequestStream();
|
|
break;
|
|
case 1:
|
|
stream = await webRequest.GetRequestStreamAsync();
|
|
break;
|
|
case 2:
|
|
{
|
|
object state = new object();
|
|
using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.ManualReset);
|
|
IAsyncResult asyncResult = webRequest.BeginGetRequestStream(
|
|
ar =>
|
|
{
|
|
Assert.Equal(state, ar.AsyncState);
|
|
handle.Set();
|
|
},
|
|
state);
|
|
stream = webRequest.EndGetRequestStream(asyncResult);
|
|
if (!handle.WaitOne(TimeSpan.FromSeconds(30)))
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
handle.Dispose();
|
|
}
|
|
|
|
break;
|
|
case 3:
|
|
{
|
|
using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.ManualReset);
|
|
object state = new object();
|
|
webRequest.BeginGetRequestStream(
|
|
ar =>
|
|
{
|
|
stream = webRequest.EndGetRequestStream(ar);
|
|
Assert.Equal(state, ar.AsyncState);
|
|
handle.Set();
|
|
},
|
|
state);
|
|
if (!handle.WaitOne(TimeSpan.FromSeconds(30)))
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
handle.Dispose();
|
|
}
|
|
|
|
break;
|
|
default:
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
Assert.NotNull(stream);
|
|
|
|
using StreamWriter writer = new StreamWriter(stream);
|
|
|
|
writer.WriteLine("hello world");
|
|
}
|
|
|
|
WebResponse webResponse = null;
|
|
switch (mode)
|
|
{
|
|
case 0:
|
|
webResponse = webRequest.GetResponse();
|
|
break;
|
|
case 1:
|
|
webResponse = await webRequest.GetResponseAsync();
|
|
break;
|
|
case 2:
|
|
{
|
|
object state = new object();
|
|
using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.ManualReset);
|
|
IAsyncResult asyncResult = webRequest.BeginGetResponse(
|
|
ar =>
|
|
{
|
|
Assert.Equal(state, ar.AsyncState);
|
|
handle.Set();
|
|
},
|
|
state);
|
|
webResponse = webRequest.EndGetResponse(asyncResult);
|
|
if (!handle.WaitOne(TimeSpan.FromSeconds(30)))
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
handle.Dispose();
|
|
}
|
|
|
|
break;
|
|
case 3:
|
|
{
|
|
using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.ManualReset);
|
|
object state = new object();
|
|
webRequest.BeginGetResponse(
|
|
ar =>
|
|
{
|
|
webResponse = webRequest.EndGetResponse(ar);
|
|
Assert.Equal(state, ar.AsyncState);
|
|
handle.Set();
|
|
},
|
|
state);
|
|
if (!handle.WaitOne(TimeSpan.FromSeconds(30)))
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
handle.Dispose();
|
|
}
|
|
|
|
break;
|
|
default:
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
Assert.NotNull(webResponse);
|
|
|
|
using StreamReader reader = new StreamReader(webResponse.GetResponseStream());
|
|
|
|
reader.ReadToEnd(); // Make sure response is not disposed.
|
|
|
|
// We should have exactly one Start and one Stop event
|
|
Assert.Equal(2, eventRecords.Records.Count);
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
|
|
|
|
// Check to make sure: The first record must be a request, the next record must be a response.
|
|
(Activity activity, HttpWebRequest startRequest) = AssertFirstEventWasStart(eventRecords);
|
|
|
|
VerifyHeaders(startRequest);
|
|
VerifyActivityStartTags(this.hostNameAndPort, method, url, activity);
|
|
|
|
Assert.True(eventRecords.Records.TryDequeue(out var stopEvent));
|
|
Assert.Equal("Stop", stopEvent.Key);
|
|
HttpWebResponse response = (HttpWebResponse)stopEvent.Value.GetCustomProperty("HttpWebRequest.Response");
|
|
Assert.NotNull(response);
|
|
|
|
VerifyActivityStopTags("200", "OK", activity);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TestTraceStateAndCorrelationContext()
|
|
{
|
|
try
|
|
{
|
|
using var eventRecords = new ActivitySourceRecorder();
|
|
|
|
var parent = new Activity("w3c activity");
|
|
parent.SetParentId(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom());
|
|
parent.TraceStateString = "some=state";
|
|
parent.AddBaggage("k", "v");
|
|
parent.Start();
|
|
|
|
// Send a random Http request to generate some events
|
|
using (var client = new HttpClient())
|
|
{
|
|
(await client.GetAsync(this.BuildRequestUrl())).Dispose();
|
|
}
|
|
|
|
parent.Stop();
|
|
|
|
Assert.Equal(2, eventRecords.Records.Count());
|
|
|
|
// Check to make sure: The first record must be a request, the next record must be a response.
|
|
(Activity activity, HttpWebRequest startRequest) = AssertFirstEventWasStart(eventRecords);
|
|
|
|
var traceparent = startRequest.Headers["traceparent"];
|
|
var tracestate = startRequest.Headers["tracestate"];
|
|
var correlationContext = startRequest.Headers["Correlation-Context"];
|
|
Assert.NotNull(traceparent);
|
|
Assert.Equal("some=state", tracestate);
|
|
Assert.Equal("k=v", correlationContext);
|
|
Assert.StartsWith($"00-{parent.TraceId.ToHexString()}-", traceparent);
|
|
Assert.Matches("^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$", traceparent);
|
|
}
|
|
finally
|
|
{
|
|
this.CleanUpActivity();
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("GET")]
|
|
[InlineData("POST")]
|
|
public async Task DoNotInjectTraceParentWhenPresent(string method)
|
|
{
|
|
try
|
|
{
|
|
using var eventRecords = new ActivitySourceRecorder();
|
|
|
|
// Send a random Http request to generate some events
|
|
using (var client = new HttpClient())
|
|
using (var request = new HttpRequestMessage(HttpMethod.Get, this.BuildRequestUrl()))
|
|
{
|
|
request.Headers.Add("traceparent", "00-abcdef0123456789abcdef0123456789-abcdef0123456789-01");
|
|
|
|
if (method == "GET")
|
|
{
|
|
request.Method = HttpMethod.Get;
|
|
}
|
|
else
|
|
{
|
|
request.Method = HttpMethod.Post;
|
|
request.Content = new StringContent("hello world");
|
|
}
|
|
|
|
(await client.SendAsync(request)).Dispose();
|
|
}
|
|
|
|
// No events are sent.
|
|
Assert.Empty(eventRecords.Records);
|
|
}
|
|
finally
|
|
{
|
|
this.CleanUpActivity();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test to make sure we get both request and response events.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("GET")]
|
|
[InlineData("POST")]
|
|
public async Task TestResponseWithoutContentEvents(string method)
|
|
{
|
|
string url = this.BuildRequestUrl(queryString: "responseCode=204");
|
|
|
|
using var eventRecords = new ActivitySourceRecorder();
|
|
|
|
// Send a random Http request to generate some events
|
|
using (var client = new HttpClient())
|
|
{
|
|
using HttpResponseMessage response = method == "GET"
|
|
? await client.GetAsync(url)
|
|
: await client.PostAsync(url, new StringContent("hello world"));
|
|
}
|
|
|
|
// We should have exactly one Start and one Stop event
|
|
Assert.Equal(2, eventRecords.Records.Count);
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
|
|
|
|
// Check to make sure: The first record must be a request, the next record must be a response.
|
|
(Activity activity, HttpWebRequest startRequest) = AssertFirstEventWasStart(eventRecords);
|
|
|
|
VerifyHeaders(startRequest);
|
|
VerifyActivityStartTags(this.hostNameAndPort, method, url, activity);
|
|
|
|
Assert.True(eventRecords.Records.TryDequeue(out var stopEvent));
|
|
Assert.Equal("Stop", stopEvent.Key);
|
|
HttpWebRequest stopRequest = (HttpWebRequest)stopEvent.Value.GetCustomProperty("HttpWebRequest.Request");
|
|
Assert.Equal(startRequest, stopRequest);
|
|
HttpWebResponse stopResponse = (HttpWebResponse)stopEvent.Value.GetCustomProperty("HttpWebRequest.Response");
|
|
Assert.NotNull(stopResponse);
|
|
|
|
VerifyActivityStopTags("204", "No Content", activity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test that if request is redirected, it gets only one Start and one Stop event.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("GET")]
|
|
[InlineData("POST")]
|
|
public async Task TestRedirectedRequest(string method)
|
|
{
|
|
using var eventRecords = new ActivitySourceRecorder();
|
|
|
|
using (var client = new HttpClient())
|
|
{
|
|
using HttpResponseMessage response = method == "GET"
|
|
? await client.GetAsync(this.BuildRequestUrl(queryString: "redirects=10"))
|
|
: await client.PostAsync(this.BuildRequestUrl(queryString: "redirects=10"), new StringContent("hello world"));
|
|
}
|
|
|
|
// We should have exactly one Start and one Stop event
|
|
Assert.Equal(2, eventRecords.Records.Count());
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test exception in request processing: exception should have expected type/status and now be swallowed by reflection hook.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("GET")]
|
|
[InlineData("POST")]
|
|
public async Task TestRequestWithException(string method)
|
|
{
|
|
string url = method == "GET"
|
|
? $"http://{Guid.NewGuid()}.com"
|
|
: $"http://{Guid.NewGuid()}.com";
|
|
|
|
using var eventRecords = new ActivitySourceRecorder();
|
|
|
|
var ex = await Assert.ThrowsAsync<HttpRequestException>(() =>
|
|
{
|
|
return method == "GET"
|
|
? new HttpClient().GetAsync(url)
|
|
: new HttpClient().PostAsync(url, new StringContent("hello world"));
|
|
});
|
|
|
|
// check that request failed because of the wrong domain name and not because of reflection
|
|
var webException = (WebException)ex.InnerException;
|
|
Assert.NotNull(webException);
|
|
Assert.True(webException.Status == WebExceptionStatus.NameResolutionFailure);
|
|
|
|
// We should have one Start event and one Stop event with an exception.
|
|
Assert.Equal(2, eventRecords.Records.Count());
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
|
|
|
|
// Check to make sure: The first record must be a request, the next record must be an exception.
|
|
(Activity activity, HttpWebRequest startRequest) = AssertFirstEventWasStart(eventRecords);
|
|
VerifyHeaders(startRequest);
|
|
VerifyActivityStartTags(null, method, url, activity);
|
|
|
|
Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair<string, Activity> exceptionEvent));
|
|
Assert.Equal("Stop", exceptionEvent.Key);
|
|
HttpWebRequest exceptionRequest = (HttpWebRequest)exceptionEvent.Value.GetCustomProperty("HttpWebRequest.Request");
|
|
Assert.Equal(startRequest, exceptionRequest);
|
|
Exception exceptionException = (Exception)exceptionEvent.Value.GetCustomProperty("HttpWebRequest.Exception");
|
|
Assert.Equal(webException, exceptionException);
|
|
|
|
Assert.Contains(activity.Tags, i => i.Key == SpanAttributeConstants.StatusCodeKey);
|
|
Assert.Contains(activity.Tags, i => i.Key == SpanAttributeConstants.StatusDescriptionKey);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test request cancellation: reflection hook does not throw.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("GET")]
|
|
[InlineData("POST")]
|
|
public async Task TestCanceledRequest(string method)
|
|
{
|
|
string url = this.BuildRequestUrl();
|
|
|
|
CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
using var eventRecords = new ActivitySourceRecorder(_ => { cts.Cancel(); });
|
|
|
|
using (var client = new HttpClient())
|
|
{
|
|
var ex = await Assert.ThrowsAnyAsync<Exception>(() =>
|
|
{
|
|
return method == "GET"
|
|
? client.GetAsync(url, cts.Token)
|
|
: client.PostAsync(url, new StringContent("hello world"), cts.Token);
|
|
});
|
|
Assert.True(ex is TaskCanceledException || ex is WebException);
|
|
}
|
|
|
|
// We should have one Start event and one Stop event with an exception.
|
|
Assert.Equal(2, eventRecords.Records.Count());
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
|
|
|
|
(Activity activity, HttpWebRequest startRequest) = AssertFirstEventWasStart(eventRecords);
|
|
VerifyHeaders(startRequest);
|
|
VerifyActivityStartTags(this.hostNameAndPort, method, url, activity);
|
|
|
|
Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair<string, Activity> exceptionEvent));
|
|
Assert.Equal("Stop", exceptionEvent.Key);
|
|
Exception exceptionException = (Exception)exceptionEvent.Value.GetCustomProperty("HttpWebRequest.Exception");
|
|
|
|
Assert.Contains(exceptionEvent.Value.Tags, i => i.Key == SpanAttributeConstants.StatusCodeKey);
|
|
Assert.DoesNotContain(exceptionEvent.Value.Tags, i => i.Key == SpanAttributeConstants.StatusDescriptionKey);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test request connection exception: reflection hook does not throw.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("GET")]
|
|
[InlineData("POST")]
|
|
public async Task TestSecureTransportFailureRequest(string method)
|
|
{
|
|
string url = "https://expired.badssl.com/";
|
|
|
|
using var eventRecords = new ActivitySourceRecorder();
|
|
|
|
using (var client = new HttpClient())
|
|
{
|
|
var ex = await Assert.ThrowsAnyAsync<Exception>(() =>
|
|
{
|
|
// https://expired.badssl.com/ has an expired certificae.
|
|
return method == "GET"
|
|
? client.GetAsync(url)
|
|
: client.PostAsync(url, new StringContent("hello world"));
|
|
});
|
|
Assert.True(ex is HttpRequestException);
|
|
}
|
|
|
|
// We should have one Start event and one Stop event with an exception.
|
|
Assert.Equal(2, eventRecords.Records.Count());
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
|
|
|
|
(Activity activity, HttpWebRequest startRequest) = AssertFirstEventWasStart(eventRecords);
|
|
VerifyHeaders(startRequest);
|
|
VerifyActivityStartTags(null, method, url, activity);
|
|
|
|
Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair<string, Activity> exceptionEvent));
|
|
Assert.Equal("Stop", exceptionEvent.Key);
|
|
Exception exceptionException = (Exception)exceptionEvent.Value.GetCustomProperty("HttpWebRequest.Exception");
|
|
|
|
Assert.Contains(exceptionEvent.Value.Tags, i => i.Key == SpanAttributeConstants.StatusCodeKey);
|
|
Assert.Contains(exceptionEvent.Value.Tags, i => i.Key == SpanAttributeConstants.StatusDescriptionKey);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test request connection retry: reflection hook does not throw.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("GET")]
|
|
[InlineData("POST")]
|
|
public async Task TestSecureTransportRetryFailureRequest(string method)
|
|
{
|
|
// This test sends an https request to an endpoint only set up for http.
|
|
// It should retry. What we want to test for is 1 start, 1 exception event even
|
|
// though multiple are actually sent.
|
|
|
|
string url = this.BuildRequestUrl(useHttps: true);
|
|
|
|
using var eventRecords = new ActivitySourceRecorder();
|
|
|
|
using (var client = new HttpClient())
|
|
{
|
|
var ex = await Assert.ThrowsAnyAsync<Exception>(() =>
|
|
{
|
|
return method == "GET"
|
|
? client.GetAsync(url)
|
|
: client.PostAsync(url, new StringContent("hello world"));
|
|
});
|
|
Assert.True(ex is HttpRequestException);
|
|
}
|
|
|
|
// We should have one Start event and one Stop event with an exception.
|
|
Assert.Equal(2, eventRecords.Records.Count());
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
|
|
|
|
(Activity activity, HttpWebRequest startRequest) = AssertFirstEventWasStart(eventRecords);
|
|
VerifyHeaders(startRequest);
|
|
VerifyActivityStartTags(this.hostNameAndPort, method, url, activity);
|
|
|
|
Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair<string, Activity> exceptionEvent));
|
|
Assert.Equal("Stop", exceptionEvent.Key);
|
|
Exception exceptionException = (Exception)exceptionEvent.Value.GetCustomProperty("HttpWebRequest.Exception");
|
|
|
|
Assert.Contains(exceptionEvent.Value.Tags, i => i.Key == SpanAttributeConstants.StatusCodeKey);
|
|
Assert.Contains(exceptionEvent.Value.Tags, i => i.Key == SpanAttributeConstants.StatusDescriptionKey);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TestInvalidBaggage()
|
|
{
|
|
var parentActivity = new Activity("parent")
|
|
.AddBaggage("key", "value")
|
|
.AddBaggage("bad/key", "value")
|
|
.AddBaggage("goodkey", "bad/value")
|
|
.Start();
|
|
using (var eventRecords = new ActivitySourceRecorder())
|
|
{
|
|
using (var client = new HttpClient())
|
|
{
|
|
(await client.GetAsync(this.BuildRequestUrl())).Dispose();
|
|
}
|
|
|
|
Assert.Equal(2, eventRecords.Records.Count());
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Start"));
|
|
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key == "Stop"));
|
|
|
|
WebRequest thisRequest = (WebRequest)eventRecords.Records.First().Value.GetCustomProperty("HttpWebRequest.Request");
|
|
string[] correlationContext = thisRequest.Headers["Correlation-Context"].Split(',');
|
|
|
|
Assert.Equal(3, correlationContext.Length);
|
|
Assert.Contains("key=value", correlationContext);
|
|
Assert.Contains("bad%2Fkey=value", correlationContext);
|
|
Assert.Contains("goodkey=bad%2Fvalue", correlationContext);
|
|
}
|
|
|
|
parentActivity.Stop();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test to make sure every event record has the right dynamic properties.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TestMultipleConcurrentRequests()
|
|
{
|
|
ServicePointManager.DefaultConnectionLimit = int.MaxValue;
|
|
var parentActivity = new Activity("parent").Start();
|
|
using var eventRecords = new ActivitySourceRecorder();
|
|
|
|
Dictionary<Uri, Tuple<WebRequest, WebResponse>> requestData = new Dictionary<Uri, Tuple<WebRequest, WebResponse>>();
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
Uri uriWithRedirect = new Uri(this.BuildRequestUrl(queryString: $"q={i}&redirects=3"));
|
|
|
|
requestData[uriWithRedirect] = null;
|
|
}
|
|
|
|
// Issue all requests simultaneously
|
|
HttpClient httpClient = new HttpClient();
|
|
Dictionary<Uri, Task<HttpResponseMessage>> tasks = new Dictionary<Uri, Task<HttpResponseMessage>>();
|
|
|
|
CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
foreach (var url in requestData.Keys)
|
|
{
|
|
tasks.Add(url, httpClient.GetAsync(url, cts.Token));
|
|
}
|
|
|
|
// wait up to 10 sec for all requests and suppress exceptions
|
|
Task.WhenAll(tasks.Select(t => t.Value).ToArray()).ContinueWith(tt =>
|
|
{
|
|
foreach (var task in tasks)
|
|
{
|
|
task.Value.Result?.Dispose();
|
|
}
|
|
}).Wait();
|
|
|
|
// Examine the result. Make sure we got all successful requests.
|
|
|
|
// Just make sure some events are written, to confirm we successfully subscribed to it. We should have
|
|
// exactly 1 Start event per request and exactly 1 Stop event per response (if request succeeded)
|
|
var successfulTasks = tasks.Where(t => t.Value.Status == TaskStatus.RanToCompletion);
|
|
|
|
Assert.Equal(tasks.Count, eventRecords.Records.Count(rec => rec.Key == "Start"));
|
|
Assert.Equal(successfulTasks.Count(), eventRecords.Records.Count(rec => rec.Key == "Stop"));
|
|
|
|
// Check to make sure: We have a WebRequest and a WebResponse for each successful request
|
|
foreach (var pair in eventRecords.Records)
|
|
{
|
|
Activity activity = pair.Value;
|
|
|
|
Assert.True(
|
|
pair.Key == "Start" ||
|
|
pair.Key == "Stop",
|
|
"An unexpected event of name " + pair.Key + "was received");
|
|
|
|
WebRequest request = (WebRequest)activity.GetCustomProperty("HttpWebRequest.Request");
|
|
Assert.Equal("HttpWebRequest", request.GetType().Name);
|
|
|
|
if (pair.Key == "Start")
|
|
{
|
|
// Make sure this is an URL that we recognize. If not, just skip
|
|
if (!requestData.TryGetValue(request.RequestUri, out var tuple))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// all requests have traceparent with proper parent Id
|
|
var traceparent = request.Headers["traceparent"];
|
|
Assert.StartsWith($"00-{parentActivity.TraceId.ToHexString()}-", traceparent);
|
|
|
|
Assert.Null(requestData[request.RequestUri]);
|
|
requestData[request.RequestUri] =
|
|
new Tuple<WebRequest, WebResponse>(request, null);
|
|
}
|
|
else
|
|
{
|
|
// This must be the response.
|
|
WebResponse response = (WebResponse)activity.GetCustomProperty("HttpWebRequest.Response");
|
|
Assert.Equal("HttpWebResponse", response.GetType().Name);
|
|
|
|
// By the time we see the response, the request object may already have been redirected with a different
|
|
// url. Hence, it's not reliable to just look up requestData by the URL/hostname. Instead, we have to look
|
|
// through each one and match by object reference on the request object.
|
|
Tuple<WebRequest, WebResponse> tuple = null;
|
|
foreach (Tuple<WebRequest, WebResponse> currentTuple in requestData.Values)
|
|
{
|
|
if (currentTuple != null && currentTuple.Item1 == request)
|
|
{
|
|
// Found it!
|
|
tuple = currentTuple;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Update the tuple with the response object
|
|
Assert.NotNull(tuple);
|
|
requestData[request.RequestUri] =
|
|
new Tuple<WebRequest, WebResponse>(request, response);
|
|
}
|
|
}
|
|
|
|
// Finally, make sure we have request and response objects for every successful request
|
|
foreach (KeyValuePair<Uri, Tuple<WebRequest, WebResponse>> pair in requestData)
|
|
{
|
|
if (successfulTasks.Any(t => t.Key == pair.Key))
|
|
{
|
|
Assert.NotNull(pair.Value);
|
|
Assert.NotNull(pair.Value.Item1);
|
|
Assert.NotNull(pair.Value.Item2);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static (Activity, HttpWebRequest) AssertFirstEventWasStart(ActivitySourceRecorder eventRecords)
|
|
{
|
|
Assert.True(eventRecords.Records.TryDequeue(out KeyValuePair<string, Activity> startEvent));
|
|
Assert.Equal("Start", startEvent.Key);
|
|
HttpWebRequest startRequest = (HttpWebRequest)startEvent.Value.GetCustomProperty("HttpWebRequest.Request");
|
|
Assert.NotNull(startRequest);
|
|
return (startEvent.Value, startRequest);
|
|
}
|
|
|
|
private static void VerifyHeaders(HttpWebRequest startRequest)
|
|
{
|
|
var traceparent = startRequest.Headers["traceparent"];
|
|
Assert.NotNull(traceparent);
|
|
Assert.Matches("^[0-9a-f][0-9a-f]-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f][0-9a-f]$", traceparent);
|
|
Assert.Null(startRequest.Headers["tracestate"]);
|
|
}
|
|
|
|
private static void VerifyActivityStartTags(string hostNameAndPort, string method, string url, Activity activity)
|
|
{
|
|
Assert.NotNull(activity.Tags);
|
|
Assert.Equal(method, activity.Tags.FirstOrDefault(i => i.Key == SemanticConventions.AttributeHttpMethod).Value);
|
|
if (hostNameAndPort != null)
|
|
{
|
|
Assert.Equal(hostNameAndPort, activity.Tags.FirstOrDefault(i => i.Key == SemanticConventions.AttributeHttpHost).Value);
|
|
}
|
|
|
|
Assert.Equal(url, activity.Tags.FirstOrDefault(i => i.Key == SemanticConventions.AttributeHttpUrl).Value);
|
|
}
|
|
|
|
private static void VerifyActivityStopTags(string statusCode, string statusText, Activity activity)
|
|
{
|
|
Assert.Equal(statusCode, activity.Tags.FirstOrDefault(i => i.Key == SemanticConventions.AttributeHttpStatusCode).Value);
|
|
Assert.Equal(statusText, activity.Tags.FirstOrDefault(i => i.Key == SpanAttributeConstants.StatusDescriptionKey).Value);
|
|
}
|
|
|
|
private string BuildRequestUrl(bool useHttps = false, string path = "echo", string queryString = null)
|
|
{
|
|
return $"{(useHttps ? "https" : "http")}://{this.testServerHost}:{this.testServerPort}/{path}{(string.IsNullOrWhiteSpace(queryString) ? string.Empty : $"?{queryString}")}";
|
|
}
|
|
|
|
private void CleanUpActivity()
|
|
{
|
|
while (Activity.Current != null)
|
|
{
|
|
Activity.Current.Stop();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="ActivitySourceRecorder"/> is a helper class for recording <see cref="HttpWebRequestActivitySource.ActivitySourceName"/> events.
|
|
/// </summary>
|
|
private class ActivitySourceRecorder : IDisposable
|
|
{
|
|
private readonly Action<KeyValuePair<string, Activity>> onEvent;
|
|
private readonly ActivityListener activityListener;
|
|
|
|
public ActivitySourceRecorder(Action<KeyValuePair<string, Activity>> onEvent = null, ActivityDataRequest activityDataRequest = ActivityDataRequest.AllDataAndRecorded)
|
|
{
|
|
this.activityListener = new ActivityListener
|
|
{
|
|
ShouldListenTo = (activitySource) => activitySource.Name == HttpWebRequestActivitySource.ActivitySourceName,
|
|
ActivityStarted = this.ActivityStarted,
|
|
ActivityStopped = this.ActivityStopped,
|
|
GetRequestedDataUsingContext = (ref ActivityCreationOptions<ActivityContext> options) => activityDataRequest,
|
|
};
|
|
|
|
ActivitySource.AddActivityListener(this.activityListener);
|
|
|
|
this.onEvent = onEvent;
|
|
}
|
|
|
|
public ConcurrentQueue<KeyValuePair<string, Activity>> Records { get; } = new ConcurrentQueue<KeyValuePair<string, Activity>>();
|
|
|
|
public void Dispose()
|
|
{
|
|
this.activityListener.Dispose();
|
|
}
|
|
|
|
public void ActivityStarted(Activity activity) => this.Record("Start", activity);
|
|
|
|
public void ActivityStopped(Activity activity) => this.Record("Stop", activity);
|
|
|
|
private void Record(string eventName, Activity activity)
|
|
{
|
|
var record = new KeyValuePair<string, Activity>(eventName, activity);
|
|
|
|
this.Records.Enqueue(record);
|
|
this.onEvent?.Invoke(record);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|