diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index 042e13a59..e28c786cf 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -498,6 +498,101 @@ Any labels set by the application via the agent's public API will override globa NOTE: This option requires APM Server 7.2 or later. It will have no effect on older versions. + +[float] +[[config-span-compression-enabled]] +==== `SpanCompressionEnabled` (added[1.14]) + +<> + +Setting this option to true will enable span compression feature. Span compression reduces the collection, processing, and storage overhead, and removes clutter from the UI. +The tradeoff is that some information such as DB statements of all the compressed spans will not be collected. + +[options="header"] +|============ +| Environment variable name | IConfiguration key +| `ELASTIC_APM_SPAN_COMPRESSION_ENABLED` | `ElasticApm:SpanCompressionEnabled` +|============ + +[options="header"] +|============ +| Default | Type +| `true` | Boolean +|============ + +[float] +[[config-span-compression-exact-match-max-duration]] +==== `SpanCompressionExactMatchMaxDuration` (added[1.14]) + +<> + +Consecutive spans that are exact match and that are under this threshold will be compressed into a single composite span. +This option does not apply to composite spans. This reduces the collection, processing, and storage overhead, and removes clutter from the UI. +The tradeoff is that the DB statements of all the compressed spans will not be collected. + +[options="header"] +|============ +| Environment variable name | IConfiguration key +| `ELASTIC_APM_SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION` | `ElasticApm:SpanCompressionExactMatchMaxDuration` +|============ + +[options="header"] +|============ +| Default | Type +| `50ms` | TimeDuration +|============ + + +[float] +[[config-span-compression-same-kind-max-duration]] +==== `SpanCompressionSameKindMaxDuration` (added[1.14]) + +<> + +Consecutive spans to the same destination that are under this threshold will be compressed into a single composite span. +This option does not apply to composite spans. +This reduces the collection, processing, and storage overhead, and removes clutter from the UI. The tradeoff is that the DB statements of all the compressed spans will not be collected. + +[options="header"] +|============ +| Environment variable name | IConfiguration key +| `ELASTIC_APM_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION` | `ElasticApm:SpanCompressionSameKindMaxDuration` +|============ + +[options="header"] +|============ +| Default | Type +| `5ms` | TimeDuration +|============ + + + +[float] +[[config-exit-span-min-duration]] +==== `ExitSpanMinDuration` (added[1.14]) + +<> + +Sets the minimum duration of exit spans. Exit spans with a duration lesser than this threshold are attempted to be discarded. +If the exit span is equal or greater the threshold, it should be kept. +In some cases exit spans cannot be discarded. For example, spans that propagate the trace context to downstream services, +such as outgoing HTTP requests, can't be discarded. +However, external calls that don't propagate context, such as calls to a database, can be discarded using this threshold. +Additionally, spans that lead to an error can't be discarded. + +[options="header"] +|============ +| Environment variable name | IConfiguration key +| `ELASTIC_APM_EXIT_SPAN_MIN_DURATION` | `ElasticApm:ExitSpanMinDuration` +|============ + +[options="header"] +|============ +| Default | Type +| `1ms` | TimeDuration +|============ + + [[config-reporter]] === Reporter configuration options @@ -1130,6 +1225,7 @@ If your application is called by an {dotnet5} application that does not have an | <> | No | Core | <> | No | Core | <> | No | Stacktrace +| <> | Yes | Core, Performance | <> | No | Reporter | <> | No | Core | <> | Yes | Messaging, Performance @@ -1146,6 +1242,9 @@ If your application is called by an {dotnet5} application that does not have an | <> | No | Core | <> | No | Core | <> | No | Core +| <> | Yes | Core, Performance +| <> | Yes | Core, Performance +| <> | Yes | Core, Performance | <> | No | Stacktrace, Performance | <> | No | Stacktrace, Performance | <> | No | Core diff --git a/sample/ApiSamples/Program.cs b/sample/ApiSamples/Program.cs index d00b15759..d88610147 100644 --- a/sample/ApiSamples/Program.cs +++ b/sample/ApiSamples/Program.cs @@ -19,6 +19,10 @@ internal class Program { private static void Main(string[] args) { + CompressionSample(); + + Console.ReadKey(); + if (args.Length == 1) //in case it's started with arguments, parse DistributedTracingData from them { var distributedTracingData = DistributedTracingData.TryDeserializeFromString(args[0]); @@ -52,6 +56,37 @@ private static void Main(string[] args) } } + public static void CompressionSample() + { + Environment.SetEnvironmentVariable("ELASTIC_APM_SPAN_COMPRESSION_ENABLED", "true"); + Environment.SetEnvironmentVariable("ELASTIC_APM_SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION", "1s"); + + Agent.Tracer.CaptureTransaction("Foo", "Bar", t => + { + t.CaptureSpan("foo1", "bar", span1 => + { + for (var i = 0; i < 10; i++) + { + span1.CaptureSpan("Select * From Table1", ApiConstants.TypeDb, (s) => + { + s.Context.Db = new Database() { Type = "mssql", Instance = "01" }; + }, ApiConstants.SubtypeMssql, isExitSpan: true); + } + + span1.CaptureSpan("foo2", "randomSpan", () => { }); + + + for (var i = 0; i < 10; i++) + { + span1.CaptureSpan("Select * From Table2", ApiConstants.TypeDb, (s2) => + { + s2.Context.Db = new Database() { Type = "mssql", Instance = "01" }; + }, ApiConstants.SubtypeMssql, isExitSpan: true); + } + }); + }); + } + public static void SampleCustomTransaction() { WriteLineToConsole($"{nameof(SampleCustomTransaction)} started"); @@ -264,9 +299,7 @@ public static void SampleSpanWithCustomContextFillAll() { span.Context.Db = new Database { - Statement = "GET /_all/_search?q=tag:wow", - Type = Database.TypeElasticsearch, - Instance = "MyInstance" + Statement = "GET /_all/_search?q=tag:wow", Type = Database.TypeElasticsearch, Instance = "MyInstance" }; }); }); diff --git a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationFetcher.cs b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationFetcher.cs index 5371abdcd..1ae959404 100644 --- a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationFetcher.cs +++ b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationFetcher.cs @@ -33,7 +33,8 @@ internal class CentralConfigurationFetcher : BackendCommComponentBase, ICentralC private long _dbgIterationsCount; private EntityTagHeaderValue _eTag; - internal CentralConfigurationFetcher(IApmLogger logger, IConfigurationStore configurationStore, ICentralConfigurationResponseParser centralConfigurationResponseParser, + internal CentralConfigurationFetcher(IApmLogger logger, IConfigurationStore configurationStore, + ICentralConfigurationResponseParser centralConfigurationResponseParser, Service service, HttpMessageHandler httpMessageHandler = null, IAgentTimer agentTimer = null, string dbgName = null ) : this(logger, configurationStore, configurationStore.CurrentSnapshot, service, httpMessageHandler, agentTimer, dbgName) => @@ -49,7 +50,8 @@ internal CentralConfigurationFetcher(IApmLogger logger, IConfigurationStore conf /// snapshots) /// when passing isEnabled: initialConfigSnapshot.CentralConfig and config: initialConfigSnapshot to base /// - private CentralConfigurationFetcher(IApmLogger logger, IConfigurationStore configurationStore, IConfiguration initialConfiguration, Service service + private CentralConfigurationFetcher(IApmLogger logger, IConfigurationStore configurationStore, IConfiguration initialConfiguration, + Service service , HttpMessageHandler httpMessageHandler, IAgentTimer agentTimer, string dbgName ) : base( /* isEnabled: */ initialConfiguration.CentralConfig, logger, ThisClassName, service, initialConfiguration, httpMessageHandler) @@ -127,10 +129,11 @@ protected override async Task WorkLoopIteration() else { waitInfo = new WaitInfoS(WaitTimeIfAnyError, "Default wait time is used because exception was thrown" - + " while fetching configuration from APM Server and parsing it."); + + " while fetching configuration from APM Server and parsing it."); } - _logger.IfLevel(level)?.LogException(ex, "Exception was thrown while fetching configuration from APM Server and parsing it." + _logger.IfLevel(level) + ?.LogException(ex, "Exception was thrown while fetching configuration from APM Server and parsing it." + " ETag: `{ETag}'. URL: `{Url}'. Apm Server base URL: `{ApmServerUrl}'. WaitInterval: {WaitInterval}." + " dbgIterationsCount: {dbgIterationsCount}." + Environment.NewLine + "+-> Request:{HttpRequest}" @@ -141,7 +144,9 @@ protected override async Task WorkLoopIteration() , HttpClient.BaseAddress.Sanitize().ToString() , waitInfo.Interval.ToHms(), _dbgIterationsCount - , httpRequest == null ? " N/A" : Environment.NewLine + httpRequest.Sanitize(_configurationStore.CurrentSnapshot.SanitizeFieldNames).ToString().Indent() + , httpRequest == null + ? " N/A" + : Environment.NewLine + httpRequest.Sanitize(_configurationStore.CurrentSnapshot.SanitizeFieldNames).ToString().Indent() , httpResponse == null ? " N/A" : Environment.NewLine + httpResponse.ToString().Indent() , httpResponseBody == null ? "N/A" : httpResponseBody.Length.ToString() , httpResponseBody == null ? " N/A" : Environment.NewLine + httpResponseBody.Indent()); @@ -152,7 +157,8 @@ protected override async Task WorkLoopIteration() httpResponse?.Dispose(); } - _logger.Trace()?.Log("Waiting {WaitInterval}... {WaitReason}. dbgIterationsCount: {dbgIterationsCount}." + _logger.Trace() + ?.Log("Waiting {WaitInterval}... {WaitReason}. dbgIterationsCount: {dbgIterationsCount}." , waitInfo.Interval.ToHms(), waitInfo.Reason, _dbgIterationsCount); await _agentTimer.Delay(_agentTimer.Now + waitInfo.Interval, CancellationTokenSource.Token).ConfigureAwait(false); } @@ -186,11 +192,13 @@ private HttpRequestMessage BuildHttpRequest(EntityTagHeaderValue eTag) private async Task<(HttpResponseMessage, string)> FetchConfigHttpResponseAsync(HttpRequestMessage httpRequest) => await _agentTimer.AwaitOrTimeout(FetchConfigHttpResponseImplAsync(httpRequest) - , _agentTimer.Now + GetConfigHttpRequestTimeout, CancellationTokenSource.Token).ConfigureAwait(false); + , _agentTimer.Now + GetConfigHttpRequestTimeout, CancellationTokenSource.Token) + .ConfigureAwait(false); private void UpdateConfigStore(CentralConfigurationReader centralConfigurationReader) { - _logger.Info()?.Log("Updating " + nameof(ConfigurationStore) + ". New central configuration: {CentralConfiguration}", centralConfigurationReader); + _logger.Info() + ?.Log("Updating " + nameof(ConfigurationStore) + ". New central configuration: {CentralConfiguration}", centralConfigurationReader); _configurationStore.CurrentSnapshot = new WrappingConfiguration(_initialSnapshot, centralConfigurationReader , $"{_initialSnapshot.Description()} + central (ETag: `{centralConfigurationReader.ETag}')"); @@ -252,6 +260,7 @@ internal WrappingConfiguration(IConfiguration wrapped, CentralConfigurationReade public string Environment => _wrapped.Environment; public IReadOnlyCollection ExcludedNamespaces => _wrapped.ExcludedNamespaces; + public double ExitSpanMinDuration => _centralConfiguration.ExitSpanMinDuration ?? _wrapped.ExitSpanMinDuration; public TimeSpan FlushInterval => _wrapped.FlushInterval; @@ -285,12 +294,21 @@ internal WrappingConfiguration(IConfiguration wrapped, CentralConfigurationReade public string ServiceNodeName => _wrapped.ServiceNodeName; public string ServiceVersion => _wrapped.ServiceVersion; + public bool SpanCompressionEnabled => _centralConfiguration.SpanCompressionEnabled ?? _wrapped.SpanCompressionEnabled; + + public double SpanCompressionExactMatchMaxDuration => + _centralConfiguration.SpanCompressionExactMatchMaxDuration ?? _wrapped.SpanCompressionExactMatchMaxDuration; + + public double SpanCompressionSameKindMaxDuration => + _centralConfiguration.SpanCompressionSameKindMaxDuration ?? _wrapped.SpanCompressionSameKindMaxDuration; public double SpanFramesMinDurationInMilliseconds => _centralConfiguration.SpanFramesMinDurationInMilliseconds ?? _wrapped.SpanFramesMinDurationInMilliseconds; public int StackTraceLimit => _centralConfiguration.StackTraceLimit ?? _wrapped.StackTraceLimit; - public IReadOnlyList TransactionIgnoreUrls => _centralConfiguration.TransactionIgnoreUrls ?? _wrapped.TransactionIgnoreUrls; + + public IReadOnlyList TransactionIgnoreUrls => + _centralConfiguration.TransactionIgnoreUrls ?? _wrapped.TransactionIgnoreUrls; public int TransactionMaxSpans => _centralConfiguration.TransactionMaxSpans ?? _wrapped.TransactionMaxSpans; diff --git a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationReader.cs b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationReader.cs index ee3393201..2a58869da 100644 --- a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationReader.cs +++ b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationReader.cs @@ -18,8 +18,9 @@ internal class CentralConfigurationReader : AbstractConfigurationReader private readonly CentralConfigurationResponseParser.CentralConfigPayload _configPayload; - public CentralConfigurationReader(IApmLogger logger, CentralConfigurationResponseParser.CentralConfigPayload configPayload, string eTag) : base(logger, - ThisClassName) + public CentralConfigurationReader(IApmLogger logger, CentralConfigurationResponseParser.CentralConfigPayload configPayload, string eTag) : + base(logger, + ThisClassName) { _configPayload = configPayload; ETag = eTag; @@ -53,6 +54,14 @@ public CentralConfigurationReader(IApmLogger logger, CentralConfigurationRespons internal double? TransactionSampleRate { get; private set; } + internal bool? SpanCompressionEnabled { get; private set; } + + internal double? SpanCompressionExactMatchMaxDuration { get; private set; } + + internal double? SpanCompressionSameKindMaxDuration { get; private set; } + + internal double? ExitSpanMinDuration { get; private set; } + private void UpdateConfigurationValues() { CaptureBody = GetConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.CaptureBodyKey, ParseCaptureBody); @@ -62,12 +71,14 @@ private void UpdateConfigurationValues() ParseTransactionMaxSpans); TransactionSampleRate = GetSimpleConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.TransactionSampleRateKey, ParseTransactionSampleRate); - CaptureHeaders = GetSimpleConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.CaptureHeadersKey, ParseCaptureHeaders); + CaptureHeaders = + GetSimpleConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.CaptureHeadersKey, ParseCaptureHeaders); LogLevel = GetSimpleConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.LogLevelKey, ParseLogLevel); SpanFramesMinDurationInMilliseconds = GetSimpleConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.SpanFramesMinDurationKey, ParseSpanFramesMinDurationInMilliseconds); - StackTraceLimit = GetSimpleConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.StackTraceLimitKey, ParseStackTraceLimit); + StackTraceLimit = GetSimpleConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.StackTraceLimitKey, + ParseStackTraceLimit); Recording = GetSimpleConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.Recording, ParseRecording); SanitizeFieldNames = GetConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.SanitizeFieldNames, ParseSanitizeFieldNamesImpl); @@ -75,6 +86,17 @@ private void UpdateConfigurationValues() GetConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.TransactionIgnoreUrls, ParseTransactionIgnoreUrlsImpl); IgnoreMessageQueues = GetConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.IgnoreMessageQueues, ParseIgnoreMessageQueuesImpl); + SpanCompressionEnabled = + GetSimpleConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.SpanCompressionEnabled, + ParseSpanCompressionEnabled); + SpanCompressionExactMatchMaxDuration = + GetSimpleConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.SpanCompressionExactMatchMaxDuration, + ParseSpanCompressionExactMatchMaxDuration); + SpanCompressionSameKindMaxDuration = + GetSimpleConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.SpanCompressionSameKindMaxDuration, + ParseSpanCompressionSameKindMaxDuration); + ExitSpanMinDuration = + GetSimpleConfigurationValue(CentralConfigurationResponseParser.CentralConfigPayload.ExitSpanMinDuration, ParseExitSpanMinDuration); } private ConfigurationKeyValue BuildKv(string key, string value) => diff --git a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationResponseParser.cs b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationResponseParser.cs index b17bd1e9a..5646e5eff 100644 --- a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationResponseParser.cs +++ b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigurationResponseParser.cs @@ -147,6 +147,10 @@ internal class CentralConfigPayload internal const string TransactionIgnoreUrls = "transaction_ignore_urls"; internal const string TransactionMaxSpansKey = "transaction_max_spans"; internal const string TransactionSampleRateKey = "transaction_sample_rate"; + internal const string SpanCompressionEnabled = "span_compression_enabled"; + internal const string SpanCompressionExactMatchMaxDuration = "span_compression_exact_match_max_duration"; + internal const string SpanCompressionSameKindMaxDuration = "span_compression_same_kind_max_duration"; + internal const string ExitSpanMinDuration = "exit_span_min_duration"; internal static readonly ISet SupportedOptions = new HashSet { @@ -162,6 +166,10 @@ internal class CentralConfigPayload TransactionIgnoreUrls, TransactionMaxSpansKey, TransactionSampleRateKey, + SpanCompressionEnabled, + SpanCompressionExactMatchMaxDuration, + SpanCompressionSameKindMaxDuration, + ExitSpanMinDuration }; private readonly IDictionary _keyValues; diff --git a/src/Elastic.Apm/Config/AbstractConfigurationReader.cs b/src/Elastic.Apm/Config/AbstractConfigurationReader.cs index 613cbeff0..8112f59bc 100644 --- a/src/Elastic.Apm/Config/AbstractConfigurationReader.cs +++ b/src/Elastic.Apm/Config/AbstractConfigurationReader.cs @@ -131,6 +131,41 @@ private IReadOnlyList ParseDisableMetricsImpl(ConfigurationKeyV } } + + protected double ParseExitSpanMinDuration(ConfigurationKeyValue kv) + { + string value; + if (kv == null || string.IsNullOrWhiteSpace(kv.Value)) + value = DefaultValues.ExitSpanMinDuration; + else + value = kv.Value; + + double valueInMilliseconds; + try + { + if (!TryParseTimeInterval(value, out valueInMilliseconds, TimeSuffix.Ms)) + { + _logger?.Error() + ?.Log("Failed to parse provided ParseExitSpanMinDuration `{ProvidedExitSpanMinDuration}' - " + + "using default: {ExitSpanMinDuration}", + value, + DefaultValues.ExitSpanMinDuration); + return DefaultValues.ExitSpanMinDurationInMilliseconds; + } + } + catch (ArgumentException e) + { + _logger?.Critical() + ?.LogException(e, + nameof(ArgumentException) + " thrown from ParseExitSpanMinDuration which means a programming bug - " + + "using default: {ParseExitSpanMinDuration}", + DefaultValues.ExitSpanMinDuration); + return DefaultValues.ExitSpanMinDurationInMilliseconds; + } + + return valueInMilliseconds; + } + protected IReadOnlyList ParseIgnoreMessageQueues(ConfigurationKeyValue kv) => _cachedWildcardMatchersIgnoreMessageQueues.IfNotInited?.InitOrGet(() => ParseIgnoreMessageQueuesImpl(kv)) ?? _cachedWildcardMatchersIgnoreMessageQueues.Value; @@ -195,7 +230,8 @@ protected bool ParseEnableOpenTelemetryBridge(ConfigurationKeyValue kv) if (bool.TryParse(kv.Value, out var isOTelEnabled)) return isOTelEnabled; - _logger?.Warning()?.Log("Failed parsing value for 'EnableOpenTelemetryBridge' setting to 'bool'. Received value: {receivedValue}", kv.Value); + _logger?.Warning() + ?.Log("Failed parsing value for 'EnableOpenTelemetryBridge' setting to 'bool'. Received value: {receivedValue}", kv.Value); return DefaultValues.EnableOpenTelemetryBridge; } @@ -213,6 +249,7 @@ protected bool ParseRecording(ConfigurationKeyValue kv) protected bool ParseTraceContextIgnoreSampledFalse(ConfigurationKeyValue kv) { if (kv == null || string.IsNullOrEmpty(kv.Value)) return DefaultValues.TraceContextIgnoreSampledFalse; + // ReSharper disable once SimplifyConditionalTernaryExpression return bool.TryParse(kv.Value, out var value) ? value : DefaultValues.TraceContextIgnoreSampledFalse; } @@ -220,6 +257,7 @@ protected bool ParseTraceContextIgnoreSampledFalse(ConfigurationKeyValue kv) protected bool ParseVerifyServerCert(ConfigurationKeyValue kv) { if (kv == null || string.IsNullOrEmpty(kv.Value)) return DefaultValues.VerifyServerCert; + // ReSharper disable once SimplifyConditionalTernaryExpression return bool.TryParse(kv.Value, out var value) ? value : DefaultValues.VerifyServerCert; } @@ -273,12 +311,14 @@ private IReadOnlyList ParseServerUrlsImpl(ConfigurationKeyValue kv) switch (kv.Key) { case EnvVarNames.ServerUrls: - _logger?.Info()?.Log( - "{ServerUrls} is deprecated. Use {ServerUrl}", EnvVarNames.ServerUrls, EnvVarNames.ServerUrl); + _logger?.Info() + ?.Log( + "{ServerUrls} is deprecated. Use {ServerUrl}", EnvVarNames.ServerUrls, EnvVarNames.ServerUrl); break; case KeyNames.ServerUrls: - _logger?.Info()?.Log( - "{ServerUrls} is deprecated. Use {ServerUrl}", KeyNames.ServerUrls, KeyNames.ServerUrl); + _logger?.Info() + ?.Log( + "{ServerUrls} is deprecated. Use {ServerUrl}", KeyNames.ServerUrls, KeyNames.ServerUrl); break; } @@ -362,7 +402,9 @@ protected double ParseMetricsInterval(ConfigurationKeyValue kv) if (valueInMilliseconds < Constraints.MinMetricsIntervalInMilliseconds) { _logger?.Error() - ?.Log("Provided metrics interval `{ProvidedMetricsInterval}' is smaller than allowed minimum: {MinProvidedMetricsInterval}ms - " + + ?.Log( + "Provided metrics interval `{ProvidedMetricsInterval}' is smaller than allowed minimum: {MinProvidedMetricsInterval}ms - " + + "metrics collection will be disabled", value, Constraints.MinMetricsIntervalInMilliseconds); @@ -406,11 +448,97 @@ internal IReadOnlyList ParseTransactionIgnoreUrlsImpl(Configura } catch (Exception e) { - _logger?.Error()?.LogException(e, "Failed parsing TransactionIgnoreUrls, values in the config: {TransactionIgnoreUrlsValues}", kv.Value); + _logger?.Error() + ?.LogException(e, "Failed parsing TransactionIgnoreUrls, values in the config: {TransactionIgnoreUrlsValues}", kv.Value); return DefaultValues.TransactionIgnoreUrls; } } + protected bool ParseSpanCompressionEnabled(ConfigurationKeyValue kv) + { + if (kv == null || string.IsNullOrEmpty(kv.Value)) return DefaultValues.SpanCompressionEnabled; + + if (bool.TryParse(kv.Value, out var isSpanCompressionEnable)) + return isSpanCompressionEnable; + + _logger?.Warning() + ?.Log( + "Failed parsing value for 'ParseSpanCompressionEnabled' setting to 'bool'. Received value: {receivedValue} - using default {defaultVal}", + kv.Value, DefaultValues.SpanCompressionEnabled); + return DefaultValues.SpanCompressionEnabled; + } + + protected double ParseSpanCompressionExactMatchMaxDuration(ConfigurationKeyValue kv) + { + string value; + if (kv == null || string.IsNullOrWhiteSpace(kv.Value)) + value = DefaultValues.SpanCompressionExactMatchMaxDuration; + else + value = kv.Value; + + double valueInMilliseconds; + try + { + if (!TryParseTimeInterval(value, out valueInMilliseconds, TimeSuffix.Ms)) + { + _logger?.Error() + ?.Log("Failed to parse provided SpanCompressionExactMatchMaxDuration `{ProvidedSpanCompressionExactMatchMaxDuration}' - " + + + "using default: {DefaultSpanCompressionExactMatchMaxDuration}", + value, + DefaultValues.SpanCompressionExactMatchMaxDuration); + return DefaultValues.SpanCompressionExactMatchMaxDurationInMilliseconds; + } + } + catch (ArgumentException e) + { + _logger?.Critical() + ?.LogException(e, + nameof(ArgumentException) + " thrown from ParseSpanCompressionExactMatchMaxDuration which means a programming bug - " + + "using default: {SpanCompressionExactMatchMaxDuration}", + DefaultValues.SpanCompressionExactMatchMaxDurationInMilliseconds); + return DefaultValues.SpanCompressionExactMatchMaxDurationInMilliseconds; + } + + return valueInMilliseconds; + } + + protected double ParseSpanCompressionSameKindMaxDuration(ConfigurationKeyValue kv) + { + string value; + if (kv == null || string.IsNullOrWhiteSpace(kv.Value)) + value = DefaultValues.SpanCompressionSameKindMaxDuration; + else + value = kv.Value; + + double valueInMilliseconds; + + try + { + if (!TryParseTimeInterval(value, out valueInMilliseconds, TimeSuffix.Ms)) + { + _logger?.Error() + ?.Log("Failed to parse provided SpanCompressionSameKindMaxDuration `{ProvidedSpanCompressionSameKindMaxDuration}' - " + + "using default: {DefaultSpanCompressionSameKindMaxDuration}", + value, + DefaultValues.SpanCompressionSameKindMaxDuration); + return DefaultValues.SpanCompressionSameKindMaxDurationInMilliseconds; + } + } + catch (ArgumentException e) + { + _logger?.Critical() + ?.LogException(e, + nameof(ArgumentException) + " thrown from ParseSpanCompressionSameKindMaxDuration which means a programming bug - " + + "using default: {SpanCompressionSameKindMaxDuration}", + DefaultValues.SpanCompressionSameKindMaxDurationInMilliseconds); + return DefaultValues.SpanCompressionSameKindMaxDurationInMilliseconds; + } + + return valueInMilliseconds; + } + + protected double ParseSpanFramesMinDurationInMilliseconds(ConfigurationKeyValue kv) { string value; @@ -459,7 +587,8 @@ private int ParseMaxXyzEventCount(ConfigurationKeyValue kv, int defaultValue, st { _logger?.Error() ?.Log( - "Failed to parse provided " + dbgOptionName + ": `{Provided" + dbgOptionName + "}' - using default: {Default" + dbgOptionName + "Failed to parse provided " + dbgOptionName + ": `{Provided" + dbgOptionName + "}' - using default: {Default" + + dbgOptionName + "}", kv.Value, defaultValue); @@ -480,15 +609,18 @@ private int ParseMaxXyzEventCount(ConfigurationKeyValue kv, int defaultValue, st } protected int ParseMaxBatchEventCount(ConfigurationKeyValue kv) => - _cachedMaxBatchEventCount.IfNotInited?.InitOrGet(() => ParseMaxXyzEventCount(kv, DefaultValues.MaxBatchEventCount, "MaxBatchEventCount")) + _cachedMaxBatchEventCount.IfNotInited?.InitOrGet(() => + ParseMaxXyzEventCount(kv, DefaultValues.MaxBatchEventCount, "MaxBatchEventCount")) ?? _cachedMaxBatchEventCount.Value; protected int ParseMaxQueueEventCount(ConfigurationKeyValue kv) => - _cachedMaxQueueEventCount.IfNotInited?.InitOrGet(() => ParseMaxXyzEventCount(kv, DefaultValues.MaxQueueEventCount, "MaxQueueEventCount")) + _cachedMaxQueueEventCount.IfNotInited?.InitOrGet(() => + ParseMaxXyzEventCount(kv, DefaultValues.MaxQueueEventCount, "MaxQueueEventCount")) ?? _cachedMaxQueueEventCount.Value; protected TimeSpan ParseFlushInterval(ConfigurationKeyValue kv) => - ParsePositiveOrZeroTimeIntervalInMillisecondsImpl(kv, TimeSuffix.S, TimeSpan.FromMilliseconds(DefaultValues.FlushIntervalInMilliseconds), + ParsePositiveOrZeroTimeIntervalInMillisecondsImpl(kv, TimeSuffix.S, + TimeSpan.FromMilliseconds(DefaultValues.FlushIntervalInMilliseconds), "FlushInterval"); private TimeSpan ParsePositiveOrZeroTimeIntervalInMillisecondsImpl(ConfigurationKeyValue kv, TimeSuffix defaultSuffix, @@ -900,6 +1032,7 @@ private bool ParseBoolOption(ConfigurationKeyValue kv, bool defaultValue, string protected string ParseHostName(ConfigurationKeyValue kv) { if (kv == null || string.IsNullOrEmpty(kv.Value)) return null; + return kv.Value; } @@ -1024,6 +1157,7 @@ private static bool TryParseUri(string u, out Uri uri) // https://stackoverflow.com/a/33573337 uri = null; if (!Uri.TryCreate(u, UriKind.Absolute, out uri)) return false; + return uri.IsWellFormedOriginalString() && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps); } } diff --git a/src/Elastic.Apm/Config/AbstractConfigurationWithEnvFallbackReader.cs b/src/Elastic.Apm/Config/AbstractConfigurationWithEnvFallbackReader.cs index aae991895..f78149b2b 100644 --- a/src/Elastic.Apm/Config/AbstractConfigurationWithEnvFallbackReader.cs +++ b/src/Elastic.Apm/Config/AbstractConfigurationWithEnvFallbackReader.cs @@ -62,6 +62,8 @@ internal AbstractConfigurationWithEnvFallbackReader(IApmLogger logger, string de public IReadOnlyCollection ExcludedNamespaces => ParseExcludedNamespaces(Read(KeyNames.ExcludedNamespaces, EnvVarNames.ExcludedNamespaces)); + public double ExitSpanMinDuration => ParseExitSpanMinDuration(Read(KeyNames.ExitSpanMinDuration, EnvVarNames.ExitSpanMinDuration)); + public virtual TimeSpan FlushInterval => ParseFlushInterval(Read(KeyNames.FlushInterval, EnvVarNames.FlushInterval)); @@ -131,6 +133,12 @@ public virtual IReadOnlyList ServerUrls public virtual string ServiceVersion => ParseServiceVersion(Read(KeyNames.ServiceVersion, EnvVarNames.ServiceVersion)); + public bool SpanCompressionEnabled => ParseSpanCompressionEnabled(Read(KeyNames.SpanCompressionEnabled, EnvVarNames.SpanCompressionEnabled)); + + public double SpanCompressionExactMatchMaxDuration => ParseSpanCompressionExactMatchMaxDuration(Read(KeyNames.SpanCompressionExactMatchMaxDuration, EnvVarNames.SpanCompressionExactMatchMaxDuration)); + + public double SpanCompressionSameKindMaxDuration => ParseSpanCompressionSameKindMaxDuration(Read(KeyNames.SpanCompressionSameKindMaxDuration, EnvVarNames.SpanCompressionSameKindMaxDuration)); + public virtual double SpanFramesMinDurationInMilliseconds => _spanFramesMinDurationInMilliseconds.Value; public virtual int StackTraceLimit => _stackTraceLimit.Value; diff --git a/src/Elastic.Apm/Config/ConfigConsts.cs b/src/Elastic.Apm/Config/ConfigConsts.cs index 8969bc6cf..de9d2e3d7 100644 --- a/src/Elastic.Apm/Config/ConfigConsts.cs +++ b/src/Elastic.Apm/Config/ConfigConsts.cs @@ -1,4 +1,4 @@ -// Licensed to Elasticsearch B.V under one or more agreements. +// 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 @@ -26,12 +26,19 @@ public static class DefaultValues public const bool CentralConfig = true; public const string CloudProvider = SupportedValues.CloudProviderAuto; public const bool EnableOpenTelemetryBridge = false; + public const string ExitSpanMinDuration = "1ms"; + public const int ExitSpanMinDurationInMilliseconds = 1000; public const int FlushIntervalInMilliseconds = 10_000; // 10 seconds public const LogLevel LogLevel = Logging.LogLevel.Error; public const int MaxBatchEventCount = 10; public const int MaxQueueEventCount = 1000; public const string MetricsInterval = "30s"; public const double MetricsIntervalInMilliseconds = 30 * 1000; + public const bool SpanCompressionEnabled = false; + public const string SpanCompressionExactMatchMaxDuration = "50ms"; + public const double SpanCompressionExactMatchMaxDurationInMilliseconds = 50; + public const string SpanCompressionSameKindMaxDuration = "5ms"; + public const double SpanCompressionSameKindMaxDurationInMilliseconds = 5; public const string SpanFramesMinDuration = "5ms"; public const double SpanFramesMinDurationInMilliseconds = 5; public const int StackTraceLimit = 50; @@ -69,39 +76,39 @@ static DefaultValues() { SanitizeFieldNames = new List(); foreach (var item in new List - { - "password", - "passwd", - "pwd", - "secret", - "*key", - "*token*", - "*session*", - "*credit*", - "*card*", - "authorization", - "set-cookie" - }) + { + "password", + "passwd", + "pwd", + "secret", + "*key", + "*token*", + "*session*", + "*credit*", + "*card*", + "authorization", + "set-cookie" + }) SanitizeFieldNames.Add(WildcardMatcher.ValueOf(item)); TransactionIgnoreUrls = new List(); foreach (var item in new List - { - "/VAADIN/*", - "/heartbeat*", - "/favicon.ico", - "*.js", - "*.css", - "*.jpg", - "*.jpeg", - "*.png", - "*.gif", - "*.webp", - "*.svg", - "*.woff", - "*.woff2" - }) + { + "/VAADIN/*", + "/heartbeat*", + "/favicon.ico", + "*.js", + "*.css", + "*.jpg", + "*.jpeg", + "*.png", + "*.gif", + "*.webp", + "*.svg", + "*.woff", + "*.woff2" + }) TransactionIgnoreUrls.Add(WildcardMatcher.ValueOf(item)); } @@ -123,6 +130,7 @@ public static class EnvVarNames public const string EnableOpenTelemetryBridge = "ENABLEOPENTELEMETRYBRIDGE"; public const string Environment = Prefix + "ENVIRONMENT"; public const string ExcludedNamespaces = Prefix + "EXCLUDED_NAMESPACES"; + public const string ExitSpanMinDuration = Prefix + "EXIT_SPAN_MIN_DURATION"; public const string FlushInterval = Prefix + "FLUSH_INTERVAL"; //This setting is Full Framework only: @@ -143,6 +151,9 @@ public static class EnvVarNames public const string ServiceName = Prefix + "SERVICE_NAME"; public const string ServiceNodeName = Prefix + "SERVICE_NODE_NAME"; public const string ServiceVersion = Prefix + "SERVICE_VERSION"; + public const string SpanCompressionEnabled = Prefix + "SPAN_COMPRESSION_ENABLED"; + public const string SpanCompressionExactMatchMaxDuration = Prefix + "SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION"; + public const string SpanCompressionSameKindMaxDuration = Prefix + "SPAN_COMPRESSION_SAME_KIND_MAX_DURATION"; public const string SpanFramesMinDuration = Prefix + "SPAN_FRAMES_MIN_DURATION"; public const string StackTraceLimit = Prefix + "STACK_TRACE_LIMIT"; public const string TraceContextIgnoreSampledFalse = Prefix + "TRACE_CONTEXT_IGNORE_SAMPLED_FALSE"; @@ -168,7 +179,9 @@ public static class KeyNames public const string Environment = Prefix + nameof(Environment); public const string EnableOpenTelemetryBridge = Prefix + nameof(EnableOpenTelemetryBridge); public const string ExcludedNamespaces = Prefix + nameof(ExcludedNamespaces); + public const string ExitSpanMinDuration = Prefix + nameof(ExitSpanMinDuration); public const string FlushInterval = Prefix + nameof(FlushInterval); + //This setting is Full Framework only: public const string FullFrameworkConfigurationReaderType = Prefix + nameof(FullFrameworkConfigurationReaderType); public const string GlobalLabels = Prefix + nameof(GlobalLabels); @@ -187,6 +200,9 @@ public static class KeyNames public const string ServiceName = Prefix + nameof(ServiceName); public const string ServiceNodeName = Prefix + nameof(ServiceNodeName); public const string ServiceVersion = Prefix + nameof(ServiceVersion); + public const string SpanCompressionEnabled = Prefix + nameof(SpanCompressionEnabled); + public const string SpanCompressionExactMatchMaxDuration = Prefix + nameof(SpanCompressionExactMatchMaxDuration); + public const string SpanCompressionSameKindMaxDuration = Prefix + nameof(SpanCompressionSameKindMaxDuration); public const string SpanFramesMinDuration = Prefix + nameof(SpanFramesMinDuration); public const string TraceContextIgnoreSampledFalse = Prefix + nameof(TraceContextIgnoreSampledFalse); public const string StackTraceLimit = Prefix + nameof(StackTraceLimit); @@ -215,7 +231,11 @@ public static class SupportedValues public static readonly HashSet CloudProviders = new HashSet(StringComparer.OrdinalIgnoreCase) { - CloudProviderAuto, CloudProviderAws, CloudProviderAzure, CloudProviderGcp, CloudProviderNone + CloudProviderAuto, + CloudProviderAws, + CloudProviderAzure, + CloudProviderGcp, + CloudProviderNone }; } } diff --git a/src/Elastic.Apm/Config/ConfigurationSnapshotFromReader.cs b/src/Elastic.Apm/Config/ConfigurationSnapshotFromReader.cs index 3337abd31..40ce0e6ef 100644 --- a/src/Elastic.Apm/Config/ConfigurationSnapshotFromReader.cs +++ b/src/Elastic.Apm/Config/ConfigurationSnapshotFromReader.cs @@ -33,6 +33,7 @@ internal ConfigurationSnapshotFromReader(IConfigurationReader content, string de public bool Enabled => _content.Enabled; public string Environment => _content.Environment; public IReadOnlyCollection ExcludedNamespaces => _content.ExcludedNamespaces; + public double ExitSpanMinDuration => _content.ExitSpanMinDuration; public TimeSpan FlushInterval => _content.FlushInterval; public IReadOnlyDictionary GlobalLabels => _content.GlobalLabels; public string HostName => _content.HostName; @@ -53,6 +54,9 @@ internal ConfigurationSnapshotFromReader(IConfigurationReader content, string de public string ServiceName => _content.ServiceName; public string ServiceNodeName => _content.ServiceNodeName; public string ServiceVersion => _content.ServiceVersion; + public bool SpanCompressionEnabled => _content.SpanCompressionEnabled; + public double SpanCompressionExactMatchMaxDuration => _content.SpanCompressionExactMatchMaxDuration; + public double SpanCompressionSameKindMaxDuration => _content.SpanCompressionSameKindMaxDuration; public double SpanFramesMinDurationInMilliseconds => _content.SpanFramesMinDurationInMilliseconds; public int StackTraceLimit => _content.StackTraceLimit; diff --git a/src/Elastic.Apm/Config/EnvironmentConfigurationReader.cs b/src/Elastic.Apm/Config/EnvironmentConfigurationReader.cs index e1c5de15d..e0e3e84e7 100644 --- a/src/Elastic.Apm/Config/EnvironmentConfigurationReader.cs +++ b/src/Elastic.Apm/Config/EnvironmentConfigurationReader.cs @@ -48,6 +48,7 @@ public EnvironmentConfigurationReader(IApmLogger logger = null) : base(logger, T public string Environment => ParseEnvironment(Read(ConfigConsts.EnvVarNames.Environment)); public IReadOnlyCollection ExcludedNamespaces => ParseExcludedNamespaces(Read(ConfigConsts.EnvVarNames.ExcludedNamespaces)); + public double ExitSpanMinDuration => ParseExitSpanMinDuration(Read(ConfigConsts.EnvVarNames.ExitSpanMinDuration)); public TimeSpan FlushInterval => ParseFlushInterval(Read(ConfigConsts.EnvVarNames.FlushInterval)); @@ -107,6 +108,9 @@ public Uri ServerUrl public string ServiceNodeName => ParseServiceNodeName(Read(ConfigConsts.EnvVarNames.ServiceNodeName)); public string ServiceVersion => ParseServiceVersion(Read(ConfigConsts.EnvVarNames.ServiceVersion)); + public bool SpanCompressionEnabled => ParseSpanCompressionEnabled(Read(ConfigConsts.EnvVarNames.SpanCompressionEnabled)); + public double SpanCompressionExactMatchMaxDuration => ParseSpanCompressionExactMatchMaxDuration(Read(ConfigConsts.EnvVarNames.SpanCompressionExactMatchMaxDuration)); + public double SpanCompressionSameKindMaxDuration => ParseSpanCompressionSameKindMaxDuration(Read(ConfigConsts.EnvVarNames.SpanCompressionSameKindMaxDuration)); public double SpanFramesMinDurationInMilliseconds => _spanFramesMinDurationInMilliseconds.Value; diff --git a/src/Elastic.Apm/Config/IConfigurationReader.cs b/src/Elastic.Apm/Config/IConfigurationReader.cs index f6c1e6d80..b4ffcf554 100644 --- a/src/Elastic.Apm/Config/IConfigurationReader.cs +++ b/src/Elastic.Apm/Config/IConfigurationReader.cs @@ -88,6 +88,16 @@ public interface IConfigurationReader /// IReadOnlyCollection ExcludedNamespaces { get; } + /// + /// Sets the minimum duration of exit spans. Exit spans with a duration lesser than this threshold are attempted to be discarded. + /// If the exit span is equal or greater the threshold, it should be kept. + /// In some cases exit spans cannot be discarded. For example, spans that propagate the trace context to downstream services, + /// such as outgoing HTTP requests, can't be discarded. + /// However, external calls that don't propagate context, such as calls to a database, can be discarded using this threshold. + /// Additionally, spans that lead to an error can't be discarded. + /// + double ExitSpanMinDuration { get; } + /// /// The maximal amount of time (in seconds) events are held in queue until there is enough to send a batch. /// It's possible for a batch to contain less then events @@ -244,6 +254,29 @@ public interface IConfigurationReader /// string ServiceVersion { get; } + /// + /// Setting this option to true will enable span compression feature. + /// Span compression reduces the collection, processing, and storage overhead, and removes clutter from the UI. + /// The tradeoff is that some information such as DB statements of all the compressed spans will not be collected. + /// + bool SpanCompressionEnabled { get; } + + /// + /// Consecutive spans that are exact match and that are under this threshold will be compressed into a single composite span. + /// This option does not apply to composite spans. This reduces the collection, processing, and storage overhead, and removes clutter + /// from the UI. + /// The tradeoff is that the DB statements of all the compressed spans will not be collected. + /// + double SpanCompressionExactMatchMaxDuration { get; } + + /// + /// Consecutive spans to the same destination that are under this threshold will be compressed into a single composite span. + /// This option does not apply to composite spans. + /// This reduces the collection, processing, and storage overhead, and removes clutter from the UI. The tradeoff is that the DB statements of + /// all the compressed spans will not be collected. + /// + double SpanCompressionSameKindMaxDuration { get; } + /// /// The agent limits stack trace collection to spans with durations equal or longer than the given value /// 0: Disables stack trace collection for spans completely diff --git a/src/Elastic.Apm/Model/Span.cs b/src/Elastic.Apm/Model/Span.cs index d6f6e3455..86082a85b 100644 --- a/src/Elastic.Apm/Model/Span.cs +++ b/src/Elastic.Apm/Model/Span.cs @@ -33,6 +33,15 @@ internal class Span : ISpan private readonly Span _parentSpan; private readonly IPayloadSender _payloadSender; + private Span _compressionBuffer; + + // Indicates if the context was already propagated outside the span + // This typically means that this span was already used for distributed tracing and potentially there is a span outside of the process + // which points to this span. + private bool _hasPropagatedContext; + + private bool Discardable => IsExitSpan && !_hasPropagatedContext && Outcome == Outcome.Success; + [JsonConstructor] // ReSharper disable once UnusedMember.Local - this is meant for deserialization private Span(double duration, string id, string name, string parentId) @@ -194,13 +203,20 @@ public Outcome Outcome } [JsonIgnore] - public DistributedTracingData OutgoingDistributedTracingData => new( - TraceId, - // When transaction is not sampled then outgoing distributed tracing data should have transaction ID for parent-id part - // and not span ID as it does for sampled case. - ShouldBeSentToApmServer ? Id : TransactionId, - IsSampled, - _enclosingTransaction._traceState); + public DistributedTracingData OutgoingDistributedTracingData + { + get + { + _hasPropagatedContext = true; + return new( + TraceId, + // When transaction is not sampled then outgoing distributed tracing data should have transaction ID for parent-id part + // and not span ID as it does for sampled case. + ShouldBeSentToApmServer ? Id : TransactionId, + IsSampled, + _enclosingTransaction._traceState); + } + } [MaxLength] [JsonProperty("parent_id")] @@ -213,6 +229,8 @@ public Outcome Outcome /// internal StackTrace RawStackTrace; + public Composite Composite { get; set; } + /// /// Captures the sample rate of the agent when this span was created. /// @@ -282,7 +300,7 @@ public bool TryGetLabel(string key, out T value) public OTel Otel { get; set; } - public ISpan StartSpan(string name, string type, string subType = null, string action = null, bool isExitSpan = false) + public ISpan StartSpan(string name, string type, string subType = null, string action = null, bool isExitSpan = false) { if (Configuration.Enabled && Configuration.Recording) return StartSpanInternal(name, type, subType, action, isExitSpan: isExitSpan); @@ -291,12 +309,13 @@ public ISpan StartSpan(string name, string type, string subType = null, string a } 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, + InstrumentationFlag instrumentationFlag = InstrumentationFlag.None, bool captureStackTraceOnStart = false, long? timestamp = null, + string id = null, bool isExitSpan = false ) { var retVal = new Span(name, type, Id, TraceId, _enclosingTransaction, _payloadSender, _logger, _currentExecutionSegmentsContainer, - _apmServerInfo, this, instrumentationFlag, captureStackTraceOnStart, timestamp, isExitSpan, id); + _apmServerInfo, this, instrumentationFlag, captureStackTraceOnStart, timestamp, isExitSpan, id); if (!string.IsNullOrEmpty(subType)) retVal.Subtype = subType; @@ -379,8 +398,9 @@ public void End() _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 (_isDropped && (!string.IsNullOrEmpty(ServiceResource) + || (_context.IsValueCreated && Context?.Destination?.Service?.Resource != null))) + _enclosingTransaction.UpdateDroppedSpanStats(ServiceResource ?? Context?.Destination?.Service?.Resource, _outcome, Duration!.Value); if (ShouldBeSentToApmServer && isFirstEndCall) { @@ -391,13 +411,160 @@ public void End() || Configuration.SpanFramesMinDurationInMilliseconds < 0)) RawStackTrace = new StackTrace(true); - _payloadSender.QueueSpan(this); + var buffered = _parentSpan?._compressionBuffer ?? _enclosingTransaction.CompressionBuffer; + + if (Configuration.SpanCompressionEnabled) + { + if (!IsCompressionEligible()) + { + if (buffered != null) + { + QueueSpan(buffered); + if (_parentSpan != null) + _parentSpan._compressionBuffer = null; + _enclosingTransaction.CompressionBuffer = null; + } + + //If this is a span which has buffered children, we send the composite. + if (_compressionBuffer != null) + QueueSpan(_compressionBuffer); + + QueueSpan(this); + if (isFirstEndCall) + _currentExecutionSegmentsContainer.CurrentSpan = _parentSpan; + return; + } + if (buffered == null) + { + SetThisToParentsBuffer(); + if (isFirstEndCall) + _currentExecutionSegmentsContainer.CurrentSpan = _parentSpan; + return; + } + + if (!buffered.TryToCompress(this)) + { + QueueSpan(buffered); + SetThisToParentsBuffer(); + + if (isFirstEndCall) + _currentExecutionSegmentsContainer.CurrentSpan = _parentSpan; + } + } + else + QueueSpan(this); } if (isFirstEndCall) _currentExecutionSegmentsContainer.CurrentSpan = _parentSpan; + + void QueueSpan(Span span) + { + if (span.Composite != null) + { + var endTimestamp = TimeUtils.TimestampNow(); + span.Duration = TimeUtils.DurationBetweenTimestamps(span.Timestamp, endTimestamp); + } + + if (span.Discardable) + { + if (span.Composite != null && span.Duration < span.Configuration.ExitSpanMinDuration) + { + _enclosingTransaction.UpdateDroppedSpanStats(ServiceResource ?? Context?.Destination?.Service?.Resource, _outcome, + Duration!.Value); + _logger.Trace()?.Log("Dropping fast exit span on composite span. Composite duration: {duration}", Composite.Sum); + return; + } + if (span.Duration < span.Configuration.ExitSpanMinDuration) + { + _enclosingTransaction.UpdateDroppedSpanStats(ServiceResource ?? Context?.Destination?.Service?.Resource, _outcome, + Duration!.Value); + _logger.Trace()?.Log("Dropping fast exit span. Duration: {duration}", Duration); + return; + } + } + + _payloadSender.QueueSpan(span); + } + } + + private bool TryToCompress(Span sibling) + { + var isAlreadyComposite = Composite != null; + var canBeCompressed = isAlreadyComposite ? TryToCompressComposite(sibling) : TryToCompressRegular(sibling); + if (!canBeCompressed) + return false; + + + if (!isAlreadyComposite) + { + Composite.Count = 1; + Composite.Sum = Duration!.Value; + } + + Composite.Count++; + Composite.Sum += sibling.Duration!.Value; + return true; + } + + private bool IsSameKind(Span other) => Type == other.Type + && Subtype == other.Subtype + && _context.IsValueCreated && other._context.IsValueCreated + && Context.Destination.Service.Resource == other.Context.Destination.Service.Resource; + + private bool TryToCompressRegular(Span sibling) + { + if (!IsSameKind(sibling)) return false; + + Composite = new Composite(); + + if (Name == sibling.Name) + { + if (Duration <= Configuration.SpanCompressionExactMatchMaxDuration + && sibling.Duration <= Configuration.SpanCompressionExactMatchMaxDuration) + { + Composite.CompressionStrategy = "exact_match"; + return true; + } + + return false; + } + + if (Duration <= Configuration.SpanCompressionSameKindMaxDuration && sibling.Duration <= Configuration.SpanCompressionSameKindMaxDuration) + { + Composite.CompressionStrategy = "same_kind"; + if(_context.IsValueCreated) + Name = "Calls to " + Context.Destination.Service.Resource; + return true; + } + + return false; + } + + private bool TryToCompressComposite(Span sibling) + { + switch (Composite.CompressionStrategy) + { + case "exact_match": + return IsSameKind(sibling) && Name == sibling.Name && sibling.Duration <= Configuration.SpanCompressionExactMatchMaxDuration; + + case "same_kind": + return IsSameKind(sibling) && sibling.Duration <= Configuration.SpanCompressionSameKindMaxDuration; + } + + return false; } + private void SetThisToParentsBuffer() + { + if (_parentSpan != null) + _parentSpan._compressionBuffer = this; + else + _enclosingTransaction.CompressionBuffer = this; + } + + public bool IsCompressionEligible() => IsExitSpan && !_hasPropagatedContext && Outcome is Outcome.Success or Outcome.Unknown; + public void CaptureException(Exception exception, string culprit = null, bool isHandled = false, string parentId = null, Dictionary labels = null ) @@ -468,7 +635,7 @@ private void DeduceDestination() if (!_context.IsValueCreated) { - if(string.IsNullOrEmpty(ServiceResource)) + if (string.IsNullOrEmpty(ServiceResource)) ServiceResource = !string.IsNullOrEmpty(Subtype) ? Subtype : Type; return; } @@ -602,6 +769,28 @@ public void IncrementTimer(double duration) } } + /// + /// Composite holds details on a group of spans represented by a single one. + /// + internal class Composite + { + /// + /// A string value indicating which compression strategy was used. The valid values are `exact_match` and `same_kind` + /// + [JsonProperty("compression_strategy")] + public string CompressionStrategy { get; set; } + + /// + /// Count is the number of compressed spans the composite span represents. The minimum count is 2, as a composite span represents at least two spans. + /// + public int Count { get; set; } + + /// + /// Sum is the durations of all compressed spans this composite span represents in milliseconds. + /// + public double Sum { get; set; } + } + internal class ChildDurationTimer { private int _activeChildren; diff --git a/src/Elastic.Apm/Model/Transaction.cs b/src/Elastic.Apm/Model/Transaction.cs index 93512bd2e..46cbcfcda 100644 --- a/src/Elastic.Apm/Model/Transaction.cs +++ b/src/Elastic.Apm/Model/Transaction.cs @@ -46,6 +46,8 @@ internal class Transaction : ITransaction private readonly IApmLogger _logger; private readonly IPayloadSender _sender; + internal Span CompressionBuffer; + [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, @@ -595,6 +597,12 @@ public void End() handler?.Invoke(this, EventArgs.Empty); Ended = null; + if (CompressionBuffer != null) + { + _sender.QueueSpan(CompressionBuffer); + CompressionBuffer = null; + } + _sender.QueueTransaction(this); _currentExecutionSegmentsContainer.CurrentTransaction = null; } diff --git a/test/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs b/test/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs index 025c93231..19a5eed53 100644 --- a/test/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs +++ b/test/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs @@ -50,7 +50,7 @@ public AspNetCoreBasicTests(WebApplicationFactory factory, ITestOutputH _capturedPayload = new MockPayloadSender(); _agent = new ApmAgent(new TestAgentComponents( _logger, - new MockConfiguration(_logger, captureBody: ConfigConsts.SupportedValues.CaptureBodyAll), + new MockConfiguration(_logger, captureBody: ConfigConsts.SupportedValues.CaptureBodyAll, exitSpanMinDuration: "0"), _capturedPayload, // _agent needs to share CurrentExecutionSegmentsContainer with Agent.Instance // because the sample application used by the tests (SampleAspNetCoreApp) uses Agent.Instance.Tracer.CurrentTransaction/CurrentSpan @@ -163,7 +163,7 @@ public async Task HomeIndexTransactionWithEnabledFalse(bool withDiagnosticSource { _agent = new ApmAgent(new TestAgentComponents( _logger, - new MockConfiguration(_logger, enabled: "false"), _capturedPayload)); + new MockConfiguration(_logger, enabled: "false", exitSpanMinDuration:"0"), _capturedPayload)); _client = Helper.ConfigureHttpClient(true, withDiagnosticSourceOnly, _agent, _factory); @@ -183,7 +183,7 @@ public async Task HomeIndexTransactionWithEnabledFalse(bool withDiagnosticSource public async Task HomeIndexTransactionWithToggleRecording(bool withDiagnosticSourceOnly) { _agent = new ApmAgent(new TestAgentComponents( - _logger, new MockConfiguration(recording: "false"), _capturedPayload)); + _logger, new MockConfiguration(recording: "false", exitSpanMinDuration: "0"), _capturedPayload)); _client = Helper.ConfigureHttpClient(true, withDiagnosticSourceOnly, _agent, _factory); diff --git a/test/Elastic.Apm.AspNetCore.Tests/AspNetCoreDiagnosticListenerTest.cs b/test/Elastic.Apm.AspNetCore.Tests/AspNetCoreDiagnosticListenerTest.cs index cbeed2b9d..f1a334bdb 100644 --- a/test/Elastic.Apm.AspNetCore.Tests/AspNetCoreDiagnosticListenerTest.cs +++ b/test/Elastic.Apm.AspNetCore.Tests/AspNetCoreDiagnosticListenerTest.cs @@ -39,7 +39,7 @@ public class AspNetCoreDiagnosticListenerTest : IClassFixture { - _agent = new ApmAgent(new TestAgentComponents(payloadSender: _payloadSender)); + _agent = new ApmAgent(new TestAgentComponents(payloadSender: _payloadSender, configuration: new MockConfiguration(exitSpanMinDuration:"0"))); app.UseElasticApm(_agent, _agent.Logger); Startup.ConfigureAllExceptAgent(app); }) diff --git a/test/Elastic.Apm.AspNetCore.Tests/DiagnosticListenerTests.cs b/test/Elastic.Apm.AspNetCore.Tests/DiagnosticListenerTests.cs index 7821bf4c9..f84e5c07b 100644 --- a/test/Elastic.Apm.AspNetCore.Tests/DiagnosticListenerTests.cs +++ b/test/Elastic.Apm.AspNetCore.Tests/DiagnosticListenerTests.cs @@ -28,7 +28,7 @@ public class DiagnosticListenerTests : IClassFixture factory) { - _agent = new ApmAgent(new TestAgentComponents()); + _agent = new ApmAgent(new TestAgentComponents(configuration: new MockConfiguration(exitSpanMinDuration:"0"))); _capturedPayload = _agent.PayloadSender as MockPayloadSender; //This registers the middleware without activating any listeners, diff --git a/test/Elastic.Apm.AspNetCore.Tests/DistributedTracingAspNetCoreTests.cs b/test/Elastic.Apm.AspNetCore.Tests/DistributedTracingAspNetCoreTests.cs index 568352402..0c640450f 100644 --- a/test/Elastic.Apm.AspNetCore.Tests/DistributedTracingAspNetCoreTests.cs +++ b/test/Elastic.Apm.AspNetCore.Tests/DistributedTracingAspNetCoreTests.cs @@ -35,8 +35,8 @@ public class DistributedTracingAspNetCoreTests : IAsyncLifetime public Task InitializeAsync() { - _agent1 = new ApmAgent(new TestAgentComponents(payloadSender: _payloadSender1)); - _agent2 = new ApmAgent(new TestAgentComponents(payloadSender: _payloadSender2)); + _agent1 = new ApmAgent(new TestAgentComponents(payloadSender: _payloadSender1, configuration: new MockConfiguration(exitSpanMinDuration:"0"))); + _agent2 = new ApmAgent(new TestAgentComponents(payloadSender: _payloadSender2, configuration: new MockConfiguration(exitSpanMinDuration:"0"))); _taskForApp1 = Program.CreateWebHostBuilder(null) .ConfigureServices(services => diff --git a/test/Elastic.Apm.AspNetCore.Tests/FailedRequestTests.cs b/test/Elastic.Apm.AspNetCore.Tests/FailedRequestTests.cs index e8e57a6f5..fdb0a8dce 100644 --- a/test/Elastic.Apm.AspNetCore.Tests/FailedRequestTests.cs +++ b/test/Elastic.Apm.AspNetCore.Tests/FailedRequestTests.cs @@ -30,7 +30,7 @@ public class FailedRequestTests : IAsyncLifetime public Task InitializeAsync() { - _agent1 = new ApmAgent(new TestAgentComponents(payloadSender: _payloadSender1)); + _agent1 = new ApmAgent(new TestAgentComponents(payloadSender: _payloadSender1, configuration: new MockConfiguration(exitSpanMinDuration:"0"))); _taskForApp1 = Program.CreateWebHostBuilder(null) .ConfigureServices(services => diff --git a/test/Elastic.Apm.AspNetCore.Tests/TransactionNameTests.cs b/test/Elastic.Apm.AspNetCore.Tests/TransactionNameTests.cs index 2b09fd414..c7a04f18f 100644 --- a/test/Elastic.Apm.AspNetCore.Tests/TransactionNameTests.cs +++ b/test/Elastic.Apm.AspNetCore.Tests/TransactionNameTests.cs @@ -88,10 +88,13 @@ public async Task CustomTransactionName(bool diagnosticSourceOnly) public async Task CustomTransactionNameWithNameUsingRequestInfo(bool diagnosticSourceOnly) { var httpClient = Helper.GetClient(_agent, _factory, diagnosticSourceOnly); - await httpClient.GetAsync("home/TransactionWithCustomNameUsingRequestInfo"); + if (httpClient != null) + { + await httpClient.GetAsync("home/TransactionWithCustomNameUsingRequestInfo"); - _payloadSender.WaitForTransactions(); - _payloadSender.Transactions.Should().OnlyContain(n => n.Name == "GET /home/TransactionWithCustomNameUsingRequestInfo"); + _payloadSender.WaitForTransactions(); + _payloadSender?.Transactions?.Should()?.OnlyContain(n => n.Name == "GET /home/TransactionWithCustomNameUsingRequestInfo"); + } } /// diff --git a/test/Elastic.Apm.Elasticsearch.Tests/ElasticsearchTests.cs b/test/Elastic.Apm.Elasticsearch.Tests/ElasticsearchTests.cs index beff5b24b..37806e920 100644 --- a/test/Elastic.Apm.Elasticsearch.Tests/ElasticsearchTests.cs +++ b/test/Elastic.Apm.Elasticsearch.Tests/ElasticsearchTests.cs @@ -30,7 +30,7 @@ public ElasticsearchTests(ElasticsearchFixture fixture) public async Task Elasticsearch_Span_Does_Not_Have_Http_Child_Span() { var payloadSender = new MockPayloadSender(); - using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender))) + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, configuration: new MockConfiguration(exitSpanMinDuration:"0", spanCompressionEnabled: "false")))) using (agent.Subscribe(new ElasticsearchDiagnosticsSubscriber(), new HttpDiagnosticsSubscriber())) { var searchResponse = await agent.Tracer.CaptureTransaction("Call Client", ApiConstants.ActionExec, diff --git a/test/Elastic.Apm.Elasticsearch.Tests/TraceContextTests.cs b/test/Elastic.Apm.Elasticsearch.Tests/TraceContextTests.cs index 903600712..43f500643 100644 --- a/test/Elastic.Apm.Elasticsearch.Tests/TraceContextTests.cs +++ b/test/Elastic.Apm.Elasticsearch.Tests/TraceContextTests.cs @@ -32,7 +32,7 @@ public void Call_to_Elasticsearch_propagates_Trace_Context_when_HttpDiagnosticsS context.Response.StatusCode = 200; }); - using var agent = new ApmAgent(new TestAgentComponents(payloadSender: new MockPayloadSender())); + using var agent = new ApmAgent(new TestAgentComponents(payloadSender: new MockPayloadSender(), configuration: new MockConfiguration(exitSpanMinDuration:"0", spanCompressionEnabled: "false"))); using var subscribe = agent.Subscribe(new ElasticsearchDiagnosticsSubscriber(), new HttpDiagnosticsSubscriber()); var client = new ElasticLowLevelClient(new ConnectionConfiguration(new Uri(localServer.Uri))); diff --git a/test/Elastic.Apm.Elasticsearch.Tests/VirtualElasticsearchTests.cs b/test/Elastic.Apm.Elasticsearch.Tests/VirtualElasticsearchTests.cs index cbfef905c..c63baa23a 100644 --- a/test/Elastic.Apm.Elasticsearch.Tests/VirtualElasticsearchTests.cs +++ b/test/Elastic.Apm.Elasticsearch.Tests/VirtualElasticsearchTests.cs @@ -26,7 +26,7 @@ public async Task FailOverResultsInSpans() .StaticConnectionPool() .AllDefaults(); var client = cluster.Client; - using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender))) + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, configuration: new MockConfiguration(exitSpanMinDuration:"0", spanCompressionEnabled:"false")))) using (agent.Subscribe(new ElasticsearchDiagnosticsSubscriber())) { var searchResponse = await agent.Tracer.CaptureTransaction("Call Client", ApiConstants.ActionExec, @@ -72,7 +72,7 @@ public async Task ExceptionDoesNotCauseLoseOfSpan() .StaticConnectionPool() .AllDefaults(); var client = cluster.Client; - using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender))) + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, configuration: new MockConfiguration(exitSpanMinDuration:"0", spanCompressionEnabled:"false")))) using (agent.Subscribe(new ElasticsearchDiagnosticsSubscriber())) { try @@ -110,7 +110,7 @@ public async Task ElasticsearchClientExceptionIsReported() .StaticConnectionPool() .Settings(s => s.DisablePing()); var client = cluster.Client; - using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender))) + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, configuration: new MockConfiguration(exitSpanMinDuration:"0", spanCompressionEnabled:"false")))) using (agent.Subscribe(new ElasticsearchDiagnosticsSubscriber())) { var searchResponse = await agent.Tracer.CaptureTransaction("Call Client", ApiConstants.ActionExec, diff --git a/test/Elastic.Apm.EntityFrameworkCore.Tests/EfCoreDiagnosticListenerTests.cs b/test/Elastic.Apm.EntityFrameworkCore.Tests/EfCoreDiagnosticListenerTests.cs index d8ced584f..21829cf28 100644 --- a/test/Elastic.Apm.EntityFrameworkCore.Tests/EfCoreDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.EntityFrameworkCore.Tests/EfCoreDiagnosticListenerTests.cs @@ -36,7 +36,7 @@ public EfCoreDiagnosticListenerTests() _dbContext.Database.EnsureCreated(); _payloadSender = new MockPayloadSender(); - _apmAgent = new ApmAgent(new AgentComponents(payloadSender: _payloadSender)); + _apmAgent = new ApmAgent(new AgentComponents(payloadSender: _payloadSender, configurationReader: new MockConfiguration(exitSpanMinDuration:"0"))); _apmAgent.Subscribe(new EfCoreDiagnosticsSubscriber()); } diff --git a/test/Elastic.Apm.Feature.Tests/TestConfiguration.cs b/test/Elastic.Apm.Feature.Tests/TestConfiguration.cs index 9de772710..778d291e7 100644 --- a/test/Elastic.Apm.Feature.Tests/TestConfiguration.cs +++ b/test/Elastic.Apm.Feature.Tests/TestConfiguration.cs @@ -31,6 +31,7 @@ public class TestConfiguration : IConfiguration public bool Enabled { get; set; } = true; public string Environment { get; set; } public IReadOnlyCollection ExcludedNamespaces { get; set; } = DefaultValues.DefaultExcludedNamespaces; + public double ExitSpanMinDuration => DefaultValues.ExitSpanMinDurationInMilliseconds; public TimeSpan FlushInterval { get; set; } = TimeSpan.Zero; public IReadOnlyDictionary GlobalLabels { get; set; } = new Dictionary(); public string HostName { get; set; } @@ -48,6 +49,9 @@ public class TestConfiguration : IConfiguration public string ServiceName { get; set; } public string ServiceNodeName { get; set; } public string ServiceVersion { get; set; } + public bool SpanCompressionEnabled => DefaultValues.SpanCompressionEnabled; + public double SpanCompressionExactMatchMaxDuration => DefaultValues.SpanCompressionExactMatchMaxDurationInMilliseconds; + public double SpanCompressionSameKindMaxDuration => DefaultValues.SpanCompressionSameKindMaxDurationInMilliseconds; public double SpanFramesMinDurationInMilliseconds { get; set; } = DefaultValues.SpanFramesMinDurationInMilliseconds; public int StackTraceLimit { get; set; } = DefaultValues.StackTraceLimit; public bool TraceContextIgnoreSampledFalse { get; set; } = DefaultValues.TraceContextIgnoreSampledFalse; diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlCommandTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlCommandTests.cs index c60ce71bf..40edee441 100644 --- a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlCommandTests.cs +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/MySqlCommandTests.cs @@ -46,6 +46,8 @@ public async Task CaptureAutoInstrumentedSpans(string targetFramework) ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", ["MYSQL_CONNECTION_STRING"] = _fixture.ConnectionString, ["ELASTIC_APM_DISABLE_METRICS"] = "*", + ["ELASTIC_APM_EXIT_SPAN_MIN_DURATION"] = "0", + ["ELASTIC_APM_SPAN_COMPRESSION_ENABLED"] = "false" }; profiledApplication.Start( diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/NpgSqlCommandTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/NpgSqlCommandTests.cs index 9ba03d9bd..5396a710c 100644 --- a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/NpgSqlCommandTests.cs +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/NpgSqlCommandTests.cs @@ -74,6 +74,8 @@ public async Task CaptureAutoInstrumentedSpans(string targetFramework, string np ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", ["POSTGRES_CONNECTION_STRING"] = _fixture.ConnectionString, ["ELASTIC_APM_DISABLE_METRICS"] = "*", + ["ELASTIC_APM_EXIT_SPAN_MIN_DURATION"] = "0", + ["ELASTIC_APM_SPAN_COMPRESSION_ENABLED"] = "false" }; var msBuildProperties = npgsqlVersion is null diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleManagedDataAccessCommandTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleManagedDataAccessCommandTests.cs index fcb99e0fb..a00eb7c4f 100644 --- a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleManagedDataAccessCommandTests.cs +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleManagedDataAccessCommandTests.cs @@ -50,7 +50,9 @@ public async Task CaptureAutoInstrumentedSpans(string targetFramework) ["ORACLE_CONNECTION_STRING"] = _fixture.ConnectionString, ["ELASTIC_APM_DISABLE_METRICS"] = "*", // to fix ORA-01882 Timezone region not found on CI. - ["TZ"] = "GMT" + ["TZ"] = "GMT", + ["ELASTIC_APM_EXIT_SPAN_MIN_DURATION"] = "0", + ["ELASTIC_APM_SPAN_COMPRESSION_ENABLED"] = "false" }; profiledApplication.Start( diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleManagedDataAccessCoreCommandTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleManagedDataAccessCoreCommandTests.cs index 513838e0f..c19e056d6 100644 --- a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleManagedDataAccessCoreCommandTests.cs +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/OracleManagedDataAccessCoreCommandTests.cs @@ -47,7 +47,9 @@ public async Task CaptureAutoInstrumentedSpans(string targetFramework) ["ORACLE_CONNECTION_STRING"] = _fixture.ConnectionString, ["ELASTIC_APM_DISABLE_METRICS"] = "*", // to fix ORA-01882 Timezone region not found on CI. - ["TZ"] = "GMT" + ["TZ"] = "GMT", + ["ELASTIC_APM_EXIT_SPAN_MIN_DURATION"] = "0", + ["ELASTIC_APM_SPAN_COMPRESSION_ENABLED"] = "false" }; profiledApplication.Start( diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlCommandTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlCommandTests.cs index c1bd3b4b4..d4f7506d7 100644 --- a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlCommandTests.cs +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqlCommandTests.cs @@ -47,6 +47,8 @@ public async Task CaptureAutoInstrumentedSpans(string targetFramework) ["ELASTIC_APM_DISABLE_METRICS"] = "*", ["ELASTIC_APM_SERVICE_NAME"] = $"SqlClientSample-{targetFramework}", ["SQLSERVER_CONNECTION_STRING"] = _fixture.ConnectionString, + ["ELASTIC_APM_EXIT_SPAN_MIN_DURATION"] = "0", + ["ELASTIC_APM_SPAN_COMPRESSION_ENABLED"] = "false" }; profiledApplication.Start( diff --git a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqliteCommandTests.cs b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqliteCommandTests.cs index f5be1fa5d..9fb719919 100644 --- a/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqliteCommandTests.cs +++ b/test/Elastic.Apm.Profiler.Managed.Tests/AdoNet/SqliteCommandTests.cs @@ -38,6 +38,8 @@ public async Task CaptureAutoInstrumentedSpans(string targetFramework) { ["ELASTIC_APM_SERVER_URL"] = $"http://localhost:{port}", ["ELASTIC_APM_DISABLE_METRICS"] = "*", + ["ELASTIC_APM_EXIT_SPAN_MIN_DURATION"] = "0", + ["ELASTIC_APM_SPAN_COMPRESSION_ENABLED"] = "false" }; profiledApplication.Start( diff --git a/test/Elastic.Apm.Tests.MockApmServer/CompositeDto.cs b/test/Elastic.Apm.Tests.MockApmServer/CompositeDto.cs new file mode 100644 index 000000000..e047210b2 --- /dev/null +++ b/test/Elastic.Apm.Tests.MockApmServer/CompositeDto.cs @@ -0,0 +1,18 @@ +// 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.Libraries.Newtonsoft.Json; + +namespace Elastic.Apm.Tests.MockApmServer +{ + public class CompositeDto + { + [JsonProperty("compression_strategy")] + public string CompressionStrategy { get; set; } + + public int Count { get; set; } + + public double Sum { get; set; } + } +} diff --git a/test/Elastic.Apm.Tests.MockApmServer/DroppedSpanStatsDto.cs b/test/Elastic.Apm.Tests.MockApmServer/DroppedSpanStatsDto.cs new file mode 100644 index 000000000..afb5aa3de --- /dev/null +++ b/test/Elastic.Apm.Tests.MockApmServer/DroppedSpanStatsDto.cs @@ -0,0 +1,23 @@ +// 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.Tests.MockApmServer +{ + public class DroppedSpanStatsDto + { + [JsonProperty("destination_service_resource")] + public string DestinationServiceResource { get; } + + [JsonProperty("duration.count")] + public int DurationCount { get; set; } + + [JsonProperty("duration.sum.us")] + public double DurationSumUs { get; set; } + + public Outcome Outcome { get; } + } +} diff --git a/test/Elastic.Apm.Tests.MockApmServer/SpanDto.cs b/test/Elastic.Apm.Tests.MockApmServer/SpanDto.cs index 7cc91ad93..96bd5d2f9 100644 --- a/test/Elastic.Apm.Tests.MockApmServer/SpanDto.cs +++ b/test/Elastic.Apm.Tests.MockApmServer/SpanDto.cs @@ -45,6 +45,8 @@ internal class SpanDto : ITimedDto public string Type { get; set; } + public CompositeDto Composite { get; set; } + public override string ToString() => new ToStringBuilder(nameof(SpanDto)) { { nameof(Id), Id }, diff --git a/test/Elastic.Apm.Tests.MockApmServer/TransactionDto.cs b/test/Elastic.Apm.Tests.MockApmServer/TransactionDto.cs index baf66a43b..a78c497b0 100644 --- a/test/Elastic.Apm.Tests.MockApmServer/TransactionDto.cs +++ b/test/Elastic.Apm.Tests.MockApmServer/TransactionDto.cs @@ -2,6 +2,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 System.Collections.Generic; using Elastic.Apm.Api; using Elastic.Apm.Helpers; using Elastic.Apm.Libraries.Newtonsoft.Json; @@ -44,6 +45,10 @@ internal class TransactionDto : ITimedDto public string Type { get; set; } + + [JsonProperty("dropped_spans_stats")] + public List DroppedSpanStats { get; set; } + public override string ToString() => new ToStringBuilder(nameof(TransactionDto)) { { nameof(Id), Id }, diff --git a/test/Elastic.Apm.Tests.Utilities/MockConfiguration.cs b/test/Elastic.Apm.Tests.Utilities/MockConfiguration.cs index 0a137697e..fc45404a0 100644 --- a/test/Elastic.Apm.Tests.Utilities/MockConfiguration.cs +++ b/test/Elastic.Apm.Tests.Utilities/MockConfiguration.cs @@ -29,6 +29,7 @@ public class MockConfiguration : AbstractConfigurationReader, IConfiguration, IC private readonly string _enableOpenTelemetryBridge; private readonly string _environment; private readonly string _excludedNamespaces; + private readonly string _exitSpanMinDuration; private readonly string _flushInterval; private readonly string _globalLabels; private readonly string _hostName; @@ -46,6 +47,9 @@ public class MockConfiguration : AbstractConfigurationReader, IConfiguration, IC private readonly string _serviceName; private readonly string _serviceNodeName; private readonly string _serviceVersion; + private readonly string _spanCompressionEnabled; + private readonly string _spanCompressionExactMatchMaxDuration; + private readonly string _spanCompressionSameKindMaxDuration; private readonly string _spanFramesMinDurationInMilliseconds; private readonly string _stackTraceLimit; private readonly string _traceContextIgnoreSampledFalse; @@ -68,6 +72,7 @@ public MockConfiguration(IApmLogger logger = null, string centralConfig = null, string description = null, string enableOpenTelemetryBridge = null, + string exitSpanMinDuration = null, string transactionSampleRate = null, string transactionMaxSpans = null, string metricsInterval = null, @@ -94,7 +99,10 @@ public MockConfiguration(IApmLogger logger = null, string serverUrl = null, string serverCert = null, string ignoreMessageQueues = null, - string traceContextIgnoreSampledFalse = null + string traceContextIgnoreSampledFalse = null, + string spanCompressionEnabled = null, + string spanCompressionExactMatchMaxDuration = null, + string spanCompressionSameKindMaxDuration = null ) : base(logger, ThisClassName) { _serverUrls = serverUrls; @@ -135,6 +143,10 @@ public MockConfiguration(IApmLogger logger = null, _serverCert = serverCert; _ignoreMessageQueues = ignoreMessageQueues; _traceContextIgnoreSampledFalse = traceContextIgnoreSampledFalse; + _spanCompressionEnabled = spanCompressionEnabled; + _spanCompressionExactMatchMaxDuration = spanCompressionExactMatchMaxDuration; + _spanCompressionSameKindMaxDuration = spanCompressionSameKindMaxDuration; + _exitSpanMinDuration = exitSpanMinDuration; } public string ApiKey => ParseApiKey(Kv(EnvVarNames.ApiKey, _apiKey, Origin)); @@ -163,6 +175,8 @@ public MockConfiguration(IApmLogger logger = null, public IReadOnlyCollection ExcludedNamespaces => ParseExcludedNamespaces(new ConfigurationKeyValue(EnvVarNames.ExcludedNamespaces, _excludedNamespaces, Origin)); + public double ExitSpanMinDuration => ParseExitSpanMinDuration(Kv(EnvVarNames.ExitSpanMinDuration, _exitSpanMinDuration, Origin)); + public TimeSpan FlushInterval => ParseFlushInterval(Kv(EnvVarNames.FlushInterval, _flushInterval, Origin)); public IReadOnlyDictionary GlobalLabels => @@ -206,6 +220,14 @@ public Uri ServerUrl public string ServiceName => ParseServiceName(Kv(EnvVarNames.ServiceName, _serviceName, Origin)); public string ServiceNodeName => ParseServiceNodeName(Kv(EnvVarNames.ServiceNodeName, _serviceNodeName, Origin)); public string ServiceVersion => ParseServiceVersion(Kv(EnvVarNames.ServiceVersion, _serviceVersion, Origin)); + public bool SpanCompressionEnabled => ParseSpanCompressionEnabled(Kv(EnvVarNames.SpanCompressionEnabled, _spanCompressionEnabled, Origin)); + + public double SpanCompressionExactMatchMaxDuration => + ParseSpanCompressionExactMatchMaxDuration(Kv(EnvVarNames.SpanCompressionExactMatchMaxDuration, _spanCompressionExactMatchMaxDuration, + Origin)); + + public double SpanCompressionSameKindMaxDuration => ParseSpanCompressionSameKindMaxDuration(Kv(EnvVarNames.SpanCompressionSameKindMaxDuration, + _spanCompressionSameKindMaxDuration, Origin)); public double SpanFramesMinDurationInMilliseconds => ParseSpanFramesMinDurationInMilliseconds(Kv( EnvVarNames.SpanFramesMinDuration, diff --git a/test/Elastic.Apm.Tests/ApiTests/ApiTests.cs b/test/Elastic.Apm.Tests/ApiTests/ApiTests.cs index 5fc01e3c8..8342bcf4d 100644 --- a/test/Elastic.Apm.Tests/ApiTests/ApiTests.cs +++ b/test/Elastic.Apm.Tests/ApiTests/ApiTests.cs @@ -791,7 +791,7 @@ public void destination_properties_set_manually_have_precedence_over_automatical const int manualPort = 1234; var payloadSender = new MockPayloadSender(); - using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender))) + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, configuration: new MockConfiguration(exitSpanMinDuration:"0")))) { agent.Tracer.CaptureTransaction("test TX name", "test TX type", tx => { @@ -857,7 +857,7 @@ public void span_with_invalid_Context_Http_Url_should_not_have_destination() var mockLogger = new TestLogger(LogLevel.Trace); var payloadSender = new MockPayloadSender(); - using (var agent = new ApmAgent(new TestAgentComponents(mockLogger, payloadSender: payloadSender))) + using (var agent = new ApmAgent(new TestAgentComponents(mockLogger, payloadSender: payloadSender, configuration: new MockConfiguration(exitSpanMinDuration:"0")))) { agent.Tracer.CaptureTransaction("test TX name", "test TX type", tx => diff --git a/test/Elastic.Apm.Tests/ApiTests/ConvenientApiSpanTests.cs b/test/Elastic.Apm.Tests/ApiTests/ConvenientApiSpanTests.cs index 2a7830568..d07113893 100644 --- a/test/Elastic.Apm.Tests/ApiTests/ConvenientApiSpanTests.cs +++ b/test/Elastic.Apm.Tests/ApiTests/ConvenientApiSpanTests.cs @@ -8,1078 +8,1077 @@ using System.Threading.Tasks; using Elastic.Apm.Api; using Elastic.Apm.Model; -using Elastic.Apm.Tests.Extensions; using Elastic.Apm.Tests.HelpersTests; using Elastic.Apm.Tests.Utilities; using FluentAssertions; using Xunit; -namespace Elastic.Apm.Tests.ApiTests +namespace Elastic.Apm.Tests.ApiTests; + +/// +/// Tests the API for manual instrumentation. +/// Only tests scenarios when using the convenient API and only test spans. +/// Transactions are covered by . +/// Scenarios with manually calling , +/// , , +/// are covered by +/// Very similar to . The test cases are the same, +/// but this one tests the CaptureSpan method - including every single overload. +/// Tests with postfix `_OnSubSpan` do exactly the same as tests without the postfix, the only difference is +/// that those create a span on another span (aka sub span) and test things on a sub span. Tests without the +/// `_OnSubSpan` postfix typically start the span on a transaction and not on a span. +/// +public class ConvenientApiSpanTests { - /// - /// Tests the API for manual instrumentation. - /// Only tests scenarios when using the convenient API and only test spans. - /// Transactions are covered by . - /// Scenarios with manually calling , - /// , , - /// are covered by - /// Very similar to . The test cases are the same, - /// but this one tests the CaptureSpan method - including every single overload. - /// Tests with postfix `_OnSubSpan` do exactly the same as tests without the postfix, the only difference is - /// that those create a span on another span (aka sub span) and test things on a sub span. Tests without the - /// `_OnSubSpan` postfix typically start the span on a transaction and not on a span. - /// - public class ConvenientApiSpanTests - { - private const string ExceptionMessage = "Foo"; - private const string SpanName = "TestSpan"; - - private const string SpanType = "TestSpan"; - private const string TransactionName = "ConvenientApiTest"; - - private const string TransactionType = "Test"; - - /// - /// Tests the method. - /// It wraps a fake span (Thread.Sleep) into the CaptureSpan method - /// and it makes sure that the span is captured by the agent. - /// - [Fact] - public void SimpleAction() - => AssertWith1TransactionAnd1Span(t => { t.CaptureSpan(SpanName, SpanType, () => { WaitHelpers.Sleep2XMinimum(); }); }); - - /// - /// Tests the method with an exception. - /// It wraps a fake span (Thread.Sleep) that throws an exception into the CaptureSpan method - /// and it makes sure that the span and the exception are captured by the agent. - /// - [Fact] - public void SimpleActionWithException() - => AssertWith1TransactionAnd1SpanAnd1Error(t => - { - Action act = () => - { - t.CaptureSpan(SpanName, SpanType, new Action(() => - { - WaitHelpers.Sleep2XMinimum(); - throw new InvalidOperationException(ExceptionMessage); - })); - }; - act.Should().Throw(); - }); + private const string ExceptionMessage = "Foo"; + private const string SpanName = "TestSpan"; - [Fact] - public void SimpleActionWithException_OnSubSpan() - => AssertWith1TransactionAnd1SpanAnd1ErrorOnSubSpan(s => - { - Action act = () => - { - s.CaptureSpan(SpanName, SpanType, new Action(() => - { - WaitHelpers.Sleep2XMinimum(); - throw new InvalidOperationException(ExceptionMessage); - })); - }; - act.Should().Throw(); - }); + private const string SpanType = "TestSpan"; + private const string TransactionName = "ConvenientApiTest"; - /// - /// Tests the method. - /// It wraps a fake span (Thread.Sleep) into the CaptureSpan method with an parameter - /// and it makes sure that the span is captured by the agent and the parameter is not null - /// - [Fact] - public void SimpleActionWithParameter() - => AssertWith1TransactionAnd1Span(t => - { - t.CaptureSpan(SpanName, SpanType, - s => - { - s.Should().NotBeNull(); - WaitHelpers.Sleep2XMinimum(); - }); - }); + private const string TransactionType = "Test"; - [Fact] - public void SimpleActionWithParameter_OnSubSpan() - => AssertWith1TransactionAnd1SpanOnSubSpan(t => - { - t.CaptureSpan(SpanName, SpanType, - s => - { - s.Should().NotBeNull(); - WaitHelpers.Sleep2XMinimum(); - }); - }); + /// + /// Tests the method. + /// It wraps a fake span (Thread.Sleep) into the CaptureSpan method + /// and it makes sure that the span is captured by the agent. + /// + [Fact] + public void SimpleAction() + => AssertWith1TransactionAnd1Span(t => { t.CaptureSpan(SpanName, SpanType, () => { WaitHelpers.Sleep2XMinimum(); }); }); - /// - /// Tests the method with an - /// exception. - /// It wraps a fake span (Thread.Sleep) that throws an exception into the CaptureSpan method with an - /// parameter - /// and it makes sure that the span and the error are captured by the agent and the parameter is not - /// null - /// - [Fact] - public void SimpleActionWithExceptionAndParameter() - => AssertWith1TransactionAnd1SpanAnd1Error(t => + /// + /// Tests the method with an exception. + /// It wraps a fake span (Thread.Sleep) that throws an exception into the CaptureSpan method + /// and it makes sure that the span and the exception are captured by the agent. + /// + [Fact] + public void SimpleActionWithException() + => AssertWith1TransactionAnd1SpanAnd1Error(t => + { + var act = () => { - Action act = () => + t.CaptureSpan(SpanName, SpanType, new Action(() => { - t.CaptureSpan(SpanName, SpanType, new Action(s => - { - s.Should().NotBeNull(); - WaitHelpers.Sleep2XMinimum(); - throw new InvalidOperationException(ExceptionMessage); - })); - }; - act.Should().Throw().WithMessage(ExceptionMessage); - }); - - [Fact] - public void SimpleActionWithExceptionAndParameter_OnSubSpan() - => AssertWith1TransactionAnd1SpanAnd1ErrorOnSubSpan(s => + WaitHelpers.Sleep2XMinimum(); + throw new InvalidOperationException(ExceptionMessage); + })); + }; + act.Should().Throw(); + }); + + [Fact] + public void SimpleActionWithException_OnSubSpan() + => AssertWith1TransactionAnd1SpanAnd1ErrorOnSubSpan(s => + { + var act = () => { - Action act = () => + s.CaptureSpan(SpanName, SpanType, new Action(() => { - s.CaptureSpan(SpanName, SpanType, new Action(subSpan => - { - subSpan.Should().NotBeNull(); - WaitHelpers.Sleep2XMinimum(); - throw new InvalidOperationException(ExceptionMessage); - })); - }; - act.Should().Throw().WithMessage(ExceptionMessage); - }); + WaitHelpers.Sleep2XMinimum(); + throw new InvalidOperationException(ExceptionMessage); + })); + }; + act.Should().Throw(); + }); - /// - /// Tests the method. - /// It wraps a fake span (Thread.Sleep) with a return value into the CaptureSpan method - /// and it makes sure that the span is captured by the agent and the return value is correct. - /// - [Fact] - public void SimpleActionWithReturnType() - => AssertWith1TransactionAnd1Span(t => - { - var res = t.CaptureSpan(SpanName, SpanType, () => + /// + /// Tests the method. + /// It wraps a fake span (Thread.Sleep) into the CaptureSpan method with an parameter + /// and it makes sure that the span is captured by the agent and the parameter is not null + /// + [Fact] + public void SimpleActionWithParameter() + => AssertWith1TransactionAnd1Span(t => + { + t.CaptureSpan(SpanName, SpanType, + s => { + s.Should().NotBeNull(); WaitHelpers.Sleep2XMinimum(); - return 42; }); + }); - res.Should().Be(42); - }); - - [Fact] - public void SimpleActionWithReturnType_OnSubSpan() - => AssertWith1TransactionAnd1SpanOnSubSpan(s => - { - var res = s.CaptureSpan(SpanName, SpanType, () => + [Fact] + public void SimpleActionWithParameter_OnSubSpan() + => AssertWith1TransactionAnd1SpanOnSubSpan(t => + { + t.CaptureSpan(SpanName, SpanType, + s => { + s.Should().NotBeNull(); WaitHelpers.Sleep2XMinimum(); - return 42; }); + }); - res.Should().Be(42); - }); - - /// - /// Tests the method. - /// It wraps a fake span (Thread.Sleep) with a return value into the CaptureSpan method with an - /// parameter - /// and it makes sure that the span is captured by the agent and the return value is correct and the - /// is not null. - /// - [Fact] - public void SimpleActionWithReturnTypeAndParameter() - => AssertWith1TransactionAnd1Span(t => + /// + /// Tests the method with an + /// exception. + /// It wraps a fake span (Thread.Sleep) that throws an exception into the CaptureSpan method with an + /// parameter + /// and it makes sure that the span and the error are captured by the agent and the parameter is not + /// null + /// + [Fact] + public void SimpleActionWithExceptionAndParameter() + => AssertWith1TransactionAnd1SpanAnd1Error(t => + { + var act = () => { - var res = t.CaptureSpan(SpanName, SpanType, s => + t.CaptureSpan(SpanName, SpanType, new Action(s => { - t.Should().NotBeNull(); + s.Should().NotBeNull(); WaitHelpers.Sleep2XMinimum(); - return 42; - }); - - res.Should().Be(42); - }); - - [Fact] - public void SimpleActionWithReturnTypeAndParameter_OnSubSpan() - => AssertWith1TransactionAnd1SpanOnSubSpan(t => + throw new InvalidOperationException(ExceptionMessage); + })); + }; + act.Should().Throw().WithMessage(ExceptionMessage); + }); + + [Fact] + public void SimpleActionWithExceptionAndParameter_OnSubSpan() + => AssertWith1TransactionAnd1SpanAnd1ErrorOnSubSpan(s => + { + var act = () => { - var res = t.CaptureSpan(SpanName, SpanType, s => + s.CaptureSpan(SpanName, SpanType, new Action(subSpan => { - t.Should().NotBeNull(); + subSpan.Should().NotBeNull(); WaitHelpers.Sleep2XMinimum(); - return 42; - }); + throw new InvalidOperationException(ExceptionMessage); + })); + }; + act.Should().Throw().WithMessage(ExceptionMessage); + }); - res.Should().Be(42); + /// + /// Tests the method. + /// It wraps a fake span (Thread.Sleep) with a return value into the CaptureSpan method + /// and it makes sure that the span is captured by the agent and the return value is correct. + /// + [Fact] + public void SimpleActionWithReturnType() + => AssertWith1TransactionAnd1Span(t => + { + var res = t.CaptureSpan(SpanName, SpanType, () => + { + WaitHelpers.Sleep2XMinimum(); + return 42; }); - /// - /// Tests the method with an - /// exception. - /// It wraps a fake span (Thread.Sleep) with a return value that throws an exception into the CaptureSpan method with an - /// parameter - /// and it makes sure that the span and the error are captured by the agent and the return value is correct and the - /// is not null. - /// - [Fact] - public void SimpleActionWithReturnTypeAndExceptionAndParameter() - => AssertWith1TransactionAnd1SpanAnd1Error(t => + res.Should().Be(42); + }); + + [Fact] + public void SimpleActionWithReturnType_OnSubSpan() + => AssertWith1TransactionAnd1SpanOnSubSpan(s => + { + var res = s.CaptureSpan(SpanName, SpanType, () => { - Action act = () => - { - t.CaptureSpan(SpanName, SpanType, s => - { - s.Should().NotBeNull(); - WaitHelpers.Sleep2XMinimum(); - - if (new Random().Next(1) == 0) //avoid unreachable code warning. - throw new InvalidOperationException(ExceptionMessage); - - return 42; - }); - throw new Exception("CaptureSpan should not eat exception and continue"); - }; - act.Should().Throw().WithMessage(ExceptionMessage); + WaitHelpers.Sleep2XMinimum(); + return 42; }); - [Fact] - public void SimpleActionWithReturnTypeAndExceptionAndParameter_OnSubSpan() - => AssertWith1TransactionAnd1SpanAnd1ErrorOnSubSpan(s1 => + res.Should().Be(42); + }); + + /// + /// Tests the method. + /// It wraps a fake span (Thread.Sleep) with a return value into the CaptureSpan method with an + /// parameter + /// and it makes sure that the span is captured by the agent and the return value is correct and the + /// is not null. + /// + [Fact] + public void SimpleActionWithReturnTypeAndParameter() + => AssertWith1TransactionAnd1Span(t => + { + var res = t.CaptureSpan(SpanName, SpanType, s => { - Action act = () => - { - s1.CaptureSpan(SpanName, SpanType, s2 => - { - s2.Should().NotBeNull(); - WaitHelpers.Sleep2XMinimum(); - - if (new Random().Next(1) == 0) //avoid unreachable code warning. - throw new InvalidOperationException(ExceptionMessage); - - return 42; - }); - throw new Exception("CaptureSpan should not eat exception and continue"); - }; - act.Should().Throw().WithMessage(ExceptionMessage); + t.Should().NotBeNull(); + WaitHelpers.Sleep2XMinimum(); + return 42; }); - /// - /// Tests the method with an - /// exception. - /// It wraps a fake span (Thread.Sleep) with a return value that throws an exception into the CaptureSpan method with an - /// parameter - /// and it makes sure that the span and the error are captured by the agent and the return value is correct and the - /// is not null. - /// - [Fact] - public void SimpleActionWithReturnTypeAndException() - => AssertWith1TransactionAnd1SpanAnd1Error(t => + res.Should().Be(42); + }); + + [Fact] + public void SimpleActionWithReturnTypeAndParameter_OnSubSpan() + => AssertWith1TransactionAnd1SpanOnSubSpan(t => + { + var res = t.CaptureSpan(SpanName, SpanType, s => { - var alwaysThrow = new Random().Next(1) == 0; - Func act = () => t.CaptureSpan(SpanName, SpanType, () => + t.Should().NotBeNull(); + WaitHelpers.Sleep2XMinimum(); + return 42; + }); + + res.Should().Be(42); + }); + + /// + /// Tests the method with an + /// exception. + /// It wraps a fake span (Thread.Sleep) with a return value that throws an exception into the CaptureSpan method with an + /// parameter + /// and it makes sure that the span and the error are captured by the agent and the return value is correct and the + /// is not null. + /// + [Fact] + public void SimpleActionWithReturnTypeAndExceptionAndParameter() + => AssertWith1TransactionAnd1SpanAnd1Error(t => + { + var act = () => + { + t.CaptureSpan(SpanName, SpanType, s => { + s.Should().NotBeNull(); WaitHelpers.Sleep2XMinimum(); - if (alwaysThrow) //avoid unreachable code warning. + if (new Random().Next(1) == 0) //avoid unreachable code warning. throw new InvalidOperationException(ExceptionMessage); return 42; }); - act.Should().Throw().WithMessage(ExceptionMessage); - }); - - [Fact] - public void SimpleActionWithReturnTypeAndException_OnSubSpan() - => AssertWith1TransactionAnd1SpanAnd1ErrorOnSubSpan(s => + throw new Exception("CaptureSpan should not eat exception and continue"); + }; + act.Should().Throw().WithMessage(ExceptionMessage); + }); + + [Fact] + public void SimpleActionWithReturnTypeAndExceptionAndParameter_OnSubSpan() + => AssertWith1TransactionAnd1SpanAnd1ErrorOnSubSpan(s1 => + { + var act = () => { - var alwaysThrow = new Random().Next(1) == 0; - Func act = () => s.CaptureSpan(SpanName, SpanType, () => + s1.CaptureSpan(SpanName, SpanType, s2 => { + s2.Should().NotBeNull(); WaitHelpers.Sleep2XMinimum(); - if (alwaysThrow) //avoid unreachable code warning. + if (new Random().Next(1) == 0) //avoid unreachable code warning. throw new InvalidOperationException(ExceptionMessage); return 42; }); - act.Should().Throw().WithMessage(ExceptionMessage); - }); + throw new Exception("CaptureSpan should not eat exception and continue"); + }; + act.Should().Throw().WithMessage(ExceptionMessage); + }); - /// - /// Tests the method. - /// It wraps a fake async span (Task.Delay) into the CaptureSpan method - /// and it makes sure that the span is captured. - /// - [Fact] - public async Task AsyncTask() - => await AssertWith1TransactionAnd1SpanAsync(async t => + /// + /// Tests the method with an + /// exception. + /// It wraps a fake span (Thread.Sleep) with a return value that throws an exception into the CaptureSpan method with an + /// parameter + /// and it makes sure that the span and the error are captured by the agent and the return value is correct and the + /// is not null. + /// + [Fact] + public void SimpleActionWithReturnTypeAndException() + => AssertWith1TransactionAnd1SpanAnd1Error(t => + { + var alwaysThrow = new Random().Next(1) == 0; + var act = () => t.CaptureSpan(SpanName, SpanType, () => { - await t.CaptureSpan(SpanName, SpanType, async () => { await WaitHelpers.Delay2XMinimum(); }); - }); + WaitHelpers.Sleep2XMinimum(); - [Fact] - public async Task AsyncTask_OnSubSpan() - => await AssertWith1TransactionAnd1SpanAsyncOnSubSpan(async s => - { - await s.CaptureSpan(SpanName, SpanType, async () => { await WaitHelpers.Delay2XMinimum(); }); - }); + if (alwaysThrow) //avoid unreachable code warning. + throw new InvalidOperationException(ExceptionMessage); - /// - /// Tests the method with an - /// exception - /// It wraps a fake async span (Task.Delay) that throws an exception into the CaptureSpan method - /// and it makes sure that the span and the error are captured. - /// - [Fact] - public async Task AsyncTaskWithException() - => await AssertWith1TransactionAnd1ErrorAnd1SpanAsync(async t => - { - Func act = async () => - { - await t.CaptureSpan(SpanName, SpanType, async () => - { - await WaitHelpers.Delay2XMinimum(); - throw new InvalidOperationException(ExceptionMessage); - }); - }; - var should = await act.Should().ThrowAsync(); - should.WithMessage(ExceptionMessage); + return 42; }); + act.Should().Throw().WithMessage(ExceptionMessage); + }); - [Fact] - public async Task AsyncTaskWithExceptionOn_SubSpan() - => await AssertWith1TransactionAnd1ErrorAnd1SpanAsync(async t => + [Fact] + public void SimpleActionWithReturnTypeAndException_OnSubSpan() + => AssertWith1TransactionAnd1SpanAnd1ErrorOnSubSpan(s => + { + var alwaysThrow = new Random().Next(1) == 0; + var act = () => s.CaptureSpan(SpanName, SpanType, () => { - Func act = async () => - { - await t.CaptureSpan(SpanName, SpanType, async () => - { - await WaitHelpers.Delay2XMinimum(); - throw new InvalidOperationException(ExceptionMessage); - }); - }; - var should = await act.Should().ThrowAsync(); - should.WithMessage(ExceptionMessage); - }); + WaitHelpers.Sleep2XMinimum(); - /// - /// Tests the method. - /// It wraps a fake async span (Task.Delay) into the CaptureSpan method with an parameter - /// and it makes sure that the span is captured and the parameter is not null. - /// - [Fact] - public async Task AsyncTaskWithParameter() - => await AssertWith1TransactionAnd1SpanAsync(async t => - { - await t.CaptureSpan(SpanName, SpanType, - async s => - { - s.Should().NotBeNull(); - await WaitHelpers.Delay2XMinimum(); - }); - }); + if (alwaysThrow) //avoid unreachable code warning. + throw new InvalidOperationException(ExceptionMessage); - [Fact] - public async Task AsyncTaskWithParameter_OnSubSpan() - => await AssertWith1TransactionAnd1SpanAsyncOnSubSpan(async s => - { - await s.CaptureSpan(SpanName, SpanType, - async s2 => - { - s2.Should().NotBeNull(); - await WaitHelpers.Delay2XMinimum(); - }); + return 42; }); + act.Should().Throw().WithMessage(ExceptionMessage); + }); + + /// + /// Tests the method. + /// It wraps a fake async span (Task.Delay) into the CaptureSpan method + /// and it makes sure that the span is captured. + /// + [Fact] + public async Task AsyncTask() + => await AssertWith1TransactionAnd1SpanAsync(async t => + { + await t.CaptureSpan(SpanName, SpanType, async () => { await WaitHelpers.Delay2XMinimum(); }); + }); + + [Fact] + public async Task AsyncTask_OnSubSpan() + => await AssertWith1TransactionAnd1SpanAsyncOnSubSpan(async s => + { + await s.CaptureSpan(SpanName, SpanType, async () => { await WaitHelpers.Delay2XMinimum(); }); + }); - /// - /// Tests the method with an - /// exception. - /// It wraps a fake async span (Task.Delay) that throws an exception into the CaptureSpan method with an - /// parameter - /// and it makes sure that the span and the error are captured and the parameter is not null. - /// - [Fact] - public async Task AsyncTaskWithExceptionAndParameter() - => await AssertWith1TransactionAnd1ErrorAnd1SpanAsync(async t => + /// + /// Tests the method with an + /// exception + /// It wraps a fake async span (Task.Delay) that throws an exception into the CaptureSpan method + /// and it makes sure that the span and the error are captured. + /// + [Fact] + public async Task AsyncTaskWithException() + => await AssertWith1TransactionAnd1ErrorAnd1SpanAsync(async t => + { + var act = async () => { - Func act = async () => + await t.CaptureSpan(SpanName, SpanType, async () => { - await t.CaptureSpan(SpanName, SpanType, async s => - { - s.Should().NotBeNull(); - await WaitHelpers.Delay2XMinimum(); - throw new InvalidOperationException(ExceptionMessage); - }); - }; - await act.Should().ThrowAsync(); - }); - - [Fact] - public async Task AsyncTaskWithExceptionAndParameter_OnSubSpan() - => await AssertWith1TransactionAnd1ErrorAnd1SpanAsyncOnSubSpan(async s => + await WaitHelpers.Delay2XMinimum(); + throw new InvalidOperationException(ExceptionMessage); + }); + }; + var should = await act.Should().ThrowAsync(); + should.WithMessage(ExceptionMessage); + }); + + [Fact] + public async Task AsyncTaskWithExceptionOn_SubSpan() + => await AssertWith1TransactionAnd1ErrorAnd1SpanAsync(async t => + { + var act = async () => { - Func act = async () => + await t.CaptureSpan(SpanName, SpanType, async () => { - await s.CaptureSpan(SpanName, SpanType, async s2 => - { - s2.Should().NotBeNull(); - await WaitHelpers.Delay2XMinimum(); - throw new InvalidOperationException(ExceptionMessage); - }); - }; - await act.Should().ThrowAsync(); - }); + await WaitHelpers.Delay2XMinimum(); + throw new InvalidOperationException(ExceptionMessage); + }); + }; + var should = await act.Should().ThrowAsync(); + should.WithMessage(ExceptionMessage); + }); + + /// + /// Tests the method. + /// It wraps a fake async span (Task.Delay) into the CaptureSpan method with an parameter + /// and it makes sure that the span is captured and the parameter is not null. + /// + [Fact] + public async Task AsyncTaskWithParameter() + => await AssertWith1TransactionAnd1SpanAsync(async t => + { + await t.CaptureSpan(SpanName, SpanType, + async s => + { + s.Should().NotBeNull(); + await WaitHelpers.Delay2XMinimum(); + }); + }); + [Fact] + public async Task AsyncTaskWithParameter_OnSubSpan() + => await AssertWith1TransactionAnd1SpanAsyncOnSubSpan(async s => + { + await s.CaptureSpan(SpanName, SpanType, + async s2 => + { + s2.Should().NotBeNull(); + await WaitHelpers.Delay2XMinimum(); + }); + }); - /// - /// Tests the method. - /// It wraps a fake async span (Task.Delay) with a return value into the CaptureSpan method - /// and it makes sure that the span is captured by the agent and the return value is correct. - /// - [Fact] - public async Task AsyncTaskWithReturnType() - => await AssertWith1TransactionAnd1SpanAsync(async t => + /// + /// Tests the method with an + /// exception. + /// It wraps a fake async span (Task.Delay) that throws an exception into the CaptureSpan method with an + /// parameter + /// and it makes sure that the span and the error are captured and the parameter is not null. + /// + [Fact] + public async Task AsyncTaskWithExceptionAndParameter() + => await AssertWith1TransactionAnd1ErrorAnd1SpanAsync(async t => + { + var act = async () => { - var res = await t.CaptureSpan(SpanName, SpanType, async () => + await t.CaptureSpan(SpanName, SpanType, async s => { + s.Should().NotBeNull(); await WaitHelpers.Delay2XMinimum(); - return 42; + throw new InvalidOperationException(ExceptionMessage); }); - res.Should().Be(42); - }); + }; + await act.Should().ThrowAsync(); + }); - [Fact] - public async Task AsyncTaskWithReturnType_OnSubSpan() - => await AssertWith1TransactionAnd1SpanAsyncOnSubSpan(async s => + [Fact] + public async Task AsyncTaskWithExceptionAndParameter_OnSubSpan() + => await AssertWith1TransactionAnd1ErrorAnd1SpanAsyncOnSubSpan(async s => + { + var act = async () => { - var res = await s.CaptureSpan(SpanName, SpanType, async () => + await s.CaptureSpan(SpanName, SpanType, async s2 => { + s2.Should().NotBeNull(); await WaitHelpers.Delay2XMinimum(); - return 42; + throw new InvalidOperationException(ExceptionMessage); }); - res.Should().Be(42); - }); + }; + await act.Should().ThrowAsync(); + }); - /// - /// Tests the method. - /// It wraps a fake async span (Task.Delay) with a return value into the CaptureSpan method with an - /// parameter - /// and it makes sure that the span is captured by the agent and the return value is correct and the - /// is not null. - /// - [Fact] - public async Task AsyncTaskWithReturnTypeAndParameter() - => await AssertWith1TransactionAnd1SpanAsync(async t => + + /// + /// Tests the method. + /// It wraps a fake async span (Task.Delay) with a return value into the CaptureSpan method + /// and it makes sure that the span is captured by the agent and the return value is correct. + /// + [Fact] + public async Task AsyncTaskWithReturnType() + => await AssertWith1TransactionAnd1SpanAsync(async t => + { + var res = await t.CaptureSpan(SpanName, SpanType, async () => { - var res = await t.CaptureSpan(SpanName, SpanType, - async s => - { - s.Should().NotBeNull(); - await WaitHelpers.Delay2XMinimum(); - return 42; - }); - - res.Should().Be(42); + await WaitHelpers.Delay2XMinimum(); + return 42; }); + res.Should().Be(42); + }); - [Fact] - public async Task AsyncTaskWithReturnTypeAndParameter_OnSubSpan() - => await AssertWith1TransactionAnd1SpanAsyncOnSubSpan(async s => + [Fact] + public async Task AsyncTaskWithReturnType_OnSubSpan() + => await AssertWith1TransactionAnd1SpanAsyncOnSubSpan(async s => + { + var res = await s.CaptureSpan(SpanName, SpanType, async () => { - var res = await s.CaptureSpan(SpanName, SpanType, - async s2 => - { - s2.Should().NotBeNull(); - await WaitHelpers.Delay2XMinimum(); - return 42; - }); - - res.Should().Be(42); + await WaitHelpers.Delay2XMinimum(); + return 42; }); + res.Should().Be(42); + }); - /// - /// Tests the method with - /// an exception. - /// It wraps a fake async span (Task.Delay) with a return value that throws an exception into the CaptureSpan method with - /// an parameter - /// and it makes sure that the span and the error are captured by the agent and the return value is correct and the - /// is not null. - /// - [Fact] - public async Task AsyncTaskWithReturnTypeAndExceptionAndParameter() - => await AssertWith1TransactionAnd1ErrorAnd1SpanAsync(async t => - { - Func act = async () => + /// + /// Tests the method. + /// It wraps a fake async span (Task.Delay) with a return value into the CaptureSpan method with an + /// parameter + /// and it makes sure that the span is captured by the agent and the return value is correct and the + /// is not null. + /// + [Fact] + public async Task AsyncTaskWithReturnTypeAndParameter() + => await AssertWith1TransactionAnd1SpanAsync(async t => + { + var res = await t.CaptureSpan(SpanName, SpanType, + async s => { - await t.CaptureSpan(SpanName, SpanType, async s => - { - s.Should().NotBeNull(); - await WaitHelpers.Delay2XMinimum(); - - if (new Random().Next(1) == 0) //avoid unreachable code warning. - throw new InvalidOperationException(ExceptionMessage); - - return 42; - }); - }; - await act.Should().ThrowAsync(); - }); + s.Should().NotBeNull(); + await WaitHelpers.Delay2XMinimum(); + return 42; + }); - [Fact] - public async Task AsyncTaskWithReturnTypeAndExceptionAndParameter_OnSubSpan() - => await AssertWith1TransactionAnd1ErrorAnd1SpanAsyncOnSubSpan(async s => - { - Func act = async () => + res.Should().Be(42); + }); + + [Fact] + public async Task AsyncTaskWithReturnTypeAndParameter_OnSubSpan() + => await AssertWith1TransactionAnd1SpanAsyncOnSubSpan(async s => + { + var res = await s.CaptureSpan(SpanName, SpanType, + async s2 => { - await s.CaptureSpan(SpanName, SpanType, async s2 => - { - s2.Should().NotBeNull(); - await WaitHelpers.Delay2XMinimum(); - - if (new Random().Next(1) == 0) //avoid unreachable code warning. - throw new InvalidOperationException(ExceptionMessage); - - return 42; - }); - }; - await act.Should().ThrowAsync(); - }); + s2.Should().NotBeNull(); + await WaitHelpers.Delay2XMinimum(); + return 42; + }); - /// - /// Tests the method with an - /// exception. - /// It wraps a fake async span (Task.Delay) with a return value that throws an exception into the CaptureSpan method - /// and it makes sure that the span and the error are captured by the agent. - /// - [Fact] - public async Task AsyncTaskWithReturnTypeAndException() - => await AssertWith1TransactionAnd1ErrorAnd1SpanAsync(async t => + res.Should().Be(42); + }); + + /// + /// Tests the method with + /// an exception. + /// It wraps a fake async span (Task.Delay) with a return value that throws an exception into the CaptureSpan method with + /// an parameter + /// and it makes sure that the span and the error are captured by the agent and the return value is correct and the + /// is not null. + /// + [Fact] + public async Task AsyncTaskWithReturnTypeAndExceptionAndParameter() + => await AssertWith1TransactionAnd1ErrorAnd1SpanAsync(async t => + { + var act = async () => { - Func act = async () => + await t.CaptureSpan(SpanName, SpanType, async s => { - await t.CaptureSpan(SpanName, SpanType, async () => - { - await WaitHelpers.Delay2XMinimum(); + s.Should().NotBeNull(); + await WaitHelpers.Delay2XMinimum(); - if (new Random().Next(1) == 0) //avoid unreachable code warning. - throw new InvalidOperationException(ExceptionMessage); + if (new Random().Next(1) == 0) //avoid unreachable code warning. + throw new InvalidOperationException(ExceptionMessage); - return 42; - }); - }; - await act.Should().ThrowAsync(); - }); + return 42; + }); + }; + await act.Should().ThrowAsync(); + }); - [Fact] - public async Task AsyncTaskWithReturnTypeAndException_OnSubSpan() - => await AssertWith1TransactionAnd1ErrorAnd1SpanAsyncOnSubSpan(async s => + [Fact] + public async Task AsyncTaskWithReturnTypeAndExceptionAndParameter_OnSubSpan() + => await AssertWith1TransactionAnd1ErrorAnd1SpanAsyncOnSubSpan(async s => + { + var act = async () => { - Func act = async () => + await s.CaptureSpan(SpanName, SpanType, async s2 => { - await s.CaptureSpan(SpanName, SpanType, async () => - { - await WaitHelpers.Delay2XMinimum(); + s2.Should().NotBeNull(); + await WaitHelpers.Delay2XMinimum(); - if (new Random().Next(1) == 0) //avoid unreachable code warning. - throw new InvalidOperationException(ExceptionMessage); + if (new Random().Next(1) == 0) //avoid unreachable code warning. + throw new InvalidOperationException(ExceptionMessage); - return 42; - }); - }; - await act.Should().ThrowAsync(); - }); + return 42; + }); + }; + await act.Should().ThrowAsync(); + }); - /// - /// Wraps a cancelled task into the CaptureSpan method and - /// makes sure that the cancelled task is captured by the agent. - /// - [Fact] - public async Task CancelledAsyncTask() + /// + /// Tests the method with an + /// exception. + /// It wraps a fake async span (Task.Delay) with a return value that throws an exception into the CaptureSpan method + /// and it makes sure that the span and the error are captured by the agent. + /// + [Fact] + public async Task AsyncTaskWithReturnTypeAndException() + => await AssertWith1TransactionAnd1ErrorAnd1SpanAsync(async t => { - using var agent = new ApmAgent(new TestAgentComponents()); - - var cancellationTokenSource = new CancellationTokenSource(); - var token = cancellationTokenSource.Token; - cancellationTokenSource.Cancel(); - - await agent.Tracer.CaptureTransaction(TransactionName, TransactionType, async t => + var act = async () => { - Func act = async () => + await t.CaptureSpan(SpanName, SpanType, async () => { - await t.CaptureSpan(SpanName, SpanType, async () => - { - // ReSharper disable once MethodSupportsCancellation, we want to delay before we throw the exception - await WaitHelpers.Delay2XMinimum(); - token.ThrowIfCancellationRequested(); - }); - }; - await act.Should().ThrowAsync(); - }); - } + await WaitHelpers.Delay2XMinimum(); + + if (new Random().Next(1) == 0) //avoid unreachable code warning. + throw new InvalidOperationException(ExceptionMessage); - /// - /// Creates a custom span and adds a label to it. - /// Makes sure that the label is stored on Span.Context. - /// - [Fact] - public void LabelsOnSpan() + return 42; + }); + }; + await act.Should().ThrowAsync(); + }); + + [Fact] + public async Task AsyncTaskWithReturnTypeAndException_OnSubSpan() + => await AssertWith1TransactionAnd1ErrorAnd1SpanAsyncOnSubSpan(async s => { - var payloadSender = AssertWith1TransactionAnd1Span( - t => + var act = async () => + { + await s.CaptureSpan(SpanName, SpanType, async () => { - t.CaptureSpan(SpanName, SpanType, span => - { - WaitHelpers.Sleep2XMinimum(); - span.SetLabel("foo", "bar"); - }); + await WaitHelpers.Delay2XMinimum(); + + if (new Random().Next(1) == 0) //avoid unreachable code warning. + throw new InvalidOperationException(ExceptionMessage); + + return 42; }); + }; + await act.Should().ThrowAsync(); + }); - //According to the Intake API labels are stored on the Context (and not on Spans.Labels directly). - payloadSender.SpansOnFirstTransaction[0].Context.InternalLabels.Value.InnerDictionary["foo"].Value.Should().Be("bar"); - } + /// + /// Wraps a cancelled task into the CaptureSpan method and + /// makes sure that the cancelled task is captured by the agent. + /// + [Fact] + public async Task CancelledAsyncTask() + { + using var agent = new ApmAgent(new TestAgentComponents()); + + var cancellationTokenSource = new CancellationTokenSource(); + var token = cancellationTokenSource.Token; + cancellationTokenSource.Cancel(); - /// - /// Creates a custom async span and adds a label to it. - /// Makes sure that the label is stored on Span.Context. - /// - [Fact] - public async Task LabelsOnSpanAsync() + await agent.Tracer.CaptureTransaction(TransactionName, TransactionType, async t => { - var payloadSender = await AssertWith1TransactionAnd1SpanAsync( - async t => + var act = async () => + { + await t.CaptureSpan(SpanName, SpanType, async () => { - await t.CaptureSpan(SpanName, SpanType, async span => - { - await WaitHelpers.Delay2XMinimum(); - span.SetLabel("foo", "bar"); - }); + // ReSharper disable once MethodSupportsCancellation, we want to delay before we throw the exception + await WaitHelpers.Delay2XMinimum(); + token.ThrowIfCancellationRequested(); }); + }; + await act.Should().ThrowAsync(); + }); + } - //According to the Intake API labels are stored on the Context (and not on Spans.Labels directly). - payloadSender.SpansOnFirstTransaction[0].Context.InternalLabels.Value.MergedDictionary["foo"].Value.Should().Be("bar"); - } + /// + /// Creates a custom span and adds a label to it. + /// Makes sure that the label is stored on Span.Context. + /// + [Fact] + public void LabelsOnSpan() + { + var payloadSender = AssertWith1TransactionAnd1Span( + t => + { + t.CaptureSpan(SpanName, SpanType, span => + { + WaitHelpers.Sleep2XMinimum(); + span.SetLabel("foo", "bar"); + }); + }); - /// - /// Creates a custom async span that ends with an error and adds a label to it. - /// Makes sure that the label is stored on Span.Context. - /// - [Fact] - public async Task LabelsOnSpanAsyncError() - { - var payloadSender = await AssertWith1TransactionAnd1ErrorAnd1SpanAsync(async t => + //According to the Intake API labels are stored on the Context (and not on Spans.Labels directly). + payloadSender.SpansOnFirstTransaction[0].Context.InternalLabels.Value.InnerDictionary["foo"].Value.Should().Be("bar"); + } + + /// + /// Creates a custom async span and adds a label to it. + /// Makes sure that the label is stored on Span.Context. + /// + [Fact] + public async Task LabelsOnSpanAsync() + { + var payloadSender = await AssertWith1TransactionAnd1SpanAsync( + async t => { - Func act = async () => + await t.CaptureSpan(SpanName, SpanType, async span => { - await t.CaptureSpan(SpanName, SpanType, async span => - { - await WaitHelpers.Delay2XMinimum(); - span.SetLabel("foo", "bar"); - - if (new Random().Next(1) == 0) //avoid unreachable code warning. - throw new InvalidOperationException(ExceptionMessage); - }); - }; - await act.Should().ThrowAsync(); + await WaitHelpers.Delay2XMinimum(); + span.SetLabel("foo", "bar"); + }); }); - //According to the Intake API labels are stored on the Context (and not on Spans.Labels directly). - payloadSender.SpansOnFirstTransaction[0].Context.InternalLabels.Value.MergedDictionary["foo"].Value.Should().Be("bar"); - } + //According to the Intake API labels are stored on the Context (and not on Spans.Labels directly). + payloadSender.SpansOnFirstTransaction[0].Context.InternalLabels.Value.MergedDictionary["foo"].Value.Should().Be("bar"); + } - /// - /// Creates 1 span with db information on it and creates a 2. span with http information on it. - /// Makes sure the db and http info is captured on the span's context. - /// - [Fact] - public void FillSpanContext() + /// + /// Creates a custom async span that ends with an error and adds a label to it. + /// Makes sure that the label is stored on Span.Context. + /// + [Fact] + public async Task LabelsOnSpanAsyncError() + { + var payloadSender = await AssertWith1TransactionAnd1ErrorAnd1SpanAsync(async t => { - var payloadSender = new MockPayloadSender(); - using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); - - agent.Tracer.CaptureTransaction(TransactionName, TransactionType, t => + var act = async () => { - WaitHelpers.SleepMinimum(); - t.CaptureSpan("SampleSpan1", "SampleSpanType", - span => { span.Context.Http = new Http { Url = "http://mysite.com", Method = "GET", StatusCode = 200 }; }, isExitSpan: true); - - t.CaptureSpan("SampleSpan2", "SampleSpanType", - span => - { - span.Context.Db = new Database { Statement = "Select * from MyTable", Type = Database.TypeSql, Instance = "MyInstance" }; - }, isExitSpan: true); - }); + await t.CaptureSpan(SpanName, SpanType, async span => + { + await WaitHelpers.Delay2XMinimum(); + span.SetLabel("foo", "bar"); - payloadSender.Spans[0].Name.Should().Be("SampleSpan1"); - payloadSender.Spans[0].Context.Http.Url.Should().Be("http://mysite.com"); - payloadSender.Spans[0].Context.Http.Method.Should().Be("GET"); - payloadSender.Spans[0].Context.Http.StatusCode.Should().Be(200); - payloadSender.Spans[0].Context.Destination.Address.Should().Be("mysite.com"); - payloadSender.Spans[0].Context.Destination.Port.Should().Be(UrlUtilsTests.DefaultHttpPort); - - payloadSender.Spans[1].Name.Should().Be("SampleSpan2"); - payloadSender.Spans[1].Context.Db.Statement.Should().Be("Select * from MyTable"); - payloadSender.Spans[1].Context.Db.Type.Should().Be(Database.TypeSql); - payloadSender.Spans[1].Context.Db.Instance.Should().Be("MyInstance"); - } + if (new Random().Next(1) == 0) //avoid unreachable code warning. + throw new InvalidOperationException(ExceptionMessage); + }); + }; + await act.Should().ThrowAsync(); + }); - [Fact] - public void CaptureErrorLogOnSpan() + //According to the Intake API labels are stored on the Context (and not on Spans.Labels directly). + payloadSender.SpansOnFirstTransaction[0].Context.InternalLabels.Value.MergedDictionary["foo"].Value.Should().Be("bar"); + } + + /// + /// Creates 1 span with db information on it and creates a 2. span with http information on it. + /// Makes sure the db and http info is captured on the span's context. + /// + [Fact] + public void FillSpanContext() + { + var payloadSender = new MockPayloadSender(); + using var agent = + new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, configuration: new MockConfiguration(exitSpanMinDuration: "0"))); + + agent.Tracer.CaptureTransaction(TransactionName, TransactionType, t => { - var payloadSender = new MockPayloadSender(); - using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); + WaitHelpers.SleepMinimum(); + t.CaptureSpan("SampleSpan1", "SampleSpanType", + span => { span.Context.Http = new Http { Url = "http://mysite.com", Method = "GET", StatusCode = 200 }; }, isExitSpan: true); - var errorLog = new ErrorLog("foo") { Level = "error", ParamMessage = "42" }; + t.CaptureSpan("SampleSpan2", "SampleSpanType", + span => + { + span.Context.Db = new Database { Statement = "Select * from MyTable", Type = Database.TypeSql, Instance = "MyInstance" }; + }, isExitSpan: true); + }); + + payloadSender.Spans[0].Name.Should().Be("SampleSpan1"); + payloadSender.Spans[0].Context.Http.Url.Should().Be("http://mysite.com"); + payloadSender.Spans[0].Context.Http.Method.Should().Be("GET"); + payloadSender.Spans[0].Context.Http.StatusCode.Should().Be(200); + payloadSender.Spans[0].Context.Destination.Address.Should().Be("mysite.com"); + payloadSender.Spans[0].Context.Destination.Port.Should().Be(UrlUtilsTests.DefaultHttpPort); + + payloadSender.Spans[1].Name.Should().Be("SampleSpan2"); + payloadSender.Spans[1].Context.Db.Statement.Should().Be("Select * from MyTable"); + payloadSender.Spans[1].Context.Db.Type.Should().Be(Database.TypeSql); + payloadSender.Spans[1].Context.Db.Instance.Should().Be("MyInstance"); + } + + [Fact] + public void CaptureErrorLogOnSpan() + { + var payloadSender = new MockPayloadSender(); + using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); - agent.Tracer.CaptureTransaction("foo", "bar", t => { t.CaptureSpan("foo", "bar", s => { s.CaptureErrorLog(errorLog); }); }); + var errorLog = new ErrorLog("foo") { Level = "error", ParamMessage = "42" }; - payloadSender.WaitForAny(); + agent.Tracer.CaptureTransaction("foo", "bar", t => { t.CaptureSpan("foo", "bar", s => { s.CaptureErrorLog(errorLog); }); }); - payloadSender.Transactions.Should().HaveCount(1); - payloadSender.Spans.Should().HaveCount(1); - payloadSender.Errors.Should().HaveCount(1); - payloadSender.FirstError.Log.Message.Should().Be("foo"); - payloadSender.FirstError.Log.Level.Should().Be("error"); - payloadSender.FirstError.Log.ParamMessage.Should().Be("42"); - payloadSender.FirstError.ParentId.Should().Be(payloadSender.FirstSpan.Id); - } + payloadSender.WaitForAny(); - /// - /// Asserts on 1 transaction with 1 async span and 1 error - /// - private async Task AssertWith1TransactionAnd1ErrorAnd1SpanAsync(Func func) + payloadSender.Transactions.Should().HaveCount(1); + payloadSender.Spans.Should().HaveCount(1); + payloadSender.Errors.Should().HaveCount(1); + payloadSender.FirstError.Log.Message.Should().Be("foo"); + payloadSender.FirstError.Log.Level.Should().Be("error"); + payloadSender.FirstError.Log.ParamMessage.Should().Be("42"); + payloadSender.FirstError.ParentId.Should().Be(payloadSender.FirstSpan.Id); + } + + /// + /// Asserts on 1 transaction with 1 async span and 1 error + /// + private async Task AssertWith1TransactionAnd1ErrorAnd1SpanAsync(Func func) + { + var payloadSender = new MockPayloadSender(); + using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); + + await agent.Tracer.CaptureTransaction(TransactionName, TransactionType, async t => { - var payloadSender = new MockPayloadSender(); - using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); + await WaitHelpers.DelayMinimum(); + await func(t); + }); - await agent.Tracer.CaptureTransaction(TransactionName, TransactionType, async t => - { - await WaitHelpers.DelayMinimum(); - await func(t); - }); + payloadSender.WaitForTransactions(); + payloadSender.Transactions.Should().NotBeEmpty(); - payloadSender.WaitForTransactions(); - payloadSender.Transactions.Should().NotBeEmpty(); + payloadSender.FirstTransaction.Name.Should().Be(TransactionName); + payloadSender.FirstTransaction.Type.Should().Be(TransactionType); - payloadSender.FirstTransaction.Name.Should().Be(TransactionName); - payloadSender.FirstTransaction.Type.Should().Be(TransactionType); + var duration = payloadSender.FirstTransaction.Duration; + duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); - var duration = payloadSender.FirstTransaction.Duration; - duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); + payloadSender.WaitForSpans(); + payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); - payloadSender.WaitForSpans(); - payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); + payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); + payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); - payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); - payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); + payloadSender.WaitForErrors(); + payloadSender.Errors.Should().NotBeEmpty(); - payloadSender.WaitForErrors(); - payloadSender.Errors.Should().NotBeEmpty(); + payloadSender.FirstError.Exception.Type.Should().Be(typeof(InvalidOperationException).FullName); + payloadSender.FirstError.Exception.Message.Should().Be(ExceptionMessage); - payloadSender.FirstError.Exception.Type.Should().Be(typeof(InvalidOperationException).FullName); - payloadSender.FirstError.Exception.Message.Should().Be(ExceptionMessage); + return payloadSender; + } - return payloadSender; - } + private async Task AssertWith1TransactionAnd1ErrorAnd1SpanAsyncOnSubSpan(Func func) + { + var payloadSender = new MockPayloadSender(); + using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); - private async Task AssertWith1TransactionAnd1ErrorAnd1SpanAsyncOnSubSpan(Func func) + await agent.Tracer.CaptureTransaction(TransactionName, TransactionType, async t => { - var payloadSender = new MockPayloadSender(); - using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); + await WaitHelpers.DelayMinimum(); - await agent.Tracer.CaptureTransaction(TransactionName, TransactionType, async t => + await t.CaptureSpan("TestSpan", "TestSpan", async s => { await WaitHelpers.DelayMinimum(); - - await t.CaptureSpan("TestSpan", "TestSpanType", async s => - { - await WaitHelpers.DelayMinimum(); - await func(s); - }); + await func(s); }); + }); - payloadSender.WaitForTransactions(); - payloadSender.Transactions.Should().NotBeEmpty(); + payloadSender.WaitForTransactions(); + payloadSender.Transactions.Should().NotBeEmpty(); - payloadSender.FirstTransaction.Name.Should().Be(TransactionName); - payloadSender.FirstTransaction.Type.Should().Be(TransactionType); + payloadSender.FirstTransaction.Name.Should().Be(TransactionName); + payloadSender.FirstTransaction.Type.Should().Be(TransactionType); - var duration = payloadSender.FirstTransaction.Duration; - duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); + var duration = payloadSender.FirstTransaction.Duration; + duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); - payloadSender.WaitForSpans(); - payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); + payloadSender.WaitForSpans(); + payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); - payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); - payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); + payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); + payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); - payloadSender.WaitForErrors(); - payloadSender.Errors.Should().NotBeEmpty(); + payloadSender.WaitForErrors(); + payloadSender.Errors.Should().NotBeEmpty(); - payloadSender.FirstError.Exception.Type.Should().Be(typeof(InvalidOperationException).FullName); - payloadSender.FirstError.Exception.Message.Should().Be(ExceptionMessage); + payloadSender.FirstError.Exception.Type.Should().Be(typeof(InvalidOperationException).FullName); + payloadSender.FirstError.Exception.Message.Should().Be(ExceptionMessage); - var orderedSpans = payloadSender.Spans.OrderBy(n => n.Timestamp).ToList(); + var orderedSpans = payloadSender.Spans.OrderBy(n => n.Timestamp).ToList(); - var firstSpan = orderedSpans.First(); - var innerSpan = orderedSpans.Last(); + var firstSpan = orderedSpans.First(); + var innerSpan = orderedSpans.Last(); - firstSpan.ParentId.Should().Be(payloadSender.FirstTransaction.Id); - innerSpan.ParentId.Should().Be(firstSpan.Id); + firstSpan.ParentId.Should().Be(payloadSender.FirstTransaction.Id); + innerSpan.ParentId.Should().Be(firstSpan.Id); - firstSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); - innerSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); + firstSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); + innerSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); - return payloadSender; - } + return payloadSender; + } - /// - /// Asserts on 1 transaction with 1 async Span - /// - private async Task AssertWith1TransactionAnd1SpanAsync(Func func) + /// + /// Asserts on 1 transaction with 1 async Span + /// + private async Task AssertWith1TransactionAnd1SpanAsync(Func func) + { + var payloadSender = new MockPayloadSender(); + using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); + + await agent.Tracer.CaptureTransaction(TransactionName, TransactionType, async t => { - var payloadSender = new MockPayloadSender(); - using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); + await WaitHelpers.DelayMinimum(); + await func(t); + }); - await agent.Tracer.CaptureTransaction(TransactionName, TransactionType, async t => - { - await WaitHelpers.DelayMinimum(); - await func(t); - }); + payloadSender.WaitForTransactions(); + payloadSender.Transactions.Should().NotBeEmpty(); - payloadSender.WaitForTransactions(); - payloadSender.Transactions.Should().NotBeEmpty(); + payloadSender.FirstTransaction.Name.Should().Be(TransactionName); + payloadSender.FirstTransaction.Type.Should().Be(TransactionType); - payloadSender.FirstTransaction.Name.Should().Be(TransactionName); - payloadSender.FirstTransaction.Type.Should().Be(TransactionType); + var duration = payloadSender.FirstTransaction.Duration; + duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); - var duration = payloadSender.FirstTransaction.Duration; - duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); + payloadSender.WaitForSpans(); + payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); - payloadSender.WaitForSpans(); - payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); + payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); + payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); - payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); - payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); + return payloadSender; + } - return payloadSender; - } + private async Task AssertWith1TransactionAnd1SpanAsyncOnSubSpan(Func func) + { + var payloadSender = new MockPayloadSender(); + using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); - private async Task AssertWith1TransactionAnd1SpanAsyncOnSubSpan(Func func) + await agent.Tracer.CaptureTransaction(TransactionName, TransactionType, async t => { - var payloadSender = new MockPayloadSender(); - using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); + await WaitHelpers.DelayMinimum(); - await agent.Tracer.CaptureTransaction(TransactionName, TransactionType, async t => + await t.CaptureSpan("SubSpan", "SubSpanType", async s => { await WaitHelpers.DelayMinimum(); - - await t.CaptureSpan("SubSpan", "SubSpanType", async s => - { - await WaitHelpers.DelayMinimum(); - await func(s); - }); + await func(s); }); + }); - payloadSender.WaitForTransactions(); - payloadSender.Transactions.Should().NotBeEmpty(); + payloadSender.WaitForTransactions(); + payloadSender.Transactions.Should().NotBeEmpty(); - payloadSender.FirstTransaction.Name.Should().Be(TransactionName); - payloadSender.FirstTransaction.Type.Should().Be(TransactionType); + payloadSender.FirstTransaction.Name.Should().Be(TransactionName); + payloadSender.FirstTransaction.Type.Should().Be(TransactionType); - var duration = payloadSender.FirstTransaction.Duration; - duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); + var duration = payloadSender.FirstTransaction.Duration; + duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); - payloadSender.WaitForSpans(); - payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); + payloadSender.WaitForSpans(); + payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); - payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); - payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); + payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); + payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); - var orderedSpans = payloadSender.Spans.OrderBy(n => n.Timestamp).ToList(); + var orderedSpans = payloadSender.Spans.OrderBy(n => n.Timestamp).ToList(); - var firstSpan = orderedSpans.First(); - var innerSpan = orderedSpans.Last(); + var firstSpan = orderedSpans.First(); + var innerSpan = orderedSpans.Last(); - firstSpan.ParentId.Should().Be(payloadSender.FirstTransaction.Id); - innerSpan.ParentId.Should().Be(firstSpan.Id); + firstSpan.ParentId.Should().Be(payloadSender.FirstTransaction.Id); + innerSpan.ParentId.Should().Be(firstSpan.Id); - firstSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); - innerSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); + firstSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); + innerSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); - return payloadSender; - } + return payloadSender; + } - /// - /// Asserts on 1 transaction with 1 span - /// - private MockPayloadSender AssertWith1TransactionAnd1Span(Action action) + /// + /// Asserts on 1 transaction with 1 span + /// + private MockPayloadSender AssertWith1TransactionAnd1Span(Action action) + { + var payloadSender = new MockPayloadSender(); + using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); + + agent.Tracer.CaptureTransaction(TransactionName, TransactionType, t => { - var payloadSender = new MockPayloadSender(); - using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); + WaitHelpers.SleepMinimum(); + action(t); + }); - agent.Tracer.CaptureTransaction(TransactionName, TransactionType, t => - { - WaitHelpers.SleepMinimum(); - action(t); - }); + payloadSender.WaitForTransactions(); + payloadSender.Transactions.Should().NotBeEmpty(); - payloadSender.WaitForTransactions(); - payloadSender.Transactions.Should().NotBeEmpty(); + payloadSender.FirstTransaction.Name.Should().Be(TransactionName); + payloadSender.FirstTransaction.Type.Should().Be(TransactionType); - payloadSender.FirstTransaction.Name.Should().Be(TransactionName); - payloadSender.FirstTransaction.Type.Should().Be(TransactionType); + payloadSender.WaitForSpans(); + payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); - payloadSender.WaitForSpans(); - payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); + payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); + payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); - payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); - payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); + var duration = payloadSender.FirstTransaction.Duration; + duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); - var duration = payloadSender.FirstTransaction.Duration; - duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); + return payloadSender; + } - return payloadSender; - } + private void AssertWith1TransactionAnd1SpanOnSubSpan(Action action) + { + var payloadSender = new MockPayloadSender(); + using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); - private void AssertWith1TransactionAnd1SpanOnSubSpan(Action action) + WaitHelpers.SleepMinimum(); + agent.Tracer.CaptureTransaction(TransactionName, TransactionType, t => { - var payloadSender = new MockPayloadSender(); - using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); - WaitHelpers.SleepMinimum(); - agent.Tracer.CaptureTransaction(TransactionName, TransactionType, t => + t.CaptureSpan("aa", "bb", s => //TODO Name { WaitHelpers.SleepMinimum(); - t.CaptureSpan("aa", "bb", s => //TODO Name - { - WaitHelpers.SleepMinimum(); - action(s); - }); + action(s); }); + }); - payloadSender.WaitForTransactions(); - payloadSender.Transactions.Should().NotBeEmpty(); + payloadSender.WaitForTransactions(); + payloadSender.Transactions.Should().NotBeEmpty(); - payloadSender.FirstTransaction.Name.Should().Be(TransactionName); - payloadSender.FirstTransaction.Type.Should().Be(TransactionType); + payloadSender.FirstTransaction.Name.Should().Be(TransactionName); + payloadSender.FirstTransaction.Type.Should().Be(TransactionType); - payloadSender.WaitForSpans(); - payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); + payloadSender.WaitForSpans(); + payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); - payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); - payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); + payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); + payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); - var duration = payloadSender.FirstTransaction.Duration; - duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); + var duration = payloadSender.FirstTransaction.Duration; + duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); - var orderedSpans = payloadSender.Spans.OrderBy(n => n.Timestamp).ToList(); + var orderedSpans = payloadSender.Spans.OrderBy(n => n.Timestamp).ToList(); - var firstSpan = orderedSpans.First(); - var innerSpan = orderedSpans.Last(); + var firstSpan = orderedSpans.First(); + var innerSpan = orderedSpans.Last(); - firstSpan.ParentId.Should().Be(payloadSender.FirstTransaction.Id); - innerSpan.ParentId.Should().Be(firstSpan.Id); + firstSpan.ParentId.Should().Be(payloadSender.FirstTransaction.Id); + innerSpan.ParentId.Should().Be(firstSpan.Id); - firstSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); - innerSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); - } + firstSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); + innerSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); + } - /// - /// Asserts on 1 transaction with 1 span and 1 error - /// - private void AssertWith1TransactionAnd1SpanAnd1Error(Action action) + /// + /// Asserts on 1 transaction with 1 span and 1 error + /// + private void AssertWith1TransactionAnd1SpanAnd1Error(Action action) + { + var payloadSender = new MockPayloadSender(); + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender))) { - var payloadSender = new MockPayloadSender(); - using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender))) + agent.Tracer.CaptureTransaction(TransactionName, TransactionType, t => { - agent.Tracer.CaptureTransaction(TransactionName, TransactionType, t => - { - WaitHelpers.SleepMinimum(); - action(t); - }); - } + WaitHelpers.SleepMinimum(); + action(t); + }); + } - payloadSender.WaitForTransactions(); - payloadSender.Transactions.Should().NotBeEmpty(); + payloadSender.WaitForTransactions(); + payloadSender.Transactions.Should().NotBeEmpty(); - payloadSender.FirstTransaction.Name.Should().Be(TransactionName); - payloadSender.FirstTransaction.Type.Should().Be(TransactionType); + payloadSender.FirstTransaction.Name.Should().Be(TransactionName); + payloadSender.FirstTransaction.Type.Should().Be(TransactionType); - var duration = payloadSender.FirstTransaction.Duration; - duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); + var duration = payloadSender.FirstTransaction.Duration; + duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); - payloadSender.WaitForSpans(); - payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); + payloadSender.WaitForSpans(); + payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); - payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); - payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); + payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); + payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); - payloadSender.WaitForErrors(); - payloadSender.Errors.Should().NotBeEmpty(); + payloadSender.WaitForErrors(); + payloadSender.Errors.Should().NotBeEmpty(); - payloadSender.FirstError.Exception.Type.Should().Be(typeof(InvalidOperationException).FullName); - payloadSender.FirstError.Exception.Message.Should().Be(ExceptionMessage); - } + payloadSender.FirstError.Exception.Type.Should().Be(typeof(InvalidOperationException).FullName); + payloadSender.FirstError.Exception.Message.Should().Be(ExceptionMessage); + } - /// - /// Asserts on 1 transaction with 1 span and 1 error - /// - private void AssertWith1TransactionAnd1SpanAnd1ErrorOnSubSpan(Action action) + /// + /// Asserts on 1 transaction with 1 span and 1 error + /// + private void AssertWith1TransactionAnd1SpanAnd1ErrorOnSubSpan(Action action) + { + var payloadSender = new MockPayloadSender(); + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender))) { - var payloadSender = new MockPayloadSender(); - using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender))) + WaitHelpers.SleepMinimum(); + agent.Tracer.CaptureTransaction(TransactionName, TransactionType, t => { WaitHelpers.SleepMinimum(); - agent.Tracer.CaptureTransaction(TransactionName, TransactionType, t => + + t.CaptureSpan("aa", "bb", s => { WaitHelpers.SleepMinimum(); - - t.CaptureSpan("aa", "bb", s => - { - WaitHelpers.SleepMinimum(); - action(s); - }); + action(s); }); - } + }); + } - payloadSender.WaitForTransactions(); - payloadSender.Transactions.Should().NotBeEmpty(); + payloadSender.WaitForTransactions(); + payloadSender.Transactions.Should().NotBeEmpty(); - payloadSender.FirstTransaction.Name.Should().Be(TransactionName); - payloadSender.FirstTransaction.Type.Should().Be(TransactionType); + payloadSender.FirstTransaction.Name.Should().Be(TransactionName); + payloadSender.FirstTransaction.Type.Should().Be(TransactionType); - var duration = payloadSender.FirstTransaction.Duration; - duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); + var duration = payloadSender.FirstTransaction.Duration; + duration.Should().BeGreaterOrEqualToMinimumSleepLength(3); - payloadSender.WaitForSpans(); - payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); + payloadSender.WaitForSpans(); + payloadSender.SpansOnFirstTransaction.Should().NotBeEmpty(); - payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); - payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); + payloadSender.SpansOnFirstTransaction[0].Name.Should().Be(SpanName); + payloadSender.SpansOnFirstTransaction[0].Type.Should().Be(SpanType); - payloadSender.WaitForErrors(); - payloadSender.Errors.Should().NotBeEmpty(); - payloadSender.Errors.Should().NotBeEmpty(); + payloadSender.WaitForErrors(); + payloadSender.Errors.Should().NotBeEmpty(); + payloadSender.Errors.Should().NotBeEmpty(); - payloadSender.FirstError.Exception.Type.Should().Be(typeof(InvalidOperationException).FullName); - payloadSender.FirstError.Exception.Message.Should().Be(ExceptionMessage); + payloadSender.FirstError.Exception.Type.Should().Be(typeof(InvalidOperationException).FullName); + payloadSender.FirstError.Exception.Message.Should().Be(ExceptionMessage); - var orderedSpans = payloadSender.Spans.OrderBy(n => n.Timestamp).ToList(); + var orderedSpans = payloadSender.Spans.OrderBy(n => n.Timestamp).ToList(); - var firstSpan = orderedSpans.First(); - var innerSpan = orderedSpans.Last(); + var firstSpan = orderedSpans.First(); + var innerSpan = orderedSpans.Last(); - firstSpan.ParentId.Should().Be(payloadSender.FirstTransaction.Id); - innerSpan.ParentId.Should().Be(firstSpan.Id); + firstSpan.ParentId.Should().Be(payloadSender.FirstTransaction.Id); + innerSpan.ParentId.Should().Be(firstSpan.Id); - firstSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); - innerSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); - } + firstSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); + innerSpan.TransactionId.Should().Be(payloadSender.FirstTransaction.Id); } } diff --git a/test/Elastic.Apm.Tests/ApiTests/DistributedTracingDataTests.cs b/test/Elastic.Apm.Tests/ApiTests/DistributedTracingDataTests.cs index fa676f584..efa2af579 100644 --- a/test/Elastic.Apm.Tests/ApiTests/DistributedTracingDataTests.cs +++ b/test/Elastic.Apm.Tests/ApiTests/DistributedTracingDataTests.cs @@ -294,6 +294,7 @@ private static async Task AssertInvalidDistributedTracingData(Func { t.StartSpan("foo", "bar").End(); @@ -29,7 +29,7 @@ public void TestNonExitSpan() public void SimpleManualExitSpanWithNoContext() { var payloadSender = new MockPayloadSender(); - using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); + using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, configuration: new MockConfiguration(exitSpanMinDuration:"0"))); agent.Tracer.CaptureTransaction("foo", "bar", t => { t.StartSpan("foo", "bar", isExitSpan: true).End(); @@ -43,7 +43,7 @@ public void SimpleManualExitSpanWithNoContext() public void SimpleManualExitSpanWithContext() { var payloadSender = new MockPayloadSender(); - using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender)); + using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, configuration: new MockConfiguration(exitSpanMinDuration:"0"))); agent.Tracer.CaptureTransaction("foo", "bar", t => { var span = t.StartSpan("foo", "bar", isExitSpan: true); diff --git a/test/Elastic.Apm.Tests/ConstructorTests.cs b/test/Elastic.Apm.Tests/ConstructorTests.cs index b4aabc3cc..ddda8b156 100644 --- a/test/Elastic.Apm.Tests/ConstructorTests.cs +++ b/test/Elastic.Apm.Tests/ConstructorTests.cs @@ -49,6 +49,7 @@ private class LogConfiguration : IConfiguration, IConfigurationSnapshotDescripti public bool Enabled { get; } public string Environment { get; } public string ServiceNodeName { get; } + public double ExitSpanMinDuration => ConfigConsts.DefaultValues.ExitSpanMinDurationInMilliseconds; public TimeSpan FlushInterval => TimeSpan.FromMilliseconds(ConfigConsts.DefaultValues.FlushIntervalInMilliseconds); public IReadOnlyDictionary GlobalLabels => new Dictionary(); public string HostName { get; } @@ -64,6 +65,9 @@ private class LogConfiguration : IConfiguration, IConfigurationSnapshotDescripti public Uri ServerUrl => ConfigConsts.DefaultValues.ServerUri; public string ServiceName { get; } public string ServiceVersion { get; } + public bool SpanCompressionEnabled => ConfigConsts.DefaultValues.SpanCompressionEnabled; + public double SpanCompressionExactMatchMaxDuration => ConfigConsts.DefaultValues.SpanCompressionExactMatchMaxDurationInMilliseconds; + public double SpanCompressionSameKindMaxDuration => ConfigConsts.DefaultValues.SpanCompressionSameKindMaxDurationInMilliseconds; public IReadOnlyList DisableMetrics => ConfigConsts.DefaultValues.DisableMetrics; public IReadOnlyList IgnoreMessageQueues => ConfigConsts.DefaultValues.IgnoreMessageQueues; public double SpanFramesMinDurationInMilliseconds => ConfigConsts.DefaultValues.SpanFramesMinDurationInMilliseconds; diff --git a/test/Elastic.Apm.Tests/ExitSpanMinDurationTests.cs b/test/Elastic.Apm.Tests/ExitSpanMinDurationTests.cs new file mode 100644 index 000000000..cd0523967 --- /dev/null +++ b/test/Elastic.Apm.Tests/ExitSpanMinDurationTests.cs @@ -0,0 +1,44 @@ +// 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.Linq; +using System.Threading; +using Elastic.Apm.Tests.Utilities; +using FluentAssertions; +using Xunit; + +namespace Elastic.Apm.Tests; + +public class ExitSpanMinDurationTests +{ + [Fact] + public void FastExitSpanTest() + { + var payloadSender = new MockPayloadSender(); + using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, + configuration: new MockConfiguration(exitSpanMinDuration: "100ms"))); + agent.Tracer.CaptureTransaction("foo", "bar", t => + { + t.CaptureSpan("span1", "test", () => Thread.Sleep(150), isExitSpan:true); + t.CaptureSpan("span2", "test", () => { }, isExitSpan:true); + t.CaptureSpan("span3", "test", () => Thread.Sleep(150), isExitSpan:true); + //Fast, but not exit span + t.CaptureSpan("span4", "test", () => { }); + + }); + + payloadSender.Transactions.Should().HaveCount(1); + payloadSender.Spans.Should().HaveCount(3); + + payloadSender.FirstTransaction.DroppedSpanStats.Should().NotBeEmpty(); + payloadSender.FirstTransaction.DroppedSpanStats.First().DestinationServiceResource.Should().Be("test"); + payloadSender.FirstTransaction.DroppedSpanStats.First().DurationCount.Should().Be(1); + + payloadSender.Spans[0].Name.Should().Be("span1"); + payloadSender.Spans[1].Name.Should().Be("span3"); + payloadSender.Spans[2].Name.Should().Be("span4"); + + } +} diff --git a/test/Elastic.Apm.Tests/SpanCompressionTests.cs b/test/Elastic.Apm.Tests/SpanCompressionTests.cs new file mode 100644 index 000000000..a65c0a78a --- /dev/null +++ b/test/Elastic.Apm.Tests/SpanCompressionTests.cs @@ -0,0 +1,199 @@ +// 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 System.Threading; +using Elastic.Apm.Api; +using Elastic.Apm.Tests.Utilities; +using FluentAssertions; +using Xunit; +using Elastic.Apm.Model; + +namespace Elastic.Apm.Tests +{ + public class SpanCompressionTests + { + /// + /// Db calls with exact match + /// + [Fact] + public void BasicDbCallsWithExactMatch() + { + var spanName = "Select * From Table"; + var payloadSender = new MockPayloadSender(); + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, + configuration: new MockConfiguration(spanCompressionEnabled: "true", spanCompressionExactMatchMaxDuration: "5s")))) + Generate10DbCalls(agent, spanName); + + payloadSender.Transactions.Should().HaveCount(1); + payloadSender.Spans.Should().HaveCount(1); + payloadSender.FirstSpan.Composite.Should().NotBeNull(); + payloadSender.FirstSpan.Composite.Count.Should().Be(10); + payloadSender.FirstSpan.Composite.CompressionStrategy = "exact_match"; + payloadSender.FirstSpan.Name.Should().Be(spanName); + } + + /// + /// Makes sure if no config is set, span compression is disabled + /// + [Fact] + public void DisabledByDefault() + { + var spanName = "Select * From Table"; + var payloadSender = new MockPayloadSender(); + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender))) Generate10DbCalls(agent, spanName, true, 2); + + payloadSender.Transactions.Should().HaveCount(1); + payloadSender.Spans.Should().HaveCount(10); + payloadSender.Spans.Where(s => (s as Span).Composite != null).Should().BeEmpty(); + } + + /// + /// Db calls with same kind + /// + [Fact] + public void BasicDbCallsWithSameKind() + { + var payloadSender = new MockPayloadSender(); + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, + configuration: new MockConfiguration(spanCompressionEnabled: "true", spanCompressionSameKindMaxDuration: "15s", spanCompressionExactMatchMaxDuration:"100ms", exitSpanMinDuration:"0")))) + Generate10DbCalls(agent, null, true, 200); + + payloadSender.Transactions.Should().HaveCount(1); + payloadSender.Spans.Should().HaveCount(1); + payloadSender.FirstSpan.Composite.Should().NotBeNull(); + payloadSender.FirstSpan.Composite.Count.Should().Be(10); + payloadSender.FirstSpan.Composite.CompressionStrategy = "same_kind"; + payloadSender.FirstSpan.Name.Should().Be("Calls to mssql"); + } + + /// + /// Creates 10db spans with exact match, then creates 1 non db span (which breaks compression), then creates 10 db spans again + /// + [Fact] + public void TwentyDbSpansWithRandomSpanInBetween() + { + var spanName = "Select * From Table"; + var payloadSender = new MockPayloadSender(); + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, + configuration: new MockConfiguration(spanCompressionEnabled: "true", spanCompressionExactMatchMaxDuration: "5s", exitSpanMinDuration:"0")))) + { + agent.Tracer.CaptureTransaction("Foo", "Bar", t => + { + for (var i = 0; i < 10; i++) + { + var name = spanName; + t.CaptureSpan(name, ApiConstants.TypeDb, (s) => + { + s.Context.Db = new Database() { Type = "mssql", Instance = "01" }; + }, ApiConstants.SubtypeMssql, isExitSpan: true); + } + + t.CaptureSpan("foo", "bar", () => { }); + + for (var i = 0; i < 10; i++) + { + var name = spanName; + t.CaptureSpan(name + "2", ApiConstants.TypeDb, (s) => + { + s.Context.Db = new Database() { Type = "mssql", Instance = "01" }; + }, ApiConstants.SubtypeMssql, isExitSpan: true); + } + }); + } + + payloadSender.Transactions.Should().HaveCount(1); + payloadSender.Spans.Should().HaveCount(3); + + payloadSender.FirstSpan.Composite.Should().NotBeNull(); + payloadSender.FirstSpan.Composite.Count.Should().Be(10); + payloadSender.FirstSpan.Composite.CompressionStrategy = "exact_match"; + payloadSender.FirstSpan.Name.Should().Be(spanName); + + payloadSender.Spans[1].Name.Should().Be("foo"); + (payloadSender.Spans[1] as Span)!.Composite.Should().BeNull(); + + (payloadSender.Spans[2] as Span)!.Composite.Should().NotBeNull(); + (payloadSender.Spans[2] as Span)!.Composite.Count.Should().Be(10); + (payloadSender.Spans[2] as Span)!.Composite.CompressionStrategy = "exact_match"; + payloadSender.Spans[2].Name.Should().Be(spanName + "2"); + } + + [Fact] + public void CompressionOnParentSpan() + { + var payloadSender = new MockPayloadSender(); + using (var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender, + configuration: new MockConfiguration(spanCompressionEnabled: "true", spanCompressionExactMatchMaxDuration: "5s", exitSpanMinDuration: "0")))) + { + agent.Tracer.CaptureTransaction("Foo", "Bar", t => + { + t.CaptureSpan("foo1", "bar", span1 => + { + for (var i = 0; i < 10; i++) + { + span1.CaptureSpan("Select * From Table1", ApiConstants.TypeDb, (s) => + { + s.Context.Db = new Database() { Type = "mssql", Instance = "01" }; + }, ApiConstants.SubtypeMssql, isExitSpan: true); + } + + span1.CaptureSpan("foo2", "randomSpan", () => { }); + + + for (var i = 0; i < 10; i++) + { + span1.CaptureSpan("Select * From Table2", ApiConstants.TypeDb, (s2) => + { + s2.Context.Db = new Database() { Type = "mssql", Instance = "01" }; + }, ApiConstants.SubtypeMssql, isExitSpan: true); + } + }); + }); + } + + payloadSender.Transactions.Should().HaveCount(1); + payloadSender.Spans.Should().HaveCount(4); + + (payloadSender.Spans[3] as Span)!.Composite.Should().BeNull(); + payloadSender.Spans[3].Name.Should().Be("foo1"); + + (payloadSender.Spans[0] as Span)!.Composite.Should().NotBeNull(); + (payloadSender.Spans[0] as Span)!.Composite.Count.Should().Be(10); + (payloadSender.Spans[0] as Span)!.Composite.CompressionStrategy = "exact_match"; + payloadSender.Spans[0].Name.Should().Be("Select * From Table1"); + + payloadSender.Spans[1].Name.Should().Be("foo2"); + (payloadSender.Spans[1] as Span)!.Composite.Should().BeNull(); + + (payloadSender.Spans[2] as Span)!.Composite.Should().NotBeNull(); + (payloadSender.Spans[2] as Span)!.Composite.Count.Should().Be(10); + (payloadSender.Spans[2] as Span)!.Composite.CompressionStrategy = "exact_match"; + payloadSender.Spans[2].Name.Should().Be("Select * From Table2"); + + + payloadSender.Spans[0].ParentId.Should().Be(payloadSender.Spans[3].Id); + payloadSender.Spans[1].ParentId.Should().Be(payloadSender.Spans[3].Id); + payloadSender.Spans[1].ParentId.Should().Be(payloadSender.Spans[3].Id); + } + + private void Generate10DbCalls(IApmAgent agent, string spanName, bool shouldSleep = false, int spanDuration = 10) => + agent.Tracer.CaptureTransaction("Foo", "Bar", t => + { + var random = new Random(); + for (var i = 0; i < 10; i++) + { + var name = spanName ?? "Foo" + new Random().Next(); + t.CaptureSpan(name, ApiConstants.TypeDb, (s) => + { + s.Context.Db = new Database() { Type = "mssql", Instance = "01" }; + if (shouldSleep) + Thread.Sleep(spanDuration); + }, ApiConstants.SubtypeMssql, isExitSpan: true); + } + }); + } +} diff --git a/test/Elastic.Apm.Tests/StackTraceTests.cs b/test/Elastic.Apm.Tests/StackTraceTests.cs index 55ab8e956..38afcd5af 100644 --- a/test/Elastic.Apm.Tests/StackTraceTests.cs +++ b/test/Elastic.Apm.Tests/StackTraceTests.cs @@ -134,7 +134,7 @@ public void TypeAndMethodNameTest() [Fact] public void StackTraceWithLambda() { - Action action = () => { TestMethod(); }; + var action = () => { TestMethod(); }; var payloadSender = new MockPayloadSender(); using var agent = new ApmAgent(new TestAgentComponents(payloadSender: payloadSender));