//----------------------------------------------------------------------------- // <copyright file="AWSXRayRecorder.netcore.cs" company="Amazon.com"> // 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. // </copyright> //----------------------------------------------------------------------------- using System; using Amazon.Runtime.Internal.Util; using Amazon.XRay.Recorder.Core.Exceptions; using Amazon.XRay.Recorder.Core.Internal.Emitters; using Amazon.XRay.Recorder.Core.Internal.Entities; using Amazon.XRay.Recorder.Core.Internal.Utils; using Amazon.XRay.Recorder.Core.Sampling; using Amazon.XRay.Recorder.Core.Sampling.Local; using Microsoft.Extensions.Configuration; namespace Amazon.XRay.Recorder.Core { /// <summary> /// A collection of methods used to record tracing information for AWS X-Ray. /// </summary> /// <seealso cref="Amazon.XRay.Recorder.Core.IAWSXRayRecorder" /> public class AWSXRayRecorder : AWSXRayRecorderImpl { private static readonly Logger _logger = Logger.GetLogger(typeof(AWSXRayRecorder)); static AWSXRayRecorder _instance = new AWSXRayRecorderBuilder().Build(); public const String LambdaTaskRootKey = "LAMBDA_TASK_ROOT"; public const String LambdaTraceHeaderKey = "_X_AMZN_TRACE_ID"; private static String _lambdaVariables; private XRayOptions _xRayOptions = new XRayOptions(); /// <summary> /// Initializes a new instance of the <see cref="AWSXRayRecorder" /> class. /// with default configuration. /// </summary> public AWSXRayRecorder() : this(new UdpSegmentEmitter()) { } /// <summary> /// Initializes a new instance of the <see cref="AWSXRayRecorder" /> class with <see cref="XRayOptions"/> /// </summary> /// <param name="options">Instance of <see cref="XRayOptions"/>.</param> public AWSXRayRecorder(XRayOptions options) : this(new UdpSegmentEmitter(), options) { } /// <summary> /// Initializes a new instance of the <see cref="AWSXRayRecorder" /> class /// with given instance of <see cref="IConfiguration" />. /// </summary> /// <param name="configuration">Instance of <see cref="IConfiguration"/>.</param> [CLSCompliant(false)] public static void InitializeInstance(IConfiguration configuration) { XRayOptions xRayOptions = XRayConfiguration.GetXRayOptions(configuration); var recorderBuilder = GetBuilder(xRayOptions); Instance = recorderBuilder.Build(xRayOptions); } /// <summary> /// Initializes provided instance of the <see cref="AWSXRayRecorder" /> class with /// the instance of <see cref="IConfiguration" />. /// </summary> /// <param name="configuration">Instance of <see cref="IConfiguration"/>.</param> /// <param name="recorder">Instance of <see cref="AWSXRayRecorder"/>.</param> [CLSCompliant(false)] public static void InitializeInstance(IConfiguration configuration = null, AWSXRayRecorder recorder = null) { XRayOptions xRayOptions = XRayConfiguration.GetXRayOptions(configuration); var recorderBuilder = GetBuilder(xRayOptions); if (recorder != null) { _logger.DebugFormat("Using custom X-Ray recorder."); recorder.XRayOptions = xRayOptions; recorder = recorderBuilder.Build(recorder); } else { _logger.DebugFormat("Using default X-Ray recorder."); recorder = recorderBuilder.Build(xRayOptions); } Instance = recorder; } private static AWSXRayRecorderBuilder GetBuilder(XRayOptions xRayOptions) { var recorderBuilder = new AWSXRayRecorderBuilder().WithPluginsFromConfig(xRayOptions).WithContextMissingStrategyFromConfig(xRayOptions); return recorderBuilder; } /// <summary> /// Initializes a new instance of the <see cref="AWSXRayRecorder" /> class /// with given instance of <see cref="ISegmentEmitter" />. /// </summary> /// <param name="emitter">Segment emitter</param> internal AWSXRayRecorder(ISegmentEmitter emitter) : base(emitter) { PopulateContexts(); if (IsLambda()) { SamplingStrategy = new LocalizedSamplingStrategy(); } else { SamplingStrategy = new DefaultSamplingStrategy(); } } /// <summary> /// Initializes a new instance of the <see cref="AWSXRayRecorder" /> class /// with given instance of <see cref="ISegmentEmitter" /> and instance of <see cref="XRayOptions"/>. /// </summary> /// <param name="emitter">Instance of <see cref="ISegmentEmitter"/>.</param> /// <param name="options">Instance of <see cref="XRayOptions"/>.</param> internal AWSXRayRecorder(ISegmentEmitter emitter, XRayOptions options) : base(emitter) { XRayOptions = options; PopulateContexts(); if (IsLambda()) { SamplingStrategy = new LocalizedSamplingStrategy(options.SamplingRuleManifest); } else { SamplingStrategy = new DefaultSamplingStrategy(options.SamplingRuleManifest); } } /// <summary> /// Gets the singleton instance of <see cref="AWSXRayRecorder"/> with default configuration. /// </summary> /// <returns>An instance of <see cref="AWSXRayRecorder"/> class.</returns> public static AWSXRayRecorder Instance { get { if (_instance == null) { _instance = new AWSXRayRecorderBuilder().Build(); } return _instance; } private set { _instance = value; } } /// <summary> /// Instance of <see cref="XRayOptions"/> class. /// </summary> public XRayOptions XRayOptions { get => _xRayOptions; set => _xRayOptions = value; } /// <summary> /// Begin a tracing subsegment. A new segment will be created and added as a subsegment to previous segment/subsegment. /// </summary> /// <param name="name">Name of the operation</param> /// <param name="timestamp">Sets the start time of the subsegment</param> /// <exception cref="ArgumentNullException">The argument has a null value.</exception> /// <exception cref="EntityNotAvailableException">Entity is not available in trace context.</exception> public override void BeginSubsegment(string name, DateTime? timestamp = null) { try { if (IsTracingDisabled()) { _logger.DebugFormat("X-Ray tracing is disabled, do not start subsegment"); return; } if (IsLambda()) { ProcessSubsegmentInLambdaContext(name, timestamp); } else { AddSubsegment(new Subsegment(name), timestamp); } } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to start subsegment because the parent segment is not available."); } } /// <summary> /// Begin a tracing subsegment. A new subsegment will be created and added as a subsegment to previous facade segment or subsegment. /// </summary> private void ProcessSubsegmentInLambdaContext(string name, DateTime? timestamp = null) { if (!TraceContext.IsEntityPresent()) // No facade segment available and first subsegment of a subsegment branch needs to be added { AddFacadeSegment(name); AddSubsegmentInLambdaContext(name, timestamp); } else // Facade / Subsegment already present { var entity = TraceContext.GetEntity(); // can be Facade segment or Subsegment var environmentRootTraceId = TraceHeader.FromString(AWSXRayRecorder.GetTraceVariablesFromEnvironment()).RootTraceId; if ((null != environmentRootTraceId) && !environmentRootTraceId.Equals(entity.RootSegment.TraceId)) // If true, customer has leaked subsegments across invocation { TraceContext.ClearEntity(); // reset TraceContext BeginSubsegment(name, timestamp); // This adds Facade segment with updated environment variables } else { AddSubsegmentInLambdaContext(name, timestamp); } } } /// <summary> /// Begin a Facade Segment. /// </summary> internal void AddFacadeSegment(String name = null) { _lambdaVariables = AWSXRayRecorder.GetTraceVariablesFromEnvironment(); _logger.DebugFormat("Lambda Environment detected. Lambda variables: {0}", _lambdaVariables); if (!TraceHeader.TryParseAll(_lambdaVariables, out TraceHeader traceHeader)) { if (name != null) { _logger.DebugFormat("Lambda variables : {0} for X-Ray trace header environment variable under key : {1} are missing/not valid trace id, parent id or sampling decision, discarding subsegment : {2}", _lambdaVariables, LambdaTraceHeaderKey, name); } else { _logger.DebugFormat("Lambda variables : {0} for X-Ray trace header environment variable under key : {1} are missing/not valid trace id, parent id or sampling decision, discarding subsegment", _lambdaVariables, LambdaTraceHeaderKey); } traceHeader = new TraceHeader(); traceHeader.RootTraceId = TraceId.NewId(); traceHeader.ParentId = null; traceHeader.Sampled = SampleDecision.NotSampled; } Segment newSegment = new FacadeSegment("Facade", traceHeader.RootTraceId, traceHeader.ParentId); newSegment.Sampled = traceHeader.Sampled; TraceContext.SetEntity(newSegment); } private void AddSubsegmentInLambdaContext(string name, DateTime? timestamp = null) { // If the request is not sampled, the passed subsegment will still be available in TraceContext to // stores the information of the trace. The trace information will still propagated to // downstream service, in case downstream may overwrite the sample decision. Entity parentEntity = TraceContext.GetEntity(); Subsegment subsegment = new Subsegment(name); parentEntity.AddSubsegment(subsegment); subsegment.Sampled = parentEntity.Sampled; if (timestamp == null) { subsegment.SetStartTimeToNow(); } else { subsegment.SetStartTime(timestamp.Value); } TraceContext.SetEntity(subsegment); } private void AddSubsegment(Subsegment subsegment, DateTime? timestamp = null) { // If the request is not sampled, a segment will still be available in TraceContext to // stores the information of the trace. The trace information will still propagated to // downstream service, in case downstream may overwrite the sample decision. Entity parentEntity = TraceContext.GetEntity(); // If the segment is not sampled, do nothing and exit. if (parentEntity.Sampled != SampleDecision.Sampled) { _logger.DebugFormat("Do not start subsegment because the segment doesn't get sampled. ({0})", subsegment.Name); return; } parentEntity.AddSubsegment(subsegment); subsegment.Sampled = parentEntity.Sampled; if (timestamp == null) { subsegment.SetStartTimeToNow(); } else { subsegment.SetStartTime(timestamp.Value); } TraceContext.SetEntity(subsegment); } /// <summary> /// End a subsegment. /// </summary> /// <param name="timestamp">Sets the end time for the subsegment</param> /// <exception cref="EntityNotAvailableException">Entity is not available in trace context.</exception> public override void EndSubsegment(DateTime? timestamp = null) { try { if (IsTracingDisabled()) { _logger.DebugFormat("X-Ray tracing is disabled, do not end subsegment"); return; } if (IsLambda()) { ProcessEndSubsegmentInLambdaContext(timestamp); } else { ProcessEndSubsegment(timestamp); } } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to end subsegment because subsegment is not available in trace context."); } catch (InvalidCastException e) { HandleEntityNotAvailableException(new EntityNotAvailableException("Failed to cast the entity to Subsegment.", e), "Failed to cast the entity to Subsegment."); } } private void ProcessEndSubsegmentInLambdaContext(DateTime? timestamp = null) { var subsegment = PrepEndSubsegmentInLambdaContext(); if (timestamp == null) { subsegment.SetEndTimeToNow(); } else { subsegment.SetEndTime(timestamp.Value); } // Check emittable if (subsegment.IsEmittable()) { // Emit Emitter.Send(subsegment.RootSegment); } else if (StreamingStrategy.ShouldStream(subsegment)) { StreamingStrategy.Stream(subsegment.RootSegment, Emitter); } if (TraceContext.IsEntityPresent() && TraceContext.GetEntity().GetType() == typeof(FacadeSegment)) //implies FacadeSegment in the Trace Context { EndFacadeSegment(); return; } } private Subsegment PrepEndSubsegmentInLambdaContext() { // If the request is not sampled, a subsegment will still be available in TraceContext. //This behavor is specific to AWS Lambda environment Entity entity = TraceContext.GetEntity(); Subsegment subsegment = (Subsegment)entity; subsegment.IsInProgress = false; // Restore parent segment to trace context if (subsegment.Parent != null) { TraceContext.SetEntity(subsegment.Parent); } // Drop ref count subsegment.Release(); return subsegment; } private void EndFacadeSegment() { try { // If the request is not sampled, a segment will still be available in TraceContext. // Need to clean up the segment, but do not emit it. FacadeSegment facadeSegment = (FacadeSegment)TraceContext.GetEntity(); if (!IsTracingDisabled()) { PrepEndSegment(facadeSegment); if (facadeSegment.RootSegment != null && facadeSegment.RootSegment.Size >= 0) { StreamingStrategy.Stream(facadeSegment, Emitter); //Facade segment is not emitted, all its subsegments, if emmittable, are emitted } } TraceContext.ClearEntity(); } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to end facade segment because cannot get the segment from trace context."); } catch (InvalidCastException e) { HandleEntityNotAvailableException(new EntityNotAvailableException("Failed to cast the entity to Facade segment.", e), "Failed to cast the entity to Facade Segment."); } } /// <summary> /// Checks whether current execution is in AWS Lambda. /// </summary> /// <returns>Returns true if current execution is in AWS Lambda.</returns> public static Boolean IsLambda() { var lambdaTaskRootKey = Environment.GetEnvironmentVariable(LambdaTaskRootKey); if (!Object.Equals(lambdaTaskRootKey, null)) { return true; } return false; } /// <summary> /// Returns value set for environment variable "_X_AMZN_TRACE_ID" /// </summary> private static String GetTraceVariablesFromEnvironment() { var lambdaTraceHeader = Environment.GetEnvironmentVariable(LambdaTraceHeaderKey); return lambdaTraceHeader; } /// <summary> /// Checks whether Tracing is enabled or disabled. /// </summary> /// <returns> Returns true if Tracing is disabled else false.</returns> public override bool IsTracingDisabled() { return XRayOptions.IsXRayTracingDisabled; } /// <summary> /// Configures Logger to <see cref="Amazon.LoggingOptions"/>. /// </summary> /// <param name="loggingOptions">Enum of <see cref="Amazon.LoggingOptions"/>.</param> public static void RegisterLogger(Amazon.LoggingOptions loggingOptions) { AWSConfigs.LoggingConfig.LogTo = loggingOptions; } } }