diff --git a/pkg/translator/zipkin/zipkinv2/to_translator.go b/pkg/translator/zipkin/zipkinv2/to_translator.go index 8bff5a6731bc..e452a73754ba 100644 --- a/pkg/translator/zipkin/zipkinv2/to_translator.go +++ b/pkg/translator/zipkin/zipkinv2/to_translator.go @@ -201,7 +201,7 @@ func zTagsToSpanLinks(tags map[string]string, dest ptrace.SpanLinkSlice) error { // Convert trace id. rawTrace := [16]byte{} - errTrace := unmarshalJSON(rawTrace[:], []byte(parts[0])) + errTrace := UnmarshalJSON(rawTrace[:], []byte(parts[0])) if errTrace != nil { return errTrace } @@ -209,7 +209,7 @@ func zTagsToSpanLinks(tags map[string]string, dest ptrace.SpanLinkSlice) error { // Convert span id. rawSpan := [8]byte{} - errSpan := unmarshalJSON(rawSpan[:], []byte(parts[1])) + errSpan := UnmarshalJSON(rawSpan[:], []byte(parts[1])) if errSpan != nil { return errSpan } @@ -228,7 +228,7 @@ func zTagsToSpanLinks(tags map[string]string, dest ptrace.SpanLinkSlice) error { if err := json.Unmarshal([]byte(jsonStr), &attrs); err != nil { return err } - if err := jsonMapToAttributeMap(attrs, link.Attributes()); err != nil { + if err := JsonMapToAttributeMap(attrs, link.Attributes()); err != nil { return err } @@ -265,7 +265,7 @@ func populateSpanEvents(zspan *zipkinmodel.SpanModel, events ptrace.SpanEventSli if err := json.Unmarshal([]byte(jsonStr), &attrs); err != nil { return err } - if err := jsonMapToAttributeMap(attrs, event.Attributes()); err != nil { + if err := JsonMapToAttributeMap(attrs, event.Attributes()); err != nil { return err } @@ -278,7 +278,7 @@ func populateSpanEvents(zspan *zipkinmodel.SpanModel, events ptrace.SpanEventSli return nil } -func jsonMapToAttributeMap(attrs map[string]any, dest pcommon.Map) error { +func JsonMapToAttributeMap(attrs map[string]any, dest pcommon.Map) error { for key, val := range attrs { if s, ok := val.(string); ok { dest.PutStr(key, s) @@ -437,9 +437,9 @@ func setTimestampsV2(zspan *zipkinmodel.SpanModel, dest ptrace.Span, destAttrs p } } -// unmarshalJSON inflates trace id from hex string, possibly enclosed in quotes. +// UnmarshalJSON inflates trace id from hex string, possibly enclosed in quotes. // TODO: Find a way to avoid this duplicate code. Consider to expose this in pdata. -func unmarshalJSON(dst []byte, src []byte) error { +func UnmarshalJSON(dst []byte, src []byte) error { if l := len(src); l >= 2 && src[0] == '"' && src[l-1] == '"' { src = src[1 : l-1] } diff --git a/receiver/datadogreceiver/README.md b/receiver/datadogreceiver/README.md index 0eff13dbeb69..1d9072a00a16 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 2208e18b7f9d..dc3b4c47f583 100644 --- a/receiver/datadogreceiver/go.mod +++ b/receiver/datadogreceiver/go.mod @@ -1,6 +1,8 @@ module github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver -go 1.22.0 +go 1.22.7 + +toolchain go1.23.0 require ( github.com/DataDog/agent-payload/v5 v5.0.140 @@ -12,6 +14,7 @@ require ( github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.118.0 github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.118.0 github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.118.0 + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.118.0 github.com/stretchr/testify v1.10.0 github.com/tinylib/msgp v1.2.5 github.com/vmihailenco/msgpack/v5 v5.4.1 @@ -44,7 +47,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect @@ -68,11 +71,12 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.118.0 // indirect + github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/outcaste-io/ristretto v0.2.1 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect github.com/rs/cors v1.11.1 // indirect github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect @@ -108,7 +112,7 @@ require ( golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.8.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect google.golang.org/grpc v1.69.4 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -131,3 +135,5 @@ replace github.com/open-telemetry/opentelemetry-collector-contrib/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/translator/zipkin => ../../pkg/translator/zipkin diff --git a/receiver/datadogreceiver/go.sum b/receiver/datadogreceiver/go.sum index d5ce70a6b79a..bbf36760ee60 100644 --- a/receiver/datadogreceiver/go.sum +++ b/receiver/datadogreceiver/go.sum @@ -35,8 +35,9 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= @@ -109,6 +110,8 @@ github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.114.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.114.0/go.mod h1:C9Zgh/N3j4NR2D+1FGAA1YizhFW9OS51DwLUFJTdXN4= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.114.0 h1:I4ZYVRYW3Cjb65sPENZ9kHam/JUMXNEp2n/knJ0C0Vc= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.114.0/go.mod h1:4BhyIaOn2LS48WS+ZNix4TpP0+goq9gDEtGzth5Cr3M= +github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= github.com/outcaste-io/ristretto v0.2.1 h1:KCItuNIGJZcursqHr3ghO7fc5ddZLEHspL9UR0cQM64= github.com/outcaste-io/ristretto v0.2.1/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= @@ -117,8 +120,9 @@ github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI= github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -308,8 +312,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 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-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= diff --git a/receiver/datadogreceiver/internal/translator/traces_translator.go b/receiver/datadogreceiver/internal/translator/traces_translator.go index 59a70b012161..1ca92059f71d 100644 --- a/receiver/datadogreceiver/internal/translator/traces_translator.go +++ b/receiver/datadogreceiver/internal/translator/traces_translator.go @@ -22,6 +22,7 @@ import ( semconv "go.opentelemetry.io/collector/semconv/v1.16.0" "google.golang.org/protobuf/proto" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin/zipkinv2" "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,56 @@ 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 := [16]byte{} + errTrace := zipkinv2.UnmarshalJSON(rawTrace[:], []byte(span.TraceID)) + if errTrace != nil { + return errTrace + } + link.SetTraceID(rawTrace) + + // Convert span id. + rawSpan := [8]byte{} + errSpan := zipkinv2.UnmarshalJSON(rawSpan[:], []byte(span.SpanID)) + if errSpan != nil { + return errSpan + } + link.SetSpanID(rawSpan) + + link.TraceState().FromRaw(span.Tracestate) + + _ = zipkinv2.JsonMapToAttributeMap(span.Attributes, link.Attributes()) + } + + 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 e3bba6831829..f1ad7014d7c4 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)