diff --git a/enrichments/trace/internal/elastic/span.go b/enrichments/trace/internal/elastic/span.go index d1fdb0f..6fc340f 100644 --- a/enrichments/trace/internal/elastic/span.go +++ b/enrichments/trace/internal/elastic/span.go @@ -187,10 +187,17 @@ func (s *spanEnrichmentContext) Enrich(span ptrace.Span, cfg config.Config) { } func (s *spanEnrichmentContext) enrich(span ptrace.Span, cfg config.Config) { + + // In OTel, a local root span can represent an outgoing call or a producer span. + // In such cases, the span is still mapped into a transaction, but enriched + // with additional attributes that are specific to the outgoing call or producer span. + isExitRootSpan := s.isTransaction && span.Kind() == ptrace.SpanKindClient || span.Kind() == ptrace.SpanKindProducer + if s.isTransaction { s.enrichTransaction(span, cfg.Transaction) - } else { - s.enrichSpan(span, cfg.Span) + } + if !s.isTransaction || isExitRootSpan { + s.enrichSpan(span, cfg, isExitRootSpan) } } @@ -239,39 +246,57 @@ func (s *spanEnrichmentContext) enrichTransaction( func (s *spanEnrichmentContext) enrichSpan( span ptrace.Span, - cfg config.ElasticSpanConfig, + cfg config.Config, + isExitRootSpan bool, ) { - if cfg.TimestampUs.Enabled { + if cfg.Span.TimestampUs.Enabled { span.Attributes().PutInt(AttributeTimestampUs, getTimestampUs(span.StartTimestamp())) } - if cfg.Name.Enabled { + if cfg.Span.Name.Enabled { span.Attributes().PutStr(AttributeSpanName, span.Name()) } - if cfg.ProcessorEvent.Enabled { - span.Attributes().PutStr(AttributeProcessorEvent, "span") - } - if cfg.RepresentativeCount.Enabled { + if cfg.Span.RepresentativeCount.Enabled { repCount := getRepresentativeCount(span.TraceState().AsRaw()) span.Attributes().PutDouble(AttributeSpanRepresentativeCount, repCount) } - if cfg.TypeSubtype.Enabled { + if cfg.Span.TypeSubtype.Enabled { s.setSpanTypeSubtype(span) } - if cfg.EventOutcome.Enabled { + if cfg.Span.EventOutcome.Enabled { s.setEventOutcome(span) } - if cfg.DurationUs.Enabled { + if cfg.Span.DurationUs.Enabled { span.Attributes().PutInt(AttributeSpanDurationUs, getDurationUs(span)) } - if cfg.ServiceTarget.Enabled { + if cfg.Span.ServiceTarget.Enabled { s.setServiceTarget(span) } - if cfg.DestinationService.Enabled { + if cfg.Span.DestinationService.Enabled { s.setDestinationService(span) } - if cfg.InferredSpans.Enabled { + if cfg.Span.InferredSpans.Enabled { s.setInferredSpans(span) } + if cfg.Span.ProcessorEvent.Enabled { + if isExitRootSpan { + attrSlice := span.Attributes().PutEmptySlice(AttributeProcessorEvent) + attrSlice.AppendEmpty().SetStr("transaction") + attrSlice.AppendEmpty().SetStr("span") + } else { + span.Attributes().PutStr(AttributeProcessorEvent, "span") + } + } + + if isExitRootSpan && cfg.Transaction.Type.Enabled { + spanTypeAttr, hasType := span.Attributes().Get(AttributeSpanType) + if hasType { + transactionType := spanTypeAttr.Str() + if spanSubtypeAttr, hasSubType := span.Attributes().Get(AttributeSpanSubtype); hasSubType { + transactionType += "." + spanSubtypeAttr.Str() + } + span.Attributes().PutStr(AttributeTransactionType, transactionType) + } + } } // normalizeAttributes sets any dependent attributes that diff --git a/enrichments/trace/internal/elastic/span_test.go b/enrichments/trace/internal/elastic/span_test.go index 694346f..cf63367 100644 --- a/enrichments/trace/internal/elastic/span_test.go +++ b/enrichments/trace/internal/elastic/span_test.go @@ -386,6 +386,167 @@ func TestElasticTransactionEnrich(t *testing.T) { } } +// Tests root spans that represent a dependency and are mapped to a transaction. +func TestRootSpanAsDependencyEnrich(t *testing.T) { + for _, tc := range []struct { + name string + input ptrace.Span + config config.Config + enrichedAttrs map[string]any + expectedSpanLinks *ptrace.SpanLinkSlice + }{ + { + name: "outgoing_http_root_span", + input: func() ptrace.Span { + span := ptrace.NewSpan() + span.SetName("rootClientSpan") + span.SetSpanID([8]byte{1}) + span.SetKind(ptrace.SpanKindClient) + span.Attributes().PutStr(semconv.AttributeHTTPMethod, "GET") + span.Attributes().PutStr(semconv.AttributeHTTPURL, "http://localhost:8080") + span.Attributes().PutInt(semconv.AttributeHTTPResponseStatusCode, 200) + span.Attributes().PutStr(semconv.AttributeNetworkProtocolVersion, "1.1") + return span + }(), + config: config.Enabled(), + enrichedAttrs: map[string]any{ + AttributeTimestampUs: int64(0), + AttributeTransactionName: "rootClientSpan", + AttributeProcessorEvent: func() pcommon.Slice { + p := pcommon.NewSlice() + p.AppendEmpty().SetStr("transaction") + p.AppendEmpty().SetStr("span") + return p + }().AsRaw(), + AttributeSpanType: "external", + AttributeSpanSubtype: "http", + AttributeSpanDestinationServiceResource: "localhost:8080", + AttributeSpanName: "rootClientSpan", + AttributeEventOutcome: "success", + AttributeSuccessCount: int64(1), + AttributeServiceTargetName: "localhost:8080", + AttributeServiceTargetType: "http", + AttributeTransactionID: "0100000000000000", + AttributeTransactionDurationUs: int64(0), + AttributeTransactionRepresentativeCount: float64(1), + AttributeTransactionResult: "HTTP 2xx", + AttributeTransactionType: "external.http", + AttributeTransactionSampled: true, + AttributeTransactionRoot: true, + AttributeSpanDurationUs: int64(0), + AttributeSpanRepresentativeCount: float64(1), + }, + }, + { + name: "db_root_span", + input: func() ptrace.Span { + span := ptrace.NewSpan() + span.SetName("rootClientSpan") + span.SetSpanID([8]byte{1}) + span.SetKind(ptrace.SpanKindClient) + span.Attributes().PutStr(semconv.AttributeDBSystem, "mssql") + + span.Attributes().PutStr(semconv.AttributeDBName, "myDb") + span.Attributes().PutStr(semconv.AttributeDBOperation, "SELECT") + span.Attributes().PutStr(semconv.AttributeDBStatement, "SELECT * FROM wuser_table") + return span + }(), + config: config.Enabled(), + enrichedAttrs: map[string]any{ + AttributeTimestampUs: int64(0), + AttributeTransactionName: "rootClientSpan", + AttributeProcessorEvent: func() pcommon.Slice { + p := pcommon.NewSlice() + p.AppendEmpty().SetStr("transaction") + p.AppendEmpty().SetStr("span") + return p + }().AsRaw(), + AttributeSpanType: "db", + AttributeSpanSubtype: "mssql", + AttributeSpanDestinationServiceResource: "mssql", + AttributeSpanName: "rootClientSpan", + AttributeEventOutcome: "success", + AttributeSuccessCount: int64(1), + AttributeServiceTargetName: "myDb", + AttributeServiceTargetType: "mssql", + AttributeTransactionID: "0100000000000000", + AttributeTransactionDurationUs: int64(0), + AttributeTransactionRepresentativeCount: float64(1), + AttributeTransactionResult: "Success", + AttributeTransactionType: "db.mssql", + AttributeTransactionSampled: true, + AttributeTransactionRoot: true, + AttributeSpanDurationUs: int64(0), + AttributeSpanRepresentativeCount: float64(1), + }, + }, + { + name: "producer_messaging_span", + input: func() ptrace.Span { + span := ptrace.NewSpan() + span.SetName("rootClientSpan") + span.SetSpanID([8]byte{1}) + span.SetKind(ptrace.SpanKindProducer) + + span.Attributes().PutStr(semconv.AttributeServerAddress, "myServer") + span.Attributes().PutStr(semconv.AttributeServerPort, "1234") + span.Attributes().PutStr(semconv.AttributeMessagingSystem, "rabbitmq") + span.Attributes().PutStr(semconv.AttributeMessagingDestinationName, "T") + span.Attributes().PutStr(semconv.AttributeMessagingOperation, "publish") + span.Attributes().PutStr(semconv.AttributeMessagingClientID, "a") + return span + }(), + config: config.Enabled(), + enrichedAttrs: map[string]any{ + AttributeTimestampUs: int64(0), + AttributeTransactionName: "rootClientSpan", + AttributeProcessorEvent: func() pcommon.Slice { + p := pcommon.NewSlice() + p.AppendEmpty().SetStr("transaction") + p.AppendEmpty().SetStr("span") + return p + }().AsRaw(), + AttributeSpanType: "messaging", + AttributeSpanSubtype: "rabbitmq", + AttributeSpanDestinationServiceResource: "rabbitmq/T", + AttributeSpanName: "rootClientSpan", + AttributeEventOutcome: "success", + AttributeSuccessCount: int64(1), + AttributeServiceTargetName: "T", + AttributeServiceTargetType: "rabbitmq", + AttributeTransactionID: "0100000000000000", + AttributeTransactionDurationUs: int64(0), + AttributeTransactionRepresentativeCount: float64(1), + AttributeTransactionResult: "Success", + AttributeTransactionType: "messaging.rabbitmq", + AttributeTransactionSampled: true, + AttributeTransactionRoot: true, + AttributeSpanDurationUs: int64(0), + AttributeSpanRepresentativeCount: float64(1), + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + expectedSpan := ptrace.NewSpan() + tc.input.CopyTo(expectedSpan) + + // Merge with the expected attributes and override the span links. + for k, v := range tc.enrichedAttrs { + 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, tc.config) + assert.NoError(t, ptracetest.CompareSpan(expectedSpan, tc.input)) + }) + } +} + // Tests the enrichment logic for elastic's span definition. func TestElasticSpanEnrich(t *testing.T) { now := time.Unix(3600, 0)