Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions enrichments/trace/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type ElasticTransactionConfig struct {
Result AttributeConfig `mapstructure:"result"`
EventOutcome AttributeConfig `mapstructure:"event_outcome"`
InferredSpans AttributeConfig `mapstructure:"inferred_spans"`
UserAgent AttributeConfig `mapstructure:"user_agent"`
}

// ElasticSpanConfig configures the enrichment attributes for the spans
Expand All @@ -75,6 +76,7 @@ type ElasticSpanConfig struct {
ServiceTarget AttributeConfig `mapstructure:"service_target"`
DestinationService AttributeConfig `mapstructure:"destination_service"`
InferredSpans AttributeConfig `mapstructure:"inferred_spans"`
UserAgent AttributeConfig `mapstructure:"user_agent"`
}

// SpanEventConfig configures enrichment attributes for the span events.
Expand Down Expand Up @@ -124,6 +126,7 @@ func Enabled() Config {
EventOutcome: AttributeConfig{Enabled: true},
RepresentativeCount: AttributeConfig{Enabled: true},
InferredSpans: AttributeConfig{Enabled: true},
UserAgent: AttributeConfig{Enabled: true},
},
Span: ElasticSpanConfig{
TimestampUs: AttributeConfig{Enabled: true},
Expand All @@ -136,6 +139,7 @@ func Enabled() Config {
DestinationService: AttributeConfig{Enabled: true},
RepresentativeCount: AttributeConfig{Enabled: true},
InferredSpans: AttributeConfig{Enabled: true},
UserAgent: AttributeConfig{Enabled: true},
},
SpanEvent: SpanEventConfig{
TimestampUs: AttributeConfig{Enabled: true},
Expand Down
52 changes: 47 additions & 5 deletions enrichments/trace/internal/elastic/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (

"github.com/elastic/opentelemetry-lib/elasticattr"
"github.com/elastic/opentelemetry-lib/enrichments/trace/config"
"github.com/ua-parser/uap-go/uaparser"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/ptrace"
semconv25 "go.opentelemetry.io/collector/semconv/v1.25.0"
Expand All @@ -49,9 +50,13 @@ import (
// - Elastic spans, defined as all spans (including transactions).
// However, for the enrichment logic spans are treated as a separate
// entity i.e. all transactions are not enriched as spans and vice versa.
func EnrichSpan(span ptrace.Span, cfg config.Config) {
func EnrichSpan(
span ptrace.Span,
cfg config.Config,
userAgentParser *uaparser.Parser,
) {
var c spanEnrichmentContext
c.Enrich(span, cfg)
c.Enrich(span, cfg, userAgentParser)
}

type spanEnrichmentContext struct {
Expand All @@ -72,6 +77,13 @@ type spanEnrichmentContext struct {
messagingDestinationName string
genAiSystem string

// The inferred* attributes are derived from a base attribute
userAgentOriginal string
userAgentName string
userAgentVersion string
inferredUserAgentName string
inferredUserAgentVersion string

serverPort int64
urlPort int64
httpStatusCode int64
Expand All @@ -88,7 +100,11 @@ type spanEnrichmentContext struct {
isGenAi bool
}

func (s *spanEnrichmentContext) Enrich(span ptrace.Span, cfg config.Config) {
func (s *spanEnrichmentContext) Enrich(
span ptrace.Span,
cfg config.Config,
userAgentParser *uaparser.Parser,
) {
// Extract top level span information.
s.spanStatusCode = span.Status().Code()

Expand Down Expand Up @@ -178,11 +194,17 @@ func (s *spanEnrichmentContext) Enrich(span ptrace.Span, cfg config.Config) {
case semconv27.AttributeGenAiSystem:
s.isGenAi = true
s.genAiSystem = v.Str()
case semconv27.AttributeUserAgentOriginal:
s.userAgentOriginal = v.Str()
case semconv27.AttributeUserAgentName:
s.userAgentName = v.Str()
case semconv27.AttributeUserAgentVersion:
s.userAgentVersion = v.Str()
}
return true
})

s.normalizeAttributes()
s.normalizeAttributes(userAgentParser)
s.isTransaction = isElasticTransaction(span)
s.enrich(span, cfg)

Expand Down Expand Up @@ -249,6 +271,9 @@ func (s *spanEnrichmentContext) enrichTransaction(
if cfg.InferredSpans.Enabled {
s.setInferredSpans(span)
}
if cfg.UserAgent.Enabled {
s.setUserAgentIfRequired(span)
}
}

func (s *spanEnrichmentContext) enrichSpan(
Expand Down Expand Up @@ -290,6 +315,9 @@ func (s *spanEnrichmentContext) enrichSpan(
if cfg.ProcessorEvent.Enabled && !isExitRootSpan {
span.Attributes().PutStr(elasticattr.ProcessorEvent, "span")
}
if cfg.UserAgent.Enabled {
s.setUserAgentIfRequired(span)
}

if isExitRootSpan && transactionTypeEnabled {
if spanType != "" {
Expand All @@ -304,10 +332,15 @@ func (s *spanEnrichmentContext) enrichSpan(

// normalizeAttributes sets any dependent attributes that
// might not have been explicitly set as an attribute.
func (s *spanEnrichmentContext) normalizeAttributes() {
func (s *spanEnrichmentContext) normalizeAttributes(userAgentPraser *uaparser.Parser) {
if s.rpcSystem == "" && s.grpcStatus != "" {
s.rpcSystem = "grpc"
}
if s.userAgentOriginal != "" && userAgentPraser != nil {
ua := userAgentPraser.ParseUserAgent(s.userAgentOriginal)
s.inferredUserAgentName = ua.Family
s.inferredUserAgentVersion = ua.ToVersionString()
}
}

func (s *spanEnrichmentContext) getSampled() bool {
Expand Down Expand Up @@ -520,6 +553,15 @@ func (s *spanEnrichmentContext) setInferredSpans(span ptrace.Span) {
}
}

func (s *spanEnrichmentContext) setUserAgentIfRequired(span ptrace.Span) {
if s.userAgentName == "" && s.inferredUserAgentName != "" {
span.Attributes().PutStr(semconv27.AttributeUserAgentName, s.inferredUserAgentName)
}
if s.userAgentVersion == "" && s.inferredUserAgentVersion != "" {
span.Attributes().PutStr(semconv27.AttributeUserAgentVersion, s.inferredUserAgentVersion)
}
Comment on lines +557 to +562
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[For reviewers] I am setting the user_agent.{name, version} only if it is not already set.

}

type spanEventEnrichmentContext struct {
exceptionType string
exceptionMessage string
Expand Down
117 changes: 113 additions & 4 deletions enrichments/trace/internal/elastic/span_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/ptracetest"
"github.com/stretchr/testify/assert"
"github.com/ua-parser/uap-go/uaparser"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/ptrace"
semconv25 "go.opentelemetry.io/collector/semconv/v1.25.0"
Expand Down Expand Up @@ -364,6 +365,63 @@ func TestElasticTransactionEnrich(t *testing.T) {
return &spanLinks
}(),
},
{
name: "user_agent_parse_name_version",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd postfix these with something like _span and _transaction. So e.g. user_agent_parse_name_version_transaction here and user_agent_parse_name_version_span below, instead of having them with the same name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They would be reported as TestElastic{Transaction, Span}Enrich/user_agent_parse_name_version, is that not enough or is it for when looking at the tests individually without the context of the main test?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you are right. It's good enough. It just took me a few seconds to realize what's actually the difference is in these new tests with the same name. But yeah, this is ok - if the test fails, the output is clear.

Copy link
Contributor Author

@lahsivjar lahsivjar Mar 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I am thinking to simplify these tests with a method for default enrichments... currently the tests are getting a bit hard to read an unnecessarily verbose. Will do it later as a followup.

input: func() ptrace.Span {
span := getElasticTxn()
span.SetName("testtxn")
span.Attributes().PutStr(semconv27.AttributeUserAgentOriginal, "Mozilla/5.0 (X11; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0")
return span
}(),
config: config.Enabled().Transaction,
enrichedAttrs: map[string]any{
elasticattr.TimestampUs: startTs.AsTime().UnixMicro(),
elasticattr.TransactionSampled: true,
elasticattr.TransactionRoot: true,
elasticattr.TransactionID: "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",
semconv27.AttributeUserAgentName: "Firefox",
semconv27.AttributeUserAgentVersion: "126.0",
},
},
{
name: "user_agent_no_override",
input: func() ptrace.Span {
span := getElasticTxn()
span.SetName("testtxn")
span.Attributes().PutStr(semconv27.AttributeUserAgentOriginal, "Mozilla/5.0 (X11; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0")
// In practical situations the user_agent.{name, version} should be derived from the
// original user agent, however, for testing we are setting different values.
span.Attributes().PutStr(semconv27.AttributeUserAgentName, "Chrome")
span.Attributes().PutStr(semconv27.AttributeUserAgentVersion, "51.0.2704")
return span
}(),
config: config.Enabled().Transaction,
enrichedAttrs: map[string]any{
elasticattr.TimestampUs: startTs.AsTime().UnixMicro(),
elasticattr.TransactionSampled: true,
elasticattr.TransactionRoot: true,
elasticattr.TransactionID: "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",
// If user_agent.{name, version} are already set then don't override them.
semconv27.AttributeUserAgentName: "Chrome",
semconv27.AttributeUserAgentVersion: "51.0.2704",
},
},
} {
t.Run(tc.name, func(t *testing.T) {
expectedSpan := ptrace.NewSpan()
Expand All @@ -382,7 +440,7 @@ func TestElasticTransactionEnrich(t *testing.T) {

EnrichSpan(tc.input, config.Config{
Transaction: tc.config,
})
}, uaparser.NewFromSaved())
assert.NoError(t, ptracetest.CompareSpan(expectedSpan, tc.input))
})
}
Expand Down Expand Up @@ -528,7 +586,7 @@ func TestRootSpanAsDependencyEnrich(t *testing.T) {
expectedSpan.Links().RemoveIf(func(_ ptrace.SpanLink) bool { return true })
}

EnrichSpan(tc.input, tc.config)
EnrichSpan(tc.input, tc.config, uaparser.NewFromSaved())
assert.NoError(t, ptracetest.CompareSpan(expectedSpan, tc.input))
})
}
Expand Down Expand Up @@ -1111,6 +1169,57 @@ func TestElasticSpanEnrich(t *testing.T) {
elasticattr.SpanDestinationServiceResource: "myService",
},
},
{
name: "user_agent_parse_name_version",
input: func() ptrace.Span {
span := getElasticSpan()
span.SetName("testspan")
span.SetSpanID([8]byte{1})
span.Attributes().PutStr(semconv27.AttributeUserAgentOriginal, "Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1")
return span
}(),
config: config.Enabled().Span,
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),
semconv27.AttributeUserAgentName: "Mobile Safari",
semconv27.AttributeUserAgentVersion: "13.1.1",
},
},
{
name: "user_agent_no_override",
input: func() ptrace.Span {
span := getElasticSpan()
span.SetName("testspan")
span.SetSpanID([8]byte{1})
span.Attributes().PutStr(semconv27.AttributeUserAgentOriginal, "Mozilla/5.0 (X11; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0")
// In practical situations the user_agent.{name, version} should be derived from the
// original user agent, however, for testing we are setting different values.
span.Attributes().PutStr(semconv27.AttributeUserAgentName, "Chrome")
span.Attributes().PutStr(semconv27.AttributeUserAgentVersion, "51.0.2704")
return span
}(),
config: config.Enabled().Span,
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),
// If user_agent.{name, version} are already set then don't override them.
semconv27.AttributeUserAgentName: "Chrome",
semconv27.AttributeUserAgentVersion: "51.0.2704",
},
},
} {
t.Run(tc.name, func(t *testing.T) {
expectedSpan := ptrace.NewSpan()
Expand All @@ -1129,7 +1238,7 @@ func TestElasticSpanEnrich(t *testing.T) {

EnrichSpan(tc.input, config.Config{
Span: tc.config,
})
}, uaparser.NewFromSaved())
assert.NoError(t, ptracetest.CompareSpan(expectedSpan, tc.input))
})
}
Expand Down Expand Up @@ -1235,7 +1344,7 @@ func TestSpanEventEnrich(t *testing.T) {
tc.input.MoveTo(tc.parent.Events().AppendEmpty())
EnrichSpan(tc.parent, config.Config{
SpanEvent: tc.config,
})
}, uaparser.NewFromSaved())

actual := tc.parent.Events().At(0).Attributes()
errorID, ok := actual.Get(elasticattr.ErrorID)
Expand Down
10 changes: 8 additions & 2 deletions enrichments/trace/trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,25 @@ package trace
import (
"github.com/elastic/opentelemetry-lib/enrichments/trace/config"
"github.com/elastic/opentelemetry-lib/enrichments/trace/internal/elastic"
"github.com/ua-parser/uap-go/uaparser"
"go.opentelemetry.io/collector/pdata/ptrace"
)

// Enricher enriches the OTel traces with attributes required to power
// functionalities in the Elastic UI.
type Enricher struct {
// If there are more than one parser in the future we should consider
// abstracting the parsers in a separate internal package.
userAgentParser *uaparser.Parser

Config config.Config
}

// NewEnricher creates a new instance of Enricher.
func NewEnricher(cfg config.Config) *Enricher {
return &Enricher{
Config: cfg,
Config: cfg,
userAgentParser: uaparser.NewFromSaved(),
}
}

Expand All @@ -51,7 +57,7 @@ func (e *Enricher) Enrich(pt ptrace.Traces) {
elastic.EnrichScope(scopeSpan.Scope(), e.Config)
spans := scopeSpan.Spans()
for k := 0; k < spans.Len(); k++ {
elastic.EnrichSpan(spans.At(k), e.Config)
elastic.EnrichSpan(spans.At(k), e.Config, e.userAgentParser)
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.121.0
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.121.0
github.com/stretchr/testify v1.10.0
github.com/ua-parser/uap-go v0.0.0-20250213224047-9c035f085b90
go.opentelemetry.io/collector/component v1.27.0
go.opentelemetry.io/collector/component/componenttest v0.121.0
go.opentelemetry.io/collector/config/configcompression v1.27.0
Expand All @@ -35,6 +36,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect
Expand Down Expand Up @@ -66,5 +68,6 @@ require (
golang.org/x/text v0.22.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading
Loading