in src/Elastic.Apm/Model/Transaction.cs [114:347]
internal Transaction(
IApmLogger logger,
string name,
string type,
Sampler sampler,
DistributedTracingData distributedTracingData,
IPayloadSender sender,
IConfiguration configuration,
ICurrentExecutionSegmentsContainer currentExecutionSegmentsContainer,
IApmServerInfo apmServerInfo,
BreakdownMetricsProvider breakdownMetricsProvider,
bool ignoreActivity = false,
long? timestamp = null,
string id = null,
string traceId = null,
IEnumerable<SpanLink> links = null,
Activity current = null
)
{
Configuration = configuration;
Timestamp = timestamp ?? TimeUtils.TimestampNow();
_logger = logger?.Scoped(nameof(Transaction));
_apmServerInfo = apmServerInfo;
_sender = sender;
_currentExecutionSegmentsContainer = currentExecutionSegmentsContainer;
_breakdownMetricsProvider = breakdownMetricsProvider;
Name = name;
HasCustomName = false;
Type = type;
var spanLinks = links as SpanLink[] ?? links?.ToArray();
Links = spanLinks;
// Restart the trace when:
// - `TraceContinuationStrategy == Restart` OR
// - `TraceContinuationStrategy == RestartExternal` AND
// - `TraceState` is not present (Elastic Agent would have added it) OR
// - `TraceState` is present but the SampleRate is not present (Elastic agent adds SampleRate to TraceState)
var shouldRestartTrace = configuration.TraceContinuationStrategy == ConfigConsts.SupportedValues.Restart ||
(configuration.TraceContinuationStrategy == ConfigConsts.SupportedValues.RestartExternal
&& (distributedTracingData?.TraceState == null || distributedTracingData is { TraceState: { SampleRate: null } }));
// For each new transaction, start an Activity if we're not ignoring them.
// If Activity.Current is not null, the started activity will be a child activity,
// so the traceid and tracestate of the parent will flow to it.
// If the transaction is created as the result of an activity that is passed directly use that as the activity representing this
// transaction
if (current != null)
_activity = current;
// Otherwise we will start an activity explicitly and ensure its trace_id and trace_state respect our bookkeeping.
// Unless explicitly asked not to through `ignoreActivity`: (https://github.com/elastic/apm-agent-dotnet/issues/867#issuecomment-650170150)
else if (!ignoreActivity)
_activity = StartActivity(shouldRestartTrace);
var isSamplingFromDistributedTracingData = false;
if (distributedTracingData == null || shouldRestartTrace)
{
// We consider a newly created transaction **without** explicitly passed distributed tracing data
// to be a root transaction.
// Ignore the created activity ActivityTraceFlags because it starts out without setting the IsSampled flag,
// so relying on that would mean a transaction is never sampled.
if (_activity != null)
{
// If an activity was created, reuse its id
Id = _activity.SpanId.ToHexString();
TraceId = _activity.TraceId.ToHexString();
var idBytesFromActivity = new Span<byte>(new byte[16]);
_activity.TraceId.CopyTo(idBytesFromActivity);
// Read right most bits. From W3C TraceContext: "it is important for trace-id to carry "uniqueness" and "randomness"
// in the right part of the trace-id..."
idBytesFromActivity = idBytesFromActivity.Slice(8);
_traceState = new TraceState();
// If activity has a tracestate, populate the transaction tracestate with it.
if (!string.IsNullOrEmpty(_activity.TraceStateString))
_traceState.AddTextHeader(_activity.TraceStateString);
IsSampled = sampler.DecideIfToSample(idBytesFromActivity.ToArray());
if (shouldRestartTrace && distributedTracingData != null)
{
if (Links == null || spanLinks == null)
Links = new List<SpanLink> { new(distributedTracingData.ParentId, distributedTracingData.TraceId) };
else
Links = new List<SpanLink>(spanLinks) { new(distributedTracingData.ParentId, distributedTracingData.TraceId) };
}
// In the unlikely event that tracestate populated from activity contains an es vendor key, the tracestate
// is mutated to set the sample rate defined by the sampler, because we consider a transaction without
// explicitly passed distributedTracingData to be a **root** transaction. The end result
// is that activity tracestate will be propagated, along with the sample rate defined by this transaction.
if (IsSampled)
{
SampleRate = sampler.Rate;
_traceState.SetSampleRate(sampler.Rate);
}
else
{
SampleRate = 0;
_traceState.SetSampleRate(0);
}
// sync the activity tracestate with the tracestate of the transaction
_activity.TraceStateString = _traceState.ToTextHeader();
}
else
{
// If no activity is created, create new random ids
var idBytes = new byte[8];
if (id == null)
Id = RandomGenerator.GenerateRandomBytesAsString(idBytes);
else
Id = id;
IsSampled = sampler.DecideIfToSample(idBytes);
if (traceId == null)
{
idBytes = new byte[16];
TraceId = RandomGenerator.GenerateRandomBytesAsString(idBytes);
}
else
TraceId = traceId;
if (IsSampled)
{
_traceState = new TraceState(sampler.Rate);
SampleRate = sampler.Rate;
}
else
{
_traceState = new TraceState(0);
SampleRate = 0;
}
}
// ParentId could be also set here, but currently in the UI each trace must start with a transaction where the ParentId is null,
// so to avoid https://github.com/elastic/apm-agent-dotnet/issues/883 we don't set it yet.
}
else
{
var idBytes = new byte[8];
if (_activity != null)
{
Id = _activity.SpanId.ToHexString();
_activity.SpanId.CopyTo(new Span<byte>(idBytes));
// try to set the parent id and tracestate on the created activity, based on passed distributed tracing data.
// This is so that the distributed tracing data will flow to any child activities
try
{
_activity.SetParentId(
ActivityTraceId.CreateFromString(distributedTracingData.TraceId.AsSpan()),
ActivitySpanId.CreateFromString(distributedTracingData.ParentId.AsSpan()),
distributedTracingData.FlagRecorded ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None);
if (distributedTracingData.HasTraceState)
_activity.TraceStateString = distributedTracingData.TraceState.ToTextHeader();
}
catch (Exception e)
{
_logger?.Error()?.LogException(e, "Error setting trace context on created activity");
}
}
else
Id = RandomGenerator.GenerateRandomBytesAsString(idBytes);
TraceId = distributedTracingData.TraceId;
ParentId = distributedTracingData.ParentId;
isSamplingFromDistributedTracingData = true;
_traceState = distributedTracingData.TraceState;
// If TraceContextIgnoreSampledFalse is set and the upstream service is not from our agent (aka no sample rate set)
// ignore the sampled flag and make a new sampling decision.
#pragma warning disable CS0618
if (configuration.TraceContextIgnoreSampledFalse && (distributedTracingData.TraceState == null
#pragma warning restore CS0618
|| (!distributedTracingData.TraceState.SampleRate.HasValue && !distributedTracingData.FlagRecorded)))
{
IsSampled = sampler.DecideIfToSample(idBytes);
_traceState?.SetSampleRate(sampler.Rate);
// In order to have a root transaction, we also unset the ParentId.
// This ensures there is a root transaction within elastic.
ParentId = null;
}
else
IsSampled = distributedTracingData.FlagRecorded;
// If there is no tracestate or no valid "es" vendor entry with an "s" (sample rate) attribute, then the agent must
// omit sample rate from non-root transactions and their spans.
// See https://github.com/elastic/apm/blob/main/specs/agents/tracing-sampling.md#propagation
if (_traceState?.SampleRate is null)
SampleRate = null;
else
SampleRate = _traceState.SampleRate.Value;
}
// Also mark the sampling decision on the Activity
if (IsSampled && _activity != null)
_activity.ActivityTraceFlags |= ActivityTraceFlags.Recorded;
CheckAndCaptureBaggage();
SpanCount = new SpanCount();
_currentExecutionSegmentsContainer.CurrentTransaction = this;
var formattedTimestamp = _logger.IsEnabled(LogLevel.Trace) ? TimeUtils.FormatTimestampForLog(Timestamp) : string.Empty;
if (isSamplingFromDistributedTracingData)
{
_logger?.Trace()?.Log("New Transaction instance created: {Transaction}. " +
"IsSampled ({IsSampled}) and SampleRate ({SampleRate}) is based on incoming distributed tracing data ({DistributedTracingData})."
+
" Start time: {Time} (as timestamp: {Timestamp})",
this, IsSampled, SampleRate, distributedTracingData, formattedTimestamp, Timestamp);
}
else
{
_logger?.Trace()?.Log("New Transaction instance created: {Transaction}. " +
"IsSampled ({IsSampled}) is based on the given sampler ({Sampler})." +
" Start time: {Time} (as timestamp: {Timestamp})",
this, IsSampled, sampler, formattedTimestamp, Timestamp);
}
}