diff --git a/src/Datadog.Trace/Agent/Api.cs b/src/Datadog.Trace/Agent/Api.cs index b296d7539..9267cbfbd 100644 --- a/src/Datadog.Trace/Agent/Api.cs +++ b/src/Datadog.Trace/Agent/Api.cs @@ -32,7 +32,7 @@ namespace Datadog.Trace.Agent _tracesEndpoint = new Uri(baseEndpoint, TracesPath); _statsd = statsd; _containerId = ContainerMetadata.GetContainerId(); - _apiRequestFactory = apiRequestFactory ?? new ApiWebRequestFactory(); + _apiRequestFactory = apiRequestFactory ?? CreateRequestFactory(); // report runtime details try @@ -152,6 +152,15 @@ namespace Datadog.Trace.Agent } } + private static IApiRequestFactory CreateRequestFactory() + { +#if NETCOREAPP + return new HttpClientRequestFactory(); +#else + return new ApiWebRequestFactory(); +#endif + } + private static HashSet GetUniqueTraceIds(Span[][] traces) { var uniqueTraceIds = new HashSet(); @@ -203,7 +212,7 @@ namespace Datadog.Trace.Agent try { - if (response.ContentLength > 0 && Tracer.Instance.Sampler != null) + if (response.ContentLength != 0 && Tracer.Instance.Sampler != null) { var responseContent = await response.ReadAsStringAsync().ConfigureAwait(false); diff --git a/src/Datadog.Trace/Agent/ApiWebResponse.cs b/src/Datadog.Trace/Agent/ApiWebResponse.cs index 93c109287..aec15f621 100644 --- a/src/Datadog.Trace/Agent/ApiWebResponse.cs +++ b/src/Datadog.Trace/Agent/ApiWebResponse.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net; -using System.Text; using System.Threading.Tasks; namespace Datadog.Trace.Agent diff --git a/src/Datadog.Trace/Agent/HttpClientRequest.cs b/src/Datadog.Trace/Agent/HttpClientRequest.cs new file mode 100644 index 000000000..e69d098a4 --- /dev/null +++ b/src/Datadog.Trace/Agent/HttpClientRequest.cs @@ -0,0 +1,39 @@ +#if NETCOREAPP +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Datadog.Trace.Agent.MessagePack; + +namespace Datadog.Trace.Agent +{ + internal class HttpClientRequest : IApiRequest + { + private readonly HttpClient _client; + private readonly HttpRequestMessage _request; + + public HttpClientRequest(HttpClient client, Uri endpoint) + { + _client = client; + _request = new HttpRequestMessage(HttpMethod.Post, endpoint); + } + + public void AddHeader(string name, string value) + { + _request.Headers.Add(name, value); + } + + public async Task PostAsync(Span[][] traces, FormatterResolverWrapper formatterResolver) + { + // re-create HttpContent on every retry because some versions of HttpClient always dispose of it, so we can't reuse. + using (var content = new TracesMessagePackContent(traces, formatterResolver)) + { + _request.Content = content; + + var response = await _client.SendAsync(_request).ConfigureAwait(false); + + return new HttpClientResponse(response); + } + } + } +} +#endif diff --git a/src/Datadog.Trace/Agent/HttpClientRequestFactory.cs b/src/Datadog.Trace/Agent/HttpClientRequestFactory.cs new file mode 100644 index 000000000..4cbe999bc --- /dev/null +++ b/src/Datadog.Trace/Agent/HttpClientRequestFactory.cs @@ -0,0 +1,29 @@ +#if NETCOREAPP +using System; +using System.Net.Http; + +namespace Datadog.Trace.Agent +{ + internal class HttpClientRequestFactory : IApiRequestFactory + { + private readonly HttpClient _client; + + public HttpClientRequestFactory(HttpMessageHandler handler = null) + { + _client = handler == null ? new HttpClient() : new HttpClient(handler); + + // Default headers + _client.DefaultRequestHeaders.Add(AgentHttpHeaderNames.Language, ".NET"); + _client.DefaultRequestHeaders.Add(AgentHttpHeaderNames.TracerVersion, TracerConstants.AssemblyVersion); + + // don't add automatic instrumentation to requests from this HttpClient + _client.DefaultRequestHeaders.Add(HttpHeaderNames.TracingEnabled, "false"); + } + + public IApiRequest Create(Uri endpoint) + { + return new HttpClientRequest(_client, endpoint); + } + } +} +#endif diff --git a/src/Datadog.Trace/Agent/HttpClientResponse.cs b/src/Datadog.Trace/Agent/HttpClientResponse.cs new file mode 100644 index 000000000..0e623b6cf --- /dev/null +++ b/src/Datadog.Trace/Agent/HttpClientResponse.cs @@ -0,0 +1,31 @@ +#if NETCOREAPP +using System.Net.Http; +using System.Threading.Tasks; + +namespace Datadog.Trace.Agent +{ + internal class HttpClientResponse : IApiResponse + { + private readonly HttpResponseMessage _response; + + public HttpClientResponse(HttpResponseMessage response) + { + _response = response; + } + + public int StatusCode => (int)_response.StatusCode; + + public long ContentLength => _response.Content.Headers.ContentLength ?? -1; + + public void Dispose() + { + _response.Dispose(); + } + + public Task ReadAsStringAsync() + { + return _response.Content.ReadAsStringAsync(); + } + } +} +#endif diff --git a/src/Datadog.Trace/Agent/MessagePack/TracesMessagePackContent.cs b/src/Datadog.Trace/Agent/MessagePack/TracesMessagePackContent.cs new file mode 100644 index 000000000..61aac777e --- /dev/null +++ b/src/Datadog.Trace/Agent/MessagePack/TracesMessagePackContent.cs @@ -0,0 +1,47 @@ +#if NETCOREAPP +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Datadog.Trace.Vendors.MessagePack; + +namespace Datadog.Trace.Agent.MessagePack +{ + internal class TracesMessagePackContent : HttpContent + { + private readonly FormatterResolverWrapper _resolver; + + /// + /// Initializes a new instance of the class. + /// + /// The value to serialize into the content stream as MessagePack. + /// The to use when serializing . + public TracesMessagePackContent(Span[][] traces, FormatterResolverWrapper resolver) + { + Traces = traces; + _resolver = resolver; + + Headers.ContentType = new MediaTypeHeaderValue("application/msgpack"); + } + + public Span[][] Traces { get; } + + /// Serialize the HTTP content to a stream as an asynchronous operation. + /// The target stream. + /// Information about the transport (channel binding token, for example). This parameter may be . + /// The task object representing the asynchronous operation. + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + return CachedSerializer.Instance.SerializeAsync(stream, Traces, _resolver); + } + + protected override bool TryComputeLength(out long length) + { + // We don't want compute the length beforehand + length = -1; + return false; + } + } +} +#endif diff --git a/test/Datadog.Trace.Tests/HttpClientRequestTests.cs b/test/Datadog.Trace.Tests/HttpClientRequestTests.cs new file mode 100644 index 000000000..b78f64238 --- /dev/null +++ b/test/Datadog.Trace.Tests/HttpClientRequestTests.cs @@ -0,0 +1,64 @@ +#if NETCOREAPP3_1 +using System; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Datadog.Trace.Agent; +using Datadog.Trace.Agent.MessagePack; +using Xunit; + +namespace Datadog.Trace.Tests +{ + public class HttpClientRequestTests + { + [Fact] + public async Task SetHeaders() + { + var handler = new CustomHandler(); + + var factory = new HttpClientRequestFactory(handler); + var request = factory.Create(new Uri("http://localhost/")); + + request.AddHeader("Hello", "World"); + + await request.PostAsync(new Span[0][], new FormatterResolverWrapper(SpanFormatterResolver.Instance)); + + var message = handler.Message; + + Assert.NotNull(message); + Assert.Equal(".NET", message.Headers.GetValues(AgentHttpHeaderNames.Language).First()); + Assert.Equal(TracerConstants.AssemblyVersion, message.Headers.GetValues(AgentHttpHeaderNames.TracerVersion).First()); + Assert.Equal("false", message.Headers.GetValues(HttpHeaderNames.TracingEnabled).First()); + Assert.Equal("World", message.Headers.GetValues("Hello").First()); + } + + [Fact] + public async Task SerializeSpans() + { + var handler = new CustomHandler(); + + var factory = new HttpClientRequestFactory(handler); + var request = factory.Create(new Uri("http://localhost/")); + + await request.PostAsync(new Span[0][], new FormatterResolverWrapper(SpanFormatterResolver.Instance)); + + var message = handler.Message; + + Assert.IsAssignableFrom(message.Content); + } + + private class CustomHandler : DelegatingHandler + { + public HttpRequestMessage Message { get; private set; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Message = request; + return Task.FromResult(new HttpResponseMessage()); + } + } + } +} + +#endif