diff --git a/sample/AspNetFullFrameworkSampleApp/Controllers/DatabaseController.cs b/sample/AspNetFullFrameworkSampleApp/Controllers/DatabaseController.cs index 0f2b0da86..cba726b9a 100644 --- a/sample/AspNetFullFrameworkSampleApp/Controllers/DatabaseController.cs +++ b/sample/AspNetFullFrameworkSampleApp/Controllers/DatabaseController.cs @@ -107,16 +107,16 @@ public async Task Bulk(IEnumerable mode return JsonBadRequest(new { success = false, message = "Invalid samples" }); var sampleData = model.Select(m => new SampleData { Name = m.Name }); + + var aggregatedModel = new SampleData { Name = model.Aggregate("", (current, next) => current + ", " + next.Name) }; int changes; using (var context = new SampleDataDbContext()) - using (var transaction = context.Database.BeginTransaction()) { context.Configuration.AutoDetectChangesEnabled = false; context.Configuration.ValidateOnSaveEnabled = false; - context.Set().AddRange(sampleData); + context.Set().Add(aggregatedModel); changes = await context.SaveChangesAsync(); - transaction.Commit(); } return Json(new { success = true, changes }); diff --git a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs index 1df6d5f4a..e2ae89ab6 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs @@ -1,4 +1,4 @@ -// Licensed to Elasticsearch B.V under +// Licensed to Elasticsearch B.V under // one or more agreements. // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information @@ -252,9 +252,7 @@ private void OnSendStart(KeyValuePair kv, string action) Address = destinationAddress, Service = new Destination.DestinationService { - Name = ServiceBus.SubType, - Resource = queueName is null ? ServiceBus.SubType : $"{ServiceBus.SubType}/{queueName}", - Type = ApiConstants.TypeMessaging + Resource = queueName is null ? ServiceBus.SubType : $"{ServiceBus.SubType}/{queueName}" } }; diff --git a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs index 4457c25aa..c584e0b29 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs @@ -1,4 +1,4 @@ -// Licensed to Elasticsearch B.V under +// Licensed to Elasticsearch B.V under // one or more agreements. // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information @@ -236,9 +236,7 @@ private void OnSendStart(KeyValuePair kv, string action, Propert Address = destinationAddress?.AbsoluteUri, Service = new Destination.DestinationService { - Name = ServiceBus.SubType, - Resource = queueName is null ? ServiceBus.SubType : $"{ServiceBus.SubType}/{queueName}", - Type = ApiConstants.TypeMessaging + Resource = queueName is null ? ServiceBus.SubType : $"{ServiceBus.SubType}/{queueName}" } }; diff --git a/src/Elastic.Apm.GrpcClient/GrpcClientDiagnosticListener.cs b/src/Elastic.Apm.GrpcClient/GrpcClientDiagnosticListener.cs index 987ccffac..93879dade 100644 --- a/src/Elastic.Apm.GrpcClient/GrpcClientDiagnosticListener.cs +++ b/src/Elastic.Apm.GrpcClient/GrpcClientDiagnosticListener.cs @@ -64,7 +64,7 @@ protected override void HandleOnNext(KeyValuePair kv) span.Outcome = GrpcHelper.GrpcClientReturnCodeToOutcome(GrpcHelper.GrpcReturnCodeToString(grpcStatusCode)); span.Context.Destination = UrlUtils.ExtractDestination(requestObject.RequestUri, Logger); - span.Context.Destination.Service = UrlUtils.ExtractService(requestObject.RequestUri, span); + span.Context.Destination.Service = new() { Resource = UrlUtils.ExtractService(requestObject.RequestUri, span) }; span.End(); } diff --git a/src/Elastic.Apm/DiagnosticListeners/HttpDiagnosticListenerImplBase.cs b/src/Elastic.Apm/DiagnosticListeners/HttpDiagnosticListenerImplBase.cs index 4c6c453a6..74263c03f 100644 --- a/src/Elastic.Apm/DiagnosticListeners/HttpDiagnosticListenerImplBase.cs +++ b/src/Elastic.Apm/DiagnosticListeners/HttpDiagnosticListenerImplBase.cs @@ -194,7 +194,10 @@ private void ProcessStartEvent(TRequest request, Uri requestUrl) if (span is Span realSpan) { if (!realSpan.ShouldBeSentToApmServer) + { + realSpan.ServiceResource = UrlUtils.ExtractService(requestUrl, realSpan); return; + } } span.Context.Http = new Http { Method = method }; diff --git a/src/Elastic.Apm/Helpers/UrlUtils.cs b/src/Elastic.Apm/Helpers/UrlUtils.cs index b15babed9..efa1bc9b5 100644 --- a/src/Elastic.Apm/Helpers/UrlUtils.cs +++ b/src/Elastic.Apm/Helpers/UrlUtils.cs @@ -33,7 +33,6 @@ internal static Destination ExtractDestination(Uri url, IApmLogger logger) return new Destination { Address = host, Port = url.Port == -1 ? (int?)null : url.Port }; } - internal static Destination.DestinationService ExtractService(Uri url, ISpan span) => - new() { Resource = $"{url.Host}:{url.Port}" }; + internal static string ExtractService(Uri url, ISpan span) => $"{url.Host}:{url.Port}"; } } diff --git a/src/Elastic.Apm/Model/DbSpanCommon.cs b/src/Elastic.Apm/Model/DbSpanCommon.cs index f6a8da177..2d993d2f6 100644 --- a/src/Elastic.Apm/Model/DbSpanCommon.cs +++ b/src/Elastic.Apm/Model/DbSpanCommon.cs @@ -50,13 +50,13 @@ internal void EndSpan(ISpan span, IDbCommand dbCommand, Outcome outcome, TimeSpa { capturedSpan.Context.Db = new Database { - Statement = GetDbSpanName(dbCommand), - Instance = dbCommand.Connection.Database, - Type = Database.TypeSql + Statement = GetDbSpanName(dbCommand), Instance = dbCommand.Connection.Database, Type = Database.TypeSql }; capturedSpan.Context.Destination = GetDestination(dbCommand.Connection?.ConnectionString, defaultPort); } + else + capturedSpan.ServiceResource = !string.IsNullOrEmpty(capturedSpan.Subtype) ? capturedSpan.Subtype : Database.TypeSql + dbCommand.Connection.Database; capturedSpan.Outcome = outcome; } diff --git a/src/Elastic.Apm/Model/DroppedSpanStats.cs b/src/Elastic.Apm/Model/DroppedSpanStats.cs new file mode 100644 index 000000000..c325bd51c --- /dev/null +++ b/src/Elastic.Apm/Model/DroppedSpanStats.cs @@ -0,0 +1,52 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Apm.Api; +using Elastic.Apm.Libraries.Newtonsoft.Json; + +namespace Elastic.Apm.Model +{ + /// + /// DroppedSpanStats holds information about spans that were dropped (for example due to transaction_max_spans or + /// exit_span_min_duration). + /// + internal class DroppedSpanStats + { + public DroppedSpanStats(string destinationServiceResource, Outcome outcome, double durationSumUs) + { + DurationCount = 1; + DestinationServiceResource = destinationServiceResource; + Outcome = outcome; + DurationSumUs = durationSumUs; + } + + /// + /// DestinationServiceResource identifies the destination service resource being operated on. e.g. 'http://elastic.co:80', + /// 'elasticsearch', 'rabbitmq/queue_name'. + /// + [JsonProperty("destination_service_resource")] + public string DestinationServiceResource { get; } + + /// + /// Duration holds duration aggregations about the dropped span. + /// Count holds the number of times the dropped span happened. + /// + [JsonProperty("duration.count")] + public int DurationCount { get; set; } + + + /// + /// 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; } + + /// + /// Outcome of the aggregated spans. + /// + public Outcome Outcome { get; } + } +} diff --git a/src/Elastic.Apm/Model/Span.cs b/src/Elastic.Apm/Model/Span.cs index 751ef4515..564a673e5 100644 --- a/src/Elastic.Apm/Model/Span.cs +++ b/src/Elastic.Apm/Model/Span.cs @@ -132,6 +132,14 @@ public Span( [MaxLength] public string Action { get; set; } + /// + /// Stores Context.Destination.Service.Resource on the top level. + /// With this field, we can set Resource for dropped spans without instantiating Context. + /// Only set for dropped spans. + /// + [JsonIgnore] + public string ServiceResource { get; set; } + [JsonIgnore] internal IConfiguration Configuration => _enclosingTransaction.Configuration; @@ -362,17 +370,20 @@ public void End() else _enclosingTransaction.SpanTimings.TryAdd(new SpanTimerKey(Type, Subtype), new SpanTimer(SelfDuration)); - if (ShouldBeSentToApmServer && isFirstEndCall) + try { - try - { - DeduceDestination(); - } - catch (Exception e) - { - _logger.Warning()?.LogException(e, "Failed deducing destination fields for span."); - } + DeduceDestination(); + } + catch (Exception e) + { + _logger.Warning()?.LogException(e, "Failed deducing destination fields for span."); + } + if (_isDropped && (!string.IsNullOrEmpty(ServiceResource) || (_context.IsValueCreated && Context?.Destination?.Service?.Resource != null))) + _enclosingTransaction.UpdateDroppedSpanStats(ServiceResource ?? Context?.Destination?.Service?.Resource, _outcome, Duration.Value); + + if (ShouldBeSentToApmServer && isFirstEndCall) + { // Spans are sent only for sampled transactions so it's only worth capturing stack trace for sampled spans // ReSharper disable once CompareOfFloatsByEqualityOperator if (Configuration.StackTraceLimit != 0 && Configuration.SpanFramesMinDurationInMilliseconds != 0 && RawStackTrace == null @@ -455,6 +466,13 @@ private void DeduceDestination() if (!IsExitSpan) return; + if (!_context.IsValueCreated) + { + if(string.IsNullOrEmpty(ServiceResource)) + ServiceResource = !string.IsNullOrEmpty(Subtype) ? Subtype : Type; + return; + } + if (Context.Http != null) { var destination = DeduceHttpDestination(); @@ -489,7 +507,7 @@ void FillDestinationService() else if (Context.Http?.Url != null) { if (!string.IsNullOrEmpty(_context?.Value?.Http?.Url)) - Context.Destination.Service = UrlUtils.ExtractService(_context.Value.Http.OriginalUrl, this); + Context.Destination.Service = new() { Resource = UrlUtils.ExtractService(_context.Value.Http.OriginalUrl, this) }; else Context.Destination.Service.Resource = !string.IsNullOrEmpty(Subtype) ? Subtype : Type; } diff --git a/src/Elastic.Apm/Model/Transaction.cs b/src/Elastic.Apm/Model/Transaction.cs index 7edaca9b9..1071c59ab 100644 --- a/src/Elastic.Apm/Model/Transaction.cs +++ b/src/Elastic.Apm/Model/Transaction.cs @@ -14,11 +14,11 @@ using Elastic.Apm.Config; using Elastic.Apm.DistributedTracing; using Elastic.Apm.Helpers; +using Elastic.Apm.Libraries.Newtonsoft.Json; using Elastic.Apm.Logging; +using Elastic.Apm.Metrics.MetricsProvider; using Elastic.Apm.Report; using Elastic.Apm.ServerInfo; -using Elastic.Apm.Libraries.Newtonsoft.Json; -using Elastic.Apm.Metrics.MetricsProvider; namespace Elastic.Apm.Model { @@ -37,12 +37,13 @@ internal class Transaction : ITransaction /// 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 Lazy _context = new Lazy(); + private readonly BreakdownMetricsProvider _breakdownMetricsProvider; + private readonly Lazy _context = new(); private readonly ICurrentExecutionSegmentsContainer _currentExecutionSegmentsContainer; private readonly IApmLogger _logger; private readonly IPayloadSender _sender; - private readonly BreakdownMetricsProvider _breakdownMetricsProvider; [JsonConstructor] // ReSharper disable once UnusedMember.Local - this constructor is meant for serialization @@ -81,7 +82,10 @@ internal Transaction(ApmAgent agent, string name, string type, long? timestamp = /// /// 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 + /// + /// 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 @@ -281,25 +285,14 @@ internal Transaction( } } - private bool _isEnded; - - private string _name; - internal ChildDurationTimer ChildDurationTimer { get; } = new(); - /// - /// 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. + /// Internal dictionary to keep track of and look up dropped span stats. /// - [JsonIgnore] - public IConfiguration Configuration { get; } + private Dictionary _droppedSpanStatsMap; - /// - /// Any arbitrary contextual information regarding the event, captured by the agent, optionally provided by the user. - /// - /// - public Context Context => _context.Value; + 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 @@ -313,22 +306,29 @@ internal Transaction( private Outcome _outcome; private bool _outcomeChangedThroughApi; + internal ChildDurationTimer ChildDurationTimer { get; } = new(); /// - /// 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. + /// 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. /// - /// 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 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. @@ -387,7 +387,7 @@ public Outcome Outcome } [JsonIgnore] - public DistributedTracingData OutgoingDistributedTracingData => new DistributedTracingData(TraceId, Id, IsSampled, _traceState); + public DistributedTracingData OutgoingDistributedTracingData => new(TraceId, Id, IsSampled, _traceState); [MaxLength] [JsonProperty("parent_id")] @@ -428,6 +428,21 @@ public Outcome Outcome [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() { var activity = new Activity(ApmTransactionActivityName); @@ -436,6 +451,36 @@ private Activity StartActivity() return activity; } + internal void UpdateDroppedSpanStats(string destinationServiceResource, Outcome outcome, double duration) + { + if (_droppedSpanStatsMap == null) + { + _droppedSpanStatsMap = new Dictionary + { + { + new DroppedSpanStatsKey(destinationServiceResource, outcome), + new DroppedSpanStats(destinationServiceResource, outcome, duration) + } + }; + } + else + { + if (_droppedSpanStatsMap.Count >= 128) + return; + + if (_droppedSpanStatsMap.TryGetValue(new DroppedSpanStatsKey(destinationServiceResource, outcome), out var item)) + { + item.DurationCount++; + item.DurationSumUs += duration; + } + else + { + _droppedSpanStatsMap.Add(new DroppedSpanStatsKey(destinationServiceResource, outcome), + new DroppedSpanStats(destinationServiceResource, outcome, duration)); + } + } + } + /// public void SetService(string serviceName, string serviceVersion) { @@ -735,5 +780,37 @@ public void SetLabel(string key, long value) public void SetLabel(string key, decimal value) => _context.Value.InternalLabels.Value.InnerDictionary[key] = value; + + private readonly struct DroppedSpanStatsKey : IEquatable + { + // ReSharper disable once NotAccessedField.Local + private readonly string _destinationServiceResource; + + // ReSharper disable once NotAccessedField.Local + private readonly Outcome _outcome; + + public DroppedSpanStatsKey(string destinationServiceResource, Outcome outcome) + { + _destinationServiceResource = destinationServiceResource; + _outcome = outcome; + } + + public bool Equals(DroppedSpanStatsKey other) => + _destinationServiceResource == other._destinationServiceResource && _outcome == other._outcome; + + public override bool Equals(object obj) => obj is DroppedSpanStatsKey other && Equals(other); + + public override int GetHashCode() + { + unchecked + { + return ((_destinationServiceResource != null ? _destinationServiceResource.GetHashCode() : 0) * 397) ^ (int)_outcome; + } + } + + 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.AspNetFullFramework.Tests/HttpContextCurrentExecutionSegmentsContainerTests.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/HttpContextCurrentExecutionSegmentsContainerTests.cs index 6120feab4..66b87db8f 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/HttpContextCurrentExecutionSegmentsContainerTests.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/HttpContextCurrentExecutionSegmentsContainerTests.cs @@ -48,10 +48,6 @@ await WaitAndCustomVerifyReceivedData(received => { received.Transactions.Count.Should().Be(1); var transaction = received.Transactions.Single(); - - transaction.SpanCount.Started.Should().Be(500); - transaction.SpanCount.Dropped.Should().Be(501); - received.Spans.Count.Should().Be(500); }); } @@ -76,13 +72,13 @@ await WaitAndCustomVerifyReceivedData(received => var firstTransaction = transactions.First(); firstTransaction.Name.Should().EndWith("Bulk"); - firstTransaction.SpanCount.Started.Should().Be(100); + firstTransaction.SpanCount.Started.Should().Be(1); var secondTransaction = transactions.Last(); secondTransaction.Name.Should().EndWith("Generate"); secondTransaction.SpanCount.Started.Should().Be(3); - received.Spans.Count.Should().Be(103); + received.Spans.Count.Should().Be(4); }); } diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs index 0d6c4583e..c70369a98 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs @@ -61,9 +61,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" var destination = span.Context.Destination; destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); - destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.QueueName}"); - destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); span.Context.Message.Should().NotBeNull(); span.Context.Message.Queue.Should().NotBeNull(); @@ -94,9 +92,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" var destination = span.Context.Destination; destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); - destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.TopicName}"); - destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); span.Context.Message.Should().NotBeNull(); span.Context.Message.Queue.Should().NotBeNull(); @@ -129,9 +125,7 @@ await sender.ScheduleMessageAsync( var destination = span.Context.Destination; destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); - destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.QueueName}"); - destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); span.Context.Message.Should().NotBeNull(); span.Context.Message.Queue.Should().NotBeNull(); @@ -164,9 +158,7 @@ await sender.ScheduleMessageAsync( var destination = span.Context.Destination; destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); - destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.TopicName}"); - destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); span.Context.Message.Should().NotBeNull(); span.Context.Message.Queue.Should().NotBeNull(); diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs index b6567ce59..519c16f2d 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs @@ -62,9 +62,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" var destination = span.Context.Destination; destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); - destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.QueueName}"); - destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); span.Context.Message.Should().NotBeNull(); span.Context.Message.Queue.Should().NotBeNull(); @@ -95,9 +93,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" var destination = span.Context.Destination; destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); - destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.TopicName}"); - destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); span.Context.Message.Should().NotBeNull(); span.Context.Message.Queue.Should().NotBeNull(); @@ -130,9 +126,7 @@ await sender.ScheduleMessageAsync( var destination = span.Context.Destination; destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); - destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.QueueName}"); - destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); span.Context.Message.Should().NotBeNull(); span.Context.Message.Queue.Should().NotBeNull(); @@ -165,9 +159,7 @@ await sender.ScheduleMessageAsync( var destination = span.Context.Destination; destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); - destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.TopicName}"); - destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); span.Context.Message.Should().NotBeNull(); span.Context.Message.Queue.Should().NotBeNull(); diff --git a/test/Elastic.Apm.Benchmarks/DroppedSpansStats.cs b/test/Elastic.Apm.Benchmarks/DroppedSpansStats.cs new file mode 100644 index 000000000..b4ac0d6ef --- /dev/null +++ b/test/Elastic.Apm.Benchmarks/DroppedSpansStats.cs @@ -0,0 +1,28 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using BenchmarkDotNet.Attributes; +using Elastic.Apm.Tests.Utilities; + +namespace Elastic.Apm.Benchmarks +{ + [MemoryDiagnoser] + public class DroppedSpansStats + { + private ApmAgent _agent; + + [GlobalSetup] + public void SetupWithLowMaxSpans() + => _agent = new ApmAgent(new AgentComponents(payloadSender: new MockPayloadSender(), + configurationReader: new MockConfiguration(transactionMaxSpans: "1"))); + + [Benchmark] + public void Test10Spans() => _agent.Tracer.CaptureTransaction("foo", "bar", t => + { + for (var i = 0; i < 10; i++) + t.CaptureSpan("foo", "bar", () => { }); + }); + } +} diff --git a/test/Elastic.Apm.Tests/ApiTests/ExitSpanTests.cs b/test/Elastic.Apm.Tests/ApiTests/ExitSpanTests.cs index a84cf9eb8..529ee66b5 100644 --- a/test/Elastic.Apm.Tests/ApiTests/ExitSpanTests.cs +++ b/test/Elastic.Apm.Tests/ApiTests/ExitSpanTests.cs @@ -3,6 +3,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.Apm.Api; using Elastic.Apm.Tests.Utilities; using FluentAssertions; using Xunit; @@ -14,7 +15,6 @@ public class ExitSpanTests [Fact] public void TestNonExitSpan() { - var payloadSender = new MockPayloadSender(); using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); agent.Tracer.CaptureTransaction("foo", "bar", t => @@ -26,7 +26,7 @@ public void TestNonExitSpan() } [Fact] - public void SimpleManualExitSpan() + public void SimpleManualExitSpanWithNoContext() { var payloadSender = new MockPayloadSender(); using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); @@ -35,8 +35,27 @@ public void SimpleManualExitSpan() t.StartSpan("foo", "bar", isExitSpan: true).End(); }); + payloadSender.FirstSpan.Context.Destination.Should().BeNull(); + payloadSender.FirstSpan.ServiceResource.Should().Be("bar"); + } + + [Fact] + public void SimpleManualExitSpanWithContext() + { + var payloadSender = new MockPayloadSender(); + using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); + agent.Tracer.CaptureTransaction("foo", "bar", t => + { + var span = t.StartSpan("foo", "bar", isExitSpan: true); + + span.Context.Http = new Http { Method = "GET", Url = "https://elastic.co", StatusCode = 200 }; + span.End(); + }); + payloadSender.FirstSpan.Context.Destination.Should().NotBeNull(); - payloadSender.FirstSpan.Context.Destination.Service.Resource.Should().Be("bar"); + payloadSender.FirstSpan.Context.Destination.Address.Should().Be("elastic.co"); + payloadSender.FirstSpan.Context.Destination.Port.Should().Be(443); + payloadSender.FirstSpan.Context.Destination.Service.Resource.Should().Be("elastic.co:443"); } } } diff --git a/test/Elastic.Apm.Tests/DroppedSpansStatsTests.cs b/test/Elastic.Apm.Tests/DroppedSpansStatsTests.cs new file mode 100644 index 000000000..00e06adf7 --- /dev/null +++ b/test/Elastic.Apm.Tests/DroppedSpansStatsTests.cs @@ -0,0 +1,176 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Linq; +using Elastic.Apm.Api; +using Elastic.Apm.Model; +using Elastic.Apm.Tests.Utilities; +using FluentAssertions; +using Xunit; + +namespace Elastic.Apm.Tests +{ + /// + /// Tests + /// + public class DroppedSpansStatsTests + { + [Fact] + public void SingleDroppedSpanTest() + { + var payloadSender = new MockPayloadSender(); + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, + configuration: new MockConfiguration(transactionMaxSpans: "1")))) + { + var transaction = agent.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(); + } + + payloadSender.Spans.Should().HaveCount(1); + + payloadSender.FirstTransaction.DroppedSpanStats.Should().NotBeNullOrEmpty(); + payloadSender.FirstTransaction.DroppedSpanStats.Should().HaveCount(1); + payloadSender.FirstTransaction.DroppedSpanStats.First().DestinationServiceResource.Should().Be("foo.bar:443"); + 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); + } + + [Fact] + public void MultipleDroppedSpanTest() + { + var payloadSender = new MockPayloadSender(); + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, + configuration: new MockConfiguration(transactionMaxSpans: "1")))) + { + var transaction = agent.Tracer.StartTransaction("foo", "test"); + //This is the span which won't be dropped + transaction.CaptureSpan("fooSpan", "test", () => { }); + + //Next spans 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(); + + var span2 = transaction.StartSpan("foo", "bar", isExitSpan: true); + span2.Context.Http = new Http { Method = "GET", StatusCode = 200, Url = "https://foo.bar" }; + span2.Duration = 150; + span2.End(); + + var span3 = transaction.StartSpan("foo", "bar", isExitSpan: true); + span3.Context.Http = new Http { Method = "GET", StatusCode = 400, Url = "https://foo.bar" }; + span3.Outcome = Outcome.Failure; + span3.Duration = 50; + span3.End(); + + var span4 = transaction.StartSpan("foo", "bar", isExitSpan: true); + span4.Context.Http = new Http { Method = "GET", StatusCode = 400, Url = "https://foo2.bar" }; + span4.Duration = 15; + span4.End(); + + for (var i = 0; i < 50; i++) + { + var span5 = transaction.StartSpan("foo", "bar", isExitSpan: true); + span5.Context.Destination = new Destination { Service = new Destination.DestinationService { Resource = "mysql" } }; + span5.Context.Db = new Database { Instance = "instance1", Type = "mysql", Statement = "Select Foo From Bar" }; + span5.Duration = 50; + span5.End(); + } + + transaction.End(); + } + + payloadSender.Spans.Should().HaveCount(1); + + payloadSender.FirstTransaction.DroppedSpanStats.Should().NotBeNullOrEmpty(); + payloadSender.FirstTransaction.DroppedSpanStats.Should().HaveCount(4); + + payloadSender.FirstTransaction.DroppedSpanStats.Should() + .Contain(n => n.Outcome == Outcome.Success + && n.DurationCount == 2 && Math.Abs(n.DurationSumUs - 250) < 1 && n.DestinationServiceResource == "foo.bar:443"); + + payloadSender.FirstTransaction.DroppedSpanStats.Should() + .Contain(n => n.Outcome == Outcome.Failure + && n.DurationCount == 1 && Math.Abs(n.DurationSumUs - 50) < 1 && n.DestinationServiceResource == "foo.bar:443"); + + payloadSender.FirstTransaction.DroppedSpanStats.Should() + .Contain(n => n.Outcome == Outcome.Success + && n.DurationCount == 1 && Math.Abs(n.DurationSumUs - 15) < 1 && n.DestinationServiceResource == "foo2.bar:443"); + + payloadSender.FirstTransaction.DroppedSpanStats.Should() + .Contain(n => n.Outcome == Outcome.Success + && n.DurationCount == 50 && Math.Abs(n.DurationSumUs - 50 * 50) < 1 && n.DestinationServiceResource == "mysql"); + } + + /// + /// Tests the fix 128 upper limit + /// + [Fact] + public void MaxDroppedSpanStatsTest() + { + var payloadSender = new MockPayloadSender(); + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, + configuration: new MockConfiguration(transactionMaxSpans: "1")))) + { + var transaction = agent.Tracer.StartTransaction("foo", "test"); + //This is the span which won't be dropped + transaction.CaptureSpan("fooSpan", "test", () => { }); + + //Next spans will be dropped + for (var i = 0; i < 500; i++) + { + var span1 = transaction.StartSpan("foo", "bar", isExitSpan: true); + span1.Context.Http = new Http { Method = "GET", StatusCode = 200, Url = $"https://foo{i}.bar" }; + span1.Duration = 100; + span1.End(); + } + + transaction.End(); + } + + payloadSender.FirstTransaction.DroppedSpanStats.Should().HaveCount(128); + } + + /// + /// Testing with custom spans without touching Span.Context + /// + [Fact] + public void SimpleDroppedSpans() + { + var payloadSender = new MockPayloadSender(); + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, + configuration: new MockConfiguration(transactionMaxSpans: "1")))) + { + var transaction = agent.Tracer.StartTransaction("foo", "test"); + //This is the span which won't be dropped + transaction.CaptureSpan("fooSpan", "test", () => { }); + + //Next spans will be dropped + for (var i = 0; i < 500; i++) + { + var span = transaction.StartSpan("foo", "bar", isExitSpan: true); + span.End(); + } + + transaction.End(); + } + payloadSender.FirstTransaction.DroppedSpanStats.Should().HaveCount(1); + + payloadSender.FirstTransaction.DroppedSpanStats.First().DestinationServiceResource.Should().Be("bar"); + payloadSender.FirstTransaction.DroppedSpanStats.First().DurationCount.Should().Be(500); + } + } +}