diff --git a/.chloggen/datadog-receiver-span-links.yaml b/.chloggen/datadog-receiver-span-links.yaml new file mode 100644 index 0000000000000..833aa19b4ee40 --- /dev/null +++ b/.chloggen/datadog-receiver-span-links.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: datadogreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Implement support for span links + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [37449] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [user] diff --git a/receiver/datadogreceiver/README.md b/receiver/datadogreceiver/README.md index 0eff13dbeb695..1d9072a00a166 100644 --- a/receiver/datadogreceiver/README.md +++ b/receiver/datadogreceiver/README.md @@ -58,6 +58,11 @@ https://github.com/open-telemetry/opentelemetry-collector/tree/main/config/confi - `dd.span.Resource`: The datadog resource name (as distinct from the span name) +### Optional Attributes + +- `_dd.span_links`: This receiver supports DD Agent's `_dd.span_links` attribute for span links creation, as produced by Datadog's tracing libraries. +Format example can be found [here](./internal/translator/traces_translator_test.go). + ### Datadog's API support **Traces** diff --git a/receiver/datadogreceiver/go.mod b/receiver/datadogreceiver/go.mod index 1e1a51cfb45ea..bf17cde77c374 100644 --- a/receiver/datadogreceiver/go.mod +++ b/receiver/datadogreceiver/go.mod @@ -1,6 +1,6 @@ module github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver -go 1.22.0 +go 1.22.7 require ( github.com/DataDog/agent-payload/v5 v5.0.144 @@ -27,6 +27,7 @@ require ( go.opentelemetry.io/collector/receiver v0.119.1-0.20250210123122-44b3eeda354c go.opentelemetry.io/collector/receiver/receivertest v0.119.1-0.20250210123122-44b3eeda354c go.opentelemetry.io/collector/semconv v0.119.1-0.20250210123122-44b3eeda354c + go.opentelemetry.io/otel/trace v1.34.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 google.golang.org/protobuf v1.36.5 @@ -103,14 +104,13 @@ require ( go.opentelemetry.io/otel/metric v1.34.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.34.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.8.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -125,10 +125,10 @@ retract ( replace github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics => ../../internal/exp/metrics -replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest => ../../pkg/pdatatest - -replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden => ../../pkg/golden - replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil => ../../pkg/pdatautil replace github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal => ../../internal/coreinternal + +replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden => ../../pkg/golden + +replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest => ../../pkg/pdatatest diff --git a/receiver/datadogreceiver/go.sum b/receiver/datadogreceiver/go.sum index d304f3fd21962..b27f705296c67 100644 --- a/receiver/datadogreceiver/go.sum +++ b/receiver/datadogreceiver/go.sum @@ -284,8 +284,8 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -304,8 +304,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= diff --git a/receiver/datadogreceiver/internal/translator/traces_translator.go b/receiver/datadogreceiver/internal/translator/traces_translator.go index 59a70b0121611..2ccad7ed53989 100644 --- a/receiver/datadogreceiver/internal/translator/traces_translator.go +++ b/receiver/datadogreceiver/internal/translator/traces_translator.go @@ -20,6 +20,7 @@ import ( "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" semconv "go.opentelemetry.io/collector/semconv/v1.16.0" + "go.opentelemetry.io/otel/trace" "google.golang.org/protobuf/proto" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/internal/translator/header" @@ -98,6 +99,8 @@ func ToTraces(payload *pb.TracerPayload, req *http.Request) ptrace.Traces { } newSpan := slice.AppendEmpty() + _ = tagsToSpanLinks(span.GetMeta(), newSpan.Links()) + newSpan.SetTraceID(uInt64ToTraceID(0, span.TraceID)) newSpan.SetSpanID(uInt64ToSpanID(span.SpanID)) newSpan.SetStartTimestamp(pcommon.Timestamp(span.Start)) @@ -165,6 +168,57 @@ func ToTraces(payload *pb.TracerPayload, req *http.Request) ptrace.Traces { return results } +// DDSpanLink represents the structure of each JSON object +type DDSpanLink struct { + TraceID string `json:"trace_id"` + SpanID string `json:"span_id"` + Tracestate string `json:"tracestate"` + Attributes map[string]any `json:"attributes"` +} + +func tagsToSpanLinks(tags map[string]string, dest ptrace.SpanLinkSlice) error { + key := "_dd.span_links" + val, ok := tags[key] + if !ok { + return nil + } + delete(tags, key) + + var spans []DDSpanLink + err := json.Unmarshal([]byte(val), &spans) + if err != nil { + return err + } + + for i := 0; i < len(spans); i++ { + span := spans[i] + link := dest.AppendEmpty() + + // Convert trace id. + rawTrace, errTrace := trace.TraceIDFromHex(span.TraceID) + if errTrace != nil { + return errTrace + } + link.SetTraceID(pcommon.TraceID(rawTrace)) + + // Convert span id. + rawSpan, errSpan := trace.SpanIDFromHex(span.SpanID) + if errSpan != nil { + return errSpan + } + link.SetSpanID(pcommon.SpanID(rawSpan)) + + link.TraceState().FromRaw(span.Tracestate) + + err = link.Attributes().FromRaw(span.Attributes) + if err != nil { + return err + } + } + + return nil +} + var bufferPool = sync.Pool{ New: func() any { return new(bytes.Buffer) diff --git a/receiver/datadogreceiver/internal/translator/traces_translator_test.go b/receiver/datadogreceiver/internal/translator/traces_translator_test.go index e3bba6831829b..f1ad7014d7c44 100644 --- a/receiver/datadogreceiver/internal/translator/traces_translator_test.go +++ b/receiver/datadogreceiver/internal/translator/traces_translator_test.go @@ -38,6 +38,8 @@ var data = [2]any{ 11: "service.name", 12: "1.0.1", 13: "version", + 14: "_dd.span_links", + 15: `[{"attributes":{"attr1":"val1","attr2":"val2"},"span_id":"70666bf9dee4a3fe","trace_id":"0eacdb57bebc935038bf5b4802ccabd5","tracestate":"dd=k:v"}]`, }, 1: [][][12]any{ { @@ -57,6 +59,7 @@ var data = [2]any{ 2: 3, 11: 6, 13: 12, + 14: 15, }, map[any]float64{ 5: 1.2, @@ -107,6 +110,13 @@ func TestTracePayloadV05Unmarshalling(t *testing.T) { numericAttributeValue, _ := span.Attributes().Get("numeric_attribute") numericAttributeFloat, _ := strconv.ParseFloat(numericAttributeValue.AsString(), 64) assert.Equal(t, 1.2, numericAttributeFloat) + + spanLink := span.Links().At(0) + assert.Equal(t, "70666bf9dee4a3fe", spanLink.SpanID().String()) + assert.Equal(t, "0eacdb57bebc935038bf5b4802ccabd5", spanLink.TraceID().String()) + assert.Equal(t, "dd=k:v", spanLink.TraceState().AsRaw()) + spanLinkAttrVal, _ := spanLink.Attributes().Get("attr1") + assert.Equal(t, "val1", spanLinkAttrVal.Str()) } func TestTracePayloadV07Unmarshalling(t *testing.T) { @@ -126,7 +136,7 @@ func TestTracePayloadV07Unmarshalling(t *testing.T) { translated := translatedPayloads[0] span := translated.GetChunks()[0].GetSpans()[0] assert.NotNil(t, span) - assert.Len(t, span.GetMeta(), 5, "missing attributes") + assert.Len(t, span.GetMeta(), 6, "missing attributes") value, exists := span.GetMeta()["service.name"] assert.True(t, exists, "service.name missing") assert.Equal(t, "my-service", value, "service.name attribute value incorrect") @@ -166,7 +176,7 @@ func TestTracePayloadApiV02Unmarshalling(t *testing.T) { span := translated.Chunks[0].Spans[0] assert.NotNil(t, span) - assert.Len(t, span.Meta, 5, "missing attributes") + assert.Len(t, span.Meta, 6, "missing attributes") assert.Equal(t, "my-service", span.Meta["service.name"]) assert.Equal(t, "my-name", span.Name) assert.Equal(t, "my-resource", span.Resource)