diff --git a/src/Elastic.Apm/Model/DroppedSpanStats.cs b/src/Elastic.Apm/Model/DroppedSpanStats.cs index 2f608263c..2254a6f78 100644 --- a/src/Elastic.Apm/Model/DroppedSpanStats.cs +++ b/src/Elastic.Apm/Model/DroppedSpanStats.cs @@ -18,12 +18,11 @@ public DroppedSpanStats(string serviceTargetType, string serviceTargetName, stri double durationSumUs ) { - DurationCount = 1; + Duration = new DroppedSpanDuration { Count = 1, Sum = new DroppedSpanDuration.DroppedSpanDurationSum { Us = durationSumUs } }; ServiceTargetType = serviceTargetType; ServiceTargetName = serviceTargetName; DestinationServiceResource = destinationServiceResource; Outcome = outcome; - DurationSumUs = durationSumUs; } /// @@ -46,23 +45,32 @@ double durationSumUs public string ServiceTargetName { get; } /// - /// Duration holds duration aggregations about the dropped span. - /// Count holds the number of times the dropped span happened. + /// Outcome of the aggregated spans. /// - [JsonProperty("duration.count")] - public int DurationCount { get; set; } + public Outcome Outcome { get; } /// /// Duration holds duration aggregations about the dropped span. - /// Sum holds dimensions about the dropped span's duration. /// - [JsonProperty("duration.sum.us")] - public double DurationSumUs { get; set; } + public DroppedSpanDuration Duration { get; set; } - /// - /// Outcome of the aggregated spans. - /// - public Outcome Outcome { get; } + internal class DroppedSpanDuration + { + /// + /// Count holds the number of times the dropped span happened. + /// + public int Count { get; set; } + + /// + /// Sum holds dimensions about the dropped span's duration. + /// + public DroppedSpanDurationSum Sum { get; set; } + + internal class DroppedSpanDurationSum + { + public double Us { get; set; } + } + } } } diff --git a/src/Elastic.Apm/Model/Transaction.cs b/src/Elastic.Apm/Model/Transaction.cs index 880e058dc..c1f401d59 100644 --- a/src/Elastic.Apm/Model/Transaction.cs +++ b/src/Elastic.Apm/Model/Transaction.cs @@ -21,943 +21,945 @@ using Elastic.Apm.Report; using Elastic.Apm.ServerInfo; -namespace Elastic.Apm.Model +namespace Elastic.Apm.Model; + +internal class Transaction : ITransaction { - internal class Transaction : ITransaction + internal static readonly string ApmTransactionActivityName = "ElasticApm.Transaction"; + + internal readonly TraceState _traceState; + + internal readonly ConcurrentDictionary SpanTimings = new(); + + /// + /// The agent also starts an Activity when a transaction is started and stops it when the transaction ends. + /// The TraceId of this activity is always the same as the TraceId of the transaction. + /// With this, in case Activity.Current is null, the agent will set it and when the next Activity gets created it'll + /// have this activity as its parent and the TraceId will flow to all Activity instances. + /// + private readonly Activity _activity; + + private readonly IApmServerInfo _apmServerInfo; + private readonly BreakdownMetricsProvider _breakdownMetricsProvider; + private readonly Lazy _context = new(); + private readonly ICurrentExecutionSegmentsContainer _currentExecutionSegmentsContainer; + private readonly IApmLogger _logger; + private readonly IPayloadSender _sender; + + [JsonConstructor] + // ReSharper disable once UnusedMember.Local - this constructor is meant for serialization + private Transaction(Context context, string name, string type, double duration, long timestamp, string id, string traceId, string parentId, + bool isSampled, string result, SpanCount spanCount + ) { - internal static readonly string ApmTransactionActivityName = "ElasticApm.Transaction"; - - internal readonly TraceState _traceState; - - internal readonly ConcurrentDictionary SpanTimings = new(); - - /// - /// The agent also starts an Activity when a transaction is started and stops it when the transaction ends. - /// The TraceId of this activity is always the same as the TraceId of the transaction. - /// With this, in case Activity.Current is null, the agent will set it and when the next Activity gets created it'll - /// have this activity as its parent and the TraceId will flow to all Activity instances. - /// - private readonly Activity _activity; - - private readonly IApmServerInfo _apmServerInfo; - private readonly BreakdownMetricsProvider _breakdownMetricsProvider; - private readonly Lazy _context = new(); - private readonly ICurrentExecutionSegmentsContainer _currentExecutionSegmentsContainer; - private readonly IApmLogger _logger; - private readonly IPayloadSender _sender; - - [JsonConstructor] - // ReSharper disable once UnusedMember.Local - this constructor is meant for serialization - private Transaction(Context context, string name, string type, double duration, long timestamp, string id, string traceId, string parentId, - bool isSampled, string result, SpanCount spanCount - ) - { - _context = new Lazy(() => context); - Name = name; - Duration = duration; - Timestamp = timestamp; - Type = type; - Id = id; - TraceId = traceId; - ParentId = parentId; - IsSampled = isSampled; - Result = result; - SpanCount = spanCount; - } + _context = new Lazy(() => context); + Name = name; + Duration = duration; + Timestamp = timestamp; + Type = type; + Id = id; + TraceId = traceId; + ParentId = parentId; + IsSampled = isSampled; + Result = result; + SpanCount = spanCount; + } - // This constructor is used only by tests that don't care about sampling and distributed tracing - internal Transaction(ApmAgent agent, string name, string type, long? timestamp = null) - : this(agent.Logger, name, type, new Sampler(1.0), null, agent.PayloadSender, agent.ConfigurationStore.CurrentSnapshot, - agent.TracerInternal.CurrentExecutionSegmentsContainer, null, null, timestamp: timestamp) - { } - - /// - /// Creates a new transaction - /// - /// The logger which logs debug information during the transaction creation process - /// The name of the transaction - /// The type of the transaction - /// The sampler implementation which makes the sampling decision - /// Distributed tracing data, in case this transaction is part of a distributed trace - /// The IPayloadSender implementation which will record this transaction - /// The current configuration snapshot which contains the up-do-date config setting values - /// - /// The ExecutionSegmentsContainer which makes sure this transaction flows - /// Component to fetch info about APM Server (e.g. APM Server version) - /// - /// The instance which will capture the - /// breakdown metrics - /// - /// - /// 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 - /// - /// - /// The timestamp of the transaction. If it's null then the current timestamp - /// will be captured, which is typically the desired behaviour. Setting the timestamp to a specific value is typically - /// useful for testing. - /// - /// An optional parameter to pass the id of the transaction - /// An optional parameter to pass a trace id which will be applied to the transaction - /// Span links associated with this transaction - /// Current activity that represents this transaction - 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 links = null, - Activity current = null - ) + // This constructor is used only by tests that don't care about sampling and distributed tracing + internal Transaction(ApmAgent agent, string name, string type, long? timestamp = null) + : this(agent.Logger, name, type, new Sampler(1.0), null, agent.PayloadSender, agent.ConfigurationStore.CurrentSnapshot, + agent.TracerInternal.CurrentExecutionSegmentsContainer, null, null, timestamp: timestamp) + { } + + /// + /// Creates a new transaction + /// + /// The logger which logs debug information during the transaction creation process + /// The name of the transaction + /// The type of the transaction + /// The sampler implementation which makes the sampling decision + /// Distributed tracing data, in case this transaction is part of a distributed trace + /// The IPayloadSender implementation which will record this transaction + /// The current configuration snapshot which contains the up-do-date config setting values + /// + /// The ExecutionSegmentsContainer which makes sure this transaction flows + /// Component to fetch info about APM Server (e.g. APM Server version) + /// + /// The instance which will capture the + /// breakdown metrics + /// + /// + /// 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 + /// + /// + /// The timestamp of the transaction. If it's null then the current timestamp + /// will be captured, which is typically the desired behaviour. Setting the timestamp to a specific value is typically + /// useful for testing. + /// + /// An optional parameter to pass the id of the transaction + /// An optional parameter to pass a trace id which will be applied to the transaction + /// Span links associated with this transaction + /// Current activity that represents this transaction + 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 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 it 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) { - 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; + // 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(); - // 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 } })); + var idBytesFromActivity = new Span(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); - // 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. + _traceState = new TraceState(); - // 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; + // If activity has a tracestate, populate the transaction tracestate with it. + if (!string.IsNullOrEmpty(_activity.TraceStateString)) + _traceState.AddTextHeader(_activity.TraceStateString); - // Otherwise we will start an activity explicitly and ensure it 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); + IsSampled = sampler.DecideIfToSample(idBytesFromActivity.ToArray()); - 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 (shouldRestartTrace && distributedTracingData != null) { - // If an activity was created, reuse its id - Id = _activity.SpanId.ToHexString(); - TraceId = _activity.TraceId.ToHexString(); - - var idBytesFromActivity = new Span(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 { new(distributedTracingData.ParentId, distributedTracingData.TraceId) }; - else - Links = new List(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); - } + if (Links == null || spanLinks == null) + Links = new List { new(distributedTracingData.ParentId, distributedTracingData.TraceId) }; else - { - SampleRate = 0; - _traceState.SetSampleRate(0); - } + Links = new List(spanLinks) { new(distributedTracingData.ParentId, distributedTracingData.TraceId) }; + } - // sync the activity tracestate with the tracestate of the transaction - _activity.TraceStateString = _traceState.ToTextHeader(); + // 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 { - // 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; - } + SampleRate = 0; + _traceState.SetSampleRate(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. + // 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 (_activity != null) + if (traceId == null) { - Id = _activity.SpanId.ToHexString(); - _activity.SpanId.CopyTo(new Span(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"); - } + idBytes = new byte[16]; + TraceId = RandomGenerator.GenerateRandomBytesAsString(idBytes); } else - Id = RandomGenerator.GenerateRandomBytesAsString(idBytes); - - TraceId = distributedTracingData.TraceId; - ParentId = distributedTracingData.ParentId; - isSamplingFromDistributedTracingData = true; - _traceState = distributedTracingData.TraceState; + TraceId = traceId; - // 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)) + if (IsSampled) { - 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; + _traceState = new TraceState(sampler.Rate); + SampleRate = sampler.Rate; } 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; + { + _traceState = new TraceState(0); + SampleRate = 0; + } } - // Also mark the sampling decision on the Activity - if (IsSampled && _activity != null) - _activity.ActivityTraceFlags |= ActivityTraceFlags.Recorded; - - CheckAndCaptureBaggage(); + // 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]; - SpanCount = new SpanCount(); - _currentExecutionSegmentsContainer.CurrentTransaction = this; - if (isSamplingFromDistributedTracingData) + if (_activity != null) { - _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); + Id = _activity.SpanId.ToHexString(); + _activity.SpanId.CopyTo(new Span(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))) { - _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); + 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; } - private void CheckAndCaptureBaggage() + // Also mark the sampling decision on the Activity + if (IsSampled && _activity != null) + _activity.ActivityTraceFlags |= ActivityTraceFlags.Recorded; + + CheckAndCaptureBaggage(); + + SpanCount = new SpanCount(); + _currentExecutionSegmentsContainer.CurrentTransaction = this; + + if (isSamplingFromDistributedTracingData) { - if (Activity.Current == null || !Activity.Current.Baggage.Any()) - return; + _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); + } + } - foreach (var baggage in Activity.Current.Baggage) - { - if (!WildcardMatcher.IsAnyMatch(Configuration.BaggageToAttach, baggage.Key)) - continue; + private void CheckAndCaptureBaggage() + { + if (Activity.Current == null || !Activity.Current.Baggage.Any()) + return; - Otel ??= new OTel() { Attributes = new Dictionary() }; - Otel.Attributes.Add(baggage.Key, baggage.Value); - } + foreach (var baggage in Activity.Current.Baggage) + { + if (!WildcardMatcher.IsAnyMatch(Configuration.BaggageToAttach, baggage.Key)) + continue; + + Otel ??= new OTel() { Attributes = new Dictionary() }; + Otel.Attributes.Add(baggage.Key, baggage.Value); } + } - /// - /// Internal dictionary to keep track of and look up dropped span stats. - /// - private Dictionary _droppedSpanStatsMap; - - private bool _isEnded; - - private string _name; - - /// - /// In general if there is an error on the span, the outcome will be Outcome.Failure otherwise it'll be - /// Outcome.Success .. - /// There are some exceptions to this (see spec: - /// https://github.com/elastic/apm/blob/main/specs/agents/tracing-spans.md#span-outcome) when it can be - /// Outcome.Unknown/>. - /// Use to check if it was specifically set to Outcome.Unknown, or if - /// it's just the default value. - /// - private Outcome _outcome; - - private bool _outcomeChangedThroughApi; - internal ChildDurationTimer ChildDurationTimer { get; } = new(); - - internal Span CompressionBuffer; - - /// - /// Holds configuration snapshot (which is immutable) that was current when this transaction started. - /// We would like transaction data to be consistent and not to be affected by possible changes in agent's configuration - /// between the start and the end of the transaction. That is why the way all the data is collected for the transaction - /// and its spans is controlled by this configuration snapshot. - /// - [JsonIgnore] - public IConfiguration Configuration { get; } - - /// - /// Any arbitrary contextual information regarding the event, captured by the agent, optionally provided by the user. - /// - /// - public Context Context => _context.Value; - - [JsonIgnore] - public Dictionary Custom => Context.Custom; - - [JsonProperty("dropped_spans_stats")] - public IEnumerable DroppedSpanStats => _droppedSpanStatsMap?.Values.ToList(); - - /// - /// - /// The duration of the transaction in ms with 3 decimal points. - /// If it's not set (HasValue returns false) then the value - /// is automatically calculated when is called. - /// - /// The duration. - public double? Duration { get; set; } - - /// - /// If true, then the transaction name was modified by external code, and transaction name should not be changed - /// or "fixed" automatically. - /// - [JsonIgnore] - internal bool HasCustomName { get; private set; } - - [MaxLength] - public string Id { get; } - - [JsonIgnore] - internal bool IsContextCreated => _context.IsValueCreated; - - [JsonProperty("sampled")] - public bool IsSampled { get; } - - [JsonIgnore] - [Obsolete( - "Instead of this dictionary, use the `SetLabel` method which supports more types than just string. This property will be removed in a future release.")] - public Dictionary Labels => Context.Labels; - - /// - /// Links holds links to other spans, potentially in other traces. - /// - public IEnumerable Links { get; private set; } - - internal void InsertSpanLinkInternal(IEnumerable links) + /// + /// Internal dictionary to keep track of and look up dropped span stats. + /// + private Dictionary _droppedSpanStatsMap; + + private bool _isEnded; + + private string _name; + + /// + /// In general if there is an error on the span, the outcome will be Outcome.Failure otherwise it'll be + /// Outcome.Success .. + /// There are some exceptions to this (see spec: + /// https://github.com/elastic/apm/blob/main/specs/agents/tracing-spans.md#span-outcome) when it can be + /// Outcome.Unknown/>. + /// Use to check if it was specifically set to Outcome.Unknown, or if + /// it's just the default value. + /// + private Outcome _outcome; + + private bool _outcomeChangedThroughApi; + internal ChildDurationTimer ChildDurationTimer { get; } = new(); + + internal Span CompressionBuffer; + + /// + /// Holds configuration snapshot (which is immutable) that was current when this transaction started. + /// We would like transaction data to be consistent and not to be affected by possible changes in agent's configuration + /// between the start and the end of the transaction. That is why the way all the data is collected for the transaction + /// and its spans is controlled by this configuration snapshot. + /// + [JsonIgnore] + public IConfiguration Configuration { get; } + + /// + /// Any arbitrary contextual information regarding the event, captured by the agent, optionally provided by the user. + /// + /// + public Context Context => _context.Value; + + [JsonIgnore] + public Dictionary Custom => Context.Custom; + + [JsonProperty("dropped_spans_stats")] + public IEnumerable DroppedSpanStats => _droppedSpanStatsMap?.Values.ToList(); + + /// + /// + /// The duration of the transaction in ms with 3 decimal points. + /// If it's not set (HasValue returns false) then the value + /// is automatically calculated when is called. + /// + /// The duration. + public double? Duration { get; set; } + + /// + /// If true, then the transaction name was modified by external code, and transaction name should not be changed + /// or "fixed" automatically. + /// + [JsonIgnore] + internal bool HasCustomName { get; private set; } + + [MaxLength] + public string Id { get; } + + [JsonIgnore] + internal bool IsContextCreated => _context.IsValueCreated; + + [JsonProperty("sampled")] + public bool IsSampled { get; } + + [JsonIgnore] + [Obsolete( + "Instead of this dictionary, use the `SetLabel` method which supports more types than just string. This property will be removed in a future release.")] + public Dictionary Labels => Context.Labels; + + /// + /// Links holds links to other spans, potentially in other traces. + /// + public IEnumerable Links { get; private set; } + + internal void InsertSpanLinkInternal(IEnumerable links) + { + var spanLinks = links as SpanLink[] ?? links.ToArray(); + if (Links == null || !Links.Any()) + Links = spanLinks; + else { - var spanLinks = links as SpanLink[] ?? links.ToArray(); - if (Links == null || !Links.Any()) - Links = spanLinks; - else - { - var newList = new List(Links); - newList.AddRange(spanLinks); - Links = new List(newList); - } + var newList = new List(Links); + newList.AddRange(spanLinks); + Links = new List(newList); } + } - [MaxLength] - public string Name + [MaxLength] + public string Name + { + get => _name; + set { - get => _name; - set - { - HasCustomName = true; - _name = value; - } + HasCustomName = true; + _name = value; } + } - public OTel Otel { get; set; } + public OTel Otel { get; set; } - /// - /// Contains data related to FaaS (Function as a Service) events. - /// - public Faas FaaS { get; set; } + /// + /// Contains data related to FaaS (Function as a Service) events. + /// + public Faas FaaS { get; set; } - /// - /// The outcome of the transaction: success, failure, or unknown. - /// This is similar to 'result', but has a limited set of permitted values describing the success or failure of the - /// transaction from the service's perspective. - /// This field can be used for calculating error rates for incoming requests. - /// - public Outcome Outcome + /// + /// The outcome of the transaction: success, failure, or unknown. + /// This is similar to 'result', but has a limited set of permitted values describing the success or failure of the + /// transaction from the service's perspective. + /// This field can be used for calculating error rates for incoming requests. + /// + public Outcome Outcome + { + get => _outcome; + set { - get => _outcome; - set - { - _outcomeChangedThroughApi = true; - _outcome = value; - } + _outcomeChangedThroughApi = true; + _outcome = value; } + } - [JsonIgnore] - public DistributedTracingData OutgoingDistributedTracingData => new(TraceId, Id, IsSampled, _traceState); - - [MaxLength] - [JsonProperty("parent_id")] - public string ParentId { get; set; } - - /// - /// - /// A string describing the result of the transaction. - /// This is typically the HTTP status code, or e.g. "success" for a background task. - /// - /// The result. - [MaxLength] - public string Result { get; set; } - - /// - /// Captures the sample rate of the agent when this transaction was created. - /// - [JsonProperty("sample_rate")] - internal double? SampleRate { get; } - - internal double SelfDuration => Duration.HasValue ? Duration.Value - ChildDurationTimer.Duration : 0; - - internal Service Service; - - - [JsonProperty("span_count")] - public SpanCount SpanCount { get; set; } - - /// - /// Recorded time of the event, UTC based and formatted as microseconds since Unix epoch - /// - public long Timestamp { get; } - - [MaxLength] - [JsonProperty("trace_id")] - public string TraceId { get; } - - [MaxLength] - public string Type { get; set; } - - /// - /// Changes the by checking the flag. - /// This method is intended for all auto instrumentation usages where the property needs to be set. - /// Setting outcome via the property is intended for users who use the public API. - /// - /// - /// The outcome of the transaction will be set to this value if it wasn't change to the public API - /// previously - /// - internal void SetOutcome(Outcome outcome) - { - if (!_outcomeChangedThroughApi) - _outcome = outcome; - } + [JsonIgnore] + public DistributedTracingData OutgoingDistributedTracingData => new(TraceId, Id, IsSampled, _traceState); + + [MaxLength] + [JsonProperty("parent_id")] + public string ParentId { get; set; } + + /// + /// + /// A string describing the result of the transaction. + /// This is typically the HTTP status code, or e.g. "success" for a background task. + /// + /// The result. + [MaxLength] + public string Result { get; set; } + + /// + /// Captures the sample rate of the agent when this transaction was created. + /// + [JsonProperty("sample_rate")] + internal double? SampleRate { get; } + + internal double SelfDuration => Duration.HasValue ? Duration.Value - ChildDurationTimer.Duration : 0; + + internal Service Service; + + + [JsonProperty("span_count")] + public SpanCount SpanCount { get; set; } + + /// + /// Recorded time of the event, UTC based and formatted as microseconds since Unix epoch + /// + public long Timestamp { get; } + + [MaxLength] + [JsonProperty("trace_id")] + public string TraceId { get; } + + [MaxLength] + public string Type { get; set; } + + /// + /// Changes the by checking the flag. + /// This method is intended for all auto instrumentation usages where the property needs to be set. + /// Setting outcome via the property is intended for users who use the public API. + /// + /// + /// The outcome of the transaction will be set to this value if it wasn't change to the public API + /// previously + /// + internal void SetOutcome(Outcome outcome) + { + if (!_outcomeChangedThroughApi) + _outcome = outcome; + } - private Activity StartActivity(bool shouldRestartTrace) + private Activity StartActivity(bool shouldRestartTrace) + { + var activity = new Activity(KnownListeners.ApmTransactionActivityName); + if (shouldRestartTrace) { - var activity = new Activity(KnownListeners.ApmTransactionActivityName); - if (shouldRestartTrace) - { - activity.SetParentId(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), - Activity.Current != null ? Activity.Current.ActivityTraceFlags : ActivityTraceFlags.None); - } - activity.SetIdFormat(ActivityIdFormat.W3C); - activity.Start(); - return activity; + activity.SetParentId(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), + Activity.Current != null ? Activity.Current.ActivityTraceFlags : ActivityTraceFlags.None); } + activity.SetIdFormat(ActivityIdFormat.W3C); + activity.Start(); + return activity; + } - internal void UpdateDroppedSpanStats(string serviceTargetType, string serviceTargetName, string destinationServiceResource, Outcome outcome, - double duration - ) + internal void UpdateDroppedSpanStats(string serviceTargetType, string serviceTargetName, string destinationServiceResource, Outcome outcome, + double duration + ) + { + if (_droppedSpanStatsMap == null) { - if (_droppedSpanStatsMap == null) - { - _droppedSpanStatsMap = new Dictionary - { - { - new DroppedSpanStatsKey(serviceTargetType, serviceTargetName, outcome), - new DroppedSpanStats(serviceTargetType, serviceTargetName, destinationServiceResource, outcome, duration) - } - }; - } - else + _droppedSpanStatsMap = new Dictionary { - if (_droppedSpanStatsMap.Count >= 128) - return; - - if (_droppedSpanStatsMap.TryGetValue(new DroppedSpanStatsKey(serviceTargetType, serviceTargetName, outcome), out var item)) - { - item.DurationCount++; - item.DurationSumUs += duration; - } - else { - _droppedSpanStatsMap.Add(new DroppedSpanStatsKey(serviceTargetType, serviceTargetName, outcome), - new DroppedSpanStats(serviceTargetType, serviceTargetName, destinationServiceResource, outcome, duration)); + new DroppedSpanStatsKey(serviceTargetType, serviceTargetName, outcome), + new DroppedSpanStats(serviceTargetType, serviceTargetName, destinationServiceResource, outcome, duration) } - } + }; } - - /// - public void SetService(string serviceName, string serviceVersion) + else { - if (Context.Service == null) - Context.Service = new Service(serviceName, serviceVersion); + if (_droppedSpanStatsMap.Count >= 128) + return; + + if (_droppedSpanStatsMap.TryGetValue(new DroppedSpanStatsKey(serviceTargetType, serviceTargetName, outcome), out var item)) + { + item.Duration ??= + new DroppedSpanStats.DroppedSpanDuration { Sum = new DroppedSpanStats.DroppedSpanDuration.DroppedSpanDurationSum() }; + + item.Duration.Count++; + item.Duration.Sum.Us += duration; + } else { - Context.Service.Name = serviceName; - Context.Service.Version = serviceVersion; + _droppedSpanStatsMap.Add(new DroppedSpanStatsKey(serviceTargetType, serviceTargetName, outcome), + new DroppedSpanStats(serviceTargetType, serviceTargetName, destinationServiceResource, outcome, duration)); } } + } - public string EnsureParentId() + /// + public void SetService(string serviceName, string serviceVersion) + { + if (Context.Service == null) + Context.Service = new Service(serviceName, serviceVersion); + else { - if (!string.IsNullOrEmpty(ParentId)) - return ParentId; - - var idBytes = new byte[8]; - ParentId = RandomGenerator.GenerateRandomBytesAsString(idBytes); - _logger?.Debug()?.Log("Setting ParentId to transaction, {transaction}", this); - return ParentId; + Context.Service.Name = serviceName; + Context.Service.Version = serviceVersion; } + } - /// - /// Method to conditionally serialize because context should be serialized only when the transaction - /// is sampled. - /// See - /// the relevant Json.NET Documentation - /// - public bool ShouldSerializeContext() => IsSampled; + public string EnsureParentId() + { + if (!string.IsNullOrEmpty(ParentId)) + return ParentId; - public override string ToString() => new ToStringBuilder(nameof(Transaction)) - { - { nameof(Id), Id }, - { nameof(TraceId), TraceId }, - { nameof(ParentId), ParentId }, - { nameof(Name), Name }, - { nameof(Type), Type }, - { nameof(Outcome), Outcome }, - { nameof(IsSampled), IsSampled } - }.ToString(); - - /// - /// When the transaction has ended and before being queued to send to APM server - /// - public event EventHandler Ended; - - public void End() - { - // If the outcome is still unknown and it was not specifically set to unknown, then it's success - if (Outcome == Outcome.Unknown && !_outcomeChangedThroughApi) - Outcome = Outcome.Success; + var idBytes = new byte[8]; + ParentId = RandomGenerator.GenerateRandomBytesAsString(idBytes); + _logger?.Debug()?.Log("Setting ParentId to transaction, {transaction}", this); + return ParentId; + } - if (Duration.HasValue) - { - _logger.Trace() - ?.Log("Ended {Transaction} (with Duration already set)." + - " Start time: {Time} (as timestamp: {Timestamp}), Duration: {Duration}ms", - this, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp, Duration); + /// + /// Method to conditionally serialize because context should be serialized only when the transaction + /// is sampled. + /// See + /// the relevant Json.NET Documentation + /// + public bool ShouldSerializeContext() => IsSampled; - ChildDurationTimer.OnSpanEnd((long)(Timestamp + Duration.Value * 1000)); - } - else - { - Assertion.IfEnabled?.That(!_isEnded, - $"Transaction's Duration doesn't have value even though {nameof(End)} method was already called." + - $" It contradicts the invariant enforced by {nameof(End)} method - Duration should have value when {nameof(End)} method exits" + - $" and {nameof(_isEnded)} field is set to true only when {nameof(End)} method exits." + - $" Context: this: {this}; {nameof(_isEnded)}: {_isEnded}"); - - var endTimestamp = TimeUtils.TimestampNow(); - ChildDurationTimer.OnSpanEnd(endTimestamp); - Duration = TimeUtils.DurationBetweenTimestamps(Timestamp, endTimestamp); - _logger.Trace() - ?.Log("Ended {Transaction}. Start time: {Time} (as timestamp: {Timestamp})," + - " End time: {Time} (as timestamp: {Timestamp}), Duration: {Duration}ms", - this, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp, - TimeUtils.FormatTimestampForLog(endTimestamp), endTimestamp, Duration); - } + public override string ToString() => new ToStringBuilder(nameof(Transaction)) + { + { nameof(Id), Id }, + { nameof(TraceId), TraceId }, + { nameof(ParentId), ParentId }, + { nameof(Name), Name }, + { nameof(Type), Type }, + { nameof(Outcome), Outcome }, + { nameof(IsSampled), IsSampled } + }.ToString(); + + /// + /// When the transaction has ended and before being queued to send to APM server + /// + public event EventHandler Ended; + + public void End() + { + // If the outcome is still unknown and it was not specifically set to unknown, then it's success + if (Outcome == Outcome.Unknown && !_outcomeChangedThroughApi) + Outcome = Outcome.Success; - _activity?.Stop(); + if (Duration.HasValue) + { + _logger.Trace() + ?.Log("Ended {Transaction} (with Duration already set)." + + " Start time: {Time} (as timestamp: {Timestamp}), Duration: {Duration}ms", + this, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp, Duration); - var isFirstEndCall = !_isEnded; - _isEnded = true; - if (!isFirstEndCall) - return; + ChildDurationTimer.OnSpanEnd((long)(Timestamp + Duration.Value * 1000)); + } + else + { + Assertion.IfEnabled?.That(!_isEnded, + $"Transaction's Duration doesn't have value even though {nameof(End)} method was already called." + + $" It contradicts the invariant enforced by {nameof(End)} method - Duration should have value when {nameof(End)} method exits" + + $" and {nameof(_isEnded)} field is set to true only when {nameof(End)} method exits." + + $" Context: this: {this}; {nameof(_isEnded)}: {_isEnded}"); + + var endTimestamp = TimeUtils.TimestampNow(); + ChildDurationTimer.OnSpanEnd(endTimestamp); + Duration = TimeUtils.DurationBetweenTimestamps(Timestamp, endTimestamp); + _logger.Trace() + ?.Log("Ended {Transaction}. Start time: {Time} (as timestamp: {Timestamp})," + + " End time: {Time} (as timestamp: {Timestamp}), Duration: {Duration}ms", + this, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp, + TimeUtils.FormatTimestampForLog(endTimestamp), endTimestamp, Duration); + } - if (SpanTimings.TryGetValue(SpanTimerKey.AppSpanType, out var timing)) - timing.IncrementTimer(SelfDuration); - else - SpanTimings.TryAdd(SpanTimerKey.AppSpanType, new SpanTimer(SelfDuration)); + _activity?.Stop(); - _breakdownMetricsProvider?.CaptureTransaction(this); + var isFirstEndCall = !_isEnded; + _isEnded = true; + if (!isFirstEndCall) + return; - var handler = Ended; - handler?.Invoke(this, EventArgs.Empty); - Ended = null; + if (SpanTimings.TryGetValue(SpanTimerKey.AppSpanType, out var timing)) + timing.IncrementTimer(SelfDuration); + else + SpanTimings.TryAdd(SpanTimerKey.AppSpanType, new SpanTimer(SelfDuration)); - if (CompressionBuffer != null) - { - if (!CompressionBuffer.IsSampled && _apmServerInfo?.Version >= new ElasticVersion(8, 0, 0, string.Empty)) - { - _logger?.Debug() - ?.Log("Dropping unsampled compressed span - unsampled span won't be sent on APM Server v8+. SpanId: {id}", - CompressionBuffer.Id); - } - else - _sender.QueueSpan(CompressionBuffer); + _breakdownMetricsProvider?.CaptureTransaction(this); - CompressionBuffer = null; - } + var handler = Ended; + handler?.Invoke(this, EventArgs.Empty); + Ended = null; - if (IsSampled || _apmServerInfo?.Version < new ElasticVersion(8, 0, 0, string.Empty)) - _sender.QueueTransaction(this); - else + if (CompressionBuffer != null) + { + if (!CompressionBuffer.IsSampled && _apmServerInfo?.Version >= new ElasticVersion(8, 0, 0, string.Empty)) { _logger?.Debug() - ?.Log("Dropping unsampled transaction - unsampled transactions won't be sent on APM Server v8+. TransactionId: {id}", Id); + ?.Log("Dropping unsampled compressed span - unsampled span won't be sent on APM Server v8+. SpanId: {id}", + CompressionBuffer.Id); } + else + _sender.QueueSpan(CompressionBuffer); - _currentExecutionSegmentsContainer.CurrentTransaction = null; + CompressionBuffer = null; } - public bool TryGetLabel(string key, out T value) + if (IsSampled || _apmServerInfo?.Version < new ElasticVersion(8, 0, 0, string.Empty)) + _sender.QueueTransaction(this); + else { - if (Context.InternalLabels.Value.InnerDictionary.TryGetValue(key, out var label)) + _logger?.Debug() + ?.Log("Dropping unsampled transaction - unsampled transactions won't be sent on APM Server v8+. TransactionId: {id}", Id); + } + + _currentExecutionSegmentsContainer.CurrentTransaction = null; + } + + public bool TryGetLabel(string key, out T value) + { + if (Context.InternalLabels.Value.InnerDictionary.TryGetValue(key, out var label)) + { + if (label?.Value is T t) { - if (label?.Value is T t) - { - value = t; - return true; - } + value = t; + return true; } - - value = default; - return false; } - public ISpan StartSpan(string name, string type, string subType = null, string action = null, bool isExitSpan = false, - IEnumerable links = null - ) - { - if (Configuration.Enabled && Configuration.Recording) - return StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links); + value = default; + return false; + } - return new NoopSpan(name, type, subType, action, _currentExecutionSegmentsContainer, Id, TraceId); - } + public ISpan StartSpan(string name, string type, string subType = null, string action = null, bool isExitSpan = false, + IEnumerable links = null + ) + { + if (Configuration.Enabled && Configuration.Recording) + return StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links); - internal Span StartSpanInternal(string name, string type, string subType = null, string action = null, - InstrumentationFlag instrumentationFlag = InstrumentationFlag.None, bool captureStackTraceOnStart = false, long? timestamp = null, - string id = null, bool isExitSpan = false, IEnumerable links = null, Activity current = null - ) - { - var retVal = new Span(name, type, Id, TraceId, this, _sender, _logger, _currentExecutionSegmentsContainer, _apmServerInfo, - instrumentationFlag: instrumentationFlag, captureStackTraceOnStart: captureStackTraceOnStart, timestamp: timestamp, - isExitSpan: isExitSpan, id: id, links: links, current: current); + return new NoopSpan(name, type, subType, action, _currentExecutionSegmentsContainer, Id, TraceId); + } - ChildDurationTimer.OnChildStart(retVal.Timestamp); - if (!string.IsNullOrEmpty(subType)) - retVal.Subtype = subType; + internal Span StartSpanInternal(string name, string type, string subType = null, string action = null, + InstrumentationFlag instrumentationFlag = InstrumentationFlag.None, bool captureStackTraceOnStart = false, long? timestamp = null, + string id = null, bool isExitSpan = false, IEnumerable links = null, Activity current = null + ) + { + var retVal = new Span(name, type, Id, TraceId, this, _sender, _logger, _currentExecutionSegmentsContainer, _apmServerInfo, + instrumentationFlag: instrumentationFlag, captureStackTraceOnStart: captureStackTraceOnStart, timestamp: timestamp, + isExitSpan: isExitSpan, id: id, links: links, current: current); - if (!string.IsNullOrEmpty(action)) - retVal.Action = action; + ChildDurationTimer.OnChildStart(retVal.Timestamp); + if (!string.IsNullOrEmpty(subType)) + retVal.Subtype = subType; - _logger.Trace()?.Log("Starting {SpanDetails}", retVal.ToString()); - return retVal; - } + if (!string.IsNullOrEmpty(action)) + retVal.Action = action; - public void CaptureException(Exception exception, string culprit = null, bool isHandled = false, string parentId = null, - Dictionary labels = null - ) - => ExecutionSegmentCommon.CaptureException( - exception, - _logger, - _sender, - this, - Configuration, - this, - _apmServerInfo, - culprit, - isHandled, - parentId, - labels - ); - - public void CaptureError(string message, string culprit, StackFrame[] frames, string parentId = null, Dictionary labels = null) - => ExecutionSegmentCommon.CaptureError( - message, - culprit, - frames, - _sender, - _logger, - this, - Configuration, - this, - _apmServerInfo, - parentId, - labels - ); - - public void CaptureSpan(string name, string type, Action capturedAction, string subType = null, string action = null, - bool isExitSpan = false, IEnumerable links = null - ) - => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), - capturedAction); - - public void CaptureSpan(string name, string type, Action capturedAction, string subType = null, string action = null, bool isExitSpan = false, - IEnumerable links = null - ) - => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), - capturedAction); - - public T CaptureSpan(string name, string type, Func func, string subType = null, string action = null, bool isExitSpan = false, - IEnumerable links = null - ) - => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), func); - - public T CaptureSpan(string name, string type, Func func, string subType = null, string action = null, bool isExitSpan = false, - IEnumerable links = null - ) - => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), func); - - public Task CaptureSpan(string name, string type, Func func, string subType = null, string action = null, bool isExitSpan = false, - IEnumerable links = null - ) - => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), func); - - public Task CaptureSpan(string name, string type, Func func, string subType = null, string action = null, - bool isExitSpan = false, IEnumerable links = null - ) - => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), func); - - public Task CaptureSpan(string name, string type, Func> func, string subType = null, string action = null, - bool isExitSpan = false, IEnumerable links = null - ) - => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), func); - - public Task CaptureSpan(string name, string type, Func> func, string subType = null, string action = null, - bool isExitSpan = false, IEnumerable links = null - ) - => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), func); - - internal static string StatusCodeToResult(string protocolName, int statusCode) => $"{protocolName} {statusCode.ToString()[0]}xx"; - - /// - /// Determines a name from the route values - /// - /// - /// Based on: https://github.com/Microsoft/ApplicationInsights-aspnetcore - /// - internal static string GetNameFromRouteContext(IDictionary routeValues) - { - if (routeValues.Count <= 0) - return null; + _logger.Trace()?.Log("Starting {SpanDetails}", retVal.ToString()); + return retVal; + } - string name = null; - var count = routeValues.TryGetValue("controller", out var controller) ? 1 : 0; - var controllerString = controller == null ? string.Empty : controller.ToString(); + public void CaptureException(Exception exception, string culprit = null, bool isHandled = false, string parentId = null, + Dictionary labels = null + ) + => ExecutionSegmentCommon.CaptureException( + exception, + _logger, + _sender, + this, + Configuration, + this, + _apmServerInfo, + culprit, + isHandled, + parentId, + labels + ); + + public void CaptureError(string message, string culprit, StackFrame[] frames, string parentId = null, Dictionary labels = null) + => ExecutionSegmentCommon.CaptureError( + message, + culprit, + frames, + _sender, + _logger, + this, + Configuration, + this, + _apmServerInfo, + parentId, + labels + ); + + public void CaptureSpan(string name, string type, Action capturedAction, string subType = null, string action = null, + bool isExitSpan = false, IEnumerable links = null + ) + => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), + capturedAction); + + public void CaptureSpan(string name, string type, Action capturedAction, string subType = null, string action = null, bool isExitSpan = false, + IEnumerable links = null + ) + => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), + capturedAction); + + public T CaptureSpan(string name, string type, Func func, string subType = null, string action = null, bool isExitSpan = false, + IEnumerable links = null + ) + => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), func); + + public T CaptureSpan(string name, string type, Func func, string subType = null, string action = null, bool isExitSpan = false, + IEnumerable links = null + ) + => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), func); + + public Task CaptureSpan(string name, string type, Func func, string subType = null, string action = null, bool isExitSpan = false, + IEnumerable links = null + ) + => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), func); + + public Task CaptureSpan(string name, string type, Func func, string subType = null, string action = null, + bool isExitSpan = false, IEnumerable links = null + ) + => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), func); + + public Task CaptureSpan(string name, string type, Func> func, string subType = null, string action = null, + bool isExitSpan = false, IEnumerable links = null + ) + => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), func); + + public Task CaptureSpan(string name, string type, Func> func, string subType = null, string action = null, + bool isExitSpan = false, IEnumerable links = null + ) + => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan, links: links), func); + + internal static string StatusCodeToResult(string protocolName, int statusCode) => $"{protocolName} {statusCode.ToString()[0]}xx"; + + /// + /// Determines a name from the route values + /// + /// + /// Based on: https://github.com/Microsoft/ApplicationInsights-aspnetcore + /// + internal static string GetNameFromRouteContext(IDictionary routeValues) + { + if (routeValues.Count <= 0) + return null; - if (!string.IsNullOrEmpty(controllerString)) - { - // Check for MVC areas - string areaString = null; - if (routeValues.TryGetValue("area", out var area) && area != null) - { - count++; - areaString = area.ToString(); - } + string name = null; + var count = routeValues.TryGetValue("controller", out var controller) ? 1 : 0; + var controllerString = controller == null ? string.Empty : controller.ToString(); - name = !string.IsNullOrEmpty(areaString) - ? areaString + "/" + controllerString - : controllerString; + if (!string.IsNullOrEmpty(controllerString)) + { + // Check for MVC areas + string areaString = null; + if (routeValues.TryGetValue("area", out var area) && area != null) + { + count++; + areaString = area.ToString(); + } - count = routeValues.TryGetValue("action", out var action) ? count + 1 : count; - var actionString = action == null ? string.Empty : action.ToString(); + name = !string.IsNullOrEmpty(areaString) + ? areaString + "/" + controllerString + : controllerString; - if (!string.IsNullOrEmpty(actionString)) - name += "/" + actionString; + count = routeValues.TryGetValue("action", out var action) ? count + 1 : count; + var actionString = action == null ? string.Empty : action.ToString(); - // if there are no other key/values other than area/controller/action, skip parsing parameters - if (routeValues.Keys.Count == count) - return name; + if (!string.IsNullOrEmpty(actionString)) + name += "/" + actionString; - // Add parameters - var sortedKeys = routeValues.Keys - .Where(key => - !string.Equals(key, "area", StringComparison.OrdinalIgnoreCase) && - !string.Equals(key, "controller", StringComparison.OrdinalIgnoreCase) && - !string.Equals(key, "action", StringComparison.OrdinalIgnoreCase) && - !string.Equals(key, "!__route_group", StringComparison.OrdinalIgnoreCase)) - .OrderBy(key => key, StringComparer.OrdinalIgnoreCase) - .ToArray(); + // if there are no other key/values other than area/controller/action, skip parsing parameters + if (routeValues.Keys.Count == count) + return name; - if (sortedKeys.Length <= 0) - return name; + // Add parameters + var sortedKeys = routeValues.Keys + .Where(key => + !string.Equals(key, "area", StringComparison.OrdinalIgnoreCase) && + !string.Equals(key, "controller", StringComparison.OrdinalIgnoreCase) && + !string.Equals(key, "action", StringComparison.OrdinalIgnoreCase) && + !string.Equals(key, "!__route_group", StringComparison.OrdinalIgnoreCase)) + .OrderBy(key => key, StringComparer.OrdinalIgnoreCase) + .ToArray(); - var arguments = string.Join(@"/", sortedKeys); - name += " {" + arguments + "}"; - } - else - { - routeValues.TryGetValue("page", out var page); - var pageString = page == null ? string.Empty : page.ToString(); - if (!string.IsNullOrEmpty(pageString)) - name = pageString; - } + if (sortedKeys.Length <= 0) + return name; - return name; + var arguments = string.Join(@"/", sortedKeys); + name += " {" + arguments + "}"; + } + else + { + routeValues.TryGetValue("page", out var page); + var pageString = page == null ? string.Empty : page.ToString(); + if (!string.IsNullOrEmpty(pageString)) + name = pageString; } - public void CaptureErrorLog(ErrorLog errorLog, string parentId = null, Exception exception = null, Dictionary labels = null) - => ExecutionSegmentCommon.CaptureErrorLog( - errorLog, - _sender, - _logger, - this, - Configuration, - this, - null, - _apmServerInfo, - exception, - labels - ); + return name; + } - public void SetLabel(string key, string value) - => _context.Value.InternalLabels.Value.InnerDictionary[key] = value; + public void CaptureErrorLog(ErrorLog errorLog, string parentId = null, Exception exception = null, Dictionary labels = null) + => ExecutionSegmentCommon.CaptureErrorLog( + errorLog, + _sender, + _logger, + this, + Configuration, + this, + null, + _apmServerInfo, + exception, + labels + ); - public void SetLabel(string key, bool value) - => _context.Value.InternalLabels.Value.InnerDictionary[key] = value; + public void SetLabel(string key, string value) + => _context.Value.InternalLabels.Value.InnerDictionary[key] = value; - public void SetLabel(string key, double value) - => _context.Value.InternalLabels.Value.InnerDictionary[key] = value; + public void SetLabel(string key, bool value) + => _context.Value.InternalLabels.Value.InnerDictionary[key] = value; - public void SetLabel(string key, int value) - => _context.Value.InternalLabels.Value.InnerDictionary[key] = value; + public void SetLabel(string key, double value) + => _context.Value.InternalLabels.Value.InnerDictionary[key] = value; - public void SetLabel(string key, long value) - => _context.Value.InternalLabels.Value.InnerDictionary[key] = value; + public void SetLabel(string key, int value) + => _context.Value.InternalLabels.Value.InnerDictionary[key] = value; - public void SetLabel(string key, decimal value) - => _context.Value.InternalLabels.Value.InnerDictionary[key] = value; + public void SetLabel(string key, long value) + => _context.Value.InternalLabels.Value.InnerDictionary[key] = value; - private readonly struct DroppedSpanStatsKey : IEquatable + public void SetLabel(string key, decimal value) + => _context.Value.InternalLabels.Value.InnerDictionary[key] = value; + + private readonly struct DroppedSpanStatsKey : IEquatable + { + public override int GetHashCode() { - public override int GetHashCode() + unchecked { - unchecked - { - var hashCode = (int)_outcome; - hashCode = (hashCode * 397) ^ (_serviceTargetType != null ? _serviceTargetType.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (_serviceTargetName != null ? _serviceTargetName.GetHashCode() : 0); - return hashCode; - } + var hashCode = (int)_outcome; + hashCode = (hashCode * 397) ^ (_serviceTargetType != null ? _serviceTargetType.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (_serviceTargetName != null ? _serviceTargetName.GetHashCode() : 0); + return hashCode; } + } - private readonly string _serviceTargetType; - private readonly string _serviceTargetName; + private readonly string _serviceTargetType; + private readonly string _serviceTargetName; - // ReSharper disable once NotAccessedField.Local - private readonly Outcome _outcome; + // ReSharper disable once NotAccessedField.Local + private readonly Outcome _outcome; - public DroppedSpanStatsKey(string serviceTargetType, string serviceTargetName, Outcome outcome) - { - _serviceTargetName = serviceTargetName; - _serviceTargetType = serviceTargetType; - _outcome = outcome; - } + public DroppedSpanStatsKey(string serviceTargetType, string serviceTargetName, Outcome outcome) + { + _serviceTargetName = serviceTargetName; + _serviceTargetType = serviceTargetType; + _outcome = outcome; + } - public bool Equals(DroppedSpanStatsKey other) => - _serviceTargetType == other._serviceTargetType && _serviceTargetName == other._serviceTargetName && _outcome == other._outcome; + public bool Equals(DroppedSpanStatsKey other) => + _serviceTargetType == other._serviceTargetType && _serviceTargetName == other._serviceTargetName && _outcome == other._outcome; - public override bool Equals(object obj) => obj is DroppedSpanStatsKey other && Equals(other); + public override bool Equals(object obj) => obj is DroppedSpanStatsKey other && Equals(other); - public static bool operator ==(DroppedSpanStatsKey left, DroppedSpanStatsKey right) => left.Equals(right); + public static bool operator ==(DroppedSpanStatsKey left, DroppedSpanStatsKey right) => left.Equals(right); - public static bool operator !=(DroppedSpanStatsKey left, DroppedSpanStatsKey right) => !left.Equals(right); - } + public static bool operator !=(DroppedSpanStatsKey left, DroppedSpanStatsKey right) => !left.Equals(right); } } diff --git a/test/Elastic.Apm.Tests/DroppedSpansStatsTests.cs b/test/Elastic.Apm.Tests/DroppedSpansStatsTests.cs index db0cfa96b..e01434844 100644 --- a/test/Elastic.Apm.Tests/DroppedSpansStatsTests.cs +++ b/test/Elastic.Apm.Tests/DroppedSpansStatsTests.cs @@ -74,8 +74,8 @@ public void SingleDroppedSpanTest() payloadSender.FirstTransaction.DroppedSpanStats.First().ServiceTargetName.Should().Be("foo.bar:443"); payloadSender.FirstTransaction.DroppedSpanStats.First().ServiceTargetType.Should().Be("bar"); payloadSender.FirstTransaction.DroppedSpanStats.First().Outcome.Should().Be(Outcome.Success); - payloadSender.FirstTransaction.DroppedSpanStats.First().DurationCount.Should().Be(1); - payloadSender.FirstTransaction.DroppedSpanStats.First().DurationSumUs.Should().Be(100); + payloadSender.FirstTransaction.DroppedSpanStats.First().Duration.Count.Should().Be(1); + payloadSender.FirstTransaction.DroppedSpanStats.First().Duration.Sum.Us.Should().Be(100); } [Fact] @@ -130,22 +130,22 @@ public void MultipleDroppedSpanTest() payloadSender.FirstTransaction.DroppedSpanStats.Should() .Contain(n => n.Outcome == Outcome.Success - && n.DurationCount == 2 && Math.Abs(n.DurationSumUs - 250) < 1 && n.DestinationServiceResource == "foo.bar:443" + && n.Duration.Count == 2 && Math.Abs(n.Duration.Sum.Us - 250) < 1 && n.DestinationServiceResource == "foo.bar:443" && n.ServiceTargetName == "foo.bar:443" && n.ServiceTargetType == "bar"); payloadSender.FirstTransaction.DroppedSpanStats.Should() .Contain(n => n.Outcome == Outcome.Failure - && n.DurationCount == 1 && Math.Abs(n.DurationSumUs - 50) < 1 && n.DestinationServiceResource == "foo.bar:443" + && n.Duration.Count == 1 && Math.Abs(n.Duration.Sum.Us - 50) < 1 && n.DestinationServiceResource == "foo.bar:443" && n.ServiceTargetName == "foo.bar:443" && n.ServiceTargetType == "bar"); payloadSender.FirstTransaction.DroppedSpanStats.Should() .Contain(n => n.Outcome == Outcome.Success - && n.DurationCount == 1 && Math.Abs(n.DurationSumUs - 15) < 1 && n.DestinationServiceResource == "foo2.bar:443" + && n.Duration.Count == 1 && Math.Abs(n.Duration.Sum.Us - 15) < 1 && n.DestinationServiceResource == "foo2.bar:443" && n.ServiceTargetName == "foo2.bar:443" && n.ServiceTargetType == "bar"); payloadSender.FirstTransaction.DroppedSpanStats.Should() .Contain(n => n.Outcome == Outcome.Success - && n.DurationCount == 50 && Math.Abs(n.DurationSumUs - 50 * 50) < 1 && n.DestinationServiceResource == "mysql"); + && n.Duration.Count == 50 && Math.Abs(n.Duration.Sum.Us - 50 * 50) < 1 && n.DestinationServiceResource == "mysql"); } /// @@ -205,7 +205,7 @@ public void SimpleDroppedSpans() payloadSender.FirstTransaction.DroppedSpanStats.First().DestinationServiceResource.Should().Be("bar"); payloadSender.FirstTransaction.DroppedSpanStats.First().ServiceTargetType.Should().Be("bar"); payloadSender.FirstTransaction.DroppedSpanStats.First().ServiceTargetName.Should().BeNullOrEmpty(); - payloadSender.FirstTransaction.DroppedSpanStats.First().DurationCount.Should().Be(500); + payloadSender.FirstTransaction.DroppedSpanStats.First().Duration.Count.Should().Be(500); } } } diff --git a/test/Elastic.Apm.Tests/ExitSpanMinDurationTests.cs b/test/Elastic.Apm.Tests/ExitSpanMinDurationTests.cs index 7753315e5..8b5a04683 100644 --- a/test/Elastic.Apm.Tests/ExitSpanMinDurationTests.cs +++ b/test/Elastic.Apm.Tests/ExitSpanMinDurationTests.cs @@ -34,7 +34,7 @@ public void FastExitSpanTest() payloadSender.FirstTransaction.DroppedSpanStats.Should().NotBeEmpty(); payloadSender.FirstTransaction.DroppedSpanStats.First().DestinationServiceResource.Should().Be("test"); - payloadSender.FirstTransaction.DroppedSpanStats.First().DurationCount.Should().Be(1); + payloadSender.FirstTransaction.DroppedSpanStats.First().Duration.Count.Should().Be(1); payloadSender.Spans[0].Name.Should().Be("span1"); payloadSender.Spans[1].Name.Should().Be("span3"); diff --git a/test/Elastic.Apm.Tests/SerializationTests.cs b/test/Elastic.Apm.Tests/SerializationTests.cs index 4279284c0..cfe8586a2 100644 --- a/test/Elastic.Apm.Tests/SerializationTests.cs +++ b/test/Elastic.Apm.Tests/SerializationTests.cs @@ -268,13 +268,15 @@ public void TransactionContextShouldBeSerializedOnlyWhenSampled() var agent = new TestAgentComponents(); // Create a transaction that is sampled (because the sampler is constant sampling-everything sampler var sampledTransaction = new Transaction(agent.Logger, "dummy_name", "dumm_type", new Sampler(1.0), /* distributedTracingData: */ null, - agent.PayloadSender, new MockConfiguration(new NoopLogger()), agent.TracerInternal.CurrentExecutionSegmentsContainer, MockApmServerInfo.Version710, null); + agent.PayloadSender, new MockConfiguration(new NoopLogger()), agent.TracerInternal.CurrentExecutionSegmentsContainer, + MockApmServerInfo.Version710, null); sampledTransaction.Context.Request = new Request("GET", new Url { Full = "https://elastic.co", Raw = "https://elastic.co", HostName = "elastic.co", Protocol = "HTTP" }); // Create a transaction that is not sampled (because the sampler is constant not-sampling-anything sampler var nonSampledTransaction = new Transaction(agent.Logger, "dummy_name", "dumm_type", new Sampler(0.0), /* distributedTracingData: */ null, - agent.PayloadSender, new MockConfiguration(new NoopLogger()), agent.TracerInternal.CurrentExecutionSegmentsContainer, MockApmServerInfo.Version710, null); + agent.PayloadSender, new MockConfiguration(new NoopLogger()), agent.TracerInternal.CurrentExecutionSegmentsContainer, + MockApmServerInfo.Version710, null); nonSampledTransaction.Context.Request = sampledTransaction.Context.Request; var serializedSampledTransaction = _payloadItemSerializer.Serialize(sampledTransaction); @@ -471,6 +473,31 @@ public void SpanContext_Destination_Service_Should_Serialize_Name_And_Type_As_Em json.Should().Contain("\"name\":\"\"").And.Contain("\"type\":\"\""); } + /// + /// Asserts that dropped span statistic is not flattened but sent as an object to APM Server. + /// APM Server expects object and flattening caused issues. + /// + [Fact] + public void DroppedSpanStatsTest() + { + using var apmAgent = new ApmAgent(new TestAgentComponents(configuration: new MockConfiguration(transactionMaxSpans: "1"))); + + var transaction = apmAgent.Tracer.StartTransaction("foo", "test"); + //This is the span which won't be dropped + transaction.CaptureSpan("fooSpan", "test", () => { }); + + //This span will be dropped + var span1 = transaction.StartSpan("foo", "bar", isExitSpan: true); + span1.Context.Http = new Http { Method = "GET", StatusCode = 200, Url = "https://foo.bar" }; + span1.Duration = 100; + span1.End(); + + transaction.End(); + + var json = _payloadItemSerializer.Serialize(transaction); + json.Should().Contain("\"duration\":{\"count\":1,\"sum\":{\"us\":100.0}}"); + } + /// /// A dummy type for tests. ///