Wire Single Span Sampling#2142
Conversation
bf7c6d7 to
ca91ac8
Compare
Codecov Report
@@ Coverage Diff @@
## single-span-fix-priority-sample #2142 +/- ##
===================================================================
- Coverage 97.53% 97.53% -0.01%
===================================================================
Files 1047 1047
Lines 54410 54596 +186
===================================================================
+ Hits 53070 53249 +179
- Misses 1340 1347 +7
📣 Codecov can now indicate which changes are the most critical in Pull Requests. Learn more |
ivoanjo
left a comment
There was a problem hiding this comment.
Hey 👋
I've taken a stab at this one, from my usual POV of someone who knows less about the logic here and is trying to follow the intent of the change and the existing code.
Probably worth having someone else give an extra check because I definitely will have missed things :)
| def full_flush?(trace_op) | ||
| trace_op && trace_op.sampled? && trace_op.finished? | ||
| trace_op && trace_op.finished? | ||
| end |
There was a problem hiding this comment.
I assume this change (and the change below) needs to happen because now even if the trace has not been marked as sampled?, we may have some spans inside it that we want to flush.
But... this change is kinda weird. The whole point of this class (and partial, below) seems to be to determine "is this trace in a flushable state". But now that's basically a lie -- it's always going to say "yes", and then the actual decision of flushing is moved elsewhere.
I don't have a great suggestion here, other than I think these classes are so thin in logic that they should maybe folded back to the tracer class...(?)
There was a problem hiding this comment.
I agree that for the Finished class, it's bare bones now. But the Partial one has legs still, and performs non-trivial work.
They don't look too great, as they know quite a lot about the TraceOperation internals. I'm not 100% if I have a good alternative right now.
| Datadog::Tracing::TraceOperation, | ||
| origin: origin, | ||
| sampled?: sampled, | ||
| finished?: finished, | ||
| flush!: trace | ||
| ) | ||
| end | ||
|
|
||
| let(:origin) { 'ci-origin' } | ||
| let(:sampled) { true } | ||
| let(:finished) { true } | ||
|
|
There was a problem hiding this comment.
Is it just me or is Datadog::CI::Flush::Tagging a really bizarre part of the code? E.g. the class that is supposed to decide if something is ready for flushing or not has added behavior to mutate all spans to add an extra tag. Weeeird way of doing it... 👀
There was a problem hiding this comment.
It seems like it's due to a lack of a clear interception point between the Tracing and CI products.
A CI formatter might make more sense.
| context 'with rule matching' do | ||
| context 'on name' do | ||
| context 'with a dropped span' do | ||
| let(:rules) { [{ name: 'my.op', sample_rate: 0.0 }] } | ||
|
|
||
| it_behaves_like 'flushed complete trace' | ||
| it_behaves_like 'does not modify spans' | ||
|
|
||
| context 'by rate limiting' do | ||
| let(:rules) { [{ name: 'my.op', sample_rate: 1.0, max_per_second: 0 }] } | ||
|
|
||
| it_behaves_like 'flushed complete trace' | ||
| it_behaves_like 'does not modify spans' | ||
| end | ||
| end | ||
|
|
||
| context 'with a kept span' do | ||
| let(:rules) { [{ name: 'my.op', sample_rate: 1.0 }] } | ||
|
|
||
| it_behaves_like 'flushed complete trace' | ||
| it_behaves_like 'set single span sampling tags' | ||
| end | ||
| end | ||
|
|
||
| context 'on service' do | ||
| context 'with a dropped span' do | ||
| let(:rules) { [{ service: 'my-ser*', sample_rate: 0.0 }] } | ||
|
|
||
| it_behaves_like 'flushed complete trace' | ||
| it_behaves_like 'does not modify spans' | ||
|
|
||
| context 'by rate limiting' do | ||
| let(:rules) { [{ service: 'my-ser*', sample_rate: 1.0, max_per_second: 0 }] } | ||
|
|
||
| it_behaves_like 'flushed complete trace' | ||
| it_behaves_like 'does not modify spans' | ||
| end | ||
| end | ||
|
|
||
| context 'with a kept span' do | ||
| let(:rules) { [{ service: 'my-ser*', sample_rate: 1.0 }] } | ||
|
|
||
| it_behaves_like 'flushed complete trace' | ||
| it_behaves_like 'set single span sampling tags' | ||
| end | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
This being an integration test, I'm not sure we need to exhaustively test all features of the rule matcher -- after all, we have tests for that, and we're just duplicating them.
Instead, I would suggest simplifying this to only testing a situation where the matches matches, and another where it doesn't. All other variants are effectively tests for the local logic in the sampler that don't otherwise reflect differently on tracing-as-a-whole.
There was a problem hiding this comment.
Hmmm, I see duplication for sure.
I tried to match the somewhat higher level requirements from the RFC here in the integrations suite.
I'll see if I can simply these without removing too much in that I'm not as confident in the feature anymore.
There was a problem hiding this comment.
Ok, I removed half the tests inside context 'with a dropped trace', specifically the distinct cases where rules could match based on span name or service name: now the tests only test if the span matches; the field that was used for matching is not important.
| Datadog.logger.warn( | ||
| "Error parsing Span Sampling Rules `#{rules.inspect}`: "\ | ||
| "#{e.class.name} #{e.message} at #{Array(e.backtrace).first}" | ||
| ) |
There was a problem hiding this comment.
Yeah, sometimes I let it do its thing, and then come in after and tweak it so it's still "rubocop-acceptable" but does not look positively awful.
I'm still not convinced of the value of many of the rules in rubocop. Having a linter in general is ok, but rubocop is incredibly picky on irrelevant things, to the point of hindering productivity (at least for me).
There was a problem hiding this comment.
Agreed. We should axe the rules that don't help. It's supposed to be an aid, not a hinderance.
There was a problem hiding this comment.
I agree, I think there's a bug in Rubocop regarding multi line argument indentation.
TonyCTHsu
left a comment
There was a problem hiding this comment.
It generally make sense to me
| # that has been accepted by trace-level sampling rules: all spans from such | ||
| # trace are guaranteed to reach the Datadog App. | ||
| class Sampler | ||
| attr_reader :rules |
There was a problem hiding this comment.
Is there a reason to make this public? I did not find other using this, or is this only a shortcut for test?
There was a problem hiding this comment.
The class itself is not part of the public API either.
delner
left a comment
There was a problem hiding this comment.
I have a number of questions I'd like to explore first before approving this. I wonder if we're coupling sampling logic too closely with spans (by putting sample/flush logic in the trace/span classes).
My concern is that I want to be able to re-use Span and TraceOperation as basic building blocks that are agnostic to the concept of sampling altogether in the future, if the future tracer decides not to sample.
| end | ||
|
|
||
| def full_flush?(trace_op) | ||
| trace_op && trace_op.sampled? && trace_op.finished? |
There was a problem hiding this comment.
Hmmm, why trace_op.sampled? omitted? Trying to figure out the purpose of this.
| # @param [String] tag the tag or metric to check for presence | ||
| # @return [Boolean] if the tag is present and not nil | ||
| def tag?(tag) | ||
| !get_tag(tag).nil? # nil is considered not present, thus we can't use `Hash#has_key?` |
There was a problem hiding this comment.
Is there a slightly more direct/performant way of doing this? Access the @ var itself? Or do we need to manage both the meta and metric vars?
On the fence about extending the API like this. I guess it's harmless, but it feels like adding in a test abstraction like this into production code seems not quite right. "Leaky." If there's any legit use of this outside the test suite, I think this is fine.
Otherwise, I think it would be wise to consider implementing such abstractions from within the test suite, if at all possible.
| # @return [Array<Datadog::Tracing::Sampling::Span::Rule>] a list of parsed rules | ||
| # @return [nil] if parsing failed | ||
| def parse_json(rules) | ||
| return nil unless rules |
| Datadog.logger.warn( | ||
| "Error parsing Span Sampling Rules `#{rules.inspect}`: "\ | ||
| "#{e.class.name} #{e.message} at #{Array(e.backtrace).first}" | ||
| ) |
There was a problem hiding this comment.
Agreed. We should axe the rules that don't help. It's supposed to be an aid, not a hinderance.
|
|
||
| # Single Span Sampling has chosen to keep this span | ||
| # regardless of the trace-level sampling decision | ||
| # @!visibility private |
There was a problem hiding this comment.
If this is meant to be private, can we move it behind the private keyword? I don't like soft rules like that which can be easily violated if you didn't read the docs.
There was a problem hiding this comment.
My main concern is performance:
Warming up --------------------------------------
obj.foo 1.421M i/100ms
obj.send(:foo) 363.874k i/100ms
Calculating -------------------------------------
obj.foo 17.501M (± 2.3%) i/s - 53.987M in 3.086433s
obj.send(:foo) 10.233M (± 3.0%) i/s - 30.929M in 3.025515s
Comparison:
obj.foo: 17500810.6 i/s
obj.send(:foo): 10232543.6 i/s - 1.71x (± 0.00) slower
And how much of a sore obj.send(:foo) looks compared to obj.foo.
I'll change this method to be really private and use obj.send(:foo) when calling it in this PR.
| # Single Span Sampling has chosen to keep this span | ||
| # regardless of the trace-level sampling decision | ||
| # @!visibility private | ||
| def single_sampled? |
There was a problem hiding this comment.
I don't feel like this responsibility is quite right. Span should only be describing its own state, but now it seems to know something about the nature of span sampling. I don't like the presence of sampling logic and references in Span because I don't think it's a span's responsibility to know how it's sampled. It's supposed to be dumb.
It may be better to extract out this single_sampled? logic elsewhere, presumably to the thing that is aware of sampling that uses this. If this isn't possible/practical, I think at minimum this should be reduced to a simple variable e.g. @single_sampled.
| else | ||
| # Only spans where the span-level sampling overrides | ||
| # the trace-level sampling can be flushed. | ||
| flush_single_sampled_spans_only! |
There was a problem hiding this comment.
Similar to my feedback regarding Span and sampling, I think the same logic may apply here. Perhaps the better place for this kind of responsibility is in the Flush classes. They are supposed to know how to extract the "right" spans from an active trace.
| end | ||
|
|
||
| # Flush single sampled span only, as the trace as a whole was dropped by trace-level sampling | ||
| def flush_single_sampled_spans_only! |
There was a problem hiding this comment.
This logic seems very duplicative of the above/original logic. Should we be be adding an option to flush! that allows both of these paths to be driven based on optional input?
I addressed all points we discussed in person
|
With all comments addressed, I'm going to merge this PR. See you guys at our last stand: #2128! |
This PR finishes the Single Span Sampling feature, but creating a
Tracing::Sampling::Span::Samplerinstance and wiring it into theTracerinstance.This PR also adds integration testing for the Single Span Sampling feature.
Other changes, done to finish up the feature:
Finished,Partial(and their CI counterparts) andTraceOperationclasses. This part I'd like feedback on, as I'm not 100% confident on what the cleanest solution is here.lib/ddtrace/transport/ext.rb,lib/ddtrace/transport/http.rb, andspec/ddtrace/transport/http_spec.rb.lib/datadog/tracing/sampling/span/rule_parser.rbto improve resilience when receivingnilas it's main argument, something that can happen when sampling is not configured.