//-----------------------------------------------------------------------------
//
// 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;
}
}
}