Skip to content

Commit d72b04c

Browse files
authored
Add support for synchronous HttpClient.Send() (#1162)
* Add HttpClientHandler sync Send integration Virtually identical to SendAsync integration, so extracted common code to HttpClientHandlerCommon Kept existing HttpClientHandlerIntegration name for back compat reasons Only available in .NET 5.0 + * Add SocketsHttpHandler sync CallTarget integration * Add HttpClient.Send() tests
1 parent 6ceaddd commit d72b04c

14 files changed

Lines changed: 765 additions & 145 deletions

File tree

integrations.json

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,30 @@
5050
"action": "CallTargetModification"
5151
}
5252
},
53+
{
54+
"caller": {},
55+
"target": {
56+
"assembly": "System.Net.Http",
57+
"type": "System.Net.Http.SocketsHttpHandler",
58+
"method": "Send",
59+
"signature_types": [
60+
"System.Net.Http.HttpResponseMessage",
61+
"System.Net.Http.HttpRequestMessage",
62+
"System.Threading.CancellationToken"
63+
],
64+
"minimum_major": 5,
65+
"minimum_minor": 0,
66+
"minimum_patch": 0,
67+
"maximum_major": 5,
68+
"maximum_minor": 65535,
69+
"maximum_patch": 65535
70+
},
71+
"wrapper": {
72+
"assembly": "Datadog.Trace.ClrProfiler.Managed, Version=1.22.1.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb",
73+
"type": "Datadog.Trace.ClrProfiler.AutoInstrumentation.SocketsHttpHandler.SocketsHttpHandlerSyncIntegration",
74+
"action": "CallTargetModification"
75+
}
76+
},
5377
{
5478
"caller": {},
5579
"target": {
@@ -73,6 +97,30 @@
7397
"type": "Datadog.Trace.ClrProfiler.AutoInstrumentation.HttpClientHandler.HttpClientHandlerIntegration",
7498
"action": "CallTargetModification"
7599
}
100+
},
101+
{
102+
"caller": {},
103+
"target": {
104+
"assembly": "System.Net.Http",
105+
"type": "System.Net.Http.HttpClientHandler",
106+
"method": "Send",
107+
"signature_types": [
108+
"System.Net.Http.HttpResponseMessage",
109+
"System.Net.Http.HttpRequestMessage",
110+
"System.Threading.CancellationToken"
111+
],
112+
"minimum_major": 5,
113+
"minimum_minor": 0,
114+
"minimum_patch": 0,
115+
"maximum_major": 5,
116+
"maximum_minor": 65535,
117+
"maximum_patch": 65535
118+
},
119+
"wrapper": {
120+
"assembly": "Datadog.Trace.ClrProfiler.Managed, Version=1.22.1.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb",
121+
"type": "Datadog.Trace.ClrProfiler.AutoInstrumentation.HttpClientHandler.HttpClientHandlerSyncIntegration",
122+
"action": "CallTargetModification"
123+
}
76124
}
77125
]
78126
},
@@ -2372,6 +2420,32 @@
23722420
"action": "ReplaceTargetMethod"
23732421
}
23742422
},
2423+
{
2424+
"caller": {},
2425+
"target": {
2426+
"assembly": "System.Net.Http",
2427+
"type": "System.Net.Http.HttpMessageHandler",
2428+
"method": "Send",
2429+
"signature_types": [
2430+
"System.Net.Http.HttpResponseMessage",
2431+
"System.Net.Http.HttpRequestMessage",
2432+
"System.Threading.CancellationToken"
2433+
],
2434+
"minimum_major": 5,
2435+
"minimum_minor": 0,
2436+
"minimum_patch": 0,
2437+
"maximum_major": 5,
2438+
"maximum_minor": 65535,
2439+
"maximum_patch": 65535
2440+
},
2441+
"wrapper": {
2442+
"assembly": "Datadog.Trace.ClrProfiler.Managed, Version=1.22.1.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb",
2443+
"type": "Datadog.Trace.ClrProfiler.Integrations.HttpMessageHandlerIntegration",
2444+
"method": "HttpMessageHandler_Send",
2445+
"signature": "00 06 1C 1C 1C 1C 08 08 0A",
2446+
"action": "ReplaceTargetMethod"
2447+
}
2448+
},
23752449
{
23762450
"caller": {},
23772451
"target": {
@@ -2397,6 +2471,32 @@
23972471
"signature": "00 06 1C 1C 1C 1C 08 08 0A",
23982472
"action": "ReplaceTargetMethod"
23992473
}
2474+
},
2475+
{
2476+
"caller": {},
2477+
"target": {
2478+
"assembly": "System.Net.Http",
2479+
"type": "System.Net.Http.HttpClientHandler",
2480+
"method": "Send",
2481+
"signature_types": [
2482+
"System.Net.Http.HttpResponseMessage",
2483+
"System.Net.Http.HttpRequestMessage",
2484+
"System.Threading.CancellationToken"
2485+
],
2486+
"minimum_major": 5,
2487+
"minimum_minor": 0,
2488+
"minimum_patch": 0,
2489+
"maximum_major": 5,
2490+
"maximum_minor": 65535,
2491+
"maximum_patch": 65535
2492+
},
2493+
"wrapper": {
2494+
"assembly": "Datadog.Trace.ClrProfiler.Managed, Version=1.22.1.0, Culture=neutral, PublicKeyToken=def86d061d0d2eeb",
2495+
"type": "Datadog.Trace.ClrProfiler.Integrations.HttpMessageHandlerIntegration",
2496+
"method": "HttpClientHandler_Send",
2497+
"signature": "00 06 1C 1C 1C 1C 08 08 0A",
2498+
"action": "ReplaceTargetMethod"
2499+
}
24002500
}
24012501
]
24022502
},

src/Datadog.Trace.ClrProfiler.Managed.Core/ClrNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ internal static class ClrNames
2828
public const string AsyncCallback = "System.AsyncCallback";
2929

3030
public const string HttpRequestMessage = "System.Net.Http.HttpRequestMessage";
31+
public const string HttpResponseMessage = "System.Net.Http.HttpResponseMessage";
3132
public const string HttpResponseMessageTask = "System.Threading.Tasks.Task`1<System.Net.Http.HttpResponseMessage>";
3233

3334
public const string GenericTask = "System.Threading.Tasks.Task`1";
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading;
4+
using Datadog.Trace.ClrProfiler.CallTarget;
5+
using Datadog.Trace.Configuration;
6+
using Datadog.Trace.ExtensionMethods;
7+
using Datadog.Trace.Tagging;
8+
9+
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.HttpClientHandler
10+
{
11+
internal class HttpClientHandlerCommon
12+
{
13+
private const string IntegrationName = nameof(IntegrationIds.HttpMessageHandler);
14+
private static readonly IntegrationInfo IntegrationId = IntegrationRegistry.GetIntegrationInfo(IntegrationName);
15+
16+
public static CallTargetState OnMethodBegin<TTarget, TRequest>(TTarget instance, TRequest requestMessage, CancellationToken cancellationToken)
17+
where TRequest : IHttpRequestMessage
18+
{
19+
if (IsTracingEnabled(requestMessage.Headers))
20+
{
21+
Scope scope = ScopeFactory.CreateOutboundHttpScope(Tracer.Instance, requestMessage.Method.Method, requestMessage.RequestUri, IntegrationId, out HttpTags tags);
22+
if (scope != null)
23+
{
24+
tags.HttpClientHandlerType = instance.GetType().FullName;
25+
26+
// add distributed tracing headers to the HTTP request
27+
SpanContextPropagator.Instance.Inject(scope.Span.Context, new HttpHeadersCollection(requestMessage.Headers));
28+
29+
return new CallTargetState(scope);
30+
}
31+
}
32+
33+
return CallTargetState.GetDefault();
34+
}
35+
36+
public static TResponse OnMethodEnd<TTarget, TResponse>(TTarget instance, TResponse responseMessage, Exception exception, CallTargetState state)
37+
where TResponse : IHttpResponseMessage
38+
{
39+
Scope scope = state.Scope;
40+
41+
if (scope is null)
42+
{
43+
return responseMessage;
44+
}
45+
46+
try
47+
{
48+
scope.Span.SetHttpStatusCode(responseMessage.StatusCode, isServer: false);
49+
50+
if (exception != null)
51+
{
52+
scope.Span.SetException(exception);
53+
}
54+
}
55+
finally
56+
{
57+
scope.Dispose();
58+
}
59+
60+
return responseMessage;
61+
}
62+
63+
private static bool IsTracingEnabled(IRequestHeaders headers)
64+
{
65+
if (headers.TryGetValues(HttpHeaderNames.TracingEnabled, out var headerValues))
66+
{
67+
if (headerValues is string[] arrayValues)
68+
{
69+
for (var i = 0; i < arrayValues.Length; i++)
70+
{
71+
if (string.Equals(arrayValues[i], "false", StringComparison.OrdinalIgnoreCase))
72+
{
73+
return false;
74+
}
75+
}
76+
77+
return true;
78+
}
79+
80+
if (headerValues != null && headerValues.Any(s => string.Equals(s, "false", StringComparison.OrdinalIgnoreCase)))
81+
{
82+
// tracing is disabled for this request via http header
83+
return false;
84+
}
85+
}
86+
87+
return true;
88+
}
89+
}
90+
}
Lines changed: 2 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
using System;
2-
using System.Linq;
32
using System.Threading;
43
using Datadog.Trace.ClrProfiler.CallTarget;
54
using Datadog.Trace.Configuration;
6-
using Datadog.Trace.ExtensionMethods;
7-
using Datadog.Trace.Tagging;
85

96
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.HttpClientHandler
107
{
@@ -23,7 +20,6 @@ namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.HttpClientHandler
2320
public class HttpClientHandlerIntegration
2421
{
2522
private const string IntegrationName = nameof(IntegrationIds.HttpMessageHandler);
26-
private static readonly IntegrationInfo IntegrationId = IntegrationRegistry.GetIntegrationInfo(IntegrationName);
2723

2824
/// <summary>
2925
/// OnMethodBegin callback
@@ -37,21 +33,7 @@ public class HttpClientHandlerIntegration
3733
public static CallTargetState OnMethodBegin<TTarget, TRequest>(TTarget instance, TRequest requestMessage, CancellationToken cancellationToken)
3834
where TRequest : IHttpRequestMessage
3935
{
40-
if (IsTracingEnabled(requestMessage.Headers))
41-
{
42-
Scope scope = ScopeFactory.CreateOutboundHttpScope(Tracer.Instance, requestMessage.Method.Method, requestMessage.RequestUri, IntegrationId, out HttpTags tags);
43-
if (scope != null)
44-
{
45-
tags.HttpClientHandlerType = instance.GetType().FullName;
46-
47-
// add distributed tracing headers to the HTTP request
48-
SpanContextPropagator.Instance.Inject(scope.Span.Context, new HttpHeadersCollection(requestMessage.Headers));
49-
50-
return new CallTargetState(scope);
51-
}
52-
}
53-
54-
return CallTargetState.GetDefault();
36+
return HttpClientHandlerCommon.OnMethodBegin(instance, requestMessage, cancellationToken);
5537
}
5638

5739
/// <summary>
@@ -67,55 +49,7 @@ public static CallTargetState OnMethodBegin<TTarget, TRequest>(TTarget instance,
6749
public static TResponse OnAsyncMethodEnd<TTarget, TResponse>(TTarget instance, TResponse responseMessage, Exception exception, CallTargetState state)
6850
where TResponse : IHttpResponseMessage
6951
{
70-
Scope scope = state.Scope;
71-
72-
if (scope is null)
73-
{
74-
return responseMessage;
75-
}
76-
77-
try
78-
{
79-
scope.Span.SetHttpStatusCode(responseMessage.StatusCode, isServer: false);
80-
81-
if (exception != null)
82-
{
83-
scope.Span.SetException(exception);
84-
}
85-
}
86-
finally
87-
{
88-
scope.Dispose();
89-
}
90-
91-
return responseMessage;
92-
}
93-
94-
private static bool IsTracingEnabled(IRequestHeaders headers)
95-
{
96-
if (headers.TryGetValues(HttpHeaderNames.TracingEnabled, out var headerValues))
97-
{
98-
if (headerValues is string[] arrayValues)
99-
{
100-
for (var i = 0; i < arrayValues.Length; i++)
101-
{
102-
if (string.Equals(arrayValues[i], "false", StringComparison.OrdinalIgnoreCase))
103-
{
104-
return false;
105-
}
106-
}
107-
108-
return true;
109-
}
110-
111-
if (headerValues != null && headerValues.Any(s => string.Equals(s, "false", StringComparison.OrdinalIgnoreCase)))
112-
{
113-
// tracing is disabled for this request via http header
114-
return false;
115-
}
116-
}
117-
118-
return true;
52+
return HttpClientHandlerCommon.OnMethodEnd(instance, responseMessage, exception, state);
11953
}
12054
}
12155
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System;
2+
using System.Threading;
3+
using Datadog.Trace.ClrProfiler.CallTarget;
4+
using Datadog.Trace.Configuration;
5+
6+
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.HttpClientHandler
7+
{
8+
/// <summary>
9+
/// System.Net.Http.HttpClientHandler calltarget instrumentation
10+
/// </summary>
11+
[InstrumentMethod(
12+
AssemblyName = "System.Net.Http",
13+
TypeName = "System.Net.Http.HttpClientHandler",
14+
MethodName = "Send",
15+
ReturnTypeName = ClrNames.HttpResponseMessage,
16+
ParameterTypeNames = new[] { ClrNames.HttpRequestMessage, ClrNames.CancellationToken },
17+
MinimumVersion = "5.0.0",
18+
MaximumVersion = "5.*.*",
19+
IntegrationName = IntegrationName)]
20+
public class HttpClientHandlerSyncIntegration
21+
{
22+
private const string IntegrationName = nameof(IntegrationIds.HttpMessageHandler);
23+
24+
/// <summary>
25+
/// OnMethodBegin callback
26+
/// </summary>
27+
/// <typeparam name="TTarget">Type of the target</typeparam>
28+
/// <typeparam name="TRequest">Type of the request</typeparam>
29+
/// <param name="instance">Instance value, aka `this` of the instrumented method.</param>
30+
/// <param name="requestMessage">HttpRequest message instance</param>
31+
/// <param name="cancellationToken">CancellationToken value</param>
32+
/// <returns>Calltarget state value</returns>
33+
public static CallTargetState OnMethodBegin<TTarget, TRequest>(TTarget instance, TRequest requestMessage, CancellationToken cancellationToken)
34+
where TRequest : IHttpRequestMessage
35+
{
36+
return HttpClientHandlerCommon.OnMethodBegin(instance, requestMessage, cancellationToken);
37+
}
38+
39+
/// <summary>
40+
/// OnMethodEnd callback
41+
/// </summary>
42+
/// <typeparam name="TTarget">Type of the target</typeparam>
43+
/// <typeparam name="TResponse">Type of the response</typeparam>
44+
/// <param name="instance">Instance value, aka `this` of the instrumented method.</param>
45+
/// <param name="responseMessage">HttpResponse message instance</param>
46+
/// <param name="exception">Exception instance in case the original code threw an exception.</param>
47+
/// <param name="state">Calltarget state value</param>
48+
/// <returns>A response value, in an async scenario will be T of Task of T</returns>
49+
public static CallTargetReturn<TResponse> OnMethodEnd<TTarget, TResponse>(TTarget instance, TResponse responseMessage, Exception exception, CallTargetState state)
50+
where TResponse : IHttpResponseMessage
51+
{
52+
var response = HttpClientHandlerCommon.OnMethodEnd(instance, responseMessage, exception, state);
53+
return new CallTargetReturn<TResponse>(response);
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)