[Tracer] Stats computation: Drop P0 traces#3048
Conversation
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
pierotibou
left a comment
There was a problem hiding this comment.
A few general comments:
- It's a bit late for that, but I wonder if we shouldn't have given the AgentWriter the responsability to aggregate stats. We could maybe have put all stats related code in a processor called in the
TracerManager. This would have allowed us to handle in one place both dropping or not traces and aggregating stats. If I'm right we could have also moved all the extra logic inTraceContextthere. If what I'm suggesting makes sense, we could add this for a future refacto in the backlog. The only downside I see to the approach I'm suggesting is that we would need to pass the extra info that goes in headers down the pipeline, but that doesn't a big deal. - Talking about processors, I don't think we have plugged the obfuscation processors yet, have we?
- Also telemetry :)
|
|
||
| ArraySegment<Span> spansToWrite = default; | ||
|
|
||
| bool shouldKeepSpan = true; |
There was a problem hiding this comment.
Nit - ANd feel free to ignore me as I'm bad at this, but I would have named it shouldSendSpan because this is ultimately what we do
There was a problem hiding this comment.
Yeah I wasn't 100% sure myself whether to use the word "keep" or "send". We tend to have the keep/drop paradigm for sampling decisions, so I chose to use "keep" as much as possible
|
|
||
| if (span.GetMetric(Trace.Tags.Analytics) is double rate) | ||
| { | ||
| return ((span.TraceId * KnuthFactor) % TracerConstants.MaxTraceId) <= (rate * TracerConstants.MaxTraceId); |
There was a problem hiding this comment.
Not a fan of replicating this logic here, but we can rework it later, maybe in the SingleSpanIngestion OKR for instance
|
|
||
| private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor<StatsAggregator>(); | ||
|
|
||
| private readonly HashSet<StatsAggregationKey> _keys; |
There was a problem hiding this comment.
I have a hard time estimating the cardinality of a key. I assume it's driven mainly by resource right? We shouldn't have that many of them hopefully. I assume we don't but would we have this data somewhere?
There was a problem hiding this comment.
Yeah, this naïve implementation could start to build up without a way to limit the set. I think you're right that it's mainly driven by the cardinality of resource names. Here's how a key is composed, which is a unique combination of the following fields:
- Resource: Most likely to be high-cardinality
- Service: Low cardinality
- OperationName: Low cardinality?
- Type: Low cardinality?
- HttpStatusCode: Low cardinality. It's only expected on type
http(0 otherwise) and I expect the range of status codes to be limited - IsSyntheticsRequest: Low cardinality.
true/false
There was a problem hiding this comment.
Yep so resource is the one with the high cardinality (plus I suspect that will be the longest string as well). So the main factor for the size of this set will be resources, no?
Maybe there's a metric the stats team emit to follow the number of resources, that would give us an idea. We could ask on #apm-stats just in case.
And to be clear, without more data, I'm not challenging the implementation (best is the enemy of good)
There was a problem hiding this comment.
Just as a reminder, there's an ongoing issue with high cardinality of resource names for outgoing http requests that contain short identifier segments. There could be thousands of them 😬
I think you're right and that we can improve this with refactoring
Dangit, they're not plugged in. So, if the processors were initialized, they would be called correctly because the sequence of calls goes like this: However, I don't see them being initialized yet in our
🤦🏼 I created a JIRA to track this. |
|
What exactly is a |
|
I've added processors in #3054 |
Good question! For background, since applications can generate huge amounts of traces, it could be cost-prohibitive to send all spans to the backend. As a result, we traditionally would send all of the application's spans to the Datadog Agent, and the Datadog Agent would look at a number of factors to determine which traces should be forwarded to the backend and which could be dropped. One of the inputs into the decision-making is a field called |
Great question :) And What Zach said. If you want to learn more, this not yet officialy doc, should be your go to |
andrewlock
left a comment
There was a problem hiding this comment.
LGTM, mostly nits, though I wonder if we can refactor where the calculations happen, as you've already discussed
| var state = new SendTracesState(traces, numberOfTraces, statsComputationEnabled, numberOfDroppedP0Traces, numberOfDroppedP0Spans); | ||
|
|
||
| return SendWithRetry(_tracesEndpoint, _sendTraces, state); |
There was a problem hiding this comment.
nit (not related to this PR per-se), but as SendStatsState and SendTracesState are readonly struct, the state parameter in SendWithRetry() should be marked in to remove defensive copies. Same goes for child functions these are passed to, especially as SendTracesState is getting bigger
There was a problem hiding this comment.
That's a good point. Is it okay if we defer this topic for a follow up PR?
| /// </summary> | ||
| internal StatsBuffer CurrentBuffer => _buffers[_currentBuffer]; | ||
|
|
||
| public bool? CanComputeStats { get; private set; } = true; |
There was a problem hiding this comment.
Just noting that as per the PR description this is set to null initially and updated accordingly based on agent support in the follow up PR #3049
| if ((!span.IsTopLevel && span.GetMetric(Tags.Measured) != 1.0) || span.GetMetric(Tags.PartialSnapshot) > 0) | ||
| { | ||
| return; | ||
| return false; | ||
| } |
There was a problem hiding this comment.
This could still be a "rare" key couldn't it? It could also have an error....
There was a problem hiding this comment.
Looking at the RareSampler, the sampler only runs on top-level/measured spans, so we're consistent there.
As for the error part, I think you've identified a bug in the Java implementation. That's the reference implementation I followed, where errors are only checked on top-level/measured spans. However, the agent checks for errors on any span in the trace chunk. I will revise this.
There was a problem hiding this comment.
Thinking on this one particular issue more, the bug you identified was that we could return false even when there's an error. However, I don't think this is actually an issue. This return value is only used as a fallback after the TraceContext ran its rules. When the TraceContext closed the trace and sent it to serialization, we would have already detected that there was at least one error within the trace so we would have called AgentWriter.SerializeTrace(ArraySegment<Span> trace, bool shouldSerializeSpans) with the value true which would disregard the value returned here and send the trace to the trace agent. So, this is only a fallback and the initial decision resulted in sending the error-containing trace.
This will go away soon with the new refactor, but I thought it was worth reporting
| } | ||
|
|
||
| public void WriteTrace(ArraySegment<Span> trace) | ||
| public void WriteTrace(ArraySegment<Span> trace, bool shouldSerializeSpans) |
There was a problem hiding this comment.
I guess there's a question of how this works with CI visibility, but they seem somewhat orthogonal, so I don't see an issue with this going unused here
There was a problem hiding this comment.
For now, this is a consequence of where I inserted the decision-making in the TraceContext. With some refactoring, we may be able to move the decision-making inside the WriteTrace method so we don't have this unused paramater
| // - Is the current sampling priority > 0? Return true. | ||
| // - Have any errors been seen? Return true | ||
| // - If the current span doesn't have Metrics["_dd1.sr.eausr"] (aka AnalyticsSampleRate), skip. If it does, run the Knuth sampling decision on the metric value and return the result. | ||
| private bool ShouldKeepSpan(Span span) |
There was a problem hiding this comment.
So we currently calculate whether we should keep the span here based on the rules about, then we add the extra information from the stats aggregation later too. It seems a little off to be calculating two halves of the same thing in two different places, any chance we can merge them somehow, either pushing the stats aggregation up to here, or the span inspection down? 🤔
EDIT: I see Pierre had the same thoughts 🙂
|
|
||
| var key = BuildKey(span); | ||
|
|
||
| var isNewKey = _keys.Add(key); |
There was a problem hiding this comment.
From the trace ingestion mechanism doc, it seems we need to add one tag in that case:
_dd.rare: Sampling rate of the RareSampler
48aca98 to
efe10a3
Compare
andrewlock
left a comment
There was a problem hiding this comment.
LGTM! Sorry about the ArraySegment comment, may have just been unnecessary churn looking at the changes 😅 )
Caveat: We may have the StatsComputationSetting enabled but the agent we try to reach does not support it. That complicated mess will be addressed next.
…0-Traces" and "Datadog-Client-Dropped-P0-Spans" when we drop P0 traces
… stats point is completely new or the stats point contains an error, force the trace to be serialized to the trace agent.
… stats: _DD_TRACE_STATS_COMPUTATION_INTERVAL This value has an underscore prefix so this should be considered non-public. The main motivation for adding a configurable interval is for testing, as the 10-second default duration is excessively long for integration testing.
…, int offset, int count) => IStatsAggregator.AddRange(ArraySegment<Span> spans)
efe10a3 to
be71919
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…is time changing the way the duration is tested. Please work
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
d2bb1c6 to
58a40f7
Compare
This comment has been minimized.
This comment has been minimized.
…the default value of 10. Now that we only wait once at the end, this should be acceptable
Benchmarks Report 🐌Benchmarks for #3048 compared to master:
The following thresholds were used for comparing the benchmark speeds:
Allocation changes below 0.5% are ignored. Benchmark detailsBenchmarks.Trace.AgentWriterBenchmark - Same speed ✔️ Same allocations ✔️Raw results
Benchmarks.Trace.AppSecBodyBenchmark - Slower
|
| Benchmark | diff/base | Base Median (ns) | Diff Median (ns) | Modality |
|---|---|---|---|---|
| Benchmarks.Trace.AppSecBodyBenchmark.AllCycleSimpleBody‑net472 | 1.205 | 186.90 | 225.29 |
Raw results
| Branch | Method | Toolchain | Mean | StdError | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|---|
| master | AllCycleSimpleBody |
net472 | 187ns | 0.146ns | 0.566ns | 0.0676 | 0 | 0 | 425 B |
| master | AllCycleSimpleBody |
netcoreapp3.1 | 241ns | 0.332ns | 1.2ns | 0.0058 | 0 | 0 | 424 B |
| master | AllCycleMoreComplexBody |
net472 | 185ns | 0.144ns | 0.559ns | 0.0637 | 0 | 0 | 401 B |
| master | AllCycleMoreComplexBody |
netcoreapp3.1 | 235ns | 0.371ns | 1.44ns | 0.0054 | 0 | 0 | 400 B |
| master | BodyExtractorSimpleBody |
net472 | 253ns | 0.2ns | 0.775ns | 0.0574 | 0 | 0 | 361 B |
| master | BodyExtractorSimpleBody |
netcoreapp3.1 | 230ns | 0.265ns | 0.99ns | 0.00373 | 0 | 0 | 272 B |
| master | BodyExtractorMoreComplexBody |
net472 | 14.5μs | 19.7ns | 73.7ns | 1.21 | 0.0216 | 0 | 7.62 KB |
| master | BodyExtractorMoreComplexBody |
netcoreapp3.1 | 12.3μs | 17.3ns | 67ns | 0.0863 | 0 | 0 | 6.75 KB |
| #3048 | AllCycleSimpleBody |
net472 | 226ns | 0.387ns | 1.45ns | 0.0675 | 0 | 0 | 425 B |
| #3048 | AllCycleSimpleBody |
netcoreapp3.1 | 244ns | 0.308ns | 1.19ns | 0.00578 | 0 | 0 | 424 B |
| #3048 | AllCycleMoreComplexBody |
net472 | 189ns | 0.299ns | 1.16ns | 0.0637 | 0 | 0 | 401 B |
| #3048 | AllCycleMoreComplexBody |
netcoreapp3.1 | 243ns | 0.281ns | 1.09ns | 0.00549 | 0 | 0 | 400 B |
| #3048 | BodyExtractorSimpleBody |
net472 | 269ns | 0.638ns | 2.47ns | 0.0573 | 0 | 0 | 361 B |
| #3048 | BodyExtractorSimpleBody |
netcoreapp3.1 | 229ns | 0.503ns | 1.95ns | 0.00363 | 0 | 0 | 272 B |
| #3048 | BodyExtractorMoreComplexBody |
net472 | 15.1μs | 28.8ns | 111ns | 1.21 | 0.0148 | 0 | 7.62 KB |
| #3048 | BodyExtractorMoreComplexBody |
netcoreapp3.1 | 12.7μs | 22.5ns | 87.2ns | 0.0881 | 0 | 0 | 6.75 KB |
Benchmarks.Trace.AspNetCoreBenchmark - Same speed ✔️ Same allocations ✔️
Raw results
| Branch | Method | Toolchain | Mean | StdError | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|---|
| master | SendRequest |
net472 | 0ns | 0ns | 0ns | 0 | 0 | 0 | 0 b |
| master | SendRequest |
netcoreapp3.1 | 182μs | 201ns | 780ns | 0.273 | 0 | 0 | 20.57 KB |
| #3048 | SendRequest |
net472 | 0ns | 0ns | 0ns | 0 | 0 | 0 | 0 b |
| #3048 | SendRequest |
netcoreapp3.1 | 181μs | 156ns | 603ns | 0.18 | 0 | 0 | 20.57 KB |
Benchmarks.Trace.DbCommandBenchmark - Same speed ✔️ Same allocations ✔️
Raw results
| Branch | Method | Toolchain | Mean | StdError | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|---|
| master | ExecuteNonQuery |
net472 | 1.82μs | 0.707ns | 2.65ns | 0.15 | 0.000908 | 0 | 947 B |
| master | ExecuteNonQuery |
netcoreapp3.1 | 1.38μs | 0.452ns | 1.75ns | 0.0124 | 0 | 0 | 936 B |
| #3048 | ExecuteNonQuery |
net472 | 1.87μs | 0.622ns | 2.33ns | 0.15 | 0.000929 | 0 | 947 B |
| #3048 | ExecuteNonQuery |
netcoreapp3.1 | 1.44μs | 0.665ns | 2.49ns | 0.0126 | 0 | 0 | 936 B |
Benchmarks.Trace.ElasticsearchBenchmark - Same speed ✔️ Same allocations ✔️
Raw results
| Branch | Method | Toolchain | Mean | StdError | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|---|
| master | CallElasticsearch |
net472 | 2.52μs | 0.648ns | 2.51ns | 0.183 | 0 | 0 | 1.16 KB |
| master | CallElasticsearch |
netcoreapp3.1 | 1.54μs | 0.525ns | 2.04ns | 0.0147 | 0 | 0 | 1.1 KB |
| master | CallElasticsearchAsync |
net472 | 2.67μs | 2.83ns | 11ns | 0.205 | 0 | 0 | 1.29 KB |
| master | CallElasticsearchAsync |
netcoreapp3.1 | 1.59μs | 0.611ns | 2.29ns | 0.0167 | 0 | 0 | 1.22 KB |
| #3048 | CallElasticsearch |
net472 | 2.4μs | 0.684ns | 2.65ns | 0.183 | 0 | 0 | 1.16 KB |
| #3048 | CallElasticsearch |
netcoreapp3.1 | 1.58μs | 0.647ns | 2.42ns | 0.0151 | 0 | 0 | 1.1 KB |
| #3048 | CallElasticsearchAsync |
net472 | 2.49μs | 2.33ns | 8.71ns | 0.205 | 0 | 0 | 1.29 KB |
| #3048 | CallElasticsearchAsync |
netcoreapp3.1 | 1.64μs | 1.73ns | 6.47ns | 0.0167 | 0 | 0 | 1.22 KB |
Benchmarks.Trace.GraphQLBenchmark - Same speed ✔️ Same allocations ✔️
Raw results
| Branch | Method | Toolchain | Mean | StdError | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|---|
| master | ExecuteAsync |
net472 | 2.54μs | 9.2ns | 35.6ns | 0.224 | 0 | 0 | 1.41 KB |
| master | ExecuteAsync |
netcoreapp3.1 | 1.66μs | 4.22ns | 16.3ns | 0.0178 | 0 | 0 | 1.34 KB |
| #3048 | ExecuteAsync |
net472 | 2.7μs | 4.52ns | 17.5ns | 0.223 | 0 | 0 | 1.41 KB |
| #3048 | ExecuteAsync |
netcoreapp3.1 | 1.76μs | 1.14ns | 4.26ns | 0.0175 | 0 | 0 | 1.34 KB |
Benchmarks.Trace.HttpClientBenchmark - Same speed ✔️ Same allocations ✔️
Raw results
| Branch | Method | Toolchain | Mean | StdError | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|---|
| master | SendAsync |
net472 | 5.69μs | 24.4ns | 94.5ns | 0.439 | 0 | 0 | 2.77 KB |
| master | SendAsync |
netcoreapp3.1 | 3.57μs | 9.34ns | 36.2ns | 0.0351 | 0 | 0 | 2.6 KB |
| #3048 | SendAsync |
net472 | 5.73μs | 9.22ns | 35.7ns | 0.439 | 0 | 0 | 2.77 KB |
| #3048 | SendAsync |
netcoreapp3.1 | 3.53μs | 5.14ns | 19.2ns | 0.035 | 0 | 0 | 2.6 KB |
Benchmarks.Trace.ILoggerBenchmark - Same speed ✔️ Same allocations ✔️
Raw results
| Branch | Method | Toolchain | Mean | StdError | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|---|
| master | EnrichedLog |
net472 | 3.13μs | 2.34ns | 9.05ns | 0.287 | 0 | 0 | 1.81 KB |
| master | EnrichedLog |
netcoreapp3.1 | 2.56μs | 1.49ns | 5.59ns | 0.0242 | 0 | 0 | 1.85 KB |
| #3048 | EnrichedLog |
net472 | 3.1μs | 3.12ns | 12.1ns | 0.287 | 0 | 0 | 1.81 KB |
| #3048 | EnrichedLog |
netcoreapp3.1 | 2.54μs | 1.84ns | 6.89ns | 0.0256 | 0 | 0 | 1.85 KB |
Benchmarks.Trace.Log4netBenchmark - Same speed ✔️ Same allocations ✔️
Raw results
| Branch | Method | Toolchain | Mean | StdError | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|---|
| master | EnrichedLog |
net472 | 152μs | 128ns | 494ns | 0.686 | 0.229 | 0 | 4.65 KB |
| master | EnrichedLog |
netcoreapp3.1 | 113μs | 74.4ns | 268ns | 0.057 | 0 | 0 | 4.49 KB |
| #3048 | EnrichedLog |
net472 | 152μs | 129ns | 466ns | 0.689 | 0.23 | 0 | 4.65 KB |
| #3048 | EnrichedLog |
netcoreapp3.1 | 116μs | 276ns | 1.07μs | 0.0577 | 0 | 0 | 4.49 KB |
Benchmarks.Trace.NLogBenchmark - Same speed ✔️ Same allocations ✔️
Raw results
| Branch | Method | Toolchain | Mean | StdError | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|---|
| master | EnrichedLog |
net472 | 5.6μs | 13.9ns | 53.7ns | 0.567 | 0.00278 | 0 | 3.59 KB |
| master | EnrichedLog |
netcoreapp3.1 | 4.18μs | 12.2ns | 47.2ns | 0.0538 | 0 | 0 | 3.91 KB |
| #3048 | EnrichedLog |
net472 | 5.77μs | 6.95ns | 26.9ns | 0.567 | 0.00289 | 0 | 3.59 KB |
| #3048 | EnrichedLog |
netcoreapp3.1 | 4.38μs | 5.24ns | 19.6ns | 0.0518 | 0 | 0 | 3.91 KB |
Benchmarks.Trace.RedisBenchmark - Same speed ✔️ Same allocations ✔️
Raw results
| Branch | Method | Toolchain | Mean | StdError | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|---|
| master | SendReceive |
net472 | 2.19μs | 1.66ns | 6.44ns | 0.217 | 0 | 0 | 1.37 KB |
| master | SendReceive |
netcoreapp3.1 | 1.77μs | 0.673ns | 2.52ns | 0.0177 | 0 | 0 | 1.32 KB |
| #3048 | SendReceive |
net472 | 2.34μs | 3.02ns | 11.3ns | 0.218 | 0 | 0 | 1.37 KB |
| #3048 | SendReceive |
netcoreapp3.1 | 1.8μs | 1.76ns | 6.83ns | 0.018 | 0 | 0 | 1.32 KB |
Benchmarks.Trace.SerilogBenchmark - Same speed ✔️ Same allocations ✔️
Raw results
| Branch | Method | Toolchain | Mean | StdError | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|---|
| master | EnrichedLog |
net472 | 5.01μs | 1.37ns | 5.3ns | 0.354 | 0 | 0 | 2.23 KB |
| master | EnrichedLog |
netcoreapp3.1 | 4.26μs | 2.74ns | 10.3ns | 0.0236 | 0 | 0 | 1.8 KB |
| #3048 | EnrichedLog |
net472 | 5.16μs | 3.71ns | 14.4ns | 0.353 | 0 | 0 | 2.23 KB |
| #3048 | EnrichedLog |
netcoreapp3.1 | 4.26μs | 1.28ns | 4.79ns | 0.0234 | 0 | 0 | 1.8 KB |
Benchmarks.Trace.SpanBenchmark - Same speed ✔️ Same allocations ✔️
Raw results
| Branch | Method | Toolchain | Mean | StdError | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|---|
| master | StartFinishSpan |
net472 | 1.12μs | 0.423ns | 1.58ns | 0.128 | 0 | 0 | 810 B |
| master | StartFinishSpan |
netcoreapp3.1 | 919ns | 0.317ns | 1.23ns | 0.0101 | 0 | 0 | 760 B |
| master | StartFinishScope |
net472 | 1.37μs | 0.299ns | 1.16ns | 0.141 | 0 | 0 | 891 B |
| master | StartFinishScope |
netcoreapp3.1 | 1.05μs | 0.281ns | 1.05ns | 0.0121 | 0 | 0 | 880 B |
| #3048 | StartFinishSpan |
net472 | 1.11μs | 0.559ns | 2.01ns | 0.128 | 0 | 0 | 810 B |
| #3048 | StartFinishSpan |
netcoreapp3.1 | 949ns | 0.255ns | 0.955ns | 0.0101 | 0 | 0 | 760 B |
| #3048 | StartFinishScope |
net472 | 1.46μs | 0.412ns | 1.54ns | 0.141 | 0 | 0 | 891 B |
| #3048 | StartFinishScope |
netcoreapp3.1 | 1.1μs | 0.387ns | 1.5ns | 0.012 | 0 | 0 | 880 B |
Benchmarks.Trace.TraceAnnotationsBenchmark - Same speed ✔️ Same allocations ✔️
Raw results
| Branch | Method | Toolchain | Mean | StdError | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|---|
| master | RunOnMethodBegin |
net472 | 1.51μs | 0.321ns | 1.24ns | 0.141 | 0 | 0 | 891 B |
| master | RunOnMethodBegin |
netcoreapp3.1 | 1.19μs | 0.529ns | 2.05ns | 0.0119 | 0 | 0 | 880 B |
| #3048 | RunOnMethodBegin |
net472 | 1.53μs | 0.482ns | 1.87ns | 0.141 | 0 | 0 | 891 B |
| #3048 | RunOnMethodBegin |
netcoreapp3.1 | 1.2μs | 0.586ns | 2.19ns | 0.012 | 0 | 0 | 880 B |
Code Coverage Report 📊✔️ Merging #3048 into master will not change line coverage
View the full report for further details: Datadog.Trace Breakdown ✔️
The following classes have significant coverage changes.
View the full reports for further details: |
In #3048, the logic for keeping a trace chunk occurred twice: once in the TraceContext and then again in the StatsAggregator. This PR moves all of the logic into the StatsAggregator. Main changes - Adds a new method to run all the trace chunk sampling logic: `bool IStatsAggregator.ShouldKeepTrace(ArraySegment<Span> trace)` - Adds a new interface to cleanly define each rule: `Datadog.Trace.Agent.ITraceSampler` - The one required method is: `bool Sample(ArraySegment<Span> trace)` - Derived types include: - `AnalyticsEventSampler` - `ErrorSampler` - `PrioritySampler` - `RareSampler` - Fixes the following bugs identified in the previous PR and adds regression tests: - Spans caught by the RareSampler must have the metric `_dd.rare=1` - Ensure that traces with errors (possibly in child spans) always get reported to the trace agent - Note: the trace agent has a more nuanced ErrorSampler, but this works for now and still eliminates a great deal of traffic
Summary of changes
When stats computation is enabled and the tracer can drop P0 traces (assumed to be true, will check with agent in followup PR), P0 traces are not serialized and the
"Datadog-Client-Dropped-P0-Traces"and"Datadog-Client-Dropped-P0-Spans"request headers are set on the next trace payload.Based on top of #3047, this is PR 2/3 in an attempt to break down the massive PR #2988
Reason for change
Dropping P0 traces is the biggest advantage of enabling stats computation in the tracer, because a large amount of span serialization (by the tracer) and deserialization (by the trace agent) is removed entirely.
Implementation details
TraceContext changes
If
Tracer.CanDropP0s == true, then the following algorithm is run on each of the finished spans to determine if the span should be kept. If at least one span is kept, then the entire trace is kept. If not, then the entire trace is dropped.We then pass the keep/drop decision along with the finished span array to the
AgentWriter.WriteTraceAPI.Note: There will need to be some special handling if a trace is supposed to be sampled out but the new single span ingestion controls determine that single spans should be kept. This is documented in the RFC and will need to be handled when the feature is implemented in the .NET Tracer.
AgentWriter / IApi changes
When the
AgentWriterreceives spans, it now accumulates the number of dropped P0 traces and dropped P0 spans. Then, when a span buffer needs to be serialized andIApi.SendTracesAsyncis invoked, it passes along these counts and they are added as headers to the trace agent request.Test coverage
Adds unit tests in
TraceContextTestsand integration tests inStatsTeststo assert that P0 traces are dropped and the headers are sent correctly.Other details
This PR also adds a configuration key
_DD_TRACE_STATS_COMPUTATION_INTERVALto configure the interval for sending stats. This should be considered a non-public setting and could change at any time.Remaining Work Items
_dd.raremetric, as noted here. The other complication is that the RareSampler only runs if a span was not kept by the PrioritySampler (SamplingPriority > 0). The refactoring should make this implementation simpler