diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index c4ebc449dc6f9..a13b61df2a1e9 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -1809,6 +1809,7 @@ core,go.opentelemetry.io/collector/receiver/receiverhelper,Apache-2.0,Copyright core,go.opentelemetry.io/collector/receiver/receivertest,Apache-2.0,Copyright The OpenTelemetry Authors core,go.opentelemetry.io/collector/semconv/v1.17.0,Apache-2.0,Copyright The OpenTelemetry Authors core,go.opentelemetry.io/collector/semconv/v1.18.0,Apache-2.0,Copyright The OpenTelemetry Authors +core,go.opentelemetry.io/collector/semconv/v1.5.0,Apache-2.0,Copyright The OpenTelemetry Authors core,go.opentelemetry.io/collector/semconv/v1.6.1,Apache-2.0,Copyright The OpenTelemetry Authors core,go.opentelemetry.io/collector/service,Apache-2.0,Copyright The OpenTelemetry Authors core,go.opentelemetry.io/collector/service/extensions,Apache-2.0,Copyright The OpenTelemetry Authors diff --git a/comp/core/log/go.mod b/comp/core/log/go.mod index 2da6a64f340b9..e7210acede7cb 100644 --- a/comp/core/log/go.mod +++ b/comp/core/log/go.mod @@ -78,6 +78,7 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/knadh/koanf/providers/confmap v0.1.0 // indirect @@ -87,6 +88,8 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/outcaste-io/ristretto v0.2.1 // indirect github.com/pelletier/go-toml v1.2.0 // indirect github.com/philhofer/fwd v1.1.2 // indirect diff --git a/comp/core/log/go.sum b/comp/core/log/go.sum index 259de10f8bfea..8cd2ab259258e 100644 --- a/comp/core/log/go.sum +++ b/comp/core/log/go.sum @@ -130,6 +130,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= @@ -167,9 +169,12 @@ github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= diff --git a/pkg/trace/api/otlp.go b/pkg/trace/api/otlp.go index ec6a5fdffcbc2..fbd3ff3272dd8 100644 --- a/pkg/trace/api/otlp.go +++ b/pkg/trace/api/otlp.go @@ -35,7 +35,6 @@ import ( "go.opentelemetry.io/collector/pdata/ptrace/ptraceotlp" semconv117 "go.opentelemetry.io/collector/semconv/v1.17.0" semconv "go.opentelemetry.io/collector/semconv/v1.6.1" - "go.opentelemetry.io/otel/attribute" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) @@ -44,10 +43,6 @@ import ( // computed for the resource spans. const keyStatsComputed = "_dd.stats_computed" -var ( - signalTypeSet = attribute.NewSet(attribute.String("signal", "traces")) -) - var _ (ptraceotlp.GRPCServer) = (*OTLPReceiver)(nil) // OTLPReceiver implements an OpenTelemetry Collector receiver which accepts incoming @@ -194,7 +189,7 @@ func (o *OTLPReceiver) sample(tid uint64) sampler.SamplingPriority { func (o *OTLPReceiver) ReceiveResourceSpans(ctx context.Context, rspans ptrace.ResourceSpans, httpHeader http.Header) source.Source { // each rspans is coming from a different resource and should be considered // a separate payload; typically there is only one item in this slice - src, srcok := o.conf.OTLPReceiver.AttributesTranslator.ResourceToSource(ctx, rspans.Resource(), signalTypeSet) + src, srcok := o.conf.OTLPReceiver.AttributesTranslator.ResourceToSource(ctx, rspans.Resource(), traceutil.SignalTypeSet) hostFromMap := func(m map[string]string, key string) { // hostFromMap sets the hostname to m[key] if it is set. if v, ok := m[key]; ok { diff --git a/pkg/trace/go.mod b/pkg/trace/go.mod index 43e92e2f9780a..9ba7b10ae8a44 100644 --- a/pkg/trace/go.mod +++ b/pkg/trace/go.mod @@ -36,6 +36,7 @@ require ( go.opentelemetry.io/collector/pdata v1.0.1 go.opentelemetry.io/collector/semconv v0.93.0 go.opentelemetry.io/otel v1.22.0 + go.opentelemetry.io/otel/metric v1.22.0 go.uber.org/atomic v1.11.0 golang.org/x/sys v0.16.0 golang.org/x/time v0.3.0 @@ -97,7 +98,6 @@ require ( go.opentelemetry.io/collector/confmap v0.93.0 // indirect go.opentelemetry.io/collector/featuregate v1.0.1 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.45.0 // indirect - go.opentelemetry.io/otel/metric v1.22.0 // indirect go.opentelemetry.io/otel/sdk v1.22.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.22.0 // indirect go.opentelemetry.io/otel/trace v1.22.0 // indirect diff --git a/pkg/trace/traceutil/normalize.go b/pkg/trace/traceutil/normalize.go index bce5d9820cb95..0cda4398d7831 100644 --- a/pkg/trace/traceutil/normalize.go +++ b/pkg/trace/traceutil/normalize.go @@ -25,6 +25,8 @@ const ( MaxNameLen = 100 // MaxServiceLen the maximum length a service can have MaxServiceLen = 100 + // MaxResourceLen the maximum length a resource can have + MaxResourceLen = 5000 ) var ( diff --git a/pkg/trace/traceutil/otel_util.go b/pkg/trace/traceutil/otel_util.go new file mode 100644 index 0000000000000..103970087d136 --- /dev/null +++ b/pkg/trace/traceutil/otel_util.go @@ -0,0 +1,277 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +package traceutil + +import ( + "context" + "strings" + + "github.com/DataDog/datadog-agent/pkg/trace/log" + "github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes" + "github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes/source" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + semconv117 "go.opentelemetry.io/collector/semconv/v1.17.0" + semconv "go.opentelemetry.io/collector/semconv/v1.5.0" + "go.opentelemetry.io/otel/attribute" +) + +// Util functions for converting OTel semantics to DD semantics. +// TODO(OTEL-1726): reuse the same mapping code for ReceiveResourceSpans and Concentrator + +var ( + // SignalTypeSet is the OTel attribute set for traces. + SignalTypeSet = attribute.NewSet(attribute.String("signal", "traces")) +) + +// IndexOTelSpans iterates over the input OTel spans and returns 3 maps: +// OTel spans indexed by span ID, OTel resources indexed by span ID, OTel instrumentation scopes indexed by span ID. +// Skips spans with invalid trace ID or span ID. If there are multiple spans with the same (non-zero) span ID, the last one wins. +func IndexOTelSpans(traces ptrace.Traces) (map[pcommon.SpanID]ptrace.Span, map[pcommon.SpanID]pcommon.Resource, map[pcommon.SpanID]pcommon.InstrumentationScope) { + spanByID := make(map[pcommon.SpanID]ptrace.Span) + resByID := make(map[pcommon.SpanID]pcommon.Resource) + scopeByID := make(map[pcommon.SpanID]pcommon.InstrumentationScope) + rspanss := traces.ResourceSpans() + for i := 0; i < rspanss.Len(); i++ { + rspans := rspanss.At(i) + res := rspans.Resource() + for j := 0; j < rspans.ScopeSpans().Len(); j++ { + libspans := rspans.ScopeSpans().At(j) + for k := 0; k < libspans.Spans().Len(); k++ { + span := libspans.Spans().At(k) + if span.TraceID().IsEmpty() || span.SpanID().IsEmpty() { + continue + } + spanByID[span.SpanID()] = span + resByID[span.SpanID()] = res + scopeByID[span.SpanID()] = libspans.Scope() + } + } + } + return spanByID, resByID, scopeByID +} + +// GetTopLevelOTelSpans returns the span IDs of the top level OTel spans. +func GetTopLevelOTelSpans(spanByID map[pcommon.SpanID]ptrace.Span, resByID map[pcommon.SpanID]pcommon.Resource, topLevelByKind bool) map[pcommon.SpanID]struct{} { + topLevelSpans := make(map[pcommon.SpanID]struct{}) + for spanID, span := range spanByID { + if span.ParentSpanID().IsEmpty() { + // case 1: root span + topLevelSpans[spanID] = struct{}{} + } else if topLevelByKind { + // New behavior for computing top level OTel spans, see computeTopLevelAndMeasured in pkg/trace/api/otlp.go + spanKind := span.Kind() + if spanKind == ptrace.SpanKindServer || spanKind == ptrace.SpanKindConsumer { + // span is a server-side span, mark as top level + topLevelSpans[spanID] = struct{}{} + } + } else { + // Fall back to old behavior in ComputeTopLevel + if _, ok := spanByID[span.ParentSpanID()]; !ok { + // case 2: parent span not in the same chunk, presumably it belongs to another service + topLevelSpans[spanID] = struct{}{} + } else { + svc := GetOTelAttrVal(resByID[spanID].Attributes(), true, semconv.AttributeServiceName) + parentSvc := GetOTelAttrVal(resByID[span.ParentSpanID()].Attributes(), true, semconv.AttributeServiceName) + if svc != parentSvc { + // case 3: parent is not in the same service + topLevelSpans[spanID] = struct{}{} + } + } + } + } + return topLevelSpans +} + +// GetOTelAttrVal returns the matched value as a string in the input map with the given keys. +// If there are multiple keys present, the first matched one is returned. +// If normalize is true, normalize the return value with NormalizeTagValue. +func GetOTelAttrVal(attrs pcommon.Map, normalize bool, keys ...string) string { + val := "" + for _, key := range keys { + attrval, exists := attrs.Get(key) + if exists { + val = attrval.AsString() + } + } + + if normalize { + val = NormalizeTagValue(val) + } + + return val +} + +// GetOTelAttrValInResAndSpanAttrs returns the matched value as a string in the OTel resource attributes and span attributes with the given keys. +// If there are multiple keys present, the first matched one is returned. +// If the key is present in both resource attributes and span attributes, resource attributes take precedence. +// If normalize is true, normalize the return value with NormalizeTagValue. +func GetOTelAttrValInResAndSpanAttrs(span ptrace.Span, res pcommon.Resource, normalize bool, keys ...string) string { + if val := GetOTelAttrVal(res.Attributes(), normalize, keys...); val != "" { + return val + } + return GetOTelAttrVal(span.Attributes(), normalize, keys...) +} + +// GetOTelSpanType returns the DD span type based on OTel span kind and attributes. +func GetOTelSpanType(span ptrace.Span, res pcommon.Resource) string { + var typ string + switch span.Kind() { + case ptrace.SpanKindServer: + typ = "web" + case ptrace.SpanKindClient: + db := GetOTelAttrValInResAndSpanAttrs(span, res, true, semconv.AttributeDBSystem) + if db == "redis" || db == "memcached" { + typ = "cache" + } else if db != "" { + typ = "db" + } else { + typ = "http" + } + default: + typ = "custom" + } + return typ +} + +// GetOTelService returns the DD service name based on OTel span and resource attributes. +func GetOTelService(span ptrace.Span, res pcommon.Resource, normalize bool) string { + // No need to normalize with NormalizeTagValue since we will do NormalizeService later + svc := GetOTelAttrValInResAndSpanAttrs(span, res, false, semconv.AttributeServiceName) + if svc == "" { + svc = "otlpresourcenoservicename" + } + if normalize { + newsvc, err := NormalizeService(svc, "") + switch err { + case ErrTooLong: + log.Debugf("Fixing malformed trace. Service is too long (reason:service_truncate), truncating span.service to length=%d: %s", MaxServiceLen, svc) + case ErrInvalid: + log.Debugf("Fixing malformed trace. Service is invalid (reason:service_invalid), replacing invalid span.service=%s with fallback span.service=%s", svc, newsvc) + } + svc = newsvc + } + return svc +} + +// GetOTelResource returns the DD resource name based on OTel span and resource attributes. +func GetOTelResource(span ptrace.Span, res pcommon.Resource) (resName string) { + resName = GetOTelAttrValInResAndSpanAttrs(span, res, false, "resource.name") + if resName == "" { + if m := GetOTelAttrValInResAndSpanAttrs(span, res, false, semconv.AttributeHTTPMethod); m != "" { + // use the HTTP method + route (if available) + resName = m + if route := GetOTelAttrValInResAndSpanAttrs(span, res, false, semconv.AttributeHTTPRoute); route != "" { + resName = resName + " " + route + } + } else if m := GetOTelAttrValInResAndSpanAttrs(span, res, false, semconv.AttributeMessagingOperation); m != "" { + resName = m + // use the messaging operation + if dest := GetOTelAttrValInResAndSpanAttrs(span, res, false, semconv.AttributeMessagingDestination, semconv117.AttributeMessagingDestinationName); dest != "" { + resName = resName + " " + dest + } + } else if m := GetOTelAttrValInResAndSpanAttrs(span, res, false, semconv.AttributeRPCMethod); m != "" { + resName = m + // use the RPC method + if svc := GetOTelAttrValInResAndSpanAttrs(span, res, false, semconv.AttributeRPCService); m != "" { + // ...and service if available + resName = resName + " " + svc + } + } else { + resName = span.Name() + } + } + if len(resName) > MaxResourceLen { + resName = resName[:MaxResourceLen] + } + return +} + +// GetOTelOperationName returns the DD operation name based on OTel span and resource attributes and given configs. +func GetOTelOperationName( + span ptrace.Span, + res pcommon.Resource, + lib pcommon.InstrumentationScope, + spanNameAsResourceName bool, + spanNameRemappings map[string]string, + normalize bool) string { + // No need to normalize with NormalizeTagValue since we will do NormalizeName later + name := GetOTelAttrValInResAndSpanAttrs(span, res, false, "operation.name") + if name == "" { + if spanNameAsResourceName { + name = span.Name() + } else { + name = strings.ToLower(span.Kind().String()) + if lib.Name() != "" { + name = lib.Name() + "." + name + } else { + name = "opentelemetry." + name + } + } + } + if v, ok := spanNameRemappings[name]; ok { + name = v + } + + if normalize { + normalizeName, err := NormalizeName(name) + switch err { + case ErrEmpty: + log.Debugf("Fixing malformed trace. Name is empty (reason:span_name_empty), setting span.name=%s", normalizeName) + case ErrTooLong: + log.Debugf("Fixing malformed trace. Name is too long (reason:span_name_truncate), truncating span.name to length=%d", MaxServiceLen) + case ErrInvalid: + log.Debugf("Fixing malformed trace. Name is invalid (reason:span_name_invalid), setting span.name=%s", normalizeName) + } + name = normalizeName + } + + return name +} + +// GetOTelHostname returns the DD hostname based on OTel span and resource attributes. +func GetOTelHostname(span ptrace.Span, res pcommon.Resource, tr *attributes.Translator, fallbackHost string) string { + ctx := context.Background() + src, srcok := tr.ResourceToSource(ctx, res, SignalTypeSet) + if !srcok { + if v := GetOTelAttrValInResAndSpanAttrs(span, res, false, "_dd.hostname"); v != "" { + src = source.Source{Kind: source.HostnameKind, Identifier: v} + srcok = true + } + } + if srcok { + switch src.Kind { + case source.HostnameKind: + return src.Identifier + default: + // We are not on a hostname (serverless), hence the hostname is empty + return "" + } + } else { + // fallback hostname from Agent conf.Hostname + return fallbackHost + } +} + +// GetOTelStatusCode returns the DD status code of the OTel span. +func GetOTelStatusCode(span ptrace.Span) uint32 { + if code, ok := span.Attributes().Get(semconv.AttributeHTTPStatusCode); ok { + return uint32(code.Int()) + } + return 0 +} + +// GetOTelContainerTags returns a list of DD container tags in the OTel resource attributes. +// Tags are always normalized. +func GetOTelContainerTags(rattrs pcommon.Map) []string { + var containerTags []string + containerTagsMap := attributes.ContainerTagsFromResourceAttributes(rattrs) + for k, v := range containerTagsMap { + t := NormalizeTag(k + ":" + v) + containerTags = append(containerTags, t) + } + return containerTags +} diff --git a/pkg/trace/traceutil/otel_util_test.go b/pkg/trace/traceutil/otel_util_test.go new file mode 100644 index 0000000000000..115fbe6c40313 --- /dev/null +++ b/pkg/trace/traceutil/otel_util_test.go @@ -0,0 +1,429 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +package traceutil + +import ( + "strings" + "testing" + "time" + + "github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + semconv "go.opentelemetry.io/collector/semconv/v1.5.0" + "go.opentelemetry.io/otel/metric/noop" +) + +var ( + testTraceID = [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + testSpanID1 = [8]byte{1, 2, 3, 4, 5, 6, 7, 8} + testSpanID2 = [8]byte{2, 2, 3, 4, 5, 6, 7, 8} + testSpanID3 = [8]byte{3, 2, 3, 4, 5, 6, 7, 8} + testSpanID4 = [8]byte{4, 2, 3, 4, 5, 6, 7, 8} + testSpanID5 = [8]byte{5, 2, 3, 4, 5, 6, 7, 8} + testSpanID6 = [8]byte{6, 2, 3, 4, 5, 6, 7, 8} +) + +func TestIndexOTelSpans(t *testing.T) { + traces := ptrace.NewTraces() + + rspan1 := traces.ResourceSpans().AppendEmpty() + res1 := rspan1.Resource() + rattrs := res1.Attributes() + rattrs.PutStr(semconv.AttributeHostName, "host1") + rattrs.PutStr(semconv.AttributeServiceName, "svc1") + rattrs.PutStr(semconv.AttributeDeploymentEnvironment, "env1") + + sspan1 := rspan1.ScopeSpans().AppendEmpty() + scope1 := sspan1.Scope() + scope1.SetName("scope") + scope1.SetVersion("1.0.0") + + span1 := sspan1.Spans().AppendEmpty() + span1.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Now())) + span1.SetKind(ptrace.SpanKindClient) + span1.SetName("span_name1") + span1.SetTraceID(testTraceID) + span1.SetSpanID(testSpanID1) + + span2 := sspan1.Spans().AppendEmpty() + span2.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Now())) + span2.SetKind(ptrace.SpanKindClient) + span2.SetName("span_name2") + span2.SetTraceID(testTraceID) + span2.SetSpanID(testSpanID2) + + rspan2 := traces.ResourceSpans().AppendEmpty() + res2 := rspan2.Resource() + rattrs = res2.Attributes() + rattrs.PutStr(semconv.AttributeHostName, "host2") + rattrs.PutStr(semconv.AttributeServiceName, "svc2") + rattrs.PutStr(semconv.AttributeDeploymentEnvironment, "env2") + + sspan2 := rspan2.ScopeSpans().AppendEmpty() + scope2 := sspan2.Scope() + scope2.SetName("scope2") + scope2.SetVersion("1.0.0") + + span3 := sspan2.Spans().AppendEmpty() + span3.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Now())) + span3.SetKind(ptrace.SpanKindClient) + span3.SetName("span_name3") + span3.SetTraceID(testTraceID) + span3.SetSpanID(testSpanID3) + + // Spans with empty trace ID are discarded + span4 := sspan2.Spans().AppendEmpty() + span4.SetTraceID(pcommon.NewTraceIDEmpty()) + + // Spans with empty span ID are discarded + span5 := sspan2.Spans().AppendEmpty() + span5.SetTraceID(testTraceID) + span5.SetSpanID(pcommon.NewSpanIDEmpty()) + + spanByID, resByID, scopeByID := IndexOTelSpans(traces) + assert.Equal(t, map[pcommon.SpanID]ptrace.Span{testSpanID1: span1, testSpanID2: span2, testSpanID3: span3}, spanByID) + assert.Equal(t, map[pcommon.SpanID]pcommon.Resource{testSpanID1: res1, testSpanID2: res1, testSpanID3: res2}, resByID) + assert.Equal(t, map[pcommon.SpanID]pcommon.InstrumentationScope{testSpanID1: scope1, testSpanID2: scope1, testSpanID3: scope2}, scopeByID) +} + +func TestGetTopLevelOTelSpans(t *testing.T) { + traces := ptrace.NewTraces() + rspans := traces.ResourceSpans().AppendEmpty() + rspans.Resource().Attributes().PutStr(semconv.AttributeServiceName, "svc1") + sspans := rspans.ScopeSpans().AppendEmpty() + + // Root span + // Is top level in both new and old rules + span1 := sspans.Spans().AppendEmpty() + span1.SetTraceID(testTraceID) + span1.SetSpanID(testSpanID1) + + // Span with span kind server + // Is top-level in new rules, is not in old rules + span2 := sspans.Spans().AppendEmpty() + span2.SetTraceID(testTraceID) + span2.SetSpanID(testSpanID2) + span2.SetParentSpanID(testSpanID1) + span2.SetKind(ptrace.SpanKindServer) + + // Span with span kind consumer + // Is top-level in new rules, is not in old rules + span3 := sspans.Spans().AppendEmpty() + span3.SetTraceID(testTraceID) + span3.SetSpanID(testSpanID3) + span3.SetParentSpanID(testSpanID1) + span3.SetKind(ptrace.SpanKindConsumer) + + // Span with span kind client but parent is not in this chunk + // Is top-level in old rules, is not in new rules + span4 := sspans.Spans().AppendEmpty() + span4.SetTraceID(testTraceID) + span4.SetSpanID(testSpanID4) + span4.SetParentSpanID(testSpanID6) + span4.SetKind(ptrace.SpanKindClient) + + // Spans with span kind internal but has a different service than its parent + // Is top-level in old rules, is not in new rules + rspans2 := traces.ResourceSpans().AppendEmpty() + rspans2.Resource().Attributes().PutStr(semconv.AttributeServiceName, "svc2") + span5 := rspans2.ScopeSpans().AppendEmpty().Spans().AppendEmpty() + span5.SetTraceID(testTraceID) + span5.SetSpanID(testSpanID5) + span5.SetParentSpanID(testSpanID1) + span5.SetKind(ptrace.SpanKindInternal) + + spanByID, resByID, _ := IndexOTelSpans(traces) + topLevelSpans := GetTopLevelOTelSpans(spanByID, resByID, true) + assert.Equal(t, topLevelSpans, map[pcommon.SpanID]struct{}{ + testSpanID1: {}, + testSpanID2: {}, + testSpanID3: {}, + }) + + topLevelSpans = GetTopLevelOTelSpans(spanByID, resByID, false) + assert.Equal(t, topLevelSpans, map[pcommon.SpanID]struct{}{ + testSpanID1: {}, + testSpanID4: {}, + testSpanID5: {}, + }) +} + +func TestGetOTelSpanType(t *testing.T) { + for _, tt := range []struct { + name string + spanKind ptrace.SpanKind + rattrs map[string]string + expected string + }{ + { + name: "web span", + spanKind: ptrace.SpanKindServer, + expected: "web", + }, + { + name: "redis span", + spanKind: ptrace.SpanKindClient, + rattrs: map[string]string{semconv.AttributeDBSystem: "redis"}, + expected: "cache", + }, + { + name: "memcached span", + spanKind: ptrace.SpanKindClient, + rattrs: map[string]string{semconv.AttributeDBSystem: "memcached"}, + expected: "cache", + }, + { + name: "other db client span", + spanKind: ptrace.SpanKindClient, + rattrs: map[string]string{semconv.AttributeDBSystem: "postgres"}, + expected: "db", + }, + { + name: "http client span", + spanKind: ptrace.SpanKindClient, + expected: "http", + }, + { + name: "other custom span", + spanKind: ptrace.SpanKindInternal, + expected: "custom", + }, + } { + t.Run(tt.name, func(t *testing.T) { + span := ptrace.NewSpan() + span.SetKind(tt.spanKind) + res := pcommon.NewResource() + for k, v := range tt.rattrs { + res.Attributes().PutStr(k, v) + } + actual := GetOTelSpanType(span, res) + assert.Equal(t, tt.expected, actual) + }) + } +} + +func TestGetOTelService(t *testing.T) { + for _, tt := range []struct { + name string + rattrs map[string]string + normalize bool + expected string + }{ + { + name: "service not set", + expected: "otlpresourcenoservicename", + }, + { + name: "normal service", + rattrs: map[string]string{semconv.AttributeServiceName: "svc"}, + expected: "svc", + }, + { + name: "truncate long service", + rattrs: map[string]string{semconv.AttributeServiceName: strings.Repeat("a", MaxServiceLen+1)}, + normalize: true, + expected: strings.Repeat("a", MaxServiceLen), + }, + { + name: "invalid service", + rattrs: map[string]string{semconv.AttributeServiceName: "%$^"}, + normalize: true, + expected: DefaultServiceName, + }, + } { + t.Run(tt.name, func(t *testing.T) { + span := ptrace.NewSpan() + res := pcommon.NewResource() + for k, v := range tt.rattrs { + res.Attributes().PutStr(k, v) + } + actual := GetOTelService(span, res, tt.normalize) + assert.Equal(t, tt.expected, actual) + }) + } +} + +func TestGetOTelResource(t *testing.T) { + for _, tt := range []struct { + name string + rattrs map[string]string + sattrs map[string]string + normalize bool + expected string + }{ + { + name: "resource not set", + expected: "span_name", + }, + { + name: "normal resource", + sattrs: map[string]string{"resource.name": "res"}, + expected: "res", + }, + { + name: "truncate long resource", + sattrs: map[string]string{"resource.name": strings.Repeat("a", MaxResourceLen+1)}, + normalize: true, + expected: strings.Repeat("a", MaxResourceLen), + }, + } { + t.Run(tt.name, func(t *testing.T) { + span := ptrace.NewSpan() + span.SetName("span_name") + for k, v := range tt.sattrs { + span.Attributes().PutStr(k, v) + } + res := pcommon.NewResource() + for k, v := range tt.rattrs { + res.Attributes().PutStr(k, v) + } + actual := GetOTelResource(span, res) + assert.Equal(t, tt.expected, actual) + }) + } +} + +func TestGetOTelOperationName(t *testing.T) { + for _, tt := range []struct { + name string + rattrs map[string]string + sattrs map[string]string + normalize bool + spanKind ptrace.SpanKind + libname string + spanNameAsResourceName bool + spanNameRemappings map[string]string + expected string + }{ + { + name: "operation name from span kind", + spanKind: ptrace.SpanKindClient, + expected: "opentelemetry.client", + }, + { + name: "operation name from instrumentation scope and span kind", + spanKind: ptrace.SpanKindServer, + libname: "spring", + expected: "spring.server", + }, + { + name: "operation name from span name", + spanNameAsResourceName: true, + expected: "span_name", + }, + { + name: "operation name remapping", + spanKind: ptrace.SpanKindInternal, + spanNameRemappings: map[string]string{"opentelemetry.internal": "internal_op"}, + expected: "internal_op", + }, + { + name: "operation.name attribute", + sattrs: map[string]string{"operation.name": "op"}, + expected: "op", + }, + { + name: "normalize empty operation name", + sattrs: map[string]string{"operation.name": "op"}, + spanNameRemappings: map[string]string{"op": ""}, + normalize: true, + expected: DefaultSpanName, + }, + { + name: "normalize invalid operation name", + sattrs: map[string]string{"operation.name": "%$^"}, + normalize: true, + expected: DefaultSpanName, + }, + { + name: "truncate long operation name", + sattrs: map[string]string{"operation.name": strings.Repeat("a", MaxNameLen+1)}, + normalize: true, + expected: strings.Repeat("a", MaxNameLen), + }, + } { + t.Run(tt.name, func(t *testing.T) { + span := ptrace.NewSpan() + span.SetName("span_name") + span.SetKind(tt.spanKind) + for k, v := range tt.sattrs { + span.Attributes().PutStr(k, v) + } + res := pcommon.NewResource() + for k, v := range tt.rattrs { + res.Attributes().PutStr(k, v) + } + lib := pcommon.NewInstrumentationScope() + lib.SetName(tt.libname) + actual := GetOTelOperationName(span, res, lib, tt.spanNameAsResourceName, tt.spanNameRemappings, tt.normalize) + assert.Equal(t, tt.expected, actual) + }) + } +} + +func TestGetOTelHostname(t *testing.T) { + for _, tt := range []struct { + name string + rattrs map[string]string + sattrs map[string]string + fallbackHost string + expected string + }{ + { + name: "datadog.host.name", + rattrs: map[string]string{"datadog.host.name": "test-host"}, + expected: "test-host", + }, + { + name: "_dd.hostname", + rattrs: map[string]string{"_dd.hostname": "test-host"}, + expected: "test-host", + }, + { + name: "fallback hostname", + fallbackHost: "test-host", + expected: "test-host", + }, + } { + t.Run(tt.name, func(t *testing.T) { + span := ptrace.NewSpan() + span.SetName("span_name") + for k, v := range tt.sattrs { + span.Attributes().PutStr(k, v) + } + res := pcommon.NewResource() + for k, v := range tt.rattrs { + res.Attributes().PutStr(k, v) + } + set := componenttest.NewNopTelemetrySettings() + set.MeterProvider = noop.NewMeterProvider() + tr, err := attributes.NewTranslator(set) + assert.NoError(t, err) + actual := GetOTelHostname(span, res, tr, tt.fallbackHost) + assert.Equal(t, tt.expected, actual) + }) + } +} + +func TestGetOTelStatusCode(t *testing.T) { + span := ptrace.NewSpan() + span.SetName("span_name") + assert.Equal(t, uint32(0), GetOTelStatusCode(span)) + span.Attributes().PutInt(semconv.AttributeHTTPStatusCode, 200) + assert.Equal(t, uint32(200), GetOTelStatusCode(span)) +} + +func TestGetOTelContainerTags(t *testing.T) { + res := pcommon.NewResource() + res.Attributes().PutStr(semconv.AttributeContainerID, "cid") + res.Attributes().PutStr(semconv.AttributeContainerName, "cname") + res.Attributes().PutStr(semconv.AttributeContainerImageName, "ciname") + res.Attributes().PutStr(semconv.AttributeContainerImageTag, "citag") + assert.Contains(t, GetOTelContainerTags(res.Attributes()), "container_id:cid", "container_name:cname", "image_name:ciname", "image_tag:citag") +}