diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/.publicApi/net461/PublicAPI.Unshipped.txt index 7e00f288d20..04b7b15df9b 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/.publicApi/net461/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/.publicApi/net461/PublicAPI.Unshipped.txt @@ -1,9 +1,14 @@ -OpenTelemetry.Instrumentation.AspNet.ActivityExtensions +const OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.AspNetActivityName = "Microsoft.AspNet.HttpReqIn" -> string +const OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.AspNetSourceName = "OpenTelemetry.Instrumentation.AspNet.Telemetry" -> string OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Dispose() -> void OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Init(System.Web.HttpApplication context) -> void -OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.ParseHeaders.get -> bool -OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.ParseHeaders.set -> void +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.OnExceptionCallback.get -> System.Action +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.OnExceptionCallback.set -> void +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.OnRequestStartedCallback.get -> System.Action +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.OnRequestStartedCallback.set -> void +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.OnRequestStoppedCallback.get -> System.Action +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.OnRequestStoppedCallback.set -> void OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.TelemetryHttpModule() -> void -static OpenTelemetry.Instrumentation.AspNet.ActivityExtensions.Extract(this System.Diagnostics.Activity activity, System.Collections.Specialized.NameValueCollection requestHeaders) -> bool -static OpenTelemetry.Instrumentation.AspNet.ActivityExtensions.TryParse(this System.Diagnostics.Activity activity, System.Collections.Specialized.NameValueCollection requestHeaders) -> bool \ No newline at end of file +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.TextMapPropagator.get -> OpenTelemetry.Context.Propagation.TraceContextPropagator +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.TextMapPropagator.set -> void diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityExtensions.cs deleted file mode 100644 index e5a542c76fb..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityExtensions.cs +++ /dev/null @@ -1,159 +0,0 @@ -// -// 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. -// - -using System; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Diagnostics; - -namespace OpenTelemetry.Instrumentation.AspNet -{ - /// - /// Extensions of Activity class. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static class ActivityExtensions - { - /// - /// Http header name to carry the Request Id: https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.DiagnosticSource/src/HttpCorrelationProtocol.md. - /// - internal const string RequestIdHeaderName = "Request-Id"; - - /// - /// Http header name to carry the traceparent: https://www.w3.org/TR/trace-context/. - /// - internal const string TraceparentHeaderName = "traceparent"; - - /// - /// Http header name to carry the tracestate: https://www.w3.org/TR/trace-context/. - /// - internal const string TracestateHeaderName = "tracestate"; - - /// - /// Http header name to carry the correlation context. - /// - internal const string CorrelationContextHeaderName = "Correlation-Context"; - - /// - /// Maximum length of Correlation-Context header value. - /// - internal const int MaxCorrelationContextLength = 1024; - - /// - /// Reads Request-Id and Correlation-Context headers and sets ParentId and Baggage on Activity. - /// - /// Instance of activity that has not been started yet. - /// Request headers collection. - /// true if request was parsed successfully, false - otherwise. - public static bool Extract(this Activity activity, NameValueCollection requestHeaders) - { - if (activity == null) - { - AspNetTelemetryEventSource.Log.ActvityExtractionError("activity is null"); - return false; - } - - if (activity.ParentId != null) - { - AspNetTelemetryEventSource.Log.ActvityExtractionError("ParentId is already set on activity"); - return false; - } - - if (activity.Id != null) - { - AspNetTelemetryEventSource.Log.ActvityExtractionError("Activity is already started"); - return false; - } - - var parents = requestHeaders.GetValues(TraceparentHeaderName); - if (parents == null || parents.Length == 0) - { - parents = requestHeaders.GetValues(RequestIdHeaderName); - } - - if (parents != null && parents.Length > 0 && !string.IsNullOrEmpty(parents[0])) - { - // there may be several Request-Id or traceparent headers, but we only read the first one - activity.SetParentId(parents[0]); - - var tracestates = requestHeaders.GetValues(TracestateHeaderName); - if (tracestates != null && tracestates.Length > 0) - { - if (tracestates.Length == 1 && !string.IsNullOrEmpty(tracestates[0])) - { - activity.TraceStateString = tracestates[0]; - } - else - { - activity.TraceStateString = string.Join(",", tracestates); - } - } - - // Header format - Correlation-Context: key1=value1, key2=value2 - var baggages = requestHeaders.GetValues(CorrelationContextHeaderName); - if (baggages != null) - { - int correlationContextLength = -1; - - // there may be several Correlation-Context header - foreach (var item in baggages) - { - if (correlationContextLength >= MaxCorrelationContextLength) - { - break; - } - - foreach (var pair in item.Split(',')) - { - correlationContextLength += pair.Length + 1; // pair and comma - - if (correlationContextLength >= MaxCorrelationContextLength) - { - break; - } - - if (NameValueHeaderValue.TryParse(pair, out NameValueHeaderValue baggageItem)) - { - activity.AddBaggage(baggageItem.Name, baggageItem.Value); - } - else - { - AspNetTelemetryEventSource.Log.HeaderParsingError(CorrelationContextHeaderName, pair); - } - } - } - } - - return true; - } - - return false; - } - - /// - /// Reads Request-Id and Correlation-Context headers and sets ParentId and Baggage on Activity. - /// - /// Instance of activity that has not been started yet. - /// Request headers collection. - /// true if request was parsed successfully, false - otherwise. - [Obsolete("Method is obsolete, use Extract method instead", true)] - [EditorBrowsable(EditorBrowsableState.Never)] - public static bool TryParse(this Activity activity, NameValueCollection requestHeaders) - { - return Extract(activity, requestHeaders); - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityHelper.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityHelper.cs index 7826968996e..dd13ab6f353 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityHelper.cs +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityHelper.cs @@ -16,8 +16,10 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Diagnostics; using System.Web; +using OpenTelemetry.Context.Propagation; namespace OpenTelemetry.Instrumentation.AspNet { @@ -27,97 +29,105 @@ namespace OpenTelemetry.Instrumentation.AspNet internal static class ActivityHelper { /// - /// Listener name. + /// Key to store the activity in HttpContext. /// - public const string AspNetListenerName = "OpenTelemetry.Instrumentation.AspNet.Telemetry"; + public const string ActivityKey = "__AspnetActivity__"; - /// - /// Activity name for http request. - /// - public const string AspNetActivityName = "Microsoft.AspNet.HttpReqIn"; + private static readonly ActivitySource AspNetSource = new ActivitySource(TelemetryHttpModule.AspNetSourceName); + private static readonly Func> HttpRequestHeaderValuesGetter = (request, name) => request.Headers.GetValues(name); /// - /// Event name for the activity start event. + /// Creates root (first level) activity that describes incoming request. /// - public const string AspNetActivityStartName = "Microsoft.AspNet.HttpReqIn.Start"; + /// . + /// Current HttpContext. + /// Callback action. + /// New root activity. + public static Activity StartAspNetActivity(TextMapPropagator textMapPropagator, HttpContext context, Action onRequestStartedCallback) + { + PropagationContext propagationContext = textMapPropagator.Extract(default, context.Request, HttpRequestHeaderValuesGetter); - /// - /// Key to store the activity in HttpContext. - /// - public const string ActivityKey = "__AspnetActivity__"; + Activity activity = AspNetSource.CreateActivity(TelemetryHttpModule.AspNetActivityName, ActivityKind.Server, propagationContext.ActivityContext); - private static readonly DiagnosticListener AspNetListener = new DiagnosticListener(AspNetListenerName); + if (activity != null) + { + context.Items[ActivityKey] = activity; - private static readonly object EmptyPayload = new object(); + if (propagationContext.Baggage != default) + { + Baggage.Current = propagationContext.Baggage; + } + + try + { + onRequestStartedCallback?.Invoke(activity, context); + } + catch (Exception callbackEx) + { + AspNetTelemetryEventSource.Log.CallbackException(activity, "OnStarted", callbackEx); + } + + AspNetTelemetryEventSource.Log.ActivityStarted(activity); + } + + return activity; + } /// /// Stops the activity and notifies listeners about it. /// - /// HttpContext.Items. - public static void StopAspNetActivity(IDictionary contextItems) + /// Current HttpContext. + /// Callback action. + public static void StopAspNetActivity(HttpContext context, Action onRequestStoppedCallback) { + var contextItems = context.Items; var currentActivity = Activity.Current; Activity aspNetActivity = (Activity)contextItems[ActivityKey]; if (currentActivity != aspNetActivity) { Activity.Current = aspNetActivity; - currentActivity = aspNetActivity; } - if (currentActivity != null) + if (aspNetActivity != null) { - // stop Activity with Stop event - AspNetListener.StopActivity(currentActivity, EmptyPayload); + aspNetActivity.Stop(); contextItems[ActivityKey] = null; - } - - AspNetTelemetryEventSource.Log.ActivityStopped(currentActivity?.Id, currentActivity?.OperationName); - } - /// - /// Creates root (first level) activity that describes incoming request. - /// - /// Current HttpContext. - /// Determines if headers should be parsed get correlation ids. - /// New root activity. - public static Activity CreateRootActivity(HttpContext context, bool parseHeaders) - { - if (AspNetListener.IsEnabled() && AspNetListener.IsEnabled(AspNetActivityName)) - { - var rootActivity = new Activity(AspNetActivityName); - - if (parseHeaders) + try { - rootActivity.Extract(context.Request.Unvalidated.Headers); + onRequestStoppedCallback?.Invoke(aspNetActivity, context); } - - AspNetListener.OnActivityImport(rootActivity, null); - - if (StartAspNetActivity(rootActivity)) + catch (Exception callbackEx) { - context.Items[ActivityKey] = rootActivity; - AspNetTelemetryEventSource.Log.ActivityStarted(rootActivity.Id); - return rootActivity; + AspNetTelemetryEventSource.Log.CallbackException(aspNetActivity, "OnStopped", callbackEx); } + + AspNetTelemetryEventSource.Log.ActivityStopped(currentActivity); } - return null; + if (currentActivity != aspNetActivity) + { + Activity.Current = currentActivity; + } } - public static void WriteActivityException(IDictionary contextItems, Exception exception) + public static void WriteActivityException(IDictionary contextItems, Exception exception, Action onExceptionCallback) { Activity aspNetActivity = (Activity)contextItems[ActivityKey]; if (aspNetActivity != null) { - if (Activity.Current != aspNetActivity) + try { - Activity.Current = aspNetActivity; + onExceptionCallback?.Invoke(aspNetActivity, exception); + } + catch (Exception callbackEx) + { + AspNetTelemetryEventSource.Log.CallbackException(aspNetActivity, "OnException", callbackEx); } - AspNetListener.Write(aspNetActivity.OperationName + ".Exception", exception); - AspNetTelemetryEventSource.Log.ActivityException(aspNetActivity.Id, aspNetActivity.OperationName, exception); + AspNetTelemetryEventSource.Log.ActivityException(aspNetActivity, exception); } } @@ -136,27 +146,9 @@ internal static void RestoreActivityIfNeeded(IDictionary contextItems) if (aspNetActivity != null) { Activity.Current = aspNetActivity; + AspNetTelemetryEventSource.Log.ActivityRestored(aspNetActivity); } } } - - private static bool StartAspNetActivity(Activity activity) - { - if (AspNetListener.IsEnabled(AspNetActivityName, activity, EmptyPayload)) - { - if (AspNetListener.IsEnabled(AspNetActivityStartName)) - { - AspNetListener.StartActivity(activity, EmptyPayload); - } - else - { - activity.Start(); - } - - return true; - } - - return false; - } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/AspNetTelemetryEventSource.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/AspNetTelemetryEventSource.cs index 602c5df99e9..38c6cb452ab 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/AspNetTelemetryEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/AspNetTelemetryEventSource.cs @@ -15,6 +15,7 @@ // using System; +using System.Diagnostics; using System.Diagnostics.Tracing; namespace OpenTelemetry.Instrumentation.AspNet @@ -31,78 +32,90 @@ internal sealed class AspNetTelemetryEventSource : EventSource public static readonly AspNetTelemetryEventSource Log = new AspNetTelemetryEventSource(); [NonEvent] - public void ActivityException(string id, string eventName, Exception ex) + public void ActivityStarted(Activity activity) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + if (this.IsEnabled(EventLevel.Verbose, EventKeywords.All)) { - this.ActivityException(id, eventName, ex.ToString()); + this.ActivityStarted(activity?.Id); } } - [Event(1, Message = "Callback='{0}'", Level = EventLevel.Verbose)] - public void TraceCallback(string callback) + [NonEvent] + public void ActivityStopped(Activity activity) { - this.WriteEvent(1, callback); + if (this.IsEnabled(EventLevel.Verbose, EventKeywords.All)) + { + this.ActivityStopped(activity?.Id); + } } - [Event(2, Message = "Activity started, Id='{0}'", Level = EventLevel.Verbose)] - public void ActivityStarted(string id) + [NonEvent] + public void ActivityRestored(Activity activity) { - this.WriteEvent(2, id); + if (this.IsEnabled(EventLevel.Informational, EventKeywords.All)) + { + this.ActivityRestored(activity?.Id); + } } - [Event(3, Message = "Activity stopped, Id='{0}', Name='{1}'", Level = EventLevel.Verbose)] - public void ActivityStopped(string id, string eventName) + [NonEvent] + public void ActivityException(Activity activity, Exception ex) { - this.WriteEvent(3, id, eventName); + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.ActivityException(activity?.Id, ex.ToString()); + } } - [Event(4, Message = "Failed to parse header '{0}', value: '{1}'", Level = EventLevel.Informational)] - public void HeaderParsingError(string headerName, string headerValue) + [NonEvent] + public void CallbackException(Activity activity, string eventName, Exception ex) { - this.WriteEvent(4, headerName, headerValue); + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.CallbackException(activity?.Id, eventName, ex.ToString()); + } } - [Event(5, Message = "Failed to extract activity, reason '{0}'", Level = EventLevel.Error)] - public void ActvityExtractionError(string reason) + [Event(1, Message = "Callback='{0}'", Level = EventLevel.Verbose)] + public void TraceCallback(string callback) { - this.WriteEvent(5, reason); + this.WriteEvent(1, callback); } - [Event(6, Message = "Finished Activity is detected on the stack, Id: '{0}', Name: '{1}'", Level = EventLevel.Error)] - public void FinishedActivityIsDetected(string id, string name) + [Event(2, Message = "Activity started, Id='{0}'", Level = EventLevel.Verbose)] + public void ActivityStarted(string id) { - this.WriteEvent(6, id, name); + this.WriteEvent(2, id); } - [Event(7, Message = "System.Diagnostics.Activity stack is too deep. This is a code authoring error, Activity will not be stopped.", Level = EventLevel.Error)] - public void ActivityStackIsTooDeepError() + [Event(3, Message = "Activity stopped, Id='{0}'", Level = EventLevel.Verbose)] + public void ActivityStopped(string id) { - this.WriteEvent(7); + this.WriteEvent(3, id); } - [Event(8, Message = "Activity restored, Id='{0}'", Level = EventLevel.Informational)] + [Event(4, Message = "Activity restored, Id='{0}'", Level = EventLevel.Informational)] public void ActivityRestored(string id) { - this.WriteEvent(8, id); + this.WriteEvent(4, id); } - [Event(9, Message = "Failed to invoke OnExecuteRequestStep, Error='{0}'", Level = EventLevel.Error)] + [Event(5, Message = "Failed to invoke OnExecuteRequestStep, Error='{0}'", Level = EventLevel.Error)] public void OnExecuteRequestStepInvokationError(string error) { - this.WriteEvent(9, error); + this.WriteEvent(5, error); } - [Event(10, Message = "System.Diagnostics.Activity stack is too deep. Current Id: '{0}', Name: '{1}'", Level = EventLevel.Warning)] - public void ActivityStackIsTooDeepDetails(string id, string name) + [Event(6, Message = "Activity exception, Id='{0}': {1}", Level = EventLevel.Error)] + public void ActivityException(string id, string ex) { - this.WriteEvent(10, id, name); + this.WriteEvent(6, id, ex); } - [Event(11, Message = "Activity exception, Id='{0}', Name='{1}': {2}", Level = EventLevel.Error)] - public void ActivityException(string id, string eventName, string ex) + [Event(7, Message = "Callback exception, Id='{0}', Name='{1}': {2}", Level = EventLevel.Error)] + public void CallbackException(string id, string eventName, string ex) { - this.WriteEvent(11, id, eventName, ex); + this.WriteEvent(7, id, eventName, ex); } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/BaseHeaderParser.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/BaseHeaderParser.cs deleted file mode 100644 index 4c429212023..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/BaseHeaderParser.cs +++ /dev/null @@ -1,81 +0,0 @@ -// -// 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. -// - -namespace OpenTelemetry.Instrumentation.AspNet -{ - // Adoptation of code from https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.Net.Http.Headers/BaseHeaderParser.cs - internal abstract class BaseHeaderParser : HttpHeaderParser - { - protected BaseHeaderParser(bool supportsMultipleValues) - : base(supportsMultipleValues) - { - } - - public sealed override bool TryParseValue(string value, ref int index, out T parsedValue) - { - parsedValue = default; - - // If multiple values are supported (i.e. list of values), then accept an empty string: The header may - // be added multiple times to the request/response message. E.g. - // Accept: text/xml; q=1 - // Accept: - // Accept: text/plain; q=0.2 - if (string.IsNullOrEmpty(value) || (index == value.Length)) - { - return this.SupportsMultipleValues; - } - - var current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(value, index, this.SupportsMultipleValues, out var separatorFound); - - if (separatorFound && !this.SupportsMultipleValues) - { - return false; // leading separators not allowed if we don't support multiple values. - } - - if (current == value.Length) - { - if (this.SupportsMultipleValues) - { - index = current; - } - - return this.SupportsMultipleValues; - } - - var length = this.GetParsedValueLength(value, current, out var result); - - if (length == 0) - { - return false; - } - - current += length; - current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(value, current, this.SupportsMultipleValues, out separatorFound); - - // If we support multiple values and we've not reached the end of the string, then we must have a separator. - if ((separatorFound && !this.SupportsMultipleValues) || (!separatorFound && (current < value.Length))) - { - return false; - } - - index = current; - parsedValue = result; - return true; - } - - protected abstract int GetParsedValueLength(string value, int startIndex, out T parsedValue); - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/GenericHeaderParser.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/GenericHeaderParser.cs deleted file mode 100644 index 37824a0d021..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/GenericHeaderParser.cs +++ /dev/null @@ -1,39 +0,0 @@ -// -// 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. -// - -using System; - -namespace OpenTelemetry.Instrumentation.AspNet -{ - // Adoptation of code from https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.Net.Http.Headers/GenericHeaderParser.cs - internal sealed class GenericHeaderParser : BaseHeaderParser - { - private readonly GetParsedValueLengthDelegate getParsedValueLength; - - internal GenericHeaderParser(bool supportsMultipleValues, GetParsedValueLengthDelegate getParsedValueLength) - : base(supportsMultipleValues) - { - this.getParsedValueLength = getParsedValueLength ?? throw new ArgumentNullException(nameof(getParsedValueLength)); - } - - internal delegate int GetParsedValueLengthDelegate(string value, int startIndex, out T parsedValue); - - protected override int GetParsedValueLength(string value, int startIndex, out T parsedValue) - { - return this.getParsedValueLength(value, startIndex, out parsedValue); - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HeaderUtilities.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HeaderUtilities.cs deleted file mode 100644 index 05644b3a8bc..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HeaderUtilities.cs +++ /dev/null @@ -1,59 +0,0 @@ -// -// 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. -// - -using System.Diagnostics.Contracts; - -namespace OpenTelemetry.Instrumentation.AspNet -{ - // Adoption of the code from https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs - internal static class HeaderUtilities - { - internal static int GetNextNonEmptyOrWhitespaceIndex( - string input, - int startIndex, - bool skipEmptyValues, - out bool separatorFound) - { - Contract.Requires(input != null); - Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length. - - separatorFound = false; - var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); - - if ((current == input.Length) || (input[current] != ',')) - { - return current; - } - - // If we have a separator, skip the separator and all following whitespaces. If we support - // empty values, continue until the current character is neither a separator nor a whitespace. - separatorFound = true; - current++; // skip delimiter. - current += HttpRuleParser.GetWhitespaceLength(input, current); - - if (skipEmptyValues) - { - while ((current < input.Length) && (input[current] == ',')) - { - current++; // skip delimiter. - current += HttpRuleParser.GetWhitespaceLength(input, current); - } - } - - return current; - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpHeaderParser.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpHeaderParser.cs deleted file mode 100644 index 05584edcd8d..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpHeaderParser.cs +++ /dev/null @@ -1,40 +0,0 @@ -// -// 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. -// - -namespace OpenTelemetry.Instrumentation.AspNet -{ - // Adoptation of code from https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.Net.Http.Headers/HttpHeaderParser.cs - internal abstract class HttpHeaderParser - { - private bool supportsMultipleValues; - - protected HttpHeaderParser(bool supportsMultipleValues) - { - this.supportsMultipleValues = supportsMultipleValues; - } - - public bool SupportsMultipleValues - { - get { return this.supportsMultipleValues; } - } - - // If a parser supports multiple values, a call to ParseValue/TryParseValue should return a value for 'index' - // pointing to the next non-whitespace character after a delimiter. E.g. if called with a start index of 0 - // for string "value , second_value", then after the call completes, 'index' must point to 's', i.e. the first - // non-whitespace after the separator ','. - public abstract bool TryParseValue(string value, ref int index, out T parsedValue); - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpParseResult.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpParseResult.cs deleted file mode 100644 index 288e1faa070..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpParseResult.cs +++ /dev/null @@ -1,37 +0,0 @@ -// -// 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. -// - -namespace OpenTelemetry.Instrumentation.AspNet -{ - // Adoptation of code from https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.Net.Http.Headers/HttpParseResult.cs - internal enum HttpParseResult - { - /// - /// Parsed successfully. - /// - Parsed, - - /// - /// Was not parsed. - /// - NotParsed, - - /// - /// Invalid format. - /// - InvalidFormat, - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpRuleParser.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpRuleParser.cs deleted file mode 100644 index bd3b86b30f6..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpRuleParser.cs +++ /dev/null @@ -1,281 +0,0 @@ -// -// 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. -// - -using System.Diagnostics.Contracts; - -namespace OpenTelemetry.Instrumentation.AspNet -{ - // Adoptation of code from https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs - internal static class HttpRuleParser - { - internal const char CR = '\r'; - internal const char LF = '\n'; - internal const char SP = ' '; - internal const char Tab = '\t'; - internal const int MaxInt64Digits = 19; - internal const int MaxInt32Digits = 10; - - private const int MaxNestedCount = 5; - private static readonly bool[] TokenChars = CreateTokenChars(); - - internal static bool IsTokenChar(char character) - { - // Must be between 'space' (32) and 'DEL' (127) - if (character > 127) - { - return false; - } - - return TokenChars[character]; - } - - internal static int GetTokenLength(string input, int startIndex) - { - Contract.Requires(input != null); - Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - startIndex))); - - if (startIndex >= input.Length) - { - return 0; - } - - var current = startIndex; - - while (current < input.Length) - { - if (!IsTokenChar(input[current])) - { - return current - startIndex; - } - - current++; - } - - return input.Length - startIndex; - } - - internal static int GetWhitespaceLength(string input, int startIndex) - { - Contract.Requires(input != null); - Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - startIndex))); - - if (startIndex >= input.Length) - { - return 0; - } - - var current = startIndex; - - char c; - while (current < input.Length) - { - c = input[current]; - - if ((c == SP) || (c == Tab)) - { - current++; - continue; - } - - if (c == CR) - { - // If we have a #13 char, it must be followed by #10 and then at least one SP or HT. - if ((current + 2 < input.Length) && (input[current + 1] == LF)) - { - char spaceOrTab = input[current + 2]; - if ((spaceOrTab == SP) || (spaceOrTab == Tab)) - { - current += 3; - continue; - } - } - } - - return current - startIndex; - } - - // All characters between startIndex and the end of the string are LWS characters. - return input.Length - startIndex; - } - - internal static HttpParseResult GetQuotedStringLength(string input, int startIndex, out int length) - { - var nestedCount = 0; - return GetExpressionLength(input, startIndex, '"', '"', false, ref nestedCount, out length); - } - - // quoted-pair = "\" CHAR - // CHAR = - internal static HttpParseResult GetQuotedPairLength(string input, int startIndex, out int length) - { - Contract.Requires(input != null); - Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); - Contract.Ensures((Contract.ValueAtReturn(out _) >= 0) && - (Contract.ValueAtReturn(out _) <= (input.Length - startIndex))); - - length = 0; - - if (input[startIndex] != '\\') - { - return HttpParseResult.NotParsed; - } - - // Quoted-char has 2 characters. Check whether there are 2 chars left ('\' + char) - // If so, check whether the character is in the range 0-127. If not, it's an invalid value. - if ((startIndex + 2 > input.Length) || (input[startIndex + 1] > 127)) - { - return HttpParseResult.InvalidFormat; - } - - // We don't care what the char next to '\' is. - length = 2; - return HttpParseResult.Parsed; - } - - private static bool[] CreateTokenChars() - { - // token = 1* - // CTL = - var tokenChars = new bool[128]; // everything is false - - for (int i = 33; i < 127; i++) - { - // skip Space (32) & DEL (127) - tokenChars[i] = true; - } - - // remove separators: these are not valid token characters - tokenChars[(byte)'('] = false; - tokenChars[(byte)')'] = false; - tokenChars[(byte)'<'] = false; - tokenChars[(byte)'>'] = false; - tokenChars[(byte)'@'] = false; - tokenChars[(byte)','] = false; - tokenChars[(byte)';'] = false; - tokenChars[(byte)':'] = false; - tokenChars[(byte)'\\'] = false; - tokenChars[(byte)'"'] = false; - tokenChars[(byte)'/'] = false; - tokenChars[(byte)'['] = false; - tokenChars[(byte)']'] = false; - tokenChars[(byte)'?'] = false; - tokenChars[(byte)'='] = false; - tokenChars[(byte)'{'] = false; - tokenChars[(byte)'}'] = false; - - return tokenChars; - } - - // TEXT = - // LWS = [CRLF] 1*( SP | HT ) - // CTL = - // - // Since we don't really care about the content of a quoted string or comment, we're more tolerant and - // allow these characters. We only want to find the delimiters ('"' for quoted string and '(', ')' for comment). - // - // 'nestedCount': Comments can be nested. We allow a depth of up to 5 nested comments, i.e. something like - // "(((((comment)))))". If we wouldn't define a limit an attacker could send a comment with hundreds of nested - // comments, resulting in a stack overflow exception. In addition having more than 1 nested comment (if any) - // is unusual. - private static HttpParseResult GetExpressionLength( - string input, - int startIndex, - char openChar, - char closeChar, - bool supportsNesting, - ref int nestedCount, - out int length) - { - Contract.Requires(input != null); - Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); - Contract.Ensures((Contract.Result() != HttpParseResult.Parsed) || - (Contract.ValueAtReturn(out _) > 0)); - - length = 0; - - if (input[startIndex] != openChar) - { - return HttpParseResult.NotParsed; - } - - var current = startIndex + 1; // Start parsing with the character next to the first open-char - while (current < input.Length) - { - // Only check whether we have a quoted char, if we have at least 3 characters left to read (i.e. - // quoted char + closing char). Otherwise the closing char may be considered part of the quoted char. - if ((current + 2 < input.Length) && - (GetQuotedPairLength(input, current, out var quotedPairLength) == HttpParseResult.Parsed)) - { - // We ignore invalid quoted-pairs. Invalid quoted-pairs may mean that it looked like a quoted pair, - // but we actually have a quoted-string: e.g. '\' followed by a char >127 - quoted-pair only - // allows ASCII chars after '\'; qdtext allows both '\' and >127 chars. - current += quotedPairLength; - continue; - } - - // If we support nested expressions and we find an open-char, then parse the nested expressions. - if (supportsNesting && (input[current] == openChar)) - { - nestedCount++; - try - { - // Check if we exceeded the number of nested calls. - if (nestedCount > MaxNestedCount) - { - return HttpParseResult.InvalidFormat; - } - - HttpParseResult nestedResult = GetExpressionLength(input, current, openChar, closeChar, supportsNesting, ref nestedCount, out var nestedLength); - - switch (nestedResult) - { - case HttpParseResult.Parsed: - current += nestedLength; // add the length of the nested expression and continue. - break; - - case HttpParseResult.NotParsed: - Contract.Assert(false, "'NotParsed' is unexpected: We started nested expression parsing, because we found the open-char. So either it's a valid nested expression or it has invalid format."); - break; - - case HttpParseResult.InvalidFormat: - // If the nested expression is invalid, we can't continue, so we fail with invalid format. - return HttpParseResult.InvalidFormat; - - default: - Contract.Assert(false, "Unknown enum result: " + nestedResult); - break; - } - } - finally - { - nestedCount--; - } - } - - if (input[current] == closeChar) - { - length = current - startIndex + 1; - return HttpParseResult.Parsed; - } - - current++; - } - - // We didn't see the final quote, therefore we have an invalid expression string. - return HttpParseResult.InvalidFormat; - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/NameValueHeaderValue.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/NameValueHeaderValue.cs deleted file mode 100644 index 5add297c6f4..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/NameValueHeaderValue.cs +++ /dev/null @@ -1,134 +0,0 @@ -// -// 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. -// - -using System.Diagnostics.Contracts; - -namespace OpenTelemetry.Instrumentation.AspNet -{ - // Adoptation of code from https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs - - // According to the RFC, in places where a "parameter" is required, the value is mandatory - // (e.g. Media-Type, Accept). However, we don't introduce a dedicated type for it. So NameValueHeaderValue supports - // name-only values in addition to name/value pairs. - internal class NameValueHeaderValue - { - private static readonly HttpHeaderParser SingleValueParser - = new GenericHeaderParser(false, GetNameValueLength); - - private string name; - private string value; - - private NameValueHeaderValue() - { - // Used by the parser to create a new instance of this type. - } - - public string Name - { - get { return this.name; } - } - - public string Value - { - get { return this.value; } - } - - public static bool TryParse(string input, out NameValueHeaderValue parsedValue) - { - var index = 0; - return SingleValueParser.TryParseValue(input, ref index, out parsedValue); - } - - internal static int GetValueLength(string input, int startIndex) - { - Contract.Requires(input != null); - - if (startIndex >= input.Length) - { - return 0; - } - - var valueLength = HttpRuleParser.GetTokenLength(input, startIndex); - - if (valueLength == 0) - { - // A value can either be a token or a quoted string. Check if it is a quoted string. - if (HttpRuleParser.GetQuotedStringLength(input, startIndex, out valueLength) != HttpParseResult.Parsed) - { - // We have an invalid value. Reset the name and return. - return 0; - } - } - - return valueLength; - } - - private static int GetNameValueLength(string input, int startIndex, out NameValueHeaderValue parsedValue) - { - Contract.Requires(input != null); - Contract.Requires(startIndex >= 0); - - parsedValue = null; - - if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) - { - return 0; - } - - // Parse the name, i.e. in name/value string "=". Caller must remove - // leading whitespaces. - var nameLength = HttpRuleParser.GetTokenLength(input, startIndex); - - if (nameLength == 0) - { - return 0; - } - - var name = input.Substring(startIndex, nameLength); - var current = startIndex + nameLength; - current += HttpRuleParser.GetWhitespaceLength(input, current); - - // Parse the separator between name and value - if ((current == input.Length) || (input[current] != '=')) - { - // We only have a name and that's OK. Return. - parsedValue = new NameValueHeaderValue - { - name = name, - }; - current += HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces - return current - startIndex; - } - - current++; // skip delimiter. - current += HttpRuleParser.GetWhitespaceLength(input, current); - - // Parse the value, i.e. in name/value string "=" - int valueLength = GetValueLength(input, current); - - // Value after the '=' may be empty - // Use parameterless ctor to avoid double-parsing of name and value, i.e. skip public ctor validation. - parsedValue = new NameValueHeaderValue - { - name = name, - value = input.Substring(current, valueLength), - }; - current += valueLength; - current += HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces - return current - startIndex; - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.csproj b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.csproj index c9f90a4892d..9f9c307f674 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.csproj +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/TelemetryHttpModule.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/TelemetryHttpModule.cs index 953f047773a..6801472ec53 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/TelemetryHttpModule.cs +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/TelemetryHttpModule.cs @@ -16,8 +16,10 @@ using System; using System.ComponentModel; +using System.Diagnostics; using System.Reflection; using System.Web; +using OpenTelemetry.Context.Propagation; namespace OpenTelemetry.Instrumentation.AspNet { @@ -26,6 +28,16 @@ namespace OpenTelemetry.Instrumentation.AspNet /// public class TelemetryHttpModule : IHttpModule { + /// + /// OpenTelemetry.Instrumentation.AspNet name. + /// + public const string AspNetSourceName = "OpenTelemetry.Instrumentation.AspNet.Telemetry"; + + /// + /// for OpenTelemetry.Instrumentation.AspNet created objects. + /// + public const string AspNetActivityName = "Microsoft.AspNet.HttpReqIn"; + private const string BeginCalledFlag = "OpenTelemetry.Instrumentation.AspNet.BeginCalled"; // ServerVariable set only on rewritten HttpContext by URL Rewrite module. @@ -36,11 +48,37 @@ public class TelemetryHttpModule : IHttpModule private static readonly MethodInfo OnStepMethodInfo = typeof(HttpApplication).GetMethod("OnExecuteRequestStep"); + private TraceContextPropagator traceContextPropagator = new TraceContextPropagator(); + + /// + /// Gets or sets the to use to + /// extract from incoming requests. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TraceContextPropagator TextMapPropagator + { + get => this.traceContextPropagator; + set => this.traceContextPropagator = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// Gets or sets a callback action to be fired when a request is started. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public Action OnRequestStartedCallback { get; set; } + + /// + /// Gets or sets a callback action to be fired when a request is stopped. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public Action OnRequestStoppedCallback { get; set; } + /// - /// Gets or sets a value indicating whether TelemetryHttpModule should parse headers to get correlation ids. + /// Gets or sets a callback action to be fired when an unhandled + /// exception is thrown processing a request. /// [EditorBrowsable(EditorBrowsableState.Never)] - public bool ParseHeaders { get; set; } = true; + public Action OnExceptionCallback { get; set; } /// public void Dispose() @@ -100,7 +138,7 @@ private void Application_BeginRequest(object sender, EventArgs e) { var context = ((HttpApplication)sender).Context; AspNetTelemetryEventSource.Log.TraceCallback("Application_BeginRequest"); - ActivityHelper.CreateRootActivity(context, this.ParseHeaders); + ActivityHelper.StartAspNetActivity(this.TextMapPropagator, context, this.OnRequestStartedCallback); context.Items[BeginCalledFlag] = true; } @@ -135,13 +173,13 @@ private void Application_EndRequest(object sender, EventArgs e) else { // Activity has never been started - ActivityHelper.CreateRootActivity(context, this.ParseHeaders); + ActivityHelper.StartAspNetActivity(this.TextMapPropagator, context, this.OnRequestStartedCallback); } } if (trackActivity) { - ActivityHelper.StopAspNetActivity(context.Items); + ActivityHelper.StopAspNetActivity(context, this.OnRequestStoppedCallback); } } @@ -156,10 +194,10 @@ private void Application_Error(object sender, EventArgs e) { if (!context.Items.Contains(BeginCalledFlag)) { - ActivityHelper.CreateRootActivity(context, this.ParseHeaders); + ActivityHelper.StartAspNetActivity(this.TextMapPropagator, context, this.OnRequestStartedCallback); } - ActivityHelper.WriteActivityException(context.Items, exception); + ActivityHelper.WriteActivityException(context.Items, exception, this.OnExceptionCallback); } } }