-
Notifications
You must be signed in to change notification settings - Fork 876
[http] Add http.client.request.duration metric and .NET Framework support #4870
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 45 commits
17d67e0
800a71d
4a9239e
d7b2472
46de1a9
4e445b5
f0ca432
bed7b2b
ba04ac0
48c354a
872ce63
6999167
42e9022
527106f
e31f687
01840c0
9ef2915
47fc955
d9ca497
308ca8f
480940e
27ad5ab
025c02d
dfaa8f7
ad10683
8bd64ef
6ca5c9d
61a01f7
b4dd250
bca1361
5200afb
4d15ca6
dd07ca0
fc47558
9ae0193
9c4c3b5
dcdde2c
a063be1
7ce3c51
bef3862
812ad91
2cfc3e2
051cb42
0f37992
86c8ab0
7704a82
7e03ca7
0ce3ced
5218b65
e70629b
616f299
1502d64
34e39f4
9f1f887
f2e864b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
| #if NETFRAMEWORK | ||
| using System.Collections; | ||
| using System.Diagnostics; | ||
| using System.Diagnostics.Metrics; | ||
| using System.Net; | ||
| using System.Reflection; | ||
| using System.Reflection.Emit; | ||
|
|
@@ -39,12 +40,15 @@ internal static class HttpWebRequestActivitySource | |
| internal static readonly AssemblyName AssemblyName = typeof(HttpWebRequestActivitySource).Assembly.GetName(); | ||
| internal static readonly string ActivitySourceName = AssemblyName.Name + ".HttpWebRequest"; | ||
| internal static readonly string ActivityName = ActivitySourceName + ".HttpRequestOut"; | ||
| internal static readonly string MeterInstrumentationName = AssemblyName.Name; | ||
|
|
||
| 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); | ||
|
|
||
| private static readonly Version Version = AssemblyName.Version; | ||
| private static readonly ActivitySource WebRequestActivitySource = new ActivitySource(ActivitySourceName, Version.ToString()); | ||
| private static readonly Meter WebRequestMeter = new Meter(MeterInstrumentationName, Version.ToString()); | ||
| private static readonly Histogram<double> HttpClientDuration; | ||
|
|
||
| private static HttpClientInstrumentationOptions options; | ||
|
|
||
|
|
@@ -87,6 +91,7 @@ static HttpWebRequestActivitySource() | |
| PerformInjection(); | ||
|
|
||
| Options = new HttpClientInstrumentationOptions(); | ||
| HttpClientDuration = WebRequestMeter.CreateHistogram<double>("http.client.duration", "ms", "Measures the duration of outbound HTTP requests."); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
|
|
@@ -157,6 +162,8 @@ private static void AddRequestTagsAndInstrumentRequest(HttpWebRequest request, A | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static void AddResponseTags(HttpWebResponse response, Activity activity) | ||
| { | ||
| Debug.Assert(activity != null, "Activity must not be null"); | ||
|
|
||
| if (activity.IsAllDataRequested) | ||
| { | ||
| if (emitOldAttributes) | ||
|
|
@@ -185,6 +192,8 @@ private static void AddResponseTags(HttpWebResponse response, Activity activity) | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static void AddExceptionTags(Exception exception, Activity activity) | ||
| { | ||
| Debug.Assert(activity != null, "Activity must not be null"); | ||
|
|
||
| if (!activity.IsAllDataRequested) | ||
| { | ||
| return; | ||
|
|
@@ -265,10 +274,13 @@ private static bool IsRequestInstrumented(HttpWebRequest request) | |
|
|
||
| private static void ProcessRequest(HttpWebRequest request) | ||
| { | ||
| if (!WebRequestActivitySource.HasListeners() || !Options.EventFilterHttpWebRequest(request)) | ||
| // There are subscribers to the ActivitySource and no user-provided filter is | ||
| // filtering this request. | ||
| var enableTracing = WebRequestActivitySource.HasListeners() && Options.EventFilterHttpWebRequest(request); | ||
|
|
||
| if (!enableTracing && !HttpClientDuration.Enabled) | ||
| { | ||
| // No subscribers to the ActivitySource or User provider Filter is | ||
| // filtering this request. | ||
| // Tracing and metrics are not enabled, so we can skip generating signals | ||
| // Propagation must still be done in such cases, to allow | ||
| // downstream services to continue from parent context, if any. | ||
| // Eg: Parent could be the Asp.Net activity. | ||
|
|
@@ -283,37 +295,41 @@ private static void ProcessRequest(HttpWebRequest request) | |
| return; | ||
| } | ||
|
|
||
| var activity = WebRequestActivitySource.StartActivity(ActivityName, ActivityKind.Client); | ||
| Activity activity = null; | ||
|
|
||
| if (enableTracing) | ||
| { | ||
| activity = WebRequestActivitySource.StartActivity(ActivityName, ActivityKind.Client); | ||
| } | ||
|
|
||
| var activityContext = Activity.Current?.Context ?? default; | ||
|
|
||
| // Propagation must still be done in all cases, to allow | ||
| // downstream services to continue from parent context, if any. | ||
| // Eg: Parent could be the Asp.Net activity. | ||
| InstrumentRequest(request, activityContext); | ||
| if (activity == null) | ||
| { | ||
| // There is a listener but it decided not to sample the current request. | ||
| return; | ||
| } | ||
|
|
||
| IAsyncResult asyncContext = writeAResultAccessor(request); | ||
| if (asyncContext != null) | ||
| { | ||
| // Flow here is for [Begin]GetRequestStream[Async]. | ||
|
|
||
| AsyncCallbackWrapper callback = new AsyncCallbackWrapper(request, activity, asyncCallbackAccessor(asyncContext)); | ||
| AsyncCallbackWrapper callback = new AsyncCallbackWrapper(request, activity, asyncCallbackAccessor(asyncContext), Stopwatch.GetTimestamp()); | ||
| asyncCallbackModifier(asyncContext, callback.AsyncCallback); | ||
| } | ||
| else | ||
| { | ||
| // Flow here is for [Begin]GetResponse[Async] without a prior call to [Begin]GetRequestStream[Async]. | ||
|
|
||
| asyncContext = readAResultAccessor(request); | ||
| AsyncCallbackWrapper callback = new AsyncCallbackWrapper(request, activity, asyncCallbackAccessor(asyncContext)); | ||
| AsyncCallbackWrapper callback = new AsyncCallbackWrapper(request, activity, asyncCallbackAccessor(asyncContext), Stopwatch.GetTimestamp()); | ||
| asyncCallbackModifier(asyncContext, callback.AsyncCallback); | ||
| } | ||
|
|
||
| AddRequestTagsAndInstrumentRequest(request, activity); | ||
| if (activity != null) | ||
| { | ||
| AddRequestTagsAndInstrumentRequest(request, activity); | ||
| } | ||
| } | ||
|
|
||
| private static void HookOrProcessResult(HttpWebRequest request) | ||
|
|
@@ -340,26 +356,32 @@ private static void HookOrProcessResult(HttpWebRequest request) | |
| if (endCalledAccessor.Invoke(readAsyncContext) || readAsyncContext.CompletedSynchronously) | ||
| { | ||
| // We need to process the result directly because the read callback has already fired. Force a copy because response has likely already been disposed. | ||
| ProcessResult(readAsyncContext, null, writeAsyncContextCallback.Activity, resultAccessor(readAsyncContext), true); | ||
| ProcessResult(readAsyncContext, null, writeAsyncContextCallback.Activity, resultAccessor(readAsyncContext), true, request, writeAsyncContextCallback.StartTimestamp); | ||
| return; | ||
| } | ||
|
|
||
| // Hook into the result callback if it hasn't already fired. | ||
| AsyncCallbackWrapper callback = new AsyncCallbackWrapper(writeAsyncContextCallback.Request, writeAsyncContextCallback.Activity, asyncCallbackAccessor(readAsyncContext)); | ||
| AsyncCallbackWrapper callback = new AsyncCallbackWrapper(writeAsyncContextCallback.Request, writeAsyncContextCallback.Activity, asyncCallbackAccessor(readAsyncContext), Stopwatch.GetTimestamp()); | ||
| asyncCallbackModifier(readAsyncContext, callback.AsyncCallback); | ||
| } | ||
|
|
||
| private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncCallback, Activity activity, object result, bool forceResponseCopy) | ||
| private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncCallback, Activity activity, object result, bool forceResponseCopy, HttpWebRequest request, long startTimestamp) | ||
| { | ||
| HttpStatusCode? httpStatusCode = null; | ||
|
|
||
| // Activity may be null if we are not tracing in these cases: | ||
| // 1. No listeners | ||
| // 2. Request was filtered out | ||
| // 3. Request was not sampled | ||
| // We could be executing on a different thread now so restore the activity if needed. | ||
| if (Activity.Current != activity) | ||
| if (activity != null && Activity.Current != activity) | ||
matt-hensley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| Activity.Current = activity; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| if (result is Exception ex) | ||
| if (activity != null && result is Exception ex) | ||
| { | ||
| AddExceptionTags(ex, activity); | ||
| } | ||
|
|
@@ -382,11 +404,21 @@ private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncC | |
| isWebSocketResponseAccessor(response), connectionGroupNameAccessor(response), | ||
| }); | ||
|
|
||
| AddResponseTags(responseCopy, activity); | ||
| if (activity != null) | ||
| { | ||
| AddResponseTags(responseCopy, activity); | ||
| } | ||
|
|
||
| httpStatusCode = responseCopy.StatusCode; | ||
| } | ||
| else | ||
| { | ||
| AddResponseTags(response, activity); | ||
| if (activity != null) | ||
| { | ||
| AddResponseTags(response, activity); | ||
| } | ||
|
|
||
| httpStatusCode = response.StatusCode; | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -395,7 +427,37 @@ private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncC | |
| HttpInstrumentationEventSource.Log.FailedProcessResult(ex); | ||
| } | ||
|
|
||
| activity.Stop(); | ||
| activity?.Stop(); | ||
|
|
||
| if (HttpClientDuration.Enabled) | ||
| { | ||
| double durationMs = 0; | ||
|
|
||
| if (activity != null) | ||
| { | ||
| durationMs = activity.Duration.TotalMilliseconds; | ||
| } | ||
| else | ||
| { | ||
| var endTimestamp = Stopwatch.GetTimestamp(); | ||
| var durationS = (endTimestamp - startTimestamp) / Stopwatch.Frequency; | ||
| durationMs = durationS * 1000; | ||
vishweshbankwar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| TagList tags = default; | ||
| tags.Add(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.ProtocolVersion)); | ||
| tags.Add(SemanticConventions.AttributeHttpMethod, request.Method); | ||
| tags.Add(SemanticConventions.AttributeHttpScheme, request.RequestUri.Scheme); | ||
| tags.Add(SemanticConventions.AttributeNetPeerName, request.RequestUri.Host); | ||
| tags.Add(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port); | ||
|
|
||
| if (httpStatusCode.HasValue) | ||
| { | ||
| tags.Add(SemanticConventions.AttributeHttpStatusCode, (int)httpStatusCode.Value); | ||
| } | ||
|
|
||
| HttpClientDuration.Record(durationMs, tags); | ||
|
||
| } | ||
| } | ||
|
|
||
| private static void PrepareReflectionObjects() | ||
|
|
@@ -1109,11 +1171,12 @@ public override void Clear() | |
| /// </summary> | ||
| private sealed class AsyncCallbackWrapper | ||
| { | ||
| public AsyncCallbackWrapper(HttpWebRequest request, Activity activity, AsyncCallback originalCallback) | ||
| public AsyncCallbackWrapper(HttpWebRequest request, Activity activity, AsyncCallback originalCallback, long startTimestamp) | ||
| { | ||
| this.Request = request; | ||
| this.Activity = activity; | ||
| this.OriginalCallback = originalCallback; | ||
| this.StartTimestamp = startTimestamp; | ||
| } | ||
|
|
||
| public HttpWebRequest Request { get; } | ||
|
|
@@ -1122,12 +1185,21 @@ public AsyncCallbackWrapper(HttpWebRequest request, Activity activity, AsyncCall | |
|
|
||
| public AsyncCallback OriginalCallback { get; } | ||
|
|
||
| public long StartTimestamp { get; } | ||
|
|
||
| public void AsyncCallback(IAsyncResult asyncResult) | ||
| { | ||
| object result = resultAccessor(asyncResult); | ||
| if (result is Exception || result is HttpWebResponse) | ||
| { | ||
| ProcessResult(asyncResult, this.OriginalCallback, this.Activity, result, false); | ||
| ProcessResult( | ||
| asyncResult, | ||
| this.OriginalCallback, | ||
| this.Activity, | ||
| result, | ||
| forceResponseCopy: false, | ||
| this.Request, | ||
| this.StartTimestamp); | ||
| } | ||
|
|
||
| this.OriginalCallback?.Invoke(asyncResult); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.