From 47294c1176849a76403aa1b5f0ce2b95c7c93614 Mon Sep 17 00:00:00 2001 From: Ibraheem Aboulnaga <13734402+IbraheemA@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:58:27 -0500 Subject: [PATCH] Update OTLP span -> DD span logic for operation and resource name (#30616) --- pkg/trace/api/otlp.go | 77 ++- pkg/trace/api/otlp_test.go | 613 ++++++++++++++++-- pkg/trace/stats/otel_util.go | 8 +- pkg/trace/stats/otel_util_test.go | 4 + pkg/trace/traceutil/otel_util.go | 151 ++++- pkg/trace/traceutil/otel_util_test.go | 52 +- pkg/trace/transform/transform.go | 19 +- ...source-name-logic-v2-75929121247f2059.yaml | 10 + 8 files changed, 843 insertions(+), 91 deletions(-) create mode 100644 releasenotes/notes/operation-and-resource-name-logic-v2-75929121247f2059.yaml diff --git a/pkg/trace/api/otlp.go b/pkg/trace/api/otlp.go index 8badf01d92852..9dda5532f1338 100644 --- a/pkg/trace/api/otlp.go +++ b/pkg/trace/api/otlp.go @@ -61,6 +61,33 @@ type OTLPReceiver struct { // NewOTLPReceiver returns a new OTLPReceiver which sends any incoming traces down the out channel. func NewOTLPReceiver(out chan<- *Payload, cfg *config.AgentConfig, statsd statsd.ClientInterface, timing timing.Reporter) *OTLPReceiver { + operationAndResourceNamesV2GateEnabled := cfg.HasFeature("enable_operation_and_resource_name_logic_v2") + operationAndResourceNamesV2GateEnabledVal := 0.0 + if operationAndResourceNamesV2GateEnabled { + operationAndResourceNamesV2GateEnabledVal = 1.0 + } + _ = statsd.Gauge("datadog.trace_agent.otlp.operation_and_resource_names_v2_gate_enabled", operationAndResourceNamesV2GateEnabledVal, nil, 1) + + spanNameAsResourceNameEnabledVal := 0.0 + if cfg.OTLPReceiver.SpanNameAsResourceName { + if operationAndResourceNamesV2GateEnabled { + log.Warnf("Detected SpanNameAsResourceName in config - this feature will be deprecated in a future version, and overrides feature gate \"enable_operation_and_resource_name_logic_v2\". Please remove it and set \"operation.name\" attribute on your spans instead.") + } else { + log.Warnf("Detected SpanNameAsResourceName in config - this feature will be deprecated in a future version. Please remove it, enable feature gate \"enable_operation_and_resource_name_logic_v2\", and set \"operation.name\" attribute on your spans instead.") + } + spanNameAsResourceNameEnabledVal = 1.0 + } + _ = statsd.Gauge("datadog.trace_agent.otlp.span_name_as_resource_name_enabled", spanNameAsResourceNameEnabledVal, nil, 1) + spanNameRemappingsEnabledVal := 0.0 + if cfg.OTLPReceiver.SpanNameRemappings != nil && len(cfg.OTLPReceiver.SpanNameRemappings) > 0 { + if operationAndResourceNamesV2GateEnabled { + log.Warnf("Detected SpanNameRemappings in config - this feature will be deprecated in a future version. Please remove it to access functionality from feature gate \"enable_operation_and_resource_name_logic_v2\".") + } else { + log.Warnf("Detected SpanNameRemappings in config - this feature will be deprecated in a future version. Please remove it and enable feature gate \"enable_operation_and_resource_name_logic_v2\"") + } + spanNameRemappingsEnabledVal = 1.0 + } + _ = statsd.Gauge("datadog.trace_agent.otlp.span_name_remappings_enabled", spanNameRemappingsEnabledVal, nil, 1) computeTopLevelBySpanKindVal := 0.0 if cfg.HasFeature("enable_otlp_compute_top_level_by_span_kind") { computeTopLevelBySpanKindVal = 1.0 @@ -215,7 +242,13 @@ func (o *OTLPReceiver) receiveResourceSpansV2(ctx context.Context, rspans ptrace libspans := rspans.ScopeSpans().At(i) for j := 0; j < libspans.Spans().Len(); j++ { otelspan := libspans.Spans().At(j) - if _, exists := o.ignoreResNames[traceutil.GetOTelResource(otelspan, otelres)]; exists { + var resourceName string + if transform.OperationAndResourceNameV2Enabled(o.conf) { + resourceName = traceutil.GetOTelResourceV2(otelspan, otelres) + } else { + resourceName = traceutil.GetOTelResourceV1(otelspan, otelres) + } + if _, exists := o.ignoreResNames[resourceName]; exists { continue } @@ -583,29 +616,41 @@ func (o *OTLPReceiver) convertSpan(rattr map[string]string, lib pcommon.Instrume transform.SetMetaOTLP(span, semconv.OtelStatusDescription, msg) } transform.Status2Error(in.Status(), in.Events(), span) - if span.Name == "" { - name := in.Name() - if !o.conf.OTLPReceiver.SpanNameAsResourceName { - name = traceutil.OTelSpanKindName(in.Kind()) - if lib.Name() != "" { - name = lib.Name() + "." + name - } else { - name = "opentelemetry." + name + if transform.OperationAndResourceNameV2Enabled(o.conf) { + span.Name = traceutil.GetOTelOperationNameV2(in) + } else { + if span.Name == "" { + name := in.Name() + if !o.conf.OTLPReceiver.SpanNameAsResourceName { + name = traceutil.OTelSpanKindName(in.Kind()) + if lib.Name() != "" { + name = lib.Name() + "." + name + } else { + name = "opentelemetry." + name + } } + if v, ok := o.conf.OTLPReceiver.SpanNameRemappings[name]; ok { + name = v + } + span.Name = name } - if v, ok := o.conf.OTLPReceiver.SpanNameRemappings[name]; ok { - name = v - } - span.Name = name } if span.Service == "" { span.Service = "OTLPResourceNoServiceName" } if span.Resource == "" { - if r := resourceFromTags(span.Meta); r != "" { - span.Resource = r + if transform.OperationAndResourceNameV2Enabled(o.conf) { + res := pcommon.NewResource() + for k, v := range rattr { + res.Attributes().PutStr(k, v) + } + span.Resource = traceutil.GetOTelResourceV2(in, res) } else { - span.Resource = in.Name() + if r := resourceFromTags(span.Meta); r != "" { + span.Resource = r + } else { + span.Resource = in.Name() + } } } if span.Type == "" { diff --git a/pkg/trace/api/otlp_test.go b/pkg/trace/api/otlp_test.go index 9ea2f39e3cfb4..15d911d2cc517 100644 --- a/pkg/trace/api/otlp_test.go +++ b/pkg/trace/api/otlp_test.go @@ -227,7 +227,9 @@ func TestOTLPNameRemapping(t *testing.T) { } func testOTLPNameRemapping(enableReceiveResourceSpansV2 bool, t *testing.T) { + // Verify that while EnableOperationAndResourceNamesV2 is in alpha, SpanNameRemappings overrides it cfg := NewTestConfig(t) + cfg.Features["enable_operation_and_resource_name_logic_v2"] = struct{}{} if enableReceiveResourceSpansV2 { cfg.Features["enable_receive_resource_spans_v2"] = struct{}{} } @@ -253,6 +255,446 @@ func testOTLPNameRemapping(enableReceiveResourceSpansV2 bool, t *testing.T) { } } +func TestOTLPSpanNameV2(t *testing.T) { + t.Run("ReceiveResourceSpansV1", func(t *testing.T) { + testOTLPSpanNameV2(false, t) + }) + + t.Run("ReceiveResourceSpansV2", func(t *testing.T) { + testOTLPSpanNameV2(true, t) + }) +} + +func testOTLPSpanNameV2(enableReceiveResourceSpansV2 bool, t *testing.T) { + cfg := NewTestConfig(t) + cfg.Features["enable_operation_and_resource_name_logic_v2"] = struct{}{} + if enableReceiveResourceSpansV2 { + cfg.Features["enable_receive_resource_spans_v2"] = struct{}{} + } + out := make(chan *Payload, 1) + rcv := NewOTLPReceiver(out, cfg, &statsd.NoOpClient{}, &timing.NoopReporter{}) + require := require.New(t) + for _, tt := range []struct { + in []testutil.OTLPResourceSpan + fn func(*pb.TracerPayload) + }{ + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{{ + Attributes: map[string]interface{}{semconv.AttributeContainerID: "http.method"}, + }}, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("Internal", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Kind: ptrace.SpanKindServer, + Attributes: map[string]interface{}{semconv.AttributeHTTPMethod: "GET"}, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("http.server.request", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Kind: ptrace.SpanKindClient, + Attributes: map[string]interface{}{semconv.AttributeHTTPMethod: "GET"}, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("http.client.request", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Kind: ptrace.SpanKindClient, + Attributes: map[string]interface{}{semconv.AttributeDBSystem: "mysql"}, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("mysql.query", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Attributes: map[string]interface{}{semconv.AttributeDBSystem: "mysql"}, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("Internal", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Attributes: map[string]interface{}{semconv.AttributeMessagingSystem: "kafka"}, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("Internal", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Attributes: map[string]interface{}{ + semconv.AttributeMessagingSystem: "kafka", + semconv.AttributeMessagingOperation: "send", + }, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("Internal", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Kind: ptrace.SpanKindClient, + Attributes: map[string]interface{}{ + semconv.AttributeMessagingSystem: "kafka", + semconv.AttributeMessagingOperation: "send", + }, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("kafka.send", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Kind: ptrace.SpanKindServer, + Attributes: map[string]interface{}{ + semconv.AttributeRPCSystem: "aws-api", + }, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("aws-api.server.request", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Kind: ptrace.SpanKindClient, + Attributes: map[string]interface{}{ + semconv.AttributeRPCSystem: "aws-api", + }, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("aws.client.request", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Kind: ptrace.SpanKindClient, + Attributes: map[string]interface{}{ + semconv.AttributeRPCSystem: "aws-api", + }, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("aws.client.request", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Kind: ptrace.SpanKindClient, + Attributes: map[string]interface{}{ + semconv.AttributeRPCSystem: "aws-api", + semconv.AttributeRPCService: "s3", + }, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("aws.s3.request", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Kind: ptrace.SpanKindClient, + Attributes: map[string]interface{}{ + semconv.AttributeRPCSystem: "grpc", + }, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("grpc.client.request", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Kind: ptrace.SpanKindServer, + Attributes: map[string]interface{}{ + semconv.AttributeRPCSystem: "grpc", + }, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("grpc.server.request", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Kind: ptrace.SpanKindClient, + Attributes: map[string]interface{}{ + semconv.AttributeFaaSInvokedProvider: "gcp", + semconv.AttributeFaaSInvokedName: "foo", + }, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("gcp.foo.invoke", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Attributes: map[string]interface{}{ + semconv.AttributeFaaSInvokedProvider: "gcp", + semconv.AttributeFaaSInvokedName: "foo", + }, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("Internal", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Kind: ptrace.SpanKindServer, + Attributes: map[string]interface{}{ + semconv.AttributeFaaSTrigger: "timer", + }, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("timer.invoke", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Attributes: map[string]interface{}{ + semconv.AttributeFaaSTrigger: "timer", + }, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("Internal", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Attributes: map[string]interface{}{ + "graphql.operation.type": "query", + }, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("graphql.server.request", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Kind: ptrace.SpanKindServer, + Attributes: map[string]interface{}{ + "network.protocol.name": "tcp", + }, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("tcp.server.request", out.Chunks[0].Spans[0].Name) + }, + }, + { + in: []testutil.OTLPResourceSpan{ + { + LibName: "libname", + LibVersion: "1.2", + Attributes: map[string]interface{}{}, + Spans: []*testutil.OTLPSpan{ + { + Kind: ptrace.SpanKindServer, + Attributes: map[string]interface{}{}, + }, + }, + }, + }, + fn: func(out *pb.TracerPayload) { + require.Equal("server.request", out.Chunks[0].Spans[0].Name) + }, + }, + } { + t.Run("", func(t *testing.T) { + rspans := testutil.NewOTLPTracesRequest(tt.in).Traces().ResourceSpans().At(0) + rcv.ReceiveResourceSpans(context.Background(), rspans, http.Header{}) + timeout := time.After(500 * time.Millisecond) + select { + case <-timeout: + t.Fatal("timed out") + case p := <-out: + tt.fn(p.TracerPayload) + } + }) + } +} + func TestCreateChunks(t *testing.T) { t.Run("ReceiveResourceSpansV1", func(t *testing.T) { testCreateChunk(false, t) @@ -1163,26 +1605,45 @@ func TestOTLPHelpers(t *testing.T) { func TestOTLPConvertSpan(t *testing.T) { t.Run("ReceiveResourceSpansV1", func(t *testing.T) { - testOTLPConvertSpan(false, t) + t.Run("OperationAndResourceNameV1", func(t *testing.T) { + testOTLPConvertSpan(false, false, t) + }) + + t.Run("OperationAndResourceNameV2", func(t *testing.T) { + testOTLPConvertSpan(false, true, t) + }) }) t.Run("ReceiveResourceSpansV2", func(t *testing.T) { - testOTLPConvertSpan(true, t) + t.Run("OperationAndResourceNameV1", func(t *testing.T) { + testOTLPConvertSpan(true, false, t) + }) + + t.Run("OperationAndResourceNameV2", func(t *testing.T) { + testOTLPConvertSpan(true, true, t) + }) }) } -func testOTLPConvertSpan(enableReceiveResourceSpansV2 bool, t *testing.T) { - now := uint64(otlpTestSpan.StartTimestamp()) +func testOTLPConvertSpan(enableReceiveResourceSpansV2 bool, enableOperationAndResourceNameV2 bool, t *testing.T) { cfg := NewTestConfig(t) + now := uint64(otlpTestSpan.StartTimestamp()) if enableReceiveResourceSpansV2 { cfg.Features["enable_receive_resource_spans_v2"] = struct{}{} } + if enableOperationAndResourceNameV2 { + cfg.Features["enable_operation_and_resource_name_logic_v2"] = struct{}{} + } o := NewOTLPReceiver(nil, cfg, &statsd.NoOpClient{}, &timing.NoopReporter{}) for i, tt := range []struct { rattr map[string]string libname string libver string in ptrace.Span + operationNameV1 string + operationNameV2 string + resourceNameV1 string + resourceNameV2 string out *pb.Span outTags map[string]string topLevelOutMetrics map[string]float64 @@ -1193,13 +1654,15 @@ func testOTLPConvertSpan(enableReceiveResourceSpansV2 bool, t *testing.T) { "service.version": "v1.2.3", "env": "staging", }, - libname: "ddtracer", - libver: "v2", - in: otlpTestSpan, + libname: "ddtracer", + libver: "v2", + in: otlpTestSpan, + operationNameV1: "ddtracer.server", + operationNameV2: "server.request", + resourceNameV1: "/path", + resourceNameV2: "/path", out: &pb.Span{ Service: "pylons", - Name: "ddtracer.server", - Resource: "/path", TraceID: 2594128270069917171, SpanID: 2594128270069917171, ParentID: 0, @@ -1321,10 +1784,12 @@ func testOTLPConvertSpan(enableReceiveResourceSpansV2 bool, t *testing.T) { StatusMsg: "Error", StatusCode: ptrace.StatusCodeError, }), + operationNameV1: "ddtracer.server", + operationNameV2: "http.server.request", + resourceNameV1: "GET /path", + resourceNameV2: "GET /path", out: &pb.Span{ Service: "myservice", - Name: "ddtracer.server", - Resource: "GET /path", TraceID: 2594128270069917171, SpanID: 2594128270069917171, ParentID: 0, @@ -1449,10 +1914,12 @@ func testOTLPConvertSpan(enableReceiveResourceSpansV2 bool, t *testing.T) { StatusMsg: "Error", StatusCode: ptrace.StatusCodeError, }), + operationNameV1: "ddtracer.server", + operationNameV2: "http.server.request", + resourceNameV1: "GET /path", + resourceNameV2: "GET /path", out: &pb.Span{ Service: "pylons", - Name: "ddtracer.server", - Resource: "GET /path", TraceID: 2594128270069917171, SpanID: 2594128270069917171, ParentID: 0, @@ -1517,10 +1984,12 @@ func testOTLPConvertSpan(enableReceiveResourceSpansV2 bool, t *testing.T) { "analytics.event": true, }, }), + operationNameV1: "READ", + operationNameV2: "READ", + resourceNameV1: "/path", + resourceNameV2: "/path", out: &pb.Span{ Service: "mongo", - Name: "READ", - Resource: "/path", TraceID: 2594128270069917171, SpanID: 2594128270069917171, ParentID: 0, @@ -1577,10 +2046,12 @@ func testOTLPConvertSpan(enableReceiveResourceSpansV2 bool, t *testing.T) { "error.type": "WebSocketDisconnect", }, }), + operationNameV1: "ddtracer.server", + operationNameV2: "ddtracer.server", + resourceNameV1: "POST /uploads/:document_id", + resourceNameV2: "POST", out: &pb.Span{ Service: "document-uploader", - Name: "ddtracer.server", - Resource: "POST /uploads/:document_id", TraceID: 2594128270069917171, SpanID: 2594128270069917171, ParentID: 0, @@ -1637,10 +2108,12 @@ func testOTLPConvertSpan(enableReceiveResourceSpansV2 bool, t *testing.T) { "error.type": "WebSocketDisconnect", }, }), + operationNameV1: "ddtracer.server", + operationNameV2: "ddtracer.server", + resourceNameV1: "POST /uploads/:document_id", + resourceNameV2: "POST", out: &pb.Span{ Service: "document-uploader", - Name: "ddtracer.server", - Resource: "POST /uploads/:document_id", TraceID: 2594128270069917171, SpanID: 2594128270069917171, ParentID: 0, @@ -1695,10 +2168,12 @@ func testOTLPConvertSpan(enableReceiveResourceSpansV2 bool, t *testing.T) { "error.type": "WebSocketDisconnect", }, }), + operationNameV1: "ddtracer.server", + operationNameV2: "ddtracer.server", + resourceNameV1: "POST /uploads/:document_id", + resourceNameV2: "POST", out: &pb.Span{ Service: "document-uploader", - Name: "ddtracer.server", - Resource: "POST /uploads/:document_id", TraceID: 2594128270069917171, SpanID: 2594128270069917171, ParentID: 0, @@ -1784,6 +2259,13 @@ func testOTLPConvertSpan(enableReceiveResourceSpansV2 bool, t *testing.T) { want.Metrics = nil got.Meta = nil got.Metrics = nil + if enableOperationAndResourceNameV2 { + want.Name = tt.operationNameV2 + want.Resource = tt.resourceNameV2 + } else { + want.Name = tt.operationNameV1 + want.Resource = tt.resourceNameV1 + } assert.Equal(want, got, i) // test new top-level identification feature flag @@ -1828,26 +2310,45 @@ func TestAppendTags(t *testing.T) { func TestOTLPConvertSpanSetPeerService(t *testing.T) { t.Run("ReceiveResourceSpansV1", func(t *testing.T) { - testOTLPConvertSpanSetPeerService(false, t) + t.Run("OperationAndResourceNameV1", func(t *testing.T) { + testOTLPConvertSpanSetPeerService(false, false, t) + }) + + t.Run("OperationAndResourceNameV2", func(t *testing.T) { + testOTLPConvertSpanSetPeerService(false, true, t) + }) }) t.Run("ReceiveResourceSpansV2", func(t *testing.T) { - testOTLPConvertSpanSetPeerService(true, t) + t.Run("OperationAndResourceNameV1", func(t *testing.T) { + testOTLPConvertSpanSetPeerService(true, false, t) + }) + + t.Run("OperationAndResourceNameV2", func(t *testing.T) { + testOTLPConvertSpanSetPeerService(true, true, t) + }) }) } -func testOTLPConvertSpanSetPeerService(enableReceiveResourceSpansV2 bool, t *testing.T) { +func testOTLPConvertSpanSetPeerService(enableReceiveResourceSpansV2 bool, enableOperationAndResourceNameV2 bool, t *testing.T) { now := uint64(otlpTestSpan.StartTimestamp()) cfg := NewTestConfig(t) if enableReceiveResourceSpansV2 { - cfg.Features["receive_resource_spans_v2"] = struct{}{} + cfg.Features["enable_receive_resource_spans_v2"] = struct{}{} + } + if enableOperationAndResourceNameV2 { + cfg.Features["enable_operation_and_resource_name_logic_v2"] = struct{}{} } o := NewOTLPReceiver(nil, cfg, &statsd.NoOpClient{}, &timing.NoopReporter{}) for i, tt := range []struct { - rattr map[string]string - libname string - libver string - in ptrace.Span - out *pb.Span + rattr map[string]string + libname string + libver string + in ptrace.Span + out *pb.Span + operationNameV1 string + operationNameV2 string + resourceNameV1 string + resourceNameV2 string }{ { rattr: map[string]string{ @@ -1868,10 +2369,12 @@ func testOTLPConvertSpanSetPeerService(enableReceiveResourceSpansV2 bool, t *tes "deployment.environment": "prod", }, }), + operationNameV1: "ddtracer.server", + operationNameV2: "server.request", + resourceNameV1: "/path", + resourceNameV2: "/path", out: &pb.Span{ Service: "myservice", - Name: "ddtracer.server", - Resource: "/path", TraceID: 2594128270069917171, SpanID: 2594128270069917171, ParentID: 0, @@ -1913,10 +2416,12 @@ func testOTLPConvertSpanSetPeerService(enableReceiveResourceSpansV2 bool, t *tes "deployment.environment": "prod", }, }), + operationNameV1: "ddtracer.server", + operationNameV2: "server.request", + resourceNameV1: "/path", + resourceNameV2: "/path", out: &pb.Span{ Service: "myservice", - Name: "ddtracer.server", - Resource: "/path", TraceID: 2594128270069917171, SpanID: 2594128270069917171, ParentID: 0, @@ -1959,10 +2464,12 @@ func testOTLPConvertSpanSetPeerService(enableReceiveResourceSpansV2 bool, t *tes "deployment.environment": "prod", }, }), + operationNameV1: "ddtracer.client", + operationNameV2: "postgres.query", + resourceNameV1: "/path", + resourceNameV2: "/path", out: &pb.Span{ Service: "myservice", - Name: "ddtracer.client", - Resource: "/path", TraceID: 2594128270069917171, SpanID: 2594128270069917171, ParentID: 0, @@ -2005,10 +2512,12 @@ func testOTLPConvertSpanSetPeerService(enableReceiveResourceSpansV2 bool, t *tes "deployment.environment": "prod", }, }), + operationNameV1: "ddtracer.client", + operationNameV2: "client.request", + resourceNameV1: "/path", + resourceNameV2: "/path", out: &pb.Span{ Service: "myservice", - Name: "ddtracer.client", - Resource: "/path", TraceID: 2594128270069917171, SpanID: 2594128270069917171, ParentID: 0, @@ -2050,10 +2559,12 @@ func testOTLPConvertSpanSetPeerService(enableReceiveResourceSpansV2 bool, t *tes "deployment.environment": "prod", }, }), + operationNameV1: "ddtracer.server", + operationNameV2: "server.request", + resourceNameV1: "/path", + resourceNameV2: "/path", out: &pb.Span{ Service: "myservice", - Name: "ddtracer.server", - Resource: "/path", TraceID: 2594128270069917171, SpanID: 2594128270069917171, ParentID: 0, @@ -2094,10 +2605,12 @@ func testOTLPConvertSpanSetPeerService(enableReceiveResourceSpansV2 bool, t *tes "deployment.environment": "prod", }, }), + operationNameV1: "ddtracer.server", + operationNameV2: "server.request", + resourceNameV1: "/path", + resourceNameV2: "/path", out: &pb.Span{ Service: "myservice", - Name: "ddtracer.server", - Resource: "/path", TraceID: 2594128270069917171, SpanID: 2594128270069917171, ParentID: 0, @@ -2138,10 +2651,12 @@ func testOTLPConvertSpanSetPeerService(enableReceiveResourceSpansV2 bool, t *tes "deployment.environment": "prod", }, }), + operationNameV1: "ddtracer.server", + operationNameV2: "server.request", + resourceNameV1: "/path", + resourceNameV2: "/path", out: &pb.Span{ Service: "myservice", - Name: "ddtracer.server", - Resource: "/path", TraceID: 2594128270069917171, SpanID: 2594128270069917171, ParentID: 0, @@ -2179,7 +2694,15 @@ func testOTLPConvertSpanSetPeerService(enableReceiveResourceSpansV2 bool, t *tes } else { got = o.convertSpan(tt.rattr, lib, tt.in) } - assert.Equal(tt.out, got, i) + want := tt.out + if enableOperationAndResourceNameV2 { + want.Name = tt.operationNameV2 + want.Resource = tt.resourceNameV2 + } else { + want.Name = tt.operationNameV1 + want.Resource = tt.resourceNameV1 + } + assert.Equal(want, got, i) }) } } diff --git a/pkg/trace/stats/otel_util.go b/pkg/trace/stats/otel_util.go index d2107862bbfca..1fce97378a030 100644 --- a/pkg/trace/stats/otel_util.go +++ b/pkg/trace/stats/otel_util.go @@ -46,7 +46,13 @@ func OTLPTracesToConcentratorInputs( containerTagsByID := make(map[string][]string) for spanID, otelspan := range spanByID { otelres := resByID[spanID] - if _, exists := ignoreResNames[traceutil.GetOTelResource(otelspan, otelres)]; exists { + var resourceName string + if transform.OperationAndResourceNameV2Enabled(conf) { + resourceName = traceutil.GetOTelResourceV2(otelspan, otelres) + } else { + resourceName = traceutil.GetOTelResourceV1(otelspan, otelres) + } + if _, exists := ignoreResNames[resourceName]; exists { continue } // TODO(songy23): use AttributeDeploymentEnvironmentName once collector version upgrade is unblocked diff --git a/pkg/trace/stats/otel_util_test.go b/pkg/trace/stats/otel_util_test.go index 1422226c8ecc5..939c27f8c9f77 100644 --- a/pkg/trace/stats/otel_util_test.go +++ b/pkg/trace/stats/otel_util_test.go @@ -214,6 +214,10 @@ func TestProcessOTLPTraces(t *testing.T) { conf.PeerTagsAggregation = tt.peerTagsAggr conf.OTLPReceiver.AttributesTranslator = attributesTranslator conf.OTLPReceiver.SpanNameAsResourceName = tt.spanNameAsResourceName + if conf.OTLPReceiver.SpanNameAsResourceName { + // Verify that while EnableOperationAndResourceNamesV2 is in alpha, SpanNameAsResourceName overrides it + conf.Features["enable_operation_and_resource_name_logic_v2"] = struct{}{} + } conf.OTLPReceiver.SpanNameRemappings = tt.spanNameRemappings conf.Ignore["resource"] = tt.ignoreRes if !tt.legacyTopLevel { diff --git a/pkg/trace/traceutil/otel_util.go b/pkg/trace/traceutil/otel_util.go index df0c93641732d..5f7fa7acfafa1 100644 --- a/pkg/trace/traceutil/otel_util.go +++ b/pkg/trace/traceutil/otel_util.go @@ -172,8 +172,8 @@ func GetOTelService(span ptrace.Span, res pcommon.Resource, normalize bool) stri 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) { +// GetOTelResourceV1 returns the DD resource name based on OTel span and resource attributes. +func GetOTelResourceV1(span ptrace.Span, res pcommon.Resource) (resName string) { resName = GetOTelAttrValInResAndSpanAttrs(span, res, false, "resource.name") if resName == "" { if m := GetOTelAttrValInResAndSpanAttrs(span, res, false, "http.request.method", semconv.AttributeHTTPMethod); m != "" { @@ -205,8 +205,151 @@ func GetOTelResource(span ptrace.Span, res pcommon.Resource) (resName string) { return } -// GetOTelOperationName returns the DD operation name based on OTel span and resource attributes and given configs. -func GetOTelOperationName( +// GetOTelResourceV2 returns the DD resource name based on OTel span and resource attributes. +func GetOTelResourceV2(span ptrace.Span, res pcommon.Resource) (resName string) { + defer func() { + if len(resName) > MaxResourceLen { + resName = resName[:MaxResourceLen] + } + }() + if m := GetOTelAttrValInResAndSpanAttrs(span, res, false, "resource.name"); m != "" { + resName = m + return + } + + if m := GetOTelAttrValInResAndSpanAttrs(span, res, false, "http.request.method", semconv.AttributeHTTPMethod); m != "" { + if m == "_OTHER" { + m = "HTTP" + } + // use the HTTP method + route (if available) + resName = m + if span.Kind() == ptrace.SpanKindServer { + if route := GetOTelAttrValInResAndSpanAttrs(span, res, false, semconv.AttributeHTTPRoute); route != "" { + resName = resName + " " + route + } + } + return + } + + 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 + } + return + } + + 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 + } + return + } + resName = span.Name() + + return +} + +// GetOTelOperationNameV2 returns the DD operation name based on OTel span and resource attributes and given configs. +func GetOTelOperationNameV2( + span ptrace.Span, +) string { + if operationName := GetOTelAttrVal(span.Attributes(), false, "operation.name"); operationName != "" { + return operationName + } + + isClient := span.Kind() == ptrace.SpanKindClient + isServer := span.Kind() == ptrace.SpanKindServer + + // http + if method := GetOTelAttrVal(span.Attributes(), false, "http.request.method", semconv.AttributeHTTPMethod); method != "" { + if isServer { + return "http.server.request" + } + if isClient { + return "http.client.request" + } + } + + // database + if v := GetOTelAttrVal(span.Attributes(), true, semconv.AttributeDBSystem); v != "" && isClient { + return v + ".query" + } + + // messaging + system := GetOTelAttrVal(span.Attributes(), true, semconv.AttributeMessagingSystem) + op := GetOTelAttrVal(span.Attributes(), true, semconv.AttributeMessagingOperation) + if system != "" && op != "" { + switch span.Kind() { + case ptrace.SpanKindClient, ptrace.SpanKindServer, ptrace.SpanKindConsumer, ptrace.SpanKindProducer: + return system + "." + op + } + } + + // RPC & AWS + rpcValue := GetOTelAttrVal(span.Attributes(), true, semconv.AttributeRPCSystem) + isRPC := rpcValue != "" + isAws := isRPC && (rpcValue == "aws-api") + // AWS client + if isAws && isClient { + if service := GetOTelAttrVal(span.Attributes(), true, semconv.AttributeRPCService); service != "" { + return "aws." + service + ".request" + } + return "aws.client.request" + } + // RPC client + if isRPC && isClient { + return rpcValue + ".client.request" + } + // RPC server + if isRPC && isServer { + return rpcValue + ".server.request" + } + + // FAAS client + provider := GetOTelAttrVal(span.Attributes(), true, semconv.AttributeFaaSInvokedProvider) + invokedName := GetOTelAttrVal(span.Attributes(), true, semconv.AttributeFaaSInvokedName) + if provider != "" && invokedName != "" && isClient { + return provider + "." + invokedName + ".invoke" + } + + // FAAS server + trigger := GetOTelAttrVal(span.Attributes(), true, semconv.AttributeFaaSTrigger) + if trigger != "" && isServer { + return trigger + ".invoke" + } + + // GraphQL + if GetOTelAttrVal(span.Attributes(), true, "graphql.operation.type") != "" { + return "graphql.server.request" + } + + // if nothing matches, checking for generic http server/client + protocol := GetOTelAttrVal(span.Attributes(), true, "network.protocol.name") + if isServer { + if protocol != "" { + return protocol + ".server.request" + } + return "server.request" + } else if isClient { + if protocol != "" { + return protocol + ".client.request" + } + return "client.request" + } + + if span.Kind() != ptrace.SpanKindUnspecified { + return span.Kind().String() + } + return ptrace.SpanKindInternal.String() +} + +// GetOTelOperationNameV1 returns the DD operation name based on OTel span and resource attributes and given configs. +func GetOTelOperationNameV1( span ptrace.Span, res pcommon.Resource, lib pcommon.InstrumentationScope, diff --git a/pkg/trace/traceutil/otel_util_test.go b/pkg/trace/traceutil/otel_util_test.go index 7eed064d83d94..dfa0fa0fd30f4 100644 --- a/pkg/trace/traceutil/otel_util_test.go +++ b/pkg/trace/traceutil/otel_util_test.go @@ -257,36 +257,42 @@ func TestGetOTelService(t *testing.T) { func TestGetOTelResource(t *testing.T) { for _, tt := range []struct { - name string - rattrs map[string]string - sattrs map[string]string - normalize bool - expected string + name string + rattrs map[string]string + sattrs map[string]string + normalize bool + expectedV1 string + expectedV2 string }{ { - name: "resource not set", - expected: "span_name", + name: "resource not set", + expectedV1: "span_name", + expectedV2: "span_name", }, { - name: "normal resource", - sattrs: map[string]string{"resource.name": "res"}, - expected: "res", + name: "normal resource", + sattrs: map[string]string{"resource.name": "res"}, + expectedV1: "res", + expectedV2: "res", }, { - name: "HTTP request method resource", - sattrs: map[string]string{"http.request.method": "GET"}, - expected: "GET", + name: "HTTP request method resource", + sattrs: map[string]string{"http.request.method": "GET"}, + expectedV1: "GET", + expectedV2: "GET", }, { - name: "HTTP method and route resource", - sattrs: map[string]string{semconv.AttributeHTTPMethod: "GET", semconv.AttributeHTTPRoute: "/"}, - expected: "GET /", + name: "HTTP method and route resource", + sattrs: map[string]string{semconv.AttributeHTTPMethod: "GET", semconv.AttributeHTTPRoute: "/"}, + expectedV1: "GET /", + expectedV2: "GET", }, { - name: "truncate long resource", - sattrs: map[string]string{"resource.name": strings.Repeat("a", MaxResourceLen+1)}, - normalize: true, - expected: strings.Repeat("a", MaxResourceLen), + name: "truncate long resource", + sattrs: map[string]string{"resource.name": strings.Repeat("a", MaxResourceLen+1)}, + normalize: true, + expectedV1: strings.Repeat("a", MaxResourceLen), + expectedV2: strings.Repeat("a", MaxResourceLen), }, } { t.Run(tt.name, func(t *testing.T) { @@ -299,8 +305,8 @@ func TestGetOTelResource(t *testing.T) { for k, v := range tt.rattrs { res.Attributes().PutStr(k, v) } - actual := GetOTelResource(span, res) - assert.Equal(t, tt.expected, actual) + assert.Equal(t, tt.expectedV1, GetOTelResourceV1(span, res)) + assert.Equal(t, tt.expectedV2, GetOTelResourceV2(span, res)) }) } } @@ -384,7 +390,7 @@ func TestGetOTelOperationName(t *testing.T) { } lib := pcommon.NewInstrumentationScope() lib.SetName(tt.libname) - actual := GetOTelOperationName(span, res, lib, tt.spanNameAsResourceName, tt.spanNameRemappings, tt.normalize) + actual := GetOTelOperationNameV1(span, res, lib, tt.spanNameAsResourceName, tt.spanNameRemappings, tt.normalize) assert.Equal(t, tt.expected, actual) }) } diff --git a/pkg/trace/transform/transform.go b/pkg/trace/transform/transform.go index f19af44b5d636..dd2d5a5f08367 100644 --- a/pkg/trace/transform/transform.go +++ b/pkg/trace/transform/transform.go @@ -23,6 +23,11 @@ import ( semconv "go.opentelemetry.io/collector/semconv/v1.6.1" ) +// OperationAndResourceNameV2Enabled checks if the new operation and resource name logic should be used +func OperationAndResourceNameV2Enabled(conf *config.AgentConfig) bool { + return !conf.OTLPReceiver.SpanNameAsResourceName && (conf.OTLPReceiver.SpanNameRemappings == nil || len(conf.OTLPReceiver.SpanNameRemappings) == 0) && conf.HasFeature("enable_operation_and_resource_name_logic_v2") +} + // OtelSpanToDDSpanMinimal otelSpanToDDSpan converts an OTel span to a DD span. // The converted DD span only has the minimal number of fields for APM stats calculation and is only meant // to be used in OTLPTracesToConcentratorInputs. Do not use them for other purposes. @@ -34,10 +39,20 @@ func OtelSpanToDDSpanMinimal( conf *config.AgentConfig, peerTagKeys []string, ) *pb.Span { + var operationName string + var resourceName string + if OperationAndResourceNameV2Enabled(conf) { + operationName = traceutil.GetOTelOperationNameV2(otelspan) + resourceName = traceutil.GetOTelResourceV2(otelspan, otelres) + } else { + operationName = traceutil.GetOTelOperationNameV1(otelspan, otelres, lib, conf.OTLPReceiver.SpanNameAsResourceName, conf.OTLPReceiver.SpanNameRemappings, true) + resourceName = traceutil.GetOTelResourceV1(otelspan, otelres) + } + ddspan := &pb.Span{ Service: traceutil.GetOTelService(otelspan, otelres, true), - Name: traceutil.GetOTelOperationName(otelspan, otelres, lib, conf.OTLPReceiver.SpanNameAsResourceName, conf.OTLPReceiver.SpanNameRemappings, true), - Resource: traceutil.GetOTelResource(otelspan, otelres), + Name: operationName, + Resource: resourceName, TraceID: traceutil.OTelTraceIDToUint64(otelspan.TraceID()), SpanID: traceutil.OTelSpanIDToUint64(otelspan.SpanID()), ParentID: traceutil.OTelSpanIDToUint64(otelspan.ParentSpanID()), diff --git a/releasenotes/notes/operation-and-resource-name-logic-v2-75929121247f2059.yaml b/releasenotes/notes/operation-and-resource-name-logic-v2-75929121247f2059.yaml new file mode 100644 index 0000000000000..14baea0aee210 --- /dev/null +++ b/releasenotes/notes/operation-and-resource-name-logic-v2-75929121247f2059.yaml @@ -0,0 +1,10 @@ +# Each section from every release note are combined when the +# CHANGELOG.rst is rendered. So the text needs to be worded so that +# it does not depend on any information only available in another +# section. This may mean repeating some details, but each section +# must be readable independently of the other. +# +# Each section note must be formatted as reStructuredText. +--- +features: + - Added a new feature flag `enable_operation_and_resource_name_logic_v2` in DD_APM_FEATURES. Enabling this flag modifies the logic for computing operation and resource names from OTLP spans to produce shorter, more readable names and improve alignment with OpenTelemetry specifications.