/// <summary> /// Creates a new transaction /// </summary> /// <param name="logger">The logger which logs debug information during the transaction creation process</param> /// <param name="name">The name of the transaction</param> /// <param name="type">The type of the transaction</param> /// <param name="sampler">The sampler implementation which makes the sampling decision</param> /// <param name="distributedTracingData">Distributed tracing data, in case this transaction is part of a distributed trace</param> /// <param name="sender">The IPayloadSender implementation which will record this transaction</param> /// <param name="configSnapshot">The current configuration snapshot which contains the up-do-date config setting values</param> /// <param name="currentExecutionSegmentsContainer" /> /// The ExecutionSegmentsContainer which makes sure this transaction flows /// <param name="apmServerInfo">Component to fetch info about APM Server (e.g. APM Server version)</param> /// <param name="ignoreActivity"> /// If set the transaction will ignore Activity.Current and it's trace id, /// otherwise the agent will try to keep ids in-sync across async work-flows /// </param> internal Transaction( IApmLogger logger, string name, string type, Sampler sampler, DistributedTracingData distributedTracingData, IPayloadSender sender, IConfigSnapshot configSnapshot, ICurrentExecutionSegmentsContainer currentExecutionSegmentsContainer, IApmServerInfo apmServerInfo, bool ignoreActivity = false ) { ConfigSnapshot = configSnapshot; Timestamp = TimeUtils.TimestampNow(); _logger = logger?.Scoped(nameof(Transaction)); _apmServerInfo = apmServerInfo; _sender = sender; _currentExecutionSegmentsContainer = currentExecutionSegmentsContainer; Name = name; HasCustomName = false; Type = type; // 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 (!ignoreActivity) { _activity = StartActivity(); } var isSamplingFromDistributedTracingData = false; if (distributedTracingData == null) { // 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()); // 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]; Id = RandomGenerator.GenerateRandomBytesAsString(idBytes); IsSampled = sampler.DecideIfToSample(idBytes); idBytes = new byte[16]; TraceId = RandomGenerator.GenerateRandomBytesAsString(idBytes); 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 { if (_activity != null) { Id = _activity.SpanId.ToHexString(); // 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 { var idBytes = new byte[8]; Id = RandomGenerator.GenerateRandomBytesAsString(idBytes); } TraceId = distributedTracingData.TraceId; ParentId = distributedTracingData.ParentId; IsSampled = distributedTracingData.FlagRecorded; isSamplingFromDistributedTracingData = true; _traceState = distributedTracingData.TraceState; // 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/master/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; } SpanCount = new SpanCount(); _currentExecutionSegmentsContainer.CurrentTransaction = this; 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, TimeUtils.FormatTimestampForLog(Timestamp), 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, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp); } }
public void New_TraceState_Should_Have_Null_Header_And_SampleRate() { _traceState.ToTextHeader().Should().BeNull(); _traceState.SampleRate.Should().BeNull(); }