From 81277c77aec6d9bf204a53d851f78a992242f422 Mon Sep 17 00:00:00 2001 From: Isaac Flores Date: Wed, 14 Jan 2026 16:06:16 -0800 Subject: [PATCH] Update span enrichment to set clear the span id and span name This allows the span id and name to be only represented by the elastic attributes (`span.id`, `span.name` or `transaction.id` , `transaction.name`) only --- enrichments/config/config.go | 34 ++++- enrichments/config/config_test.go | 14 +- enrichments/internal/elastic/span.go | 12 ++ enrichments/internal/elastic/span_test.go | 177 ++++++++++++++++++++++ 4 files changed, 229 insertions(+), 8 deletions(-) diff --git a/enrichments/config/config.go b/enrichments/config/config.go index 0297502..95900f3 100644 --- a/enrichments/config/config.go +++ b/enrichments/config/config.go @@ -49,11 +49,21 @@ type ElasticTransactionConfig struct { // TimestampUs is a temporary attribute to enable higher // resolution timestamps in Elasticsearch. For more details see: // https://github.com/elastic/opentelemetry-dev/issues/374. - TimestampUs AttributeConfig `mapstructure:"timestamp_us"` - Sampled AttributeConfig `mapstructure:"sampled"` - ID AttributeConfig `mapstructure:"id"` - Root AttributeConfig `mapstructure:"root"` - Name AttributeConfig `mapstructure:"name"` + TimestampUs AttributeConfig `mapstructure:"timestamp_us"` + Sampled AttributeConfig `mapstructure:"sampled"` + ID AttributeConfig `mapstructure:"id"` + // ClearSpanID sets the span ID to an empty value so that the + // ID is only represented by the `span.ID` attribute. + // Applicable only when ID is enabled. + // Disabled by default. + ClearSpanID AttributeConfig `mapstructure:"clear_span_id"` + Root AttributeConfig `mapstructure:"root"` + Name AttributeConfig `mapstructure:"name"` + // ClearSpanName sets the span name to an empty value so that the + // name is only represented by the `span.name` attribute. + // Applicable only when Name is enabled. + // Disabled by default. + ClearSpanName AttributeConfig `mapstructure:"clear_span_name"` ProcessorEvent AttributeConfig `mapstructure:"processor_event"` RepresentativeCount AttributeConfig `mapstructure:"representative_count"` DurationUs AttributeConfig `mapstructure:"duration_us"` @@ -72,8 +82,13 @@ type ElasticSpanConfig struct { // TimestampUs is a temporary attribute to enable higher // resolution timestamps in Elasticsearch. For more details see: // https://github.com/elastic/opentelemetry-dev/issues/374. - TimestampUs AttributeConfig `mapstructure:"timestamp_us"` - ID AttributeConfig `mapstructure:"id"` + TimestampUs AttributeConfig `mapstructure:"timestamp_us"` + ID AttributeConfig `mapstructure:"id"` + // ClearSpanID sets the span ID to an empty value so that the + // ID is only represented by the `transaction.ID` attribute. + // Applicable only when ID is enabled. + // Disabled by default. + ClearSpanID AttributeConfig `mapstructure:"clear_span_id"` Name AttributeConfig `mapstructure:"name"` ProcessorEvent AttributeConfig `mapstructure:"processor_event"` RepresentativeCount AttributeConfig `mapstructure:"representative_count"` @@ -87,6 +102,11 @@ type ElasticSpanConfig struct { UserAgent AttributeConfig `mapstructure:"user_agent"` RemoveMessaging AttributeConfig `mapstructure:"remove_messaging"` MessageQueueName AttributeConfig `mapstructure:"message_queue_name"` + // ClearSpanName sets the span name to an empty value so that the + // name is only represented by the `transaction.name` attribute. + // Applicable only when Name is enabled. + // Disabled by default. + ClearSpanName AttributeConfig `mapstructure:"clear_span_name"` } // SpanEventConfig configures enrichment attributes for the span events. diff --git a/enrichments/config/config_test.go b/enrichments/config/config_test.go index b3eec4c..8b46eae 100644 --- a/enrichments/config/config_test.go +++ b/enrichments/config/config_test.go @@ -36,10 +36,22 @@ func TestEnabled(t *testing.T) { func assertAllEnabled(t *testing.T, cfg reflect.Value) { t.Helper() + // Fields that are intentionally disabled by default + disabledByDefault := map[string]bool{ + "ClearSpanID": true, + "ClearSpanName": true, + } + for i := 0; i < cfg.NumField(); i++ { + fieldName := cfg.Type().Field(i).Name rAttrCfg := cfg.Field(i).Interface() attrCfg, ok := rAttrCfg.(AttributeConfig) require.True(t, ok, "must be a type of AttributeConfig") - require.True(t, attrCfg.Enabled, "must be enabled") + + if disabledByDefault[fieldName] { + require.False(t, attrCfg.Enabled, "%s must be disabled by default", fieldName) + } else { + require.True(t, attrCfg.Enabled, "%s must be enabled", fieldName) + } } } diff --git a/enrichments/internal/elastic/span.go b/enrichments/internal/elastic/span.go index 11f9d37..7d9adde 100644 --- a/enrichments/internal/elastic/span.go +++ b/enrichments/internal/elastic/span.go @@ -264,12 +264,18 @@ func (s *spanEnrichmentContext) enrichTransaction( // https://github.com/elastic/apm-data/blob/v1.19.5/input/elasticapm/internal/modeldecoder/v2/decoder.go#L1377-L1379 // https://github.com/elastic/apm-data/blob/v1.19.5/input/otlp/traces.go#L185-L194 attribute.PutStr(span.Attributes(), elasticattr.SpanID, transactionID) + if cfg.ClearSpanID.Enabled { + span.SetSpanID(pcommon.SpanID{}) + } } if cfg.Root.Enabled { attribute.PutBool(span.Attributes(), elasticattr.TransactionRoot, isTraceRoot(span)) } if cfg.Name.Enabled { attribute.PutStr(span.Attributes(), elasticattr.TransactionName, span.Name()) + if cfg.ClearSpanName.Enabled { + span.SetName("") + } } if cfg.ProcessorEvent.Enabled { attribute.PutStr(span.Attributes(), elasticattr.ProcessorEvent, "transaction") @@ -317,9 +323,15 @@ func (s *spanEnrichmentContext) enrichSpan( } if cfg.ID.Enabled { attribute.PutStr(span.Attributes(), elasticattr.SpanID, span.SpanID().String()) + if cfg.ClearSpanID.Enabled { + span.SetSpanID(pcommon.SpanID{}) + } } if cfg.Name.Enabled { attribute.PutStr(span.Attributes(), elasticattr.SpanName, span.Name()) + if cfg.ClearSpanName.Enabled { + span.SetName("") + } } if cfg.RepresentativeCount.Enabled { repCount := getRepresentativeCount(span.TraceState().AsRaw()) diff --git a/enrichments/internal/elastic/span_test.go b/enrichments/internal/elastic/span_test.go index 86eb391..353b9e0 100644 --- a/enrichments/internal/elastic/span_test.go +++ b/enrichments/internal/elastic/span_test.go @@ -675,6 +675,91 @@ func TestElasticTransactionEnrich(t *testing.T) { elasticattr.TransactionType: "mobile", }, }, + { + name: "clear_span_id_enabled", + input: func() ptrace.Span { + span := getElasticTxn() + span.SetName("testtxn") + return span + }(), + config: func() config.ElasticTransactionConfig { + cfg := config.Enabled().Transaction + cfg.ClearSpanID = config.AttributeConfig{Enabled: true} + return cfg + }(), + enrichedAttrs: map[string]any{ + elasticattr.TimestampUs: startTs.AsTime().UnixMicro(), + elasticattr.TransactionSampled: true, + elasticattr.TransactionRoot: true, + elasticattr.TransactionID: "0100000000000000", + elasticattr.SpanID: "0100000000000000", + elasticattr.TransactionName: "testtxn", + elasticattr.ProcessorEvent: "transaction", + elasticattr.TransactionRepresentativeCount: float64(1), + elasticattr.TransactionDurationUs: expectedDuration.Microseconds(), + elasticattr.EventOutcome: "success", + elasticattr.SuccessCount: int64(1), + elasticattr.TransactionResult: "Success", + elasticattr.TransactionType: "unknown", + }, + }, + { + name: "clear_span_name_enabled", + input: func() ptrace.Span { + span := getElasticTxn() + span.SetName("testtxn") + return span + }(), + config: func() config.ElasticTransactionConfig { + cfg := config.Enabled().Transaction + cfg.ClearSpanName = config.AttributeConfig{Enabled: true} + return cfg + }(), + enrichedAttrs: map[string]any{ + elasticattr.TimestampUs: startTs.AsTime().UnixMicro(), + elasticattr.TransactionSampled: true, + elasticattr.TransactionRoot: true, + elasticattr.TransactionID: "0100000000000000", + elasticattr.SpanID: "0100000000000000", + elasticattr.TransactionName: "testtxn", + elasticattr.ProcessorEvent: "transaction", + elasticattr.TransactionRepresentativeCount: float64(1), + elasticattr.TransactionDurationUs: expectedDuration.Microseconds(), + elasticattr.EventOutcome: "success", + elasticattr.SuccessCount: int64(1), + elasticattr.TransactionResult: "Success", + elasticattr.TransactionType: "unknown", + }, + }, + { + name: "clear_span_id_and_name_enabled", + input: func() ptrace.Span { + span := getElasticTxn() + span.SetName("testtxn") + return span + }(), + config: func() config.ElasticTransactionConfig { + cfg := config.Enabled().Transaction + cfg.ClearSpanID = config.AttributeConfig{Enabled: true} + cfg.ClearSpanName = config.AttributeConfig{Enabled: true} + return cfg + }(), + enrichedAttrs: map[string]any{ + elasticattr.TimestampUs: startTs.AsTime().UnixMicro(), + elasticattr.TransactionSampled: true, + elasticattr.TransactionRoot: true, + elasticattr.TransactionID: "0100000000000000", + elasticattr.SpanID: "0100000000000000", + elasticattr.TransactionName: "testtxn", + elasticattr.ProcessorEvent: "transaction", + elasticattr.TransactionRepresentativeCount: float64(1), + elasticattr.TransactionDurationUs: expectedDuration.Microseconds(), + elasticattr.EventOutcome: "success", + elasticattr.SuccessCount: int64(1), + elasticattr.TransactionResult: "Success", + elasticattr.TransactionType: "unknown", + }, + }, } { t.Run(tc.name, func(t *testing.T) { expectedSpan := ptrace.NewSpan() @@ -697,6 +782,14 @@ func TestElasticTransactionEnrich(t *testing.T) { } else { expectedSpan.Links().RemoveIf(func(_ ptrace.SpanLink) bool { return true }) } + // Clear span ID if ID and ClearSpanID are both enabled + if tc.config.ID.Enabled && tc.config.ClearSpanID.Enabled { + expectedSpan.SetSpanID(pcommon.SpanID{}) + } + // Clear span name if Name and ClearSpanName are both enabled + if tc.config.Name.Enabled && tc.config.ClearSpanName.Enabled { + expectedSpan.SetName("") + } EnrichSpan(tc.input, config.Config{ Transaction: tc.config, @@ -2030,6 +2123,82 @@ func TestElasticSpanEnrich(t *testing.T) { elasticattr.SuccessCount: int64(99), }, }, + { + name: "clear_span_id_enabled", + input: func() ptrace.Span { + span := getElasticSpan() + span.SetName("testspan") + span.SetSpanID([8]byte{1}) + return span + }(), + config: func() config.ElasticSpanConfig { + cfg := config.Enabled().Span + cfg.ClearSpanID = config.AttributeConfig{Enabled: true} + return cfg + }(), + enrichedAttrs: map[string]any{ + elasticattr.TimestampUs: startTs.AsTime().UnixMicro(), + elasticattr.SpanName: "testspan", + elasticattr.ProcessorEvent: "span", + elasticattr.SpanRepresentativeCount: float64(1), + elasticattr.SpanType: "unknown", + elasticattr.SpanDurationUs: expectedDuration.Microseconds(), + elasticattr.EventOutcome: "success", + elasticattr.SuccessCount: int64(1), + elasticattr.SpanID: "0100000000000000", + }, + }, + { + name: "clear_span_name_enabled", + input: func() ptrace.Span { + span := getElasticSpan() + span.SetName("testspan") + span.SetSpanID([8]byte{1}) + return span + }(), + config: func() config.ElasticSpanConfig { + cfg := config.Enabled().Span + cfg.ClearSpanName = config.AttributeConfig{Enabled: true} + return cfg + }(), + enrichedAttrs: map[string]any{ + elasticattr.TimestampUs: startTs.AsTime().UnixMicro(), + elasticattr.SpanName: "testspan", + elasticattr.ProcessorEvent: "span", + elasticattr.SpanRepresentativeCount: float64(1), + elasticattr.SpanType: "unknown", + elasticattr.SpanDurationUs: expectedDuration.Microseconds(), + elasticattr.EventOutcome: "success", + elasticattr.SuccessCount: int64(1), + elasticattr.SpanID: "0100000000000000", + }, + }, + { + name: "clear_span_id_and_name_enabled", + input: func() ptrace.Span { + span := getElasticSpan() + span.SetName("testspan") + span.SetSpanID([8]byte{1}) + return span + }(), + config: func() config.ElasticSpanConfig { + cfg := config.Enabled().Span + cfg.ClearSpanID = config.AttributeConfig{Enabled: true} + cfg.ClearSpanName = config.AttributeConfig{Enabled: true} + return cfg + }(), + enrichedAttrs: map[string]any{ + elasticattr.TimestampUs: startTs.AsTime().UnixMicro(), + elasticattr.SpanName: "testspan", + elasticattr.ProcessorEvent: "span", + elasticattr.SpanRepresentativeCount: float64(1), + elasticattr.SpanType: "unknown", + elasticattr.SpanDurationUs: expectedDuration.Microseconds(), + elasticattr.EventOutcome: "success", + elasticattr.SuccessCount: int64(1), + elasticattr.SpanID: "0100000000000000", + }, + }, } { t.Run(tc.name, func(t *testing.T) { expectedSpan := ptrace.NewSpan() @@ -2061,6 +2230,14 @@ func TestElasticSpanEnrich(t *testing.T) { } else { expectedSpan.Links().RemoveIf(func(_ ptrace.SpanLink) bool { return true }) } + // Clear span ID if ID and ClearSpanID are both enabled + if tc.config.ID.Enabled && tc.config.ClearSpanID.Enabled { + expectedSpan.SetSpanID(pcommon.SpanID{}) + } + // Clear span name if Name and ClearSpanName are both enabled + if tc.config.Name.Enabled && tc.config.ClearSpanName.Enabled { + expectedSpan.SetName("") + } EnrichSpan(tc.input, enrichConfig, uaparser.NewFromSaved()) assert.NoError(t, ptracetest.CompareSpan(expectedSpan, tc.input))