From ea8d383571140bccd58aee45f8537c8b212c2976 Mon Sep 17 00:00:00 2001 From: Vishal Raj Date: Mon, 28 Oct 2024 12:18:28 +0000 Subject: [PATCH 1/2] [enrichments] Add support for inferred spans --- enrichments/trace/config/config.go | 4 ++ .../trace/internal/elastic/attributes.go | 1 + enrichments/trace/internal/elastic/span.go | 29 ++++++++++ .../trace/internal/elastic/span_test.go | 56 +++++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/enrichments/trace/config/config.go b/enrichments/trace/config/config.go index 68d59ef..bc18cbc 100644 --- a/enrichments/trace/config/config.go +++ b/enrichments/trace/config/config.go @@ -56,6 +56,7 @@ type ElasticTransactionConfig struct { Type AttributeConfig `mapstructure:"type"` Result AttributeConfig `mapstructure:"result"` EventOutcome AttributeConfig `mapstructure:"event_outcome"` + InferredSpans AttributeConfig `mapstructure:"inferred_spans"` } // ElasticSpanConfig configures the enrichment attributes for the spans @@ -73,6 +74,7 @@ type ElasticSpanConfig struct { EventOutcome AttributeConfig `mapstructure:"event_outcome"` ServiceTarget AttributeConfig `mapstructure:"service_target"` DestinationService AttributeConfig `mapstructure:"destination_service"` + InferredSpans AttributeConfig `mapstructure:"inferred_spans"` } // SpanEventConfig configures enrichment attributes for the span events. @@ -121,6 +123,7 @@ func Enabled() Config { Result: AttributeConfig{Enabled: true}, EventOutcome: AttributeConfig{Enabled: true}, RepresentativeCount: AttributeConfig{Enabled: true}, + InferredSpans: AttributeConfig{Enabled: true}, }, Span: ElasticSpanConfig{ TimestampUs: AttributeConfig{Enabled: true}, @@ -132,6 +135,7 @@ func Enabled() Config { ServiceTarget: AttributeConfig{Enabled: true}, DestinationService: AttributeConfig{Enabled: true}, RepresentativeCount: AttributeConfig{Enabled: true}, + InferredSpans: AttributeConfig{Enabled: true}, }, SpanEvent: SpanEventConfig{ TimestampUs: AttributeConfig{Enabled: true}, diff --git a/enrichments/trace/internal/elastic/attributes.go b/enrichments/trace/internal/elastic/attributes.go index b94ccdd..67d7353 100644 --- a/enrichments/trace/internal/elastic/attributes.go +++ b/enrichments/trace/internal/elastic/attributes.go @@ -47,6 +47,7 @@ const ( AttributeSpanDestinationServiceResource = "span.destination.service.resource" AttributeSpanDurationUs = "span.duration.us" AttributeSpanRepresentativeCount = "span.representative_count" + AttributeChildIDs = "child.id" // span event attributes AttributeParentID = "parent.id" diff --git a/enrichments/trace/internal/elastic/span.go b/enrichments/trace/internal/elastic/span.go index ea3cb71..ec24655 100644 --- a/enrichments/trace/internal/elastic/span.go +++ b/enrichments/trace/internal/elastic/span.go @@ -232,6 +232,9 @@ func (s *spanEnrichmentContext) enrichTransaction( if cfg.EventOutcome.Enabled { s.setEventOutcome(span) } + if cfg.InferredSpans.Enabled { + s.setInferredSpans(span) + } } func (s *spanEnrichmentContext) enrichSpan( @@ -266,6 +269,9 @@ func (s *spanEnrichmentContext) enrichSpan( if cfg.DestinationService.Enabled { s.setDestinationService(span) } + if cfg.InferredSpans.Enabled { + s.setInferredSpans(span) + } } // normalizeAttributes sets any dependent attributes that @@ -456,6 +462,29 @@ func (s *spanEnrichmentContext) setDestinationService(span ptrace.Span) { } } +func (s *spanEnrichmentContext) setInferredSpans(span ptrace.Span) { + spanLinks := span.Links() + childIDs := pcommon.NewSlice() + for i := 0; i < spanLinks.Len(); i++ { + spanLink := spanLinks.At(i) + spanID := spanLink.SpanID() + spanLink.Attributes().RemoveIf(func(k string, v pcommon.Value) bool { + switch k { + case "is_child", "elastic.is_child": + if v.Bool() && !spanID.IsEmpty() { + childIDs.AppendEmpty().SetStr(hex.EncodeToString(spanID[:])) + } + return true + } + return false + }) + } + + if childIDs.Len() > 0 { + childIDs.MoveAndAppendTo(span.Attributes().PutEmptySlice(AttributeChildIDs)) + } +} + type spanEventEnrichmentContext struct { exceptionType string exceptionMessage string diff --git a/enrichments/trace/internal/elastic/span_test.go b/enrichments/trace/internal/elastic/span_test.go index 714a1bc..4a78590 100644 --- a/enrichments/trace/internal/elastic/span_test.go +++ b/enrichments/trace/internal/elastic/span_test.go @@ -319,6 +319,36 @@ func TestElasticTransactionEnrich(t *testing.T) { AttributeTransactionType: "messaging", }, }, + { + name: "inferred_spans", + input: func() ptrace.Span { + span := getElasticTxn() + span.SetName("testtxn") + span.SetSpanID([8]byte{1}) + normalLink := span.Links().AppendEmpty() + normalLink.SetSpanID([8]byte{2}) + childLink := span.Links().AppendEmpty() + childLink.SetSpanID([8]byte{3}) + childLink.Attributes().PutBool("is_child", true) + return span + }(), + config: config.Enabled().Transaction, + enrichedAttrs: map[string]any{ + AttributeTimestampUs: startTs.AsTime().UnixMicro(), + AttributeTransactionSampled: true, + AttributeTransactionRoot: true, + AttributeTransactionID: "0100000000000000", + AttributeTransactionName: "testtxn", + AttributeProcessorEvent: "transaction", + AttributeTransactionRepresentativeCount: float64(1), + AttributeTransactionDurationUs: expectedDuration.Microseconds(), + AttributeEventOutcome: "success", + AttributeSuccessCount: int64(1), + AttributeTransactionResult: "Success", + AttributeTransactionType: "unknown", + AttributeChildIDs: []any{"0300000000000000"}, + }, + }, } { t.Run(tc.name, func(t *testing.T) { // Merge existing input attrs with the attrs added @@ -827,6 +857,32 @@ func TestElasticSpanEnrich(t *testing.T) { AttributeSpanDestinationServiceResource: "testsvc", }, }, + { + name: "inferred_spans", + input: func() ptrace.Span { + span := getElasticSpan() + span.SetName("testspan") + span.SetSpanID([8]byte{1}) + normalLink := span.Links().AppendEmpty() + normalLink.SetSpanID([8]byte{2}) + childLink := span.Links().AppendEmpty() + childLink.SetSpanID([8]byte{3}) + childLink.Attributes().PutBool("is_child", true) + return span + }(), + config: config.Enabled().Span, + enrichedAttrs: map[string]any{ + AttributeTimestampUs: startTs.AsTime().UnixMicro(), + AttributeSpanName: "testspan", + AttributeProcessorEvent: "span", + AttributeSpanRepresentativeCount: float64(1), + AttributeSpanType: "unknown", + AttributeSpanDurationUs: expectedDuration.Microseconds(), + AttributeEventOutcome: "success", + AttributeSuccessCount: int64(1), + AttributeChildIDs: []any{"0300000000000000"}, + }, + }, } { t.Run(tc.name, func(t *testing.T) { // Merge existing input attrs with the attrs added From 45c8777ba30feec60959b27d0a0d6732d722c614 Mon Sep 17 00:00:00 2001 From: Vishal Raj Date: Mon, 28 Oct 2024 16:16:07 +0000 Subject: [PATCH 2/2] Fix specs: remove span link if attributes match --- enrichments/trace/internal/elastic/span.go | 13 +-- .../trace/internal/elastic/span_test.go | 81 ++++++++++++++----- go.mod | 1 + go.sum | 2 + 4 files changed, 69 insertions(+), 28 deletions(-) diff --git a/enrichments/trace/internal/elastic/span.go b/enrichments/trace/internal/elastic/span.go index ec24655..d1fdb0f 100644 --- a/enrichments/trace/internal/elastic/span.go +++ b/enrichments/trace/internal/elastic/span.go @@ -465,20 +465,21 @@ func (s *spanEnrichmentContext) setDestinationService(span ptrace.Span) { func (s *spanEnrichmentContext) setInferredSpans(span ptrace.Span) { spanLinks := span.Links() childIDs := pcommon.NewSlice() - for i := 0; i < spanLinks.Len(); i++ { - spanLink := spanLinks.At(i) + spanLinks.RemoveIf(func(spanLink ptrace.SpanLink) (remove bool) { spanID := spanLink.SpanID() - spanLink.Attributes().RemoveIf(func(k string, v pcommon.Value) bool { + spanLink.Attributes().Range(func(k string, v pcommon.Value) bool { switch k { case "is_child", "elastic.is_child": if v.Bool() && !spanID.IsEmpty() { + remove = true // remove the span link if it has the child attrs childIDs.AppendEmpty().SetStr(hex.EncodeToString(spanID[:])) } - return true + return false // stop the loop } - return false + return true }) - } + return remove + }) if childIDs.Len() > 0 { childIDs.MoveAndAppendTo(span.Attributes().PutEmptySlice(AttributeChildIDs)) diff --git a/enrichments/trace/internal/elastic/span_test.go b/enrichments/trace/internal/elastic/span_test.go index 4a78590..694346f 100644 --- a/enrichments/trace/internal/elastic/span_test.go +++ b/enrichments/trace/internal/elastic/span_test.go @@ -26,6 +26,7 @@ import ( "github.com/elastic/opentelemetry-lib/enrichments/trace/config" "github.com/google/go-cmp/cmp" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/ptracetest" "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" @@ -48,10 +49,11 @@ func TestElasticTransactionEnrich(t *testing.T) { return span } for _, tc := range []struct { - name string - input ptrace.Span - config config.ElasticTransactionConfig - enrichedAttrs map[string]any + name string + input ptrace.Span + config config.ElasticTransactionConfig + enrichedAttrs map[string]any + expectedSpanLinks *ptrace.SpanLinkSlice }{ { // test case gives a summary of what is emitted by default @@ -327,9 +329,14 @@ func TestElasticTransactionEnrich(t *testing.T) { span.SetSpanID([8]byte{1}) normalLink := span.Links().AppendEmpty() normalLink.SetSpanID([8]byte{2}) + childLink := span.Links().AppendEmpty() childLink.SetSpanID([8]byte{3}) childLink.Attributes().PutBool("is_child", true) + + childLink2 := span.Links().AppendEmpty() + childLink2.SetSpanID([8]byte{4}) + childLink2.Attributes().PutBool("elastic.is_child", true) return span }(), config: config.Enabled().Transaction, @@ -346,23 +353,35 @@ func TestElasticTransactionEnrich(t *testing.T) { AttributeSuccessCount: int64(1), AttributeTransactionResult: "Success", AttributeTransactionType: "unknown", - AttributeChildIDs: []any{"0300000000000000"}, + AttributeChildIDs: []any{"0300000000000000", "0400000000000000"}, }, + expectedSpanLinks: func() *ptrace.SpanLinkSlice { + spanLinks := ptrace.NewSpanLinkSlice() + // Only the span link without `is_child` or `elastic.is_child` is expected + spanLinks.AppendEmpty().SetSpanID([8]byte{2}) + return &spanLinks + }(), }, } { t.Run(tc.name, func(t *testing.T) { - // Merge existing input attrs with the attrs added - // by enrichment to get the expected attributes. - expectedAttrs := tc.input.Attributes().AsRaw() + expectedSpan := ptrace.NewSpan() + tc.input.CopyTo(expectedSpan) + + // Merge with the expected attributes and override the span links. for k, v := range tc.enrichedAttrs { - expectedAttrs[k] = v + expectedSpan.Attributes().PutEmpty(k).FromRaw(v) + } + // Override span links + if tc.expectedSpanLinks != nil { + tc.expectedSpanLinks.CopyTo(expectedSpan.Links()) + } else { + expectedSpan.Links().RemoveIf(func(_ ptrace.SpanLink) bool { return true }) } EnrichSpan(tc.input, config.Config{ Transaction: tc.config, }) - - assert.Empty(t, cmp.Diff(expectedAttrs, tc.input.Attributes().AsRaw())) + assert.NoError(t, ptracetest.CompareSpan(expectedSpan, tc.input)) }) } } @@ -381,10 +400,11 @@ func TestElasticSpanEnrich(t *testing.T) { return span } for _, tc := range []struct { - name string - input ptrace.Span - config config.ElasticSpanConfig - enrichedAttrs map[string]any + name string + input ptrace.Span + config config.ElasticSpanConfig + enrichedAttrs map[string]any + expectedSpanLinks *ptrace.SpanLinkSlice }{ { // test case gives a summary of what is emitted by default @@ -865,9 +885,14 @@ func TestElasticSpanEnrich(t *testing.T) { span.SetSpanID([8]byte{1}) normalLink := span.Links().AppendEmpty() normalLink.SetSpanID([8]byte{2}) + childLink := span.Links().AppendEmpty() childLink.SetSpanID([8]byte{3}) childLink.Attributes().PutBool("is_child", true) + + childLink2 := span.Links().AppendEmpty() + childLink2.SetSpanID([8]byte{4}) + childLink2.Attributes().PutBool("elastic.is_child", true) return span }(), config: config.Enabled().Span, @@ -880,23 +905,35 @@ func TestElasticSpanEnrich(t *testing.T) { AttributeSpanDurationUs: expectedDuration.Microseconds(), AttributeEventOutcome: "success", AttributeSuccessCount: int64(1), - AttributeChildIDs: []any{"0300000000000000"}, + AttributeChildIDs: []any{"0300000000000000", "0400000000000000"}, }, + expectedSpanLinks: func() *ptrace.SpanLinkSlice { + spanLinks := ptrace.NewSpanLinkSlice() + // Only the span link without `is_child` or `elastic.is_child` is expected + spanLinks.AppendEmpty().SetSpanID([8]byte{2}) + return &spanLinks + }(), }, } { t.Run(tc.name, func(t *testing.T) { - // Merge existing input attrs with the attrs added - // by enrichment to get the expected attributes. - expectedAttrs := tc.input.Attributes().AsRaw() + expectedSpan := ptrace.NewSpan() + tc.input.CopyTo(expectedSpan) + + // Merge with the expected attributes and override the span links. for k, v := range tc.enrichedAttrs { - expectedAttrs[k] = v + expectedSpan.Attributes().PutEmpty(k).FromRaw(v) + } + // Override span links + if tc.expectedSpanLinks != nil { + tc.expectedSpanLinks.CopyTo(expectedSpan.Links()) + } else { + expectedSpan.Links().RemoveIf(func(_ ptrace.SpanLink) bool { return true }) } EnrichSpan(tc.input, config.Config{ Span: tc.config, }) - - assert.Empty(t, cmp.Diff(expectedAttrs, tc.input.Attributes().AsRaw())) + assert.NoError(t, ptracetest.CompareSpan(expectedSpan, tc.input)) }) } } diff --git a/go.mod b/go.mod index b9e96df..f926306 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.0 require ( github.com/google/go-cmp v0.6.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.112.0 + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.112.0 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/collector/pdata v1.18.0 go.opentelemetry.io/collector/semconv v0.112.0 diff --git a/go.sum b/go.sum index 8dc799f..9c80063 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.112.0 h1:+jb8oibBLgnEvhWiMomtxEf4bshEDwtnmKYTM8bf96U= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.112.0/go.mod h1:G4KniRkewEl7JaT1EVTczTWi1nfYk2bD5GAn4aqBh4o= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.112.0 h1:WUAQfu+9bm7t86tyfqkcuz6vTCJfNAxMVocTZPLnWWs= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.112.0/go.mod h1:dQCrspUDJRs7P6pXRALwj/yKIMzTYCvLa7XlzNycVFY= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.112.0 h1:FIQ/vt0Ulnwr2PSkLSD0SfdSyfm9dmBBnBcjAbngC7o= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.112.0/go.mod h1:W9HkQWHB/Zc6adYHDG3FNyxfERt9eBAw2sBqNYBBBEE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=