Skip to content

Commit

Permalink
Implement DroppedSpanStats (#1511)
Browse files Browse the repository at this point in the history
* Implement DroppedSpanStats

* Dropped spans: calculate destination for dropped spans stats

* Update Transaction.cs

Introduce DroppedSpanStatsKey

* Update Transaction.cs

* Update DroppedSpansStatsTests.cs

* Do not create Span.Context for dropped spans

* Update ExitSpanTests.cs

* Update Span.cs

* Remove obsolete fields

* Update DroppedSpansStats.cs

* Generate less spans in Transaction_And_Spans_Captured_When_Large_Request

* Update HttpContextCurrentExecutionSegmentsContainerTests.cs

* Adapt test - remove unused fields
  • Loading branch information
gregkalapos authored Nov 9, 2021
1 parent e953480 commit 4c4d2c0
Show file tree
Hide file tree
Showing 16 changed files with 431 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,16 @@ public async Task<ActionResult> Bulk(IEnumerable<CreateSampleDataViewModel> 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<SampleData>().AddRange(sampleData);
context.Set<SampleData>().Add(aggregatedModel);
changes = await context.SaveChangesAsync();
transaction.Commit();
}

return Json(new { success = true, changes });
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -252,9 +252,7 @@ private void OnSendStart(KeyValuePair<string, object> 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}"
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -236,9 +236,7 @@ private void OnSendStart(KeyValuePair<string, object> 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}"
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/Elastic.Apm.GrpcClient/GrpcClientDiagnosticListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ protected override void HandleOnNext(KeyValuePair<string, object> 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
3 changes: 1 addition & 2 deletions src/Elastic.Apm/Helpers/UrlUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
}
}
6 changes: 3 additions & 3 deletions src/Elastic.Apm/Model/DbSpanCommon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
52 changes: 52 additions & 0 deletions src/Elastic.Apm/Model/DroppedSpanStats.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// DroppedSpanStats holds information about spans that were dropped (for example due to transaction_max_spans or
/// exit_span_min_duration).
/// </summary>
internal class DroppedSpanStats
{
public DroppedSpanStats(string destinationServiceResource, Outcome outcome, double durationSumUs)
{
DurationCount = 1;
DestinationServiceResource = destinationServiceResource;
Outcome = outcome;
DurationSumUs = durationSumUs;
}

/// <summary>
/// DestinationServiceResource identifies the destination service resource being operated on. e.g. 'http://elastic.co:80',
/// 'elasticsearch', 'rabbitmq/queue_name'.
/// </summary>
[JsonProperty("destination_service_resource")]
public string DestinationServiceResource { get; }

/// <summary>
/// Duration holds duration aggregations about the dropped span.
/// Count holds the number of times the dropped span happened.
/// </summary>
[JsonProperty("duration.count")]
public int DurationCount { get; set; }


/// <summary>
/// Duration holds duration aggregations about the dropped span.
/// Sum holds dimensions about the dropped span's duration.
/// </summary>
[JsonProperty("duration.sum.us")]
public double DurationSumUs { get; set; }

/// <summary>
/// Outcome of the aggregated spans.
/// </summary>
public Outcome Outcome { get; }
}
}
38 changes: 28 additions & 10 deletions src/Elastic.Apm/Model/Span.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ public Span(
[MaxLength]
public string Action { get; set; }

/// <summary>
/// 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.
/// </summary>
[JsonIgnore]
public string ServiceResource { get; set; }

[JsonIgnore]
internal IConfiguration Configuration => _enclosingTransaction.Configuration;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
Expand Down
Loading

0 comments on commit 4c4d2c0

Please sign in to comment.