//----------------------------------------------------------------------------- // // Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). // You may not use this file except in compliance with the License. // A copy of the License is located at // // http://aws.amazon.com/apache2.0 // // or in the "license" file accompanying this file. This file 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 Amazon.Runtime.Internal.Util; using Amazon.XRay.Recorder.Core; using Amazon.XRay.Recorder.Core.Strategies; using System; using System.Web; using System.Collections.Generic; using System.Linq; using Amazon.XRay.Recorder.Core.Internal.Entities; using Amazon.XRay.Recorder.Core.Sampling; using Amazon.XRay.Recorder.Core.Exceptions; using System.Threading; using Amazon.XRay.Recorder.Core.Internal.Context; namespace Amazon.XRay.Recorder.Handlers.AspNet { /// /// The class to intercept HTTP request for ASP.NET Framework. /// For each request, will try to parse trace header /// from HTTP request header, and determine if tracing is enabled. If enabled, it will /// start a new segment before invoking inner handler. And end the segment before it returns /// the response to outer handler. /// public class AWSXRayASPNET { private static readonly Logger _logger = Logger.GetLogger(typeof(AWSXRayASPNET)); private static SegmentNamingStrategy segmentNamingStrategy; private static readonly AWSXRayRecorder _recorder; static AWSXRayASPNET() { if (!AWSXRayRecorder.IsCustomRecorder) // If custom recorder is not set { AWSXRayRecorder.Instance.SetTraceContext(new HybridContextContainer()); // configure Trace Context } _recorder = AWSXRayRecorder.Instance; } /// /// Key name that is used to store segment in the HttpApplication.Context object of the request. /// public const String XRayEntity = HybridContextContainer.XRayEntity; private static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(); /// /// Gets or sets the segment naming strategy. /// private static SegmentNamingStrategy GetSegmentNamingStrategy() { rwLock.EnterReadLock(); try { // It is safe for this thread to read from the shared resource. return segmentNamingStrategy; } finally { rwLock.ExitReadLock(); // Ensure that the lock is released. } } /// /// Gets or sets the segment naming strategy. /// private static void SetSegmentNamingStrategy(SegmentNamingStrategy value) { rwLock.EnterWriteLock(); try { // It is safe for this thread to write to the shared resource. segmentNamingStrategy = value; } finally { rwLock.ExitWriteLock(); // Ensure that the lock is released. } } private static void InitializeASPNET(string fixedName) { if (GetSegmentNamingStrategy() == null) // ensures only one time initialization among many HTTPApplication instances { InitializeASPNET(new FixedSegmentNamingStrategy(fixedName)); } } private static void InitializeASPNET(SegmentNamingStrategy segmentNamingStrategy) { if (segmentNamingStrategy == null) { throw new ArgumentNullException(nameof(segmentNamingStrategy)); } if (GetSegmentNamingStrategy() == null) // ensures only one time initialization among many HTTPApplication instances { SetSegmentNamingStrategy(segmentNamingStrategy); } } /// /// Registers X-Ray for the current object of class. , /// , event handlers are registered with X-Ray function. /// A segment is created at the beginning of the request and closed at the end of the request. /// /// Instance of class. /// Name to be used for all generated segments. public static void RegisterXRay(HttpApplication httpApplication, string segmentName) { InitializeASPNET(segmentName); httpApplication.BeginRequest += ProcessHTTPRequest; httpApplication.EndRequest += ProcessHTTPResponse; httpApplication.Error += ProcessHTTPError; } /// /// Registers X-Ray for the current object of class. , /// , event handlers are registered with X-Ray function. /// /// Instance of class. /// Instance of class. Defines segment naming strategy. [CLSCompliant(false)] public static void RegisterXRay(HttpApplication httpApplication, SegmentNamingStrategy segmentNamingStrategy) { InitializeASPNET(segmentNamingStrategy); httpApplication.BeginRequest += ProcessHTTPRequest; httpApplication.EndRequest += ProcessHTTPResponse; httpApplication.Error += ProcessHTTPError; } /// /// Processes HTTP request. /// private static void ProcessHTTPRequest(Object sender, EventArgs e) { var context = ((HttpApplication)sender).Context; string ruleName = null; var request = context.Request; TraceHeader traceHeader = GetTraceHeader(context); var segmentName = GetSegmentNamingStrategy().GetSegmentName(request); // Make sample decision if (traceHeader.Sampled == SampleDecision.Unknown || traceHeader.Sampled == SampleDecision.Requested) { SamplingResponse response = MakeSamplingDecision(request, traceHeader,segmentName); ruleName = response.RuleName; } var timestamp = context.Timestamp.ToUniversalTime(); // Gets initial timestamp of current HTTP Request SamplingResponse samplingResponse = new SamplingResponse(ruleName, traceHeader.Sampled); // get final ruleName and SampleDecision _recorder.BeginSegment(segmentName, traceHeader.RootTraceId, traceHeader.ParentId, samplingResponse, timestamp); if (!AWSXRayRecorder.Instance.IsTracingDisabled()) { Dictionary requestAttributes = new Dictionary(); ProcessRequestAttributes(request, requestAttributes); _recorder.AddHttpInformation("request", requestAttributes); } } private static void ProcessRequestAttributes(HttpRequest request, Dictionary requestAttributes) { requestAttributes["url"] = request.Url.AbsoluteUri; requestAttributes["user_agent"] = request.UserAgent; requestAttributes["method"] = request.HttpMethod; string xForwardedFor = GetXForwardedFor(request); if (xForwardedFor == null) { requestAttributes["client_ip"] = GetClientIpAddress(request); } else { requestAttributes["client_ip"] = xForwardedFor; requestAttributes["x_forwarded_for"] = true; } } /// /// Processes HTTP response. /// private static void ProcessHTTPResponse(Object sender, EventArgs e) { var context = ((HttpApplication)sender).Context; //The Rewrite module for example can result in HttpApplication.EndRequest being called multiple times - once for the original ('parent') request and once for the 'child' request which has the rewritten URL. //The 'parent' request will skip BeginRequest and will not be populated with an X-Ray Entity. //It is the 'child' request which is the important one and the 'parent' request will only fail below when AddHttpInformation is called anyway. So bail out here in the parent request. //Assume that if context.Items.Contains(XRayEntity) returns false, then it is a parent request. //https://github.com/microsoft/ApplicationInsights-dotnet/issues/1744 //https://docs.microsoft.com/en-us/troubleshoot/developer/webapps/iis/development/duplicate-aspnet-events if (!context.Items.Contains(XRayEntity)) return; var response = context.Response; if (!AWSXRayRecorder.Instance.IsTracingDisabled() && response != null) { Dictionary responseAttributes = new Dictionary(); ProcessResponseAttributes(response, responseAttributes); _recorder.AddHttpInformation("response", responseAttributes); } Exception exc = context.Error; // Record exception, if any if (exc != null) { _recorder.AddException(exc); } TraceHeader traceHeader = GetTraceHeader(context); bool isSampleDecisionRequested = traceHeader.Sampled == SampleDecision.Requested; if (traceHeader.Sampled == SampleDecision.Unknown || traceHeader.Sampled == SampleDecision.Requested) { SetSamplingDecision(traceHeader); // extracts sampling decision from the available segment } _recorder.EndSegment(); // if the sample decision is requested, add the trace header to response if (isSampleDecisionRequested) { response.Headers.Add(TraceHeader.HeaderKey, traceHeader.ToString()); } } private static void SetSamplingDecision(TraceHeader traceHeader) { try { Segment segment = (Segment)AWSXRayRecorder.Instance.GetEntity(); traceHeader.Sampled = segment.Sampled; } catch (InvalidCastException e) { _logger.Error(new EntityNotAvailableException("Failed to cast the entity to Segment.", e), "Failed to get the segment from trace context for setting sampling decision in the response."); } catch (EntityNotAvailableException e) { AWSXRayRecorder.Instance.TraceContext.HandleEntityMissing(AWSXRayRecorder.Instance, e, "Failed to get entity since it is not available in trace context while processing ASPNET request."); } } private static void ProcessResponseAttributes(HttpResponse response, Dictionary reponseAttributes) { int statusCode = (int)response.StatusCode; reponseAttributes["status"] = statusCode; if (statusCode >= 400 && statusCode <= 499) { _recorder.MarkError(); if (statusCode == 429) { _recorder.MarkThrottle(); } } else if (statusCode >= 500 && statusCode <= 599) { _recorder.MarkFault(); } } private static SamplingResponse MakeSamplingDecision(HttpRequest request, TraceHeader traceHeader, string name) { string host = request.Headers.Get("Host"); string url = request.Url.AbsolutePath; string method = request.HttpMethod; SamplingInput samplingInput = new SamplingInput(host, url, method, name, _recorder.Origin); SamplingResponse sampleResponse = _recorder.SamplingStrategy.ShouldTrace(samplingInput); traceHeader.Sampled = sampleResponse.SampleDecision; return sampleResponse; } /// /// Processes HTTP Error. /// NOTE : if we receive unhandled exception in BeginRequest() of any class implementing Interface, BeginRequest() /// of the current is not executed (so no segment is created at this point). /// private static void ProcessHTTPError(Object sender, EventArgs e) { ProcessHTTPRequest(sender, e); } /// /// Returns instance of class from given object. /// private static TraceHeader GetTraceHeader(HttpContext context) { var request = context.Request; string headerString = request.Headers.Get(TraceHeader.HeaderKey); // Trace header doesn't exist, which means this is the root node. Create a new traceId and inject the trace header. if (!TraceHeader.TryParse(headerString, out TraceHeader traceHeader)) { _logger.DebugFormat("Trace header doesn't exist or not valid : ({0}). Injecting a new one.", headerString); traceHeader = new TraceHeader { RootTraceId = TraceId.NewId(), ParentId = null, Sampled = SampleDecision.Unknown }; } return traceHeader; } private static string GetXForwardedFor(HttpRequest request) { string clientIp = request.ServerVariables["HTTP_X_FORWARDED_FOR"]; return string.IsNullOrEmpty(clientIp) ? null : clientIp.Split(',').First().Trim(); } private static string GetClientIpAddress(HttpRequest request) { return request.UserHostAddress; } } }