internal Transaction()

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