//----------------------------------------------------------------------------- // // 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 System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Threading.Tasks; using Amazon.Runtime.Internal.Util; using Amazon.XRay.Recorder.Core.Exceptions; using Amazon.XRay.Recorder.Core.Internal.Context; 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.Strategies; namespace Amazon.XRay.Recorder.Core { /// /// This class provides utilities to build an instance of with different configurations. /// public abstract class AWSXRayRecorderImpl : IAWSXRayRecorder { private static readonly Logger _logger = Logger.GetLogger(typeof(AWSXRayRecorderImpl)); #if NETFRAMEWORK private static Lazy _lazyDefaultRecorder = new Lazy(() => AWSXRayRecorderBuilder.GetDefaultBuilder().Build()); protected static Lazy LazyDefaultRecorder { get { return _lazyDefaultRecorder; } set { _lazyDefaultRecorder = value; } } #endif /// /// The environment variable that setting context missing strategy. /// public const string EnvironmentVariableContextMissingStrategy = "AWS_XRAY_CONTEXT_MISSING"; protected const long MaxSubsegmentSize = 100; private ISegmentEmitter _emitter; private bool disposed; protected ContextMissingStrategy cntxtMissingStrategy = ContextMissingStrategy.LOG_ERROR; private Dictionary serviceContext = new Dictionary(); protected AWSXRayRecorderImpl(ISegmentEmitter emitter) { this._emitter = emitter; } /// /// Gets or sets the origin of the service. /// public string Origin { get; set; } /// /// Gets or sets the sampling strategy. /// public ISamplingStrategy SamplingStrategy { get; set; } /// /// Gets or sets the context missing strategy. /// public ContextMissingStrategy ContextMissingStrategy { get { return cntxtMissingStrategy; } set { cntxtMissingStrategy = value; _logger.DebugFormat(string.Format("Context missing mode : {0}", cntxtMissingStrategy)); string modeFromEnvironmentVariable = Environment.GetEnvironmentVariable(EnvironmentVariableContextMissingStrategy); if (string.IsNullOrEmpty(modeFromEnvironmentVariable)) { _logger.DebugFormat(string.Format("{0} environment variable is not set. Do not override context missing mode.", EnvironmentVariableContextMissingStrategy)); } else if (modeFromEnvironmentVariable.Equals(ContextMissingStrategy.LOG_ERROR.ToString(), StringComparison.OrdinalIgnoreCase)) { _logger.DebugFormat(string.Format("{0} environment variable is set to {1}. Override local value.", EnvironmentVariableContextMissingStrategy, modeFromEnvironmentVariable)); cntxtMissingStrategy = ContextMissingStrategy.LOG_ERROR; } else if (modeFromEnvironmentVariable.Equals(ContextMissingStrategy.RUNTIME_ERROR.ToString(), StringComparison.OrdinalIgnoreCase)) { _logger.DebugFormat(string.Format("{0} environment variable is set to {1}. Override local value.", EnvironmentVariableContextMissingStrategy, modeFromEnvironmentVariable)); cntxtMissingStrategy = ContextMissingStrategy.RUNTIME_ERROR; } } } /// /// Instance of , used to store segment/subsegment. /// public ITraceContext TraceContext { get; set; } = DefaultTraceContext.GetTraceContext(); /// /// Gets the runtime context which is generated by plugins. /// public IDictionary RuntimeContext { get; protected set; } /// /// Emitter used to send Traces. /// public ISegmentEmitter Emitter { get => _emitter; set => _emitter = value; } protected bool Disposed { get => disposed; set => disposed = value; } protected Dictionary ServiceContext { get => serviceContext; set => serviceContext = value; } /// /// Defines exception serialization stategy to process recorded exceptions. /// public ExceptionSerializationStrategy ExceptionSerializationStrategy { get; set; } = new DefaultExceptionSerializationStrategy(); /// /// Instance of , used to define the streaming strategy for segment/subsegment. /// public IStreamingStrategy StreamingStrategy { get; set; } = new DefaultStreamingStrategy(); /// /// Begin a tracing segment. A new tracing segment will be created and started. /// /// The name of the segment /// Trace id of the segment /// Unique id of the upstream remote segment or subsegment where the downstream call originated from. /// Instance of , contains sampling decision for the segment from upstream service. If not passed, sampling decision is made based on set with the recorder instance. /// If not null, sets the start time for the segment else current time is set. /// The argument has a null value. public void BeginSegment(string name, string traceId = null, string parentId = null, SamplingResponse samplingResponse = null, DateTime? timestamp = null) { #if !NETFRAMEWORK if (AWSXRayRecorder.IsLambda()) { throw new UnsupportedOperationException("Cannot override Facade Segment. New segment not created."); } #endif Segment newSegment = new Segment(name, traceId, parentId); if (samplingResponse == null) { SamplingInput samplingInput = new SamplingInput(name); samplingResponse = SamplingStrategy.ShouldTrace(samplingInput); } if (!IsTracingDisabled()) { if (timestamp == null) { newSegment.SetStartTimeToNow(); } else { newSegment.SetStartTime(timestamp.Value); } PopulateNewSegmentAttributes(newSegment, samplingResponse); } newSegment.Sampled = samplingResponse.SampleDecision; TraceContext.SetEntity(newSegment); } /// /// End tracing of a given segment. /// /// If not null, set as endtime for the current segment. /// Entity is not available in trace context. public void EndSegment(DateTime? timestamp = null) { #if !NETFRAMEWORK if (AWSXRayRecorder.IsLambda()) { throw new UnsupportedOperationException("Cannot override Facade Segment. New segment not created."); } #endif 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. Segment segment = (Segment)TraceContext.GetEntity(); if (!IsTracingDisabled()) { if (timestamp == null) { segment.SetEndTimeToNow(); } else { segment.SetEndTime(timestamp.Value); // sets custom endtime } ProcessEndSegment(segment); } TraceContext.ClearEntity(); } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to end segment because cannot get the segment from trace context."); } catch (InvalidCastException e) { HandleEntityNotAvailableException(new EntityNotAvailableException("Failed to cast the entity to Segment.", e), "Failed to cast the entity to Segment."); } } /// /// Begin a tracing subsegment. A new subsegment will be created and added as a subsegment to previous segment. /// /// Name of the operation. /// Sets the start time of the subsegment /// The argument has a null value. /// Entity is not available in trace context. public abstract void BeginSubsegment(string name, DateTime? timestamp = null); /// /// Begin a tracing subsegment. A new subsegment will be created and added as a subsegment to previous segment. /// /// Name of the operation. public void BeginSubsegmentWithoutSampling(string name) { BeginSubsegment(name); TraceContext.GetEntity().Sampled = SampleDecision.NotSampled; } /// /// End a subsegment. /// /// Sets the end time for the subsegment public abstract void EndSubsegment(DateTime? timestamp = null); /// /// Checks whether Tracing is enabled or disabled. /// /// Returns true if Tracing is disabled else false. public abstract Boolean IsTracingDisabled(); /// /// Adds the specified key and value as annotation to current segment. /// The type of value is restricted. Only , , , /// and are supported. /// /// The key of the annotation to add. /// The value of the annotation to add. /// Entity is not available in trace context. public void AddAnnotation(string key, object value) { if (IsTracingDisabled()) { _logger.DebugFormat("X-Ray tracing is disabled, do not add annotation."); return; } try { TraceContext.GetEntity().AddAnnotation(key, value); } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to add annotation because subsegment is not available in trace context."); } } /// /// Set namespace to current segment. /// /// The value of the namespace. /// Value cannot be null or empty. public void SetNamespace(string value) { if (string.IsNullOrEmpty(value)) { throw new ArgumentException("Value cannot be null or empty.", nameof(value)); } if (IsTracingDisabled()) { _logger.DebugFormat("X-Ray tracing is disabled, do not set namespace."); return; } try { var subsegment = TraceContext.GetEntity() as Subsegment; if (subsegment == null) { _logger.DebugFormat("Failed to cast the entity from TraceContext to Subsegment. SetNamespace is only available to Subsegment."); return; } subsegment.Namespace = value; } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to set namespace because of subsegment is not available."); } } /// /// Populates runtime and service contexts for the segment. /// /// Instance of . protected void PopulateNewSegmentAttributes(Segment newSegment) { if (RuntimeContext != null) { foreach (var keyValuePair in RuntimeContext) { newSegment.Aws[keyValuePair.Key] = keyValuePair.Value; } } if (Origin != null) { newSegment.Origin = Origin; } foreach (var keyValuePair in ServiceContext) { newSegment.Service[keyValuePair.Key] = keyValuePair.Value; } } /// /// Populates runtime and service contexts for the segment. /// /// Instance of . /// Instance of . protected void PopulateNewSegmentAttributes(Segment newSegment, SamplingResponse sampleResponse) { if (RuntimeContext != null) { foreach (var keyValuePair in RuntimeContext) { newSegment.Aws[keyValuePair.Key] = keyValuePair.Value; } } AWSXRayRecorderImpl.AddRuleName(newSegment, sampleResponse); if (Origin != null) { newSegment.Origin = Origin; } foreach (var keyValuePair in ServiceContext) { newSegment.Service[keyValuePair.Key] = keyValuePair.Value; } } /// /// If non null, adds given rulename to the segment.. /// private static void AddRuleName(Segment newSegment, SamplingResponse sampleResponse) { var ruleName = sampleResponse.RuleName; string ruleNameKey = "sampling_rule_name"; if (string.IsNullOrEmpty(ruleName)) { return; } IDictionary xrayContext; if (newSegment.Aws.TryGetValue("xray", out object value)) { xrayContext = (ConcurrentDictionary)value; xrayContext[ruleNameKey] = ruleName; } else { xrayContext = new ConcurrentDictionary(); xrayContext[ruleNameKey] = ruleName; } newSegment.Aws["xray"] = xrayContext; } /// /// Adds the specified key and value as http information to current segment. /// /// The key of the http information to add. /// The value of the http information to add. /// Key is null or empty. /// Value is null. /// Entity is not available in trace context. public void AddHttpInformation(string key, object value) { if (IsTracingDisabled()) { _logger.DebugFormat("X-Ray tracing is disabled, do not add http information."); return; } if (string.IsNullOrEmpty(key)) { throw new ArgumentException("Key cannot be null or empty", nameof(key)); } if (value == null) { throw new ArgumentNullException(nameof(value)); } try { TraceContext.GetEntity().Http[key] = value; } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to add http because segment is not available in trace context."); } } /// /// Mark the current segment as fault. /// /// Entity is not available in trace context. public void MarkFault() { if (IsTracingDisabled()) { _logger.DebugFormat("X-Ray tracing is disabled, do not mark fault."); return; } try { Entity entity = TraceContext.GetEntity(); entity.HasFault = true; entity.HasError = false; } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to mark fault because segment is not available in trace context."); } } /// /// Mark the current segment as error. /// /// Entity is not available in trace context. public void MarkError() { if (IsTracingDisabled()) { _logger.DebugFormat("X-Ray tracing is disabled, do not mark error."); return; } try { Entity entity = TraceContext.GetEntity(); entity.HasError = true; entity.HasFault = false; } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to mark error because segment is not available in trace context."); } } /// /// Add the exception to current segment and also mark current segment as fault. /// /// The exception to be added. /// Entity is not available in trace context. public void AddException(Exception ex) { if (IsTracingDisabled()) { _logger.DebugFormat("X-Ray tracing is disabled, do not add exception."); return; } try { TraceContext.GetEntity().AddException(ex); } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to add exception because segment is not available in trace context."); } } /// /// Mark the current segment as being throttled. And Error will also be marked for current segment. /// /// Entity is not available in trace context. public void MarkThrottle() { if (IsTracingDisabled()) { _logger.DebugFormat("X-Ray tracing is disabled, do not mark throttle."); return; } try { TraceContext.GetEntity().IsThrottled = true; MarkError(); } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to mark throttle because segment is not available in trace context."); } } /// /// Add a precursor id. /// /// The precursor id to be added. public void AddPrecursorId(string precursorId) { if (IsTracingDisabled()) { _logger.DebugFormat("X-Ray tracing is disabled, do not add precursorId."); return; } try { var subsegment = TraceContext.GetEntity() as Subsegment; if (subsegment == null) { _logger.DebugFormat("Can't cast the Entity from TraceContext to Subsegment. The AddPrecursorId is only available for subsegment"); return; } subsegment.AddPrecursorId(precursorId); } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to add precursor id because segment is not available in trace context."); } } /// /// Add the specified key and value as SQL information to current segment. /// /// The key of the SQL information. /// The value of the http information. /// Value or key is null or empty. /// Entity is not available in trace context. public void AddSqlInformation(string key, string value) { if (IsTracingDisabled()) { _logger.DebugFormat("X-Ray tracing is disabled, do not add sql information."); return; } if (string.IsNullOrEmpty(key)) { throw new ArgumentException("Key cannot be null or empty", nameof(key)); } if (string.IsNullOrEmpty(value)) { throw new ArgumentException("Value cannot be null or empty", nameof(value)); } try { TraceContext.GetEntity().Sql[key] = value; } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to add sql information because segment is not available in trace context."); } } /// /// Adds the specified key and value to metadata under default namespace. /// /// The key. /// The value. public void AddMetadata(string key, object value) { if (IsTracingDisabled()) { _logger.DebugFormat("X-Ray tracing is disabled, do not add metadata."); return; } try { TraceContext.GetEntity().AddMetadata(key, value); } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to add metadata because segment is not available in trace context."); } } /// /// Adds the specified key and value to metadata with given namespace. /// /// The namespace. /// The key. /// The value. public void AddMetadata(string nameSpace, string key, object value) { if (IsTracingDisabled()) { _logger.DebugFormat("X-Ray tracing is disabled, do not add metadata."); return; } try { TraceContext.GetEntity().AddMetadata(nameSpace, key, value); } catch (EntityNotAvailableException e) { HandleEntityNotAvailableException(e, "Failed to add metadata because segment is not available in trace context."); } } /// /// Sets the daemon address for and if set. /// A notation of '127.0.0.1:2000' or 'tcp:127.0.0.1:2000 udp:127.0.0.2:2001' or ///'udp:127.0.0.1:2000 tcp:127.0.0.2:2001' /// are acceptable.The former one means UDP and TCP are running at /// the same address. /// If environment variable is set to specific daemon address, the call to this method /// will be ignored. /// /// The daemon address. public void SetDaemonAddress(string daemonAddress) { if (Emitter != null) { Emitter.SetDaemonAddress(daemonAddress); } if (SamplingStrategy != null && SamplingStrategy.GetType().Equals(typeof(DefaultSamplingStrategy))) { DefaultSamplingStrategy defaultSampler = (DefaultSamplingStrategy)SamplingStrategy; defaultSampler.LoadDaemonConfig(DaemonConfig.GetEndPoint(daemonAddress)); } } /// /// Configures recorder instance with . /// /// Instance of public void SetTraceContext(ITraceContext traceContext) { if (traceContext != null) { TraceContext = traceContext; } } /// /// Free resources within the object. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Free resources within the object. /// /// To dispose or not. protected virtual void Dispose(bool disposing) { if (Disposed) { return; } if (disposing) { if (Emitter != null) { Emitter.Dispose(); } Disposed = true; } } /// /// Returns subsegments. /// /// Instance of /// Subsegments of instance of . protected Subsegment[] GetSubsegmentsToStream(Entity entity) { Subsegment[] copy; lock (entity.Subsegments) { copy = new Subsegment[entity.Subsegments.Count]; entity.Subsegments.CopyTo(copy); } return copy; } /// /// Populates runtime and service contexts. /// protected void PopulateContexts() { RuntimeContext = new Dictionary(); // Prepare XRay section for runtime context var xrayContext = new ConcurrentDictionary(); #if NETFRAMEWORK xrayContext["sdk"] = "X-Ray for .NET"; #else xrayContext["sdk"] = "X-Ray for .NET Core"; #endif string currentAssemblyLocation = Assembly.GetExecutingAssembly().Location; if (!string.IsNullOrEmpty(currentAssemblyLocation)) { xrayContext["sdk_version"] = FileVersionInfo.GetVersionInfo(currentAssemblyLocation).ProductVersion; } else { xrayContext["sdk_version"] = "Unknown"; } RuntimeContext["xray"] = xrayContext; #if NETFRAMEWORK ServiceContext["runtime"] = ".NET Framework"; #else ServiceContext["runtime"] = ".NET Core Framework"; #endif ServiceContext["runtime_version"] = Environment.Version.ToString(); } /// /// If sampled and is emittable sends segments using emitter else checks for subsegments to stream. /// /// protected void ProcessEndSegment(Segment segment) { PrepEndSegment(segment); if (segment.Sampled == SampleDecision.Sampled && segment.IsEmittable()) { Emitter.Send(segment); } else if (StreamingStrategy.ShouldStream(segment)) { StreamingStrategy.Stream(segment, Emitter); } } /// /// Sets segment IsInProgress to false and releases the segment. /// /// Instance of . protected void PrepEndSegment(Segment segment) { segment.IsInProgress = false; segment.Release(); } /// /// Sends root segment of the current subsegment. /// protected void ProcessEndSubsegment(DateTime? timestamp = null) { var subsegment = PrepEndSubsegment(); if (subsegment == null) { return; } 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); } } private Subsegment PrepEndSubsegment() { // If the request is not sampled, a segment will still be available in TraceContext. Entity entity = TraceContext.GetEntity(); // If the segment is not sampled, a subsegment is not created. Do nothing and exit. if (entity.Sampled != SampleDecision.Sampled) { return null; } 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; } /// /// If entity is not available in the , exception is thrown. /// /// Instance of . /// String message. protected void HandleEntityNotAvailableException(EntityNotAvailableException e, string message) { TraceContext.HandleEntityMissing(this, e, message); } /// /// Trace a given function with return value. A subsegment will be created for this method. /// Any exception thrown by the method will be captured. /// /// The type of the return value of the method that this delegate encapsulates. /// The name of the trace subsegment for the method. /// The method to be traced. /// The return value of the given method. public TResult TraceMethod(string name, Func method) { BeginSubsegment(name); try { return method(); } catch (Exception e) { AddException(e); throw; } finally { EndSubsegment(); } } /// /// Trace a given method returns void. A subsegment will be created for this method. /// Any exception thrown by the method will be captured. /// /// The name of the trace subsegment for the method. /// The method to be traced. public void TraceMethod(string name, Action method) { BeginSubsegment(name); try { method(); } catch (Exception e) { AddException(e); throw; } finally { EndSubsegment(); } } /// /// Trace a given asynchronous function with return value. A subsegment will be created for this method. /// Any exception thrown by the method will be captured. /// /// The type of the return value of the method that this delegate encapsulates /// The name of the trace subsegment for the method /// The method to be traced /// The return value of the given method public async Task TraceMethodAsync(string name, Func> method) { BeginSubsegment(name); try { return await method(); } catch (Exception e) { AddException(e); throw; } finally { EndSubsegment(); } } /// /// Trace a given asynchronous method that returns no value. A subsegment will be created for this method. /// Any exception thrown by the method will be captured. /// /// The name of the trace subsegment for the method /// The method to be traced public async Task TraceMethodAsync(string name, Func method) { BeginSubsegment(name); try { await method(); } catch (Exception e) { AddException(e); throw; } finally { EndSubsegment(); } } /// /// Gets entity (segment/subsegment) from the . /// /// The entity (segment/subsegment) /// Thrown when the entity is not available to get. public Entity GetEntity() { return TraceContext.GetEntity(); } /// /// Set the specified entity (segment/subsegment) into . /// /// The entity to be set /// Thrown when the entity is not available to set public void SetEntity(Entity entity) { TraceContext.SetEntity(entity); } /// /// Checks whether entity is present in . /// /// True if entity is present TraceContext else false. public bool IsEntityPresent() { return TraceContext.IsEntityPresent(); } /// /// Clear entity from . /// public void ClearEntity() { TraceContext.ClearEntity(); } /// /// Configures recorder with . While setting number consider max trace size /// limit : https://aws.amazon.com/xray/pricing/ /// /// An instance of public void SetExceptionSerializationStrategy(ExceptionSerializationStrategy exceptionSerializationStartegy) { this.ExceptionSerializationStrategy = exceptionSerializationStartegy; } } }