Skip to content

Commit 77d76ae

Browse files
authored
[enrichments] Add support for inferred spans (#116)
1 parent 62bb176 commit 77d76ae

File tree

6 files changed

+151
-20
lines changed

6 files changed

+151
-20
lines changed

enrichments/trace/config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type ElasticTransactionConfig struct {
5656
Type AttributeConfig `mapstructure:"type"`
5757
Result AttributeConfig `mapstructure:"result"`
5858
EventOutcome AttributeConfig `mapstructure:"event_outcome"`
59+
InferredSpans AttributeConfig `mapstructure:"inferred_spans"`
5960
}
6061

6162
// ElasticSpanConfig configures the enrichment attributes for the spans
@@ -73,6 +74,7 @@ type ElasticSpanConfig struct {
7374
EventOutcome AttributeConfig `mapstructure:"event_outcome"`
7475
ServiceTarget AttributeConfig `mapstructure:"service_target"`
7576
DestinationService AttributeConfig `mapstructure:"destination_service"`
77+
InferredSpans AttributeConfig `mapstructure:"inferred_spans"`
7678
}
7779

7880
// SpanEventConfig configures enrichment attributes for the span events.
@@ -121,6 +123,7 @@ func Enabled() Config {
121123
Result: AttributeConfig{Enabled: true},
122124
EventOutcome: AttributeConfig{Enabled: true},
123125
RepresentativeCount: AttributeConfig{Enabled: true},
126+
InferredSpans: AttributeConfig{Enabled: true},
124127
},
125128
Span: ElasticSpanConfig{
126129
TimestampUs: AttributeConfig{Enabled: true},
@@ -132,6 +135,7 @@ func Enabled() Config {
132135
ServiceTarget: AttributeConfig{Enabled: true},
133136
DestinationService: AttributeConfig{Enabled: true},
134137
RepresentativeCount: AttributeConfig{Enabled: true},
138+
InferredSpans: AttributeConfig{Enabled: true},
135139
},
136140
SpanEvent: SpanEventConfig{
137141
TimestampUs: AttributeConfig{Enabled: true},

enrichments/trace/internal/elastic/attributes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const (
4747
AttributeSpanDestinationServiceResource = "span.destination.service.resource"
4848
AttributeSpanDurationUs = "span.duration.us"
4949
AttributeSpanRepresentativeCount = "span.representative_count"
50+
AttributeChildIDs = "child.id"
5051

5152
// span event attributes
5253
AttributeParentID = "parent.id"

enrichments/trace/internal/elastic/span.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,9 @@ func (s *spanEnrichmentContext) enrichTransaction(
232232
if cfg.EventOutcome.Enabled {
233233
s.setEventOutcome(span)
234234
}
235+
if cfg.InferredSpans.Enabled {
236+
s.setInferredSpans(span)
237+
}
235238
}
236239

237240
func (s *spanEnrichmentContext) enrichSpan(
@@ -266,6 +269,9 @@ func (s *spanEnrichmentContext) enrichSpan(
266269
if cfg.DestinationService.Enabled {
267270
s.setDestinationService(span)
268271
}
272+
if cfg.InferredSpans.Enabled {
273+
s.setInferredSpans(span)
274+
}
269275
}
270276

271277
// normalizeAttributes sets any dependent attributes that
@@ -456,6 +462,30 @@ func (s *spanEnrichmentContext) setDestinationService(span ptrace.Span) {
456462
}
457463
}
458464

465+
func (s *spanEnrichmentContext) setInferredSpans(span ptrace.Span) {
466+
spanLinks := span.Links()
467+
childIDs := pcommon.NewSlice()
468+
spanLinks.RemoveIf(func(spanLink ptrace.SpanLink) (remove bool) {
469+
spanID := spanLink.SpanID()
470+
spanLink.Attributes().Range(func(k string, v pcommon.Value) bool {
471+
switch k {
472+
case "is_child", "elastic.is_child":
473+
if v.Bool() && !spanID.IsEmpty() {
474+
remove = true // remove the span link if it has the child attrs
475+
childIDs.AppendEmpty().SetStr(hex.EncodeToString(spanID[:]))
476+
}
477+
return false // stop the loop
478+
}
479+
return true
480+
})
481+
return remove
482+
})
483+
484+
if childIDs.Len() > 0 {
485+
childIDs.MoveAndAppendTo(span.Attributes().PutEmptySlice(AttributeChildIDs))
486+
}
487+
}
488+
459489
type spanEventEnrichmentContext struct {
460490
exceptionType string
461491
exceptionMessage string

enrichments/trace/internal/elastic/span_test.go

Lines changed: 113 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
"github.com/elastic/opentelemetry-lib/enrichments/trace/config"
2828
"github.com/google/go-cmp/cmp"
29+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/ptracetest"
2930
"github.com/stretchr/testify/assert"
3031
"go.opentelemetry.io/collector/pdata/pcommon"
3132
"go.opentelemetry.io/collector/pdata/ptrace"
@@ -48,10 +49,11 @@ func TestElasticTransactionEnrich(t *testing.T) {
4849
return span
4950
}
5051
for _, tc := range []struct {
51-
name string
52-
input ptrace.Span
53-
config config.ElasticTransactionConfig
54-
enrichedAttrs map[string]any
52+
name string
53+
input ptrace.Span
54+
config config.ElasticTransactionConfig
55+
enrichedAttrs map[string]any
56+
expectedSpanLinks *ptrace.SpanLinkSlice
5557
}{
5658
{
5759
// test case gives a summary of what is emitted by default
@@ -319,20 +321,67 @@ func TestElasticTransactionEnrich(t *testing.T) {
319321
AttributeTransactionType: "messaging",
320322
},
321323
},
324+
{
325+
name: "inferred_spans",
326+
input: func() ptrace.Span {
327+
span := getElasticTxn()
328+
span.SetName("testtxn")
329+
span.SetSpanID([8]byte{1})
330+
normalLink := span.Links().AppendEmpty()
331+
normalLink.SetSpanID([8]byte{2})
332+
333+
childLink := span.Links().AppendEmpty()
334+
childLink.SetSpanID([8]byte{3})
335+
childLink.Attributes().PutBool("is_child", true)
336+
337+
childLink2 := span.Links().AppendEmpty()
338+
childLink2.SetSpanID([8]byte{4})
339+
childLink2.Attributes().PutBool("elastic.is_child", true)
340+
return span
341+
}(),
342+
config: config.Enabled().Transaction,
343+
enrichedAttrs: map[string]any{
344+
AttributeTimestampUs: startTs.AsTime().UnixMicro(),
345+
AttributeTransactionSampled: true,
346+
AttributeTransactionRoot: true,
347+
AttributeTransactionID: "0100000000000000",
348+
AttributeTransactionName: "testtxn",
349+
AttributeProcessorEvent: "transaction",
350+
AttributeTransactionRepresentativeCount: float64(1),
351+
AttributeTransactionDurationUs: expectedDuration.Microseconds(),
352+
AttributeEventOutcome: "success",
353+
AttributeSuccessCount: int64(1),
354+
AttributeTransactionResult: "Success",
355+
AttributeTransactionType: "unknown",
356+
AttributeChildIDs: []any{"0300000000000000", "0400000000000000"},
357+
},
358+
expectedSpanLinks: func() *ptrace.SpanLinkSlice {
359+
spanLinks := ptrace.NewSpanLinkSlice()
360+
// Only the span link without `is_child` or `elastic.is_child` is expected
361+
spanLinks.AppendEmpty().SetSpanID([8]byte{2})
362+
return &spanLinks
363+
}(),
364+
},
322365
} {
323366
t.Run(tc.name, func(t *testing.T) {
324-
// Merge existing input attrs with the attrs added
325-
// by enrichment to get the expected attributes.
326-
expectedAttrs := tc.input.Attributes().AsRaw()
367+
expectedSpan := ptrace.NewSpan()
368+
tc.input.CopyTo(expectedSpan)
369+
370+
// Merge with the expected attributes and override the span links.
327371
for k, v := range tc.enrichedAttrs {
328-
expectedAttrs[k] = v
372+
expectedSpan.Attributes().PutEmpty(k).FromRaw(v)
373+
}
374+
// Override span links
375+
if tc.expectedSpanLinks != nil {
376+
tc.expectedSpanLinks.CopyTo(expectedSpan.Links())
377+
} else {
378+
expectedSpan.Links().RemoveIf(func(_ ptrace.SpanLink) bool { return true })
329379
}
330380

331381
EnrichSpan(tc.input, config.Config{
332382
Transaction: tc.config,
333383
})
334-
335-
assert.Empty(t, cmp.Diff(expectedAttrs, tc.input.Attributes().AsRaw()))
384+
assert.NoError(t, ptracetest.CompareSpan(expectedSpan, tc.input))
336385
})
337386
}
338387
}
@@ -351,10 +400,11 @@ func TestElasticSpanEnrich(t *testing.T) {
351400
return span
352401
}
353402
for _, tc := range []struct {
354-
name string
355-
input ptrace.Span
356-
config config.ElasticSpanConfig
357-
enrichedAttrs map[string]any
403+
name string
404+
input ptrace.Span
405+
config config.ElasticSpanConfig
406+
enrichedAttrs map[string]any
407+
expectedSpanLinks *ptrace.SpanLinkSlice
358408
}{
359409
{
360410
// test case gives a summary of what is emitted by default
@@ -827,20 +877,63 @@ func TestElasticSpanEnrich(t *testing.T) {
827877
AttributeSpanDestinationServiceResource: "testsvc",
828878
},
829879
},
880+
{
881+
name: "inferred_spans",
882+
input: func() ptrace.Span {
883+
span := getElasticSpan()
884+
span.SetName("testspan")
885+
span.SetSpanID([8]byte{1})
886+
normalLink := span.Links().AppendEmpty()
887+
normalLink.SetSpanID([8]byte{2})
888+
889+
childLink := span.Links().AppendEmpty()
890+
childLink.SetSpanID([8]byte{3})
891+
childLink.Attributes().PutBool("is_child", true)
892+
893+
childLink2 := span.Links().AppendEmpty()
894+
childLink2.SetSpanID([8]byte{4})
895+
childLink2.Attributes().PutBool("elastic.is_child", true)
896+
return span
897+
}(),
898+
config: config.Enabled().Span,
899+
enrichedAttrs: map[string]any{
900+
AttributeTimestampUs: startTs.AsTime().UnixMicro(),
901+
AttributeSpanName: "testspan",
902+
AttributeProcessorEvent: "span",
903+
AttributeSpanRepresentativeCount: float64(1),
904+
AttributeSpanType: "unknown",
905+
AttributeSpanDurationUs: expectedDuration.Microseconds(),
906+
AttributeEventOutcome: "success",
907+
AttributeSuccessCount: int64(1),
908+
AttributeChildIDs: []any{"0300000000000000", "0400000000000000"},
909+
},
910+
expectedSpanLinks: func() *ptrace.SpanLinkSlice {
911+
spanLinks := ptrace.NewSpanLinkSlice()
912+
// Only the span link without `is_child` or `elastic.is_child` is expected
913+
spanLinks.AppendEmpty().SetSpanID([8]byte{2})
914+
return &spanLinks
915+
}(),
916+
},
830917
} {
831918
t.Run(tc.name, func(t *testing.T) {
832-
// Merge existing input attrs with the attrs added
833-
// by enrichment to get the expected attributes.
834-
expectedAttrs := tc.input.Attributes().AsRaw()
919+
expectedSpan := ptrace.NewSpan()
920+
tc.input.CopyTo(expectedSpan)
921+
922+
// Merge with the expected attributes and override the span links.
835923
for k, v := range tc.enrichedAttrs {
836-
expectedAttrs[k] = v
924+
expectedSpan.Attributes().PutEmpty(k).FromRaw(v)
925+
}
926+
// Override span links
927+
if tc.expectedSpanLinks != nil {
928+
tc.expectedSpanLinks.CopyTo(expectedSpan.Links())
929+
} else {
930+
expectedSpan.Links().RemoveIf(func(_ ptrace.SpanLink) bool { return true })
837931
}
838932

839933
EnrichSpan(tc.input, config.Config{
840934
Span: tc.config,
841935
})
842-
843-
assert.Empty(t, cmp.Diff(expectedAttrs, tc.input.Attributes().AsRaw()))
936+
assert.NoError(t, ptracetest.CompareSpan(expectedSpan, tc.input))
844937
})
845938
}
846939
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.22.0
55
require (
66
github.com/google/go-cmp v0.6.0
77
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.112.0
8+
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.112.0
89
github.com/stretchr/testify v1.9.0
910
go.opentelemetry.io/collector/pdata v1.18.0
1011
go.opentelemetry.io/collector/semconv v0.112.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
2323
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
2424
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.112.0 h1:+jb8oibBLgnEvhWiMomtxEf4bshEDwtnmKYTM8bf96U=
2525
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.112.0/go.mod h1:G4KniRkewEl7JaT1EVTczTWi1nfYk2bD5GAn4aqBh4o=
26+
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.112.0 h1:WUAQfu+9bm7t86tyfqkcuz6vTCJfNAxMVocTZPLnWWs=
27+
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.112.0/go.mod h1:dQCrspUDJRs7P6pXRALwj/yKIMzTYCvLa7XlzNycVFY=
2628
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.112.0 h1:FIQ/vt0Ulnwr2PSkLSD0SfdSyfm9dmBBnBcjAbngC7o=
2729
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.112.0/go.mod h1:W9HkQWHB/Zc6adYHDG3FNyxfERt9eBAw2sBqNYBBBEE=
2830
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

0 commit comments

Comments
 (0)