diff --git a/ddtrace/tracer/sampler_test.go b/ddtrace/tracer/sampler_test.go index b28a040037..117518b533 100644 --- a/ddtrace/tracer/sampler_test.go +++ b/ddtrace/tracer/sampler_test.go @@ -1637,3 +1637,87 @@ func TestSetGlobalSampleRate(t *testing.T) { assert.Equal(t, 0.0, rs.globalRate) assert.False(t, b) } + +func TestSampleTagsRootOnly(t *testing.T) { + assert := assert.New(t) + + t.Run("no-ctx-propagation", func(t *testing.T) { + Start(WithSamplingRules([]SamplingRule{ + TagsResourceRule(map[string]string{"tag": "20"}, "", "", "", 1), + TagsResourceRule(nil, "root", "", "", 0), + })) + tr := internal.GetGlobalTracer() + defer tr.Stop() + + root := tr.StartSpan("mysql.root", ResourceName("root")) + child := tr.StartSpan("mysql.child", ChildOf(root.Context())) + child.SetTag("tag", 20) + + // root span should be sampled with the second rule + // sampling decision is 0, thus "_dd.limit_psr" is not present + assert.Contains(root.(*span).Metrics, keyRulesSamplerAppliedRate) + assert.Equal(0., root.(*span).Metrics[keyRulesSamplerAppliedRate]) + assert.NotContains(root.(*span).Metrics, keyRulesSamplerLimiterRate) + + // neither"_dd.limit_psr", nor "_dd.rule_psr" should be present + // on the child span + assert.NotContains(child.(*span).Metrics, keyRulesSamplerAppliedRate) + assert.NotContains(child.(*span).Metrics, keyRulesSamplerLimiterRate) + + // setting this tag would change the result of sampling, + // which will occur after the span is finished + root.SetTag("tag", 20) + child.Finish() + + // first sampling rule is applied, the sampling decision is 1 + // and the "_dd.limit_psr" is present + root.Finish() + assert.Equal(1., root.(*span).Metrics[keyRulesSamplerAppliedRate]) + assert.Contains(root.(*span).Metrics, keyRulesSamplerLimiterRate) + + // neither"_dd.limit_psr", nor "_dd.rule_psr" should be present + // on the child span + assert.NotContains(child.(*span).Metrics, keyRulesSamplerAppliedRate) + assert.NotContains(child.(*span).Metrics, keyRulesSamplerLimiterRate) + }) + + t.Run("with-ctx-propagation", func(t *testing.T) { + Start(WithSamplingRules([]SamplingRule{ + TagsResourceRule(map[string]string{"tag": "20"}, "", "", "", 1), + TagsResourceRule(nil, "root", "", "", 0), + })) + tr := internal.GetGlobalTracer() + defer tr.Stop() + + root := tr.StartSpan("mysql.root", ResourceName("root")) + child := tr.StartSpan("mysql.child", ChildOf(root.Context())) + child.SetTag("tag", 20) + + // root span should be sampled with the second rule + // sampling decision is 0, thus "_dd.limit_psr" is not present + assert.Equal(0., root.(*span).Metrics[keyRulesSamplerAppliedRate]) + assert.Contains(root.(*span).Metrics, keyRulesSamplerAppliedRate) + assert.NotContains(root.(*span).Metrics, keyRulesSamplerLimiterRate) + + // neither"_dd.limit_psr", nor "_dd.rule_psr" should be present + // on the child span + assert.NotContains(child.(*span).Metrics, keyRulesSamplerAppliedRate) + assert.NotContains(child.(*span).Metrics, keyRulesSamplerLimiterRate) + + // context propagation locks the span, so no re-sampling should occur + tr.Inject(root.Context(), TextMapCarrier(map[string]string{})) + root.SetTag("tag", 20) + + child.Finish() + + // re-sampling should not occur + root.Finish() + assert.NotContains(child.(*span).Metrics, keyRulesSamplerAppliedRate) + assert.NotContains(root.(*span).Metrics, keyRulesSamplerLimiterRate) + + // neither"_dd.limit_psr", nor "_dd.rule_psr" should be present + // on the child span + assert.NotContains(child.(*span).Metrics, keyRulesSamplerAppliedRate) + assert.NotContains(child.(*span).Metrics, keyRulesSamplerLimiterRate) + }) +} diff --git a/ddtrace/tracer/span.go b/ddtrace/tracer/span.go index 323003e23a..e793c6caf7 100644 --- a/ddtrace/tracer/span.go +++ b/ddtrace/tracer/span.go @@ -500,9 +500,11 @@ func (s *span) Finish(opts ...ddtrace.FinishOption) { s.SetTag("go_execution_traced", "partial") } - if tr, ok := internal.GetGlobalTracer().(*tracer); ok && tr.rulesSampling.traces.enabled() { - if !s.context.trace.isLocked() && s.context.trace.propagatingTag(keyDecisionMaker) != "-4" { - tr.rulesSampling.SampleTrace(s) + if s.root() == s { + if tr, ok := internal.GetGlobalTracer().(*tracer); ok && tr.rulesSampling.traces.enabled() { + if !s.context.trace.isLocked() && s.context.trace.propagatingTag(keyDecisionMaker) != "-4" { + tr.rulesSampling.SampleTrace(s) + } } } @@ -600,13 +602,21 @@ func newAggregableSpan(s *span, obfuscator *obfuscate.Obfuscator) *aggregableSpa statusCode = uint32(c) } } + var isTraceRoot trilean + if s.ParentID == 0 { + isTraceRoot = trilean_true + } else { + isTraceRoot = trilean_false + } + key := aggregation{ - Name: s.Name, - Resource: obfuscatedResource(obfuscator, s.Type, s.Resource), - Service: s.Service, - Type: s.Type, - Synthetics: strings.HasPrefix(s.Meta[keyOrigin], "synthetics"), - StatusCode: statusCode, + Name: s.Name, + Resource: obfuscatedResource(obfuscator, s.Type, s.Resource), + Service: s.Service, + Type: s.Type, + Synthetics: strings.HasPrefix(s.Meta[keyOrigin], "synthetics"), + StatusCode: statusCode, + IsTraceRoot: isTraceRoot, } return &aggregableSpan{ key: key, diff --git a/ddtrace/tracer/span_test.go b/ddtrace/tracer/span_test.go index 95b1460ac0..a294b09af0 100644 --- a/ddtrace/tracer/span_test.go +++ b/ddtrace/tracer/span_test.go @@ -175,10 +175,11 @@ func TestNewAggregableSpan(t *testing.T) { Type: "sql", }, o) assert.Equal(t, aggregation{ - Name: "name", - Type: "sql", - Resource: "SELECT * FROM table WHERE password = ?", - Service: "service", + Name: "name", + Type: "sql", + Resource: "SELECT * FROM table WHERE password = ?", + Service: "service", + IsTraceRoot: 1, }, aggspan.key) }) @@ -190,10 +191,11 @@ func TestNewAggregableSpan(t *testing.T) { Type: "sql", }, nil) assert.Equal(t, aggregation{ - Name: "name", - Type: "sql", - Resource: "SELECT * FROM table WHERE password='secret'", - Service: "service", + Name: "name", + Type: "sql", + Resource: "SELECT * FROM table WHERE password='secret'", + Service: "service", + IsTraceRoot: 1, }, aggspan.key) }) } diff --git a/ddtrace/tracer/stats.go b/ddtrace/tracer/stats.go index 720a2a0230..5a3c1c2269 100644 --- a/ddtrace/tracer/stats.go +++ b/ddtrace/tracer/stats.go @@ -214,12 +214,13 @@ func (c *concentrator) flushAndSend(timenow time.Time, includeCurrent bool) { // aggregation specifies a uniquely identifiable key under which a certain set // of stats are grouped inside a bucket. type aggregation struct { - Name string - Type string - Resource string - Service string - StatusCode uint32 - Synthetics bool + Name string + Type string + Resource string + Service string + StatusCode uint32 + Synthetics bool + IsTraceRoot trilean } type rawBucket struct { @@ -278,6 +279,14 @@ func (sb *rawBucket) Export() statsBucket { return csb } +type trilean int32 + +const ( + trilean_not_set trilean = iota + trilean_true + trilean_false +) + type rawGroupedStats struct { hits uint64 topLevelHits uint64 @@ -285,6 +294,7 @@ type rawGroupedStats struct { duration uint64 okDistribution *ddsketch.DDSketch errDistribution *ddsketch.DDSketch + IsTraceRoot trilean } func newRawGroupedStats() *rawGroupedStats { @@ -335,6 +345,7 @@ func (s *rawGroupedStats) export(k aggregation) (groupedStats, error) { OkSummary: okSummary, ErrorSummary: errSummary, Synthetics: k.Synthetics, + IsTraceRoot: int32(k.IsTraceRoot), }, nil } diff --git a/ddtrace/tracer/stats_payload.go b/ddtrace/tracer/stats_payload.go index 35a68b46b9..3b77128b7a 100644 --- a/ddtrace/tracer/stats_payload.go +++ b/ddtrace/tracer/stats_payload.go @@ -53,4 +53,5 @@ type groupedStats struct { ErrorSummary []byte `json:"errorSummary,omitempty"` Synthetics bool `json:"synthetics,omitempty"` TopLevelHits uint64 `json:"topLevelHits,omitempty"` + IsTraceRoot int32 `json:"isTraceRoot,omitempty"` } diff --git a/ddtrace/tracer/tracer.go b/ddtrace/tracer/tracer.go index af794c7857..f0c74eecef 100644 --- a/ddtrace/tracer/tracer.go +++ b/ddtrace/tracer/tracer.go @@ -735,6 +735,9 @@ func (t *tracer) sample(span *span) { if t.rulesSampling.SampleTraceGlobalRate(span) { return } + if t.rulesSampling.SampleTrace(span) { + return + } t.prioritySampling.apply(span) } diff --git a/docker-compose.yaml b/docker-compose.yaml index 1b6008ac61..17788504ee 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -100,9 +100,9 @@ services: TRACE_LANGUAGE: golang ENABLED_CHECKS: trace_stall,trace_count_header,trace_peer_service,trace_dd_service PORT: 9126 - DD_SUPPRESS_TRACE_PARSE_ERRORS: true - DD_POOL_TRACE_CHECK_FAILURES: true - DD_DISABLE_ERROR_RESPONSES: true + DD_SUPPRESS_TRACE_PARSE_ERRORS: "true" + DD_POOL_TRACE_CHECK_FAILURES: "true" + DD_DISABLE_ERROR_RESPONSES: "true" ports: - "127.0.0.1:9126:9126" mongodb: