diff --git a/.gitignore b/.gitignore index fbff30913b..44460622d7 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ bin/ *.cov *.lock *.swp +.idea +dd-trace-go.iml /contrib/google.golang.org/grpc.v12/vendor/ diff --git a/ddtrace/tracer/span.go b/ddtrace/tracer/span.go index ae690390ad..0db3f13e00 100644 --- a/ddtrace/tracer/span.go +++ b/ddtrace/tracer/span.go @@ -381,4 +381,7 @@ const ( keyRulesSamplerAppliedRate = "_dd.rule_psr" keyRulesSamplerLimiterRate = "_dd.limit_psr" keyMeasured = "_dd.measured" + // keyTopLevel is the key of top level metric indicating if a span is top level. + // A top level span is a local root (parent span of the local trace) or the first span of each service. + keyTopLevel = "_top_level" ) diff --git a/ddtrace/tracer/span_test.go b/ddtrace/tracer/span_test.go index 8f9d9d0cd5..9d991ff03c 100644 --- a/ddtrace/tracer/span_test.go +++ b/ddtrace/tracer/span_test.go @@ -244,7 +244,7 @@ const ( func TestSpanSetMetric(t *testing.T) { for name, tt := range map[string]func(assert *assert.Assertions, span *span){ "init": func(assert *assert.Assertions, span *span) { - assert.Equal(2, len(span.Metrics)) + assert.Equal(3, len(span.Metrics)) _, ok := span.Metrics[keySamplingPriority] assert.True(ok) _, ok = span.Metrics[keySamplingPriorityRate] @@ -279,7 +279,7 @@ func TestSpanSetMetric(t *testing.T) { "finished": func(assert *assert.Assertions, span *span) { span.Finish() span.SetTag("finished.test", 1337) - assert.Equal(2, len(span.Metrics)) + assert.Equal(3, len(span.Metrics)) _, ok := span.Metrics["finished.test"] assert.False(ok) }, diff --git a/ddtrace/tracer/tracer.go b/ddtrace/tracer/tracer.go index e61d132bfc..acfbe16505 100644 --- a/ddtrace/tracer/tracer.go +++ b/ddtrace/tracer/tracer.go @@ -307,6 +307,11 @@ func (t *tracer) StartSpan(operationName string, options ...ddtrace.StartSpanOpt for k, v := range t.config.globalTags { span.SetTag(k, v) } + if context == nil || context.span == nil || context.span.Service != span.Service { + span.setMetric(keyTopLevel, 1) + // all top level spans are measured. So the measured tag is redundant. + delete(span.Metrics, keyMeasured) + } if t.config.version != "" && span.Service == t.config.serviceName { span.SetTag(ext.Version, t.config.version) } diff --git a/ddtrace/tracer/tracer_test.go b/ddtrace/tracer/tracer_test.go index 25d079b059..a93d066e38 100644 --- a/ddtrace/tracer/tracer_test.go +++ b/ddtrace/tracer/tracer_test.go @@ -212,10 +212,19 @@ func TestTracerStartSpan(t *testing.T) { assert.Equal(t, "/home/user", span.Resource) }) - t.Run("measured", func(t *testing.T) { + t.Run("measured_top_level", func(t *testing.T) { tracer := newTracer() span := tracer.StartSpan("/home/user", Measured()).(*span) - assert.Equal(t, 1.0, span.Metrics[keyMeasured]) + _, ok := span.Metrics[keyMeasured] + assert.False(t, ok) + assert.Equal(t, 1.0, span.Metrics[keyTopLevel]) + }) + + t.Run("measured_non_top_level", func(t *testing.T) { + tracer := newTracer() + parent := tracer.StartSpan("/home/user").(*span) + child := tracer.StartSpan("home/user", Measured(), ChildOf(parent.context)).(*span) + assert.Equal(t, 1.0, child.Metrics[keyMeasured]) }) } @@ -277,7 +286,6 @@ func TestTracerStartSpanOptions(t *testing.T) { ResourceName("test.resource"), StartTime(now), WithSpanID(420), - Measured(), } span := tracer.StartSpan("web.request", opts...).(*span) assert := assert.New(t) @@ -287,7 +295,7 @@ func TestTracerStartSpanOptions(t *testing.T) { assert.Equal(now.UnixNano(), span.Start) assert.Equal(uint64(420), span.SpanID) assert.Equal(uint64(420), span.TraceID) - assert.Equal(1.0, span.Metrics[keyMeasured]) + assert.Equal(1.0, span.Metrics[keyTopLevel]) } func TestTracerStartChildSpan(t *testing.T) { diff --git a/ddtrace/tracer/transport.go b/ddtrace/tracer/transport.go index ce459e6edf..b6448875ca 100644 --- a/ddtrace/tracer/transport.go +++ b/ddtrace/tracer/transport.go @@ -20,6 +20,12 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/internal/version" ) +const ( + // headerComputedTopLevel specifies that the client has marked top-level spans, when set. + // Any non-empty value will mean 'yes'. + headerComputedTopLevel = "Datadog-Client-Computed-Top-Level" +) + var defaultClient = &http.Client{ // We copy the transport to avoid using the default one, as it might be // augmented with tracing and we don't want these calls to be recorded. @@ -114,6 +120,7 @@ func (t *httpTransport) send(p *payload) (body io.ReadCloser, err error) { } req.Header.Set(traceCountHeader, strconv.Itoa(p.itemCount())) req.Header.Set("Content-Length", strconv.Itoa(p.size())) + req.Header.Set(headerComputedTopLevel, "yes") response, err := t.client.Do(req) if err != nil { return nil, err