Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion ddtrace/opentelemetry/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,18 @@ func (s *span) End(options ...oteltrace.SpanEndOption) {
var finishCfg = oteltrace.NewSpanEndConfig(options...)
var opts []tracer.FinishOption
if s.statusInfo.code == otelcodes.Error {
// Set unconditionally: this feeds the OTLP status message (see convertSpanStatus)
// even under semantics, where error.msg is otherwise suppressed as a raw attribute.
s.DD.SetTag(ext.ErrorMsg, s.statusInfo.description)
opts = append(opts, tracer.WithError(errors.New(s.statusInfo.description)))
if s.otelSemanticsEnabled {
// Under OTel semantics, mark the span errored for the OTLP status without
// injecting DD's error.* tags: WithError would set error.type to the reflect
// type of the status wrapper ("*errors.errorString"), clobbering the stable
// OTel error.type that instrumentation sets.
s.DD.SetTag(ext.Error, true)
} else {
opts = append(opts, tracer.WithError(errors.New(s.statusInfo.description)))
}
}
if len(s.finishOpts) != 0 {
opts = append(opts, s.finishOpts...)
Expand Down
55 changes: 55 additions & 0 deletions ddtrace/opentelemetry/span_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ func TestSpanEnd(t *testing.T) {
assert.Equal(1.0, p[0]["error"]) // this should be an error span
meta := fmt.Sprintf("%v", p[0]["meta"])
assert.Contains(meta, msg)
// Non-semantics error path: WithError injects error.type as the wrapper's reflect type.
assert.Contains(meta, "error.type:*errors.errorString")
for k, v := range attributes {
assert.Contains(meta, fmt.Sprintf("%s:%s", k, v))
}
Expand Down Expand Up @@ -1040,3 +1042,56 @@ func TestRemapStatusCodeOtelSemantics(t *testing.T) {
assert.NotContains(meta, "http.status_code")
assert.Contains(metrics, "http.response.status_code:200")
}

// TestSpanEndErrorTypeOtelSemantics verifies that finishing an error-status span under
// OTel semantics marks it errored without injecting Datadog's error.type/error.stack: an
// instrumentation-set error.type survives, and when none is set no reflect-type value is
// injected. (The non-semantics WithError path is covered by TestSpanEnd.)
func TestSpanEndErrorTypeOtelSemantics(t *testing.T) {
// finish an error-status span under semantics, optionally with an
// instrumentation-set error.type, and return the exported span.
run := func(t *testing.T, instrumentationErrorType string) map[string]any {
// Reload global config on cleanup so the flag doesn't leak; LIFO order runs this
// after t.Setenv restores the env.
t.Cleanup(func() { internalconfig.CreateNew() })
t.Setenv("DD_TRACE_OTEL_SEMANTICS_ENABLED", "true")
internalconfig.CreateNew()

_, payloads, cleanup := mockTracerProvider(t)
tr := otel.Tracer("")
defer cleanup()

_, sp := tr.Start(context.Background(), "op")
if instrumentationErrorType != "" {
sp.SetAttributes(attribute.String(ext.ErrorType, instrumentationErrorType))
}
sp.SetStatus(codes.Error, "boom")
sp.End()

tracer.Flush()
traces, err := waitForPayload(payloads)
if err != nil {
t.Fatal(err.Error())
}
return traces[0][0]
}

t.Run("preserves instrumentation error.type", func(t *testing.T) {
assert := assert.New(t)
span := run(t, "*net.OpError")
assert.Equal(1.0, span["error"]) // span still flagged errored
meta := fmt.Sprintf("%v", span["meta"])
assert.Contains(meta, "error.type:*net.OpError") // instrumentation value survives
assert.NotContains(meta, "*errors.errorString") // WithError was skipped
assert.NotContains(meta, "error.stack") // no DD stack injected
})

t.Run("injects no error.type when instrumentation sets none", func(t *testing.T) {
assert := assert.New(t)
span := run(t, "")
assert.Equal(1.0, span["error"]) // still errored via the error flag / OTLP status
meta := fmt.Sprintf("%v", span["meta"])
assert.NotContains(meta, "error.type") // no reflect-type value injected
assert.NotContains(meta, "error.stack")
})
}
3 changes: 2 additions & 1 deletion ddtrace/tracer/span_to_otlp.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,10 @@ var otelIntMetricKeys = map[string]struct{}{

// ddOnlyMetaKeys are Datadog-specific span tags omitted under OTelSemanticsEnabled;
// they have no OTel equivalent. span.kind is already carried by the OTLP SpanKind field.
// error.type is intentionally excluded: it is a stable OTel attribute that instrumentation
// sets, so it passes through.
var ddOnlyMetaKeys = map[string]struct{}{
ext.ErrorMsg: {},
ext.ErrorType: {},
ext.ErrorStack: {},
ext.ErrorHandlingStack: {},
ext.SpanKind: {},
Expand Down
13 changes: 8 additions & 5 deletions ddtrace/tracer/span_to_otlp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,29 +246,32 @@ func TestConvertSpanAttributesIntEncoding(t *testing.T) {
}

// TestConvertSpanAttributesOtelSemantics verifies that under OTel semantics the OTLP
// exporter omits Datadog-specific attributes (span identity, error.*, span.kind).
// exporter omits Datadog-specific attributes (span identity, error.message/stack,
// span.kind) but preserves error.type, which is a stable OpenTelemetry attribute.
func TestConvertSpanAttributesOtelSemantics(t *testing.T) {
s := newBasicSpan("op")
s.meta = tinternal.NewSpanMetaFromMap(map[string]string{
"http.request.method": "GET",
ext.ErrorMsg: "boom",
ext.ErrorType: "*errors.errorString",
ext.ErrorType: "*net.OpError",
ext.ErrorStack: "goroutine 1 ...",
ext.ErrorHandlingStack: "goroutine 1 ...",
ext.SpanKind: ext.SpanKindServer,
})
m := keyValuesToMap(convertSpanAttributes(s, "", true))

// DD-only span-identity, error, and span.kind attributes are suppressed.
// DD-only span-identity, error message/stack, and span.kind attributes are suppressed.
for _, k := range []string{
"operation.name", "resource.name", "span.type",
ext.ErrorMsg, ext.ErrorType, ext.ErrorStack, ext.ErrorHandlingStack, ext.SpanKind,
ext.ErrorMsg, ext.ErrorStack, ext.ErrorHandlingStack, ext.SpanKind,
} {
_, ok := m[k]
assert.Falsef(t, ok, "%q should be suppressed when OTel semantics is enabled", k)
}

// Non-DD-only meta is preserved unchanged.
// error.type is a stable OTel attribute and passes through unchanged, as does
// non-DD-only meta.
assert.Equal(t, "*net.OpError", m[ext.ErrorType])
assert.Equal(t, "GET", m["http.request.method"])
}

Expand Down
Loading