diff --git a/.chloggen/ottl-profilelocation.yaml b/.chloggen/ottl-profilelocation.yaml new file mode 100644 index 0000000000000..7de8c82488b83 --- /dev/null +++ b/.chloggen/ottl-profilelocation.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: pkg/ottl + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add OTTL support for location submessage of OTel Profiling signal. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [40163] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [api] diff --git a/pkg/ottl/contexts/internal/ctxprofilelocation/context.go b/pkg/ottl/contexts/internal/ctxprofilelocation/context.go new file mode 100644 index 0000000000000..b8a6506445ea6 --- /dev/null +++ b/pkg/ottl/contexts/internal/ctxprofilelocation/context.go @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ctxprofilelocation // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/ctxprofilelocation" + +import "go.opentelemetry.io/collector/pdata/pprofile" + +const ( + Name = "profilelocation" + DocRef = "https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/ottl/contexts/ottlprofilelocation" +) + +type Context interface { + GetProfileLocation() pprofile.Location + GetProfilesDictionary() pprofile.ProfilesDictionary +} diff --git a/pkg/ottl/contexts/internal/ctxprofilelocation/profilelocation.go b/pkg/ottl/contexts/internal/ctxprofilelocation/profilelocation.go new file mode 100644 index 0000000000000..ed5b8faabf325 --- /dev/null +++ b/pkg/ottl/contexts/internal/ctxprofilelocation/profilelocation.go @@ -0,0 +1,167 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ctxprofilelocation // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/ctxprofilelocation" + +import ( + "context" + "errors" + "math" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pprofile" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/ctxerror" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/ctxutil" +) + +var ( + errMaxValueExceed = errors.New("exceeded max value") + errInvalidValueType = errors.New("invalid value type") +) + +func PathGetSetter[K Context](path ottl.Path[K]) (ottl.GetSetter[K], error) { + if path == nil { + return nil, ctxerror.New("nil", "nil", Name, DocRef) + } + switch path.Name() { + case "mapping_index": + return accessMappingIndex[K](), nil + case "address": + return accessAddress[K](), nil + case "line": + return accessLine[K](), nil + case "attribute_indices": + return accessAttributeIndices[K](), nil + case "attributes": + if path.Keys() == nil { + return accessAttributes[K](), nil + } + return accessAttributesKey(path.Keys()), nil + default: + return nil, ctxerror.New(path.Name(), path.String(), Name, DocRef) + } +} + +func accessMappingIndex[K Context]() ottl.StandardGetSetter[K] { + return ottl.StandardGetSetter[K]{ + Getter: func(_ context.Context, tCtx K) (any, error) { + return int64(tCtx.GetProfileLocation().MappingIndex()), nil + }, + Setter: func(_ context.Context, tCtx K, val any) error { + if v, ok := val.(int64); ok { + if v >= math.MaxInt32 { + return errMaxValueExceed + } + tCtx.GetProfileLocation().SetMappingIndex(int32(v)) + return nil + } + return errInvalidValueType + }, + } +} + +func accessAddress[K Context]() ottl.StandardGetSetter[K] { + return ottl.StandardGetSetter[K]{ + Getter: func(_ context.Context, tCtx K) (any, error) { + return tCtx.GetProfileLocation().Address(), nil + }, + Setter: func(_ context.Context, tCtx K, val any) error { + if v, ok := val.(uint64); ok { + tCtx.GetProfileLocation().SetAddress(v) + return nil + } + return errInvalidValueType + }, + } +} + +func accessLine[K Context]() ottl.StandardGetSetter[K] { + return ottl.StandardGetSetter[K]{ + Getter: func(_ context.Context, tCtx K) (any, error) { + return tCtx.GetProfileLocation().Line(), nil + }, + Setter: func(_ context.Context, tCtx K, val any) error { + lines, ok := val.(pprofile.LineSlice) + if !ok { + return errInvalidValueType + } + tCtx.GetProfileLocation().Line().RemoveIf(func(_ pprofile.Line) bool { return true }) + for _, line := range lines.All() { + newLine := tCtx.GetProfileLocation().Line().AppendEmpty() + line.CopyTo(newLine) + } + return nil + }, + } +} + +func accessAttributeIndices[K Context]() ottl.StandardGetSetter[K] { + return ottl.StandardGetSetter[K]{ + Getter: func(_ context.Context, tCtx K) (any, error) { + return ctxutil.GetCommonIntSliceValues[int32](tCtx.GetProfileLocation().AttributeIndices()), nil + }, + Setter: func(_ context.Context, tCtx K, val any) error { + return ctxutil.SetCommonIntSliceValues[int32](tCtx.GetProfileLocation().AttributeIndices(), val) + }, + } +} + +func accessAttributes[K Context]() ottl.StandardGetSetter[K] { + return ottl.StandardGetSetter[K]{ + Getter: func(_ context.Context, tCtx K) (any, error) { + return pprofile.FromAttributeIndices(tCtx.GetProfilesDictionary().AttributeTable(), tCtx.GetProfileLocation()), nil + }, + Setter: func(_ context.Context, tCtx K, val any) error { + m, err := ctxutil.GetMap(val) + if err != nil { + return err + } + tCtx.GetProfileLocation().AttributeIndices().FromRaw([]int32{}) + for k, v := range m.All() { + if err := pprofile.PutAttribute(tCtx.GetProfilesDictionary().AttributeTable(), tCtx.GetProfileLocation(), k, v); err != nil { + return err + } + } + return nil + }, + } +} + +func accessAttributesKey[K Context](key []ottl.Key[K]) ottl.StandardGetSetter[K] { + return ottl.StandardGetSetter[K]{ + Getter: func(ctx context.Context, tCtx K) (any, error) { + return ctxutil.GetMapValue[K](ctx, tCtx, pprofile.FromAttributeIndices(tCtx.GetProfilesDictionary().AttributeTable(), tCtx.GetProfileLocation()), key) + }, + Setter: func(ctx context.Context, tCtx K, val any) error { + newKey, err := ctxutil.GetMapKeyName(ctx, tCtx, key[0]) + if err != nil { + return err + } + v := getAttributeValue(tCtx, *newKey) + if err := ctxutil.SetIndexableValue[K](ctx, tCtx, v, val, key[1:]); err != nil { + return err + } + return pprofile.PutAttribute(tCtx.GetProfilesDictionary().AttributeTable(), tCtx.GetProfileLocation(), *newKey, v) + }, + } +} + +func getAttributeValue[K Context](tCtx K, key string) pcommon.Value { + // Find the index of the attribute in the profile's attribute indices + // and return the corresponding value from the attribute table. + table := tCtx.GetProfilesDictionary().AttributeTable() + indices := tCtx.GetProfileLocation().AttributeIndices().AsRaw() + + for _, tableIndex := range indices { + attr := table.At(int(tableIndex)) + if attr.Key() == key { + v := pcommon.NewValueEmpty() + attr.Value().CopyTo(v) + return v + } + } + + return pcommon.NewValueEmpty() +} diff --git a/pkg/ottl/contexts/internal/ctxprofilelocation/profilelocation_test.go b/pkg/ottl/contexts/internal/ctxprofilelocation/profilelocation_test.go new file mode 100644 index 0000000000000..a26c80f5f5bf5 --- /dev/null +++ b/pkg/ottl/contexts/internal/ctxprofilelocation/profilelocation_test.go @@ -0,0 +1,90 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ctxprofilelocation // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/ctxprofilelocation" + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/pprofile" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/pathtest" +) + +func TestPathGetSetter(t *testing.T) { + lineSlice := pprofile.NewLineSlice() + for _, lineValue := range []int64{73, 74, 75} { + line := lineSlice.AppendEmpty() + line.SetLine(lineValue) + } + tests := []struct { + path string + val any + keys []ottl.Key[*profileLocationContext] + }{ + { + path: "mapping_index", + val: int64(42), + }, + { + path: "address", + val: uint64(43), + }, + { + path: "line", + val: lineSlice, + }, + { + path: "attribute_indices", + val: []int64{97, 98, 99}, + }, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + pathParts := strings.Split(tt.path, " ") + path := &pathtest.Path[*profileLocationContext]{N: pathParts[0]} + if tt.keys != nil { + path.KeySlice = tt.keys + } + if len(pathParts) > 1 { + path.NextPath = &pathtest.Path[*profileLocationContext]{N: pathParts[1]} + } + + location := pprofile.NewLocation() + dictionary := pprofile.NewProfilesDictionary() + + accessor, err := PathGetSetter(path) + require.NoError(t, err) + + err = accessor.Set(t.Context(), newProfileLocationContext(location, dictionary), tt.val) + require.NoError(t, err) + + got, err := accessor.Get(t.Context(), newProfileLocationContext(location, dictionary)) + require.NoError(t, err) + + assert.Equal(t, tt.val, got) + }) + } +} + +type profileLocationContext struct { + location pprofile.Location + dictionary pprofile.ProfilesDictionary +} + +func (p *profileLocationContext) GetProfilesDictionary() pprofile.ProfilesDictionary { + return p.dictionary +} + +func (p *profileLocationContext) GetProfileLocation() pprofile.Location { + return p.location +} + +func newProfileLocationContext(location pprofile.Location, dictionary pprofile.ProfilesDictionary) *profileLocationContext { + return &profileLocationContext{location: location, dictionary: dictionary} +} diff --git a/pkg/ottl/contexts/internal/logprofile/logging.go b/pkg/ottl/contexts/internal/logprofile/logging.go index 2b910c773e4f2..7b1e13b5ec9ba 100644 --- a/pkg/ottl/contexts/internal/logprofile/logging.go +++ b/pkg/ottl/contexts/internal/logprofile/logging.go @@ -161,6 +161,33 @@ func (s ProfileSample) MarshalLogObject(encoder zapcore.ObjectEncoder) error { return joinedErr } +type ProfileLocation struct { + pprofile.Location + Dictionary pprofile.ProfilesDictionary +} + +func (loc ProfileLocation) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + var joinedErr error + + encoder.AddInt32("mapping_index", loc.MappingIndex()) + encoder.AddUint64("address", loc.Address()) + + ls := make(lines, 0, loc.Line().Len()) + lines := loc.Line().All() + for _, line := range lines { + l, err := newLine(loc.Dictionary, line) + joinedErr = errors.Join(joinedErr, err) + ls = append(ls, l) + } + joinedErr = errors.Join(joinedErr, encoder.AddArray("line", ls)) + + ats, err := newAttributes(loc.Dictionary, loc.AttributeIndices()) + joinedErr = errors.Join(joinedErr, err) + joinedErr = errors.Join(joinedErr, encoder.AddArray("attributes", ats)) + + return joinedErr +} + type valueTypes []valueType func (s valueTypes) MarshalLogArray(encoder zapcore.ArrayEncoder) error { diff --git a/pkg/ottl/contexts/ottlprofilelocation/README.md b/pkg/ottl/contexts/ottlprofilelocation/README.md new file mode 100644 index 0000000000000..7c3ab612651be --- /dev/null +++ b/pkg/ottl/contexts/ottlprofilelocation/README.md @@ -0,0 +1,30 @@ +# Profile Location Context + +> [!NOTE] +> This documentation applies only to version `0.133.0` and later. Information on earlier versions is not available. + +The Profile Location Context is a Context implementation for [pdata Profiles](https://github.com/open-telemetry/opentelemetry-collector/tree/main/pdata/pprofile), the collector's internal representation for OTLP profile data. This Context should be used when interacted with OTLP profiles. + +## Paths +In general, the Profile Location Context supports accessing pdata using the field names from the [profiles proto](https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/profiles/v1development/profiles.proto). All integers are returned and set via `int64`. All doubles are returned and set via `float64`. + +The following paths are supported. + +| path | field accessed | type | +|------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------| +| cache | the value of the current transform context's temporary cache. cache can be used as a temporary placeholder for data during complex transformations | pcommon.Map | +| cache\[""\] | the value of an item in cache. Supports multiple indexes to access nested fields | string, bool, int64, float64, pcommon.Map, pcommon.Slice, []byte or nil | +| resource | resource of the profile being processed | pcommon.Resource | +| resource.attributes | resource attributes of the profile being processed | pcommon.Map | +| resource.attributes\[""\] | the value of the resource attribute of the profile being processed. Supports multiple indexes to access nested fields | string, bool, int64, float64, pcommon.Map, pcommon.Slice, []byte or nil | +| instrumentation_scope | instrumentation scope of the profile being processed | pcommon.InstrumentationScope | +| instrumentation_scope.name | name of the instrumentation scope of the profile being processed | string | +| instrumentation_scope.version | version of the instrumentation scope of the profile being processed | string | +| instrumentation_scope.attributes | instrumentation scope attributes of the data point being processed | pcommon.Map | +| instrumentation_scope.attributes\[""\] | the value of the instrumentation scope attribute of the data point being processed. Supports multiple indexes to access nested fields | string, bool, int64, float64, pcommon.Map, pcommon.Slice, []byte or nil | +| profilelocation.attributes | attributes of the profile being processed | pcommon.Map | +| profilelocation.attributes\[""\] | the value of the attribute of the profile being processed. Supports multiple indexes to access nested fields | string, bool, int64, float64, pcommon.Map, pcommon.Slice, []byte or nil | +| profilelocation.attribute_indices | the attribute indices of the location being processed | []int64 | +| profilelocation.mapping_index | reference to mapping in ProfilesDictionary.mapping_table | int64 | +| profilelocation.address | the instruction address for this location, if available | int64 | +| profilelocation.line | reference to one or more lines in source code | profile.LineSlice | diff --git a/pkg/ottl/contexts/ottlprofilelocation/package_test.go b/pkg/ottl/contexts/ottlprofilelocation/package_test.go new file mode 100644 index 0000000000000..533416aedd8d1 --- /dev/null +++ b/pkg/ottl/contexts/ottlprofilelocation/package_test.go @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlprofilelocation + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/pkg/ottl/contexts/ottlprofilelocation/profilelocation.go b/pkg/ottl/contexts/ottlprofilelocation/profilelocation.go new file mode 100644 index 0000000000000..457d10e6b946d --- /dev/null +++ b/pkg/ottl/contexts/ottlprofilelocation/profilelocation.go @@ -0,0 +1,193 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlprofilelocation // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/ottlprofilelocation" + +import ( + "errors" + "fmt" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pprofile" + "go.uber.org/zap/zapcore" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/ctxcache" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/ctxcommon" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/ctxprofilelocation" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/ctxresource" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/ctxscope" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/logging" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/logprofile" +) + +// ContextName is the name of the context for profiles. +// Experimental: *NOTE* this constant is subject to change or removal in the future. +const ContextName = ctxprofilelocation.Name + +var ( + _ ctxresource.Context = TransformContext{} + _ ctxscope.Context = TransformContext{} + _ ctxprofilelocation.Context = TransformContext{} + _ zapcore.ObjectMarshaler = TransformContext{} +) + +// TransformContext represents a profile and its associated hierarchy. +type TransformContext struct { + location pprofile.Location + dictionary pprofile.ProfilesDictionary + instrumentationScope pcommon.InstrumentationScope + resource pcommon.Resource + cache pcommon.Map + scopeProfiles pprofile.ScopeProfiles + resourceProfiles pprofile.ResourceProfiles +} + +// MarshalLogObject serializes the profile into a zapcore.ObjectEncoder for logging. +func (tCtx TransformContext) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + err := encoder.AddObject("resource", logging.Resource(tCtx.resource)) + err = errors.Join(err, encoder.AddObject("scope", logging.InstrumentationScope(tCtx.instrumentationScope))) + err = errors.Join(err, encoder.AddObject("location", logprofile.ProfileLocation{Location: tCtx.location, Dictionary: tCtx.dictionary})) + err = errors.Join(err, encoder.AddObject("cache", logging.Map(tCtx.cache))) + return err +} + +// TransformContextOption represents an option for configuring a TransformContext. +type TransformContextOption func(*TransformContext) + +// NewTransformContext creates a new TransformContext with the provided parameters. +func NewTransformContext(location pprofile.Location, dictionary pprofile.ProfilesDictionary, instrumentationScope pcommon.InstrumentationScope, resource pcommon.Resource, scopeProfiles pprofile.ScopeProfiles, resourceProfiles pprofile.ResourceProfiles, options ...TransformContextOption) TransformContext { + tc := TransformContext{ + location: location, + dictionary: dictionary, + instrumentationScope: instrumentationScope, + resource: resource, + cache: pcommon.NewMap(), + scopeProfiles: scopeProfiles, + resourceProfiles: resourceProfiles, + } + for _, opt := range options { + opt(&tc) + } + return tc +} + +// GetProfileLocation returns the location from the TransformContext. +func (tCtx TransformContext) GetProfileLocation() pprofile.Location { + return tCtx.location +} + +// GetProfilesDictionary returns the profiles dictionary from the TransformContext. +func (tCtx TransformContext) GetProfilesDictionary() pprofile.ProfilesDictionary { + return tCtx.dictionary +} + +// GetInstrumentationScope returns the instrumentation scope from the TransformContext. +func (tCtx TransformContext) GetInstrumentationScope() pcommon.InstrumentationScope { + return tCtx.instrumentationScope +} + +// GetResource returns the resource from the TransformContext. +func (tCtx TransformContext) GetResource() pcommon.Resource { + return tCtx.resource +} + +// GetScopeSchemaURLItem returns the scope schema URL item from the TransformContext. +func (tCtx TransformContext) GetScopeSchemaURLItem() ctxcommon.SchemaURLItem { + return tCtx.scopeProfiles +} + +// GetResourceSchemaURLItem returns the resource schema URL item from the TransformContext. +func (tCtx TransformContext) GetResourceSchemaURLItem() ctxcommon.SchemaURLItem { + return tCtx.resourceProfiles +} + +// NewParser creates a new profile parser with the provided functions and options. +func NewParser(functions map[string]ottl.Factory[TransformContext], telemetrySettings component.TelemetrySettings, options ...ottl.Option[TransformContext]) (ottl.Parser[TransformContext], error) { + return ctxcommon.NewParser( + functions, + telemetrySettings, + pathExpressionParser(getCache), + parseEnum, + options..., + ) +} + +// EnablePathContextNames enables the support for path's context names on statements. +// When this option is configured, all statement's paths must have a valid context prefix, +// otherwise an error is reported. +// +// Experimental: *NOTE* this option is subject to change or removal in the future. +func EnablePathContextNames() ottl.Option[TransformContext] { + return func(p *ottl.Parser[TransformContext]) { + ottl.WithPathContextNames[TransformContext]([]string{ + ctxprofilelocation.Name, + ctxscope.LegacyName, + ctxscope.Name, + ctxresource.Name, + })(p) + } +} + +// StatementSequenceOption represents an option for configuring a statement sequence. +type StatementSequenceOption func(*ottl.StatementSequence[TransformContext]) + +// WithStatementSequenceErrorMode sets the error mode for a statement sequence. +func WithStatementSequenceErrorMode(errorMode ottl.ErrorMode) StatementSequenceOption { + return func(s *ottl.StatementSequence[TransformContext]) { + ottl.WithStatementSequenceErrorMode[TransformContext](errorMode)(s) + } +} + +// NewStatementSequence creates a new statement sequence with the provided statements and options. +func NewStatementSequence(statements []*ottl.Statement[TransformContext], telemetrySettings component.TelemetrySettings, options ...StatementSequenceOption) ottl.StatementSequence[TransformContext] { + s := ottl.NewStatementSequence(statements, telemetrySettings) + for _, op := range options { + op(&s) + } + return s +} + +// ConditionSequenceOption represents an option for configuring a condition sequence. +type ConditionSequenceOption func(*ottl.ConditionSequence[TransformContext]) + +// WithConditionSequenceErrorMode sets the error mode for a condition sequence. +func WithConditionSequenceErrorMode(errorMode ottl.ErrorMode) ConditionSequenceOption { + return func(c *ottl.ConditionSequence[TransformContext]) { + ottl.WithConditionSequenceErrorMode[TransformContext](errorMode)(c) + } +} + +// NewConditionSequence creates a new condition sequence with the provided conditions and options. +func NewConditionSequence(conditions []*ottl.Condition[TransformContext], telemetrySettings component.TelemetrySettings, options ...ConditionSequenceOption) ottl.ConditionSequence[TransformContext] { + c := ottl.NewConditionSequence(conditions, telemetrySettings) + for _, op := range options { + op(&c) + } + return c +} + +func parseEnum(val *ottl.EnumSymbol) (*ottl.Enum, error) { + if val != nil { + return nil, fmt.Errorf("enum symbol, %s, not found", *val) + } + return nil, errors.New("enum symbol not provided") +} + +func getCache(tCtx TransformContext) pcommon.Map { + return tCtx.cache +} + +func pathExpressionParser(cacheGetter ctxcache.Getter[TransformContext]) ottl.PathExpressionParser[TransformContext] { + return ctxcommon.PathExpressionParser( + ctxprofilelocation.Name, + ctxprofilelocation.DocRef, + cacheGetter, + map[string]ottl.PathExpressionParser[TransformContext]{ + ctxresource.Name: ctxresource.PathGetSetter[TransformContext], + ctxscope.Name: ctxscope.PathGetSetter[TransformContext], + ctxscope.LegacyName: ctxscope.PathGetSetter[TransformContext], + ctxprofilelocation.Name: ctxprofilelocation.PathGetSetter[TransformContext], + }) +} diff --git a/pkg/ottl/contexts/ottlprofilelocation/profilelocation_test.go b/pkg/ottl/contexts/ottlprofilelocation/profilelocation_test.go new file mode 100644 index 0000000000000..0c52552468cc2 --- /dev/null +++ b/pkg/ottl/contexts/ottlprofilelocation/profilelocation_test.go @@ -0,0 +1,188 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlprofilelocation // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/ottlprofilelocation" + +import ( + "slices" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pprofile" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/ctxprofilelocation" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/pathtest" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottltest" +) + +func Test_newPathGetSetter_Cache(t *testing.T) { + newCache := pcommon.NewMap() + newCache.PutStr("temp", "value") + + tests := []struct { + name string + path ottl.Path[TransformContext] + orig any + newVal any + modified func(loc pprofile.Location, cache pcommon.Map) + }{ + { + name: "address", + path: &pathtest.Path[TransformContext]{ + N: "address", + }, + orig: uint64(42), + newVal: uint64(43), + modified: func(loc pprofile.Location, _ pcommon.Map) { + loc.SetAddress(43) + }, + }, + { + name: "cache", + path: &pathtest.Path[TransformContext]{ + N: "cache", + }, + orig: pcommon.NewMap(), + newVal: newCache, + modified: func(_ pprofile.Location, cache pcommon.Map) { + newCache.CopyTo(cache) + }, + }, + { + name: "cache access", + path: &pathtest.Path[TransformContext]{ + N: "cache", + KeySlice: []ottl.Key[TransformContext]{ + &pathtest.Key[TransformContext]{ + S: ottltest.Strp("temp"), + }, + }, + }, + orig: nil, + newVal: "new value", + modified: func(_ pprofile.Location, cache pcommon.Map) { + cache.PutStr("temp", "new value") + }, + }, + } + // Copy all tests cases and sets the path.Context value to the generated ones. + // It ensures all exiting field access also work when the path context is set. + for _, tt := range slices.Clone(tests) { + testWithContext := tt + testWithContext.name = "with_path_context:" + tt.name + pathWithContext := *tt.path.(*pathtest.Path[TransformContext]) + pathWithContext.C = ctxprofilelocation.Name + testWithContext.path = ottl.Path[TransformContext](&pathWithContext) + tests = append(tests, testWithContext) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testCache := pcommon.NewMap() + cacheGetter := func(_ TransformContext) pcommon.Map { + return testCache + } + accessor, err := pathExpressionParser(cacheGetter)(tt.path) + assert.NoError(t, err) + + profileLocation := createProfileLocationTelemetry() + + tCtx := NewTransformContext(profileLocation, pprofile.NewProfilesDictionary(), pcommon.NewInstrumentationScope(), pcommon.NewResource(), pprofile.NewScopeProfiles(), pprofile.NewResourceProfiles()) + got, err := accessor.Get(t.Context(), tCtx) + assert.NoError(t, err) + assert.Equal(t, tt.orig, got) + + tCtx = NewTransformContext(profileLocation, pprofile.NewProfilesDictionary(), pcommon.NewInstrumentationScope(), pcommon.NewResource(), pprofile.NewScopeProfiles(), pprofile.NewResourceProfiles()) + err = accessor.Set(t.Context(), tCtx, tt.newVal) + assert.NoError(t, err) + + exProfileLocation := createProfileLocationTelemetry() + exCache := pcommon.NewMap() + tt.modified(exProfileLocation, exCache) + + assert.Equal(t, exProfileLocation, profileLocation) + assert.Equal(t, exCache, testCache) + }) + } +} + +func Test_newPathGetSetter_higherContextPath(t *testing.T) { + resource := pcommon.NewResource() + resource.Attributes().PutStr("foo", "bar") + + instrumentationScope := pcommon.NewInstrumentationScope() + instrumentationScope.SetName("instrumentation_scope") + + ctx := NewTransformContext(pprofile.NewLocation(), pprofile.NewProfilesDictionary(), instrumentationScope, resource, pprofile.NewScopeProfiles(), pprofile.NewResourceProfiles()) + + tests := []struct { + name string + path ottl.Path[TransformContext] + expected any + }{ + { + name: "resource", + path: &pathtest.Path[TransformContext]{N: "resource", NextPath: &pathtest.Path[TransformContext]{ + N: "attributes", + KeySlice: []ottl.Key[TransformContext]{ + &pathtest.Key[TransformContext]{ + S: ottltest.Strp("foo"), + }, + }, + }}, + expected: "bar", + }, + { + name: "resource with context", + path: &pathtest.Path[TransformContext]{C: "resource", N: "attributes", KeySlice: []ottl.Key[TransformContext]{ + &pathtest.Key[TransformContext]{ + S: ottltest.Strp("foo"), + }, + }}, + expected: "bar", + }, + { + name: "instrumentation_scope", + path: &pathtest.Path[TransformContext]{N: "instrumentation_scope", NextPath: &pathtest.Path[TransformContext]{N: "name"}}, + expected: instrumentationScope.Name(), + }, + { + name: "instrumentation_scope with context", + path: &pathtest.Path[TransformContext]{C: "instrumentation_scope", N: "name"}, + expected: instrumentationScope.Name(), + }, + { + name: "scope", + path: &pathtest.Path[TransformContext]{N: "scope", NextPath: &pathtest.Path[TransformContext]{N: "name"}}, + expected: instrumentationScope.Name(), + }, + { + name: "scope with context", + path: &pathtest.Path[TransformContext]{C: "scope", N: "name"}, + expected: instrumentationScope.Name(), + }, + } + + testCache := pcommon.NewMap() + cacheGetter := func(_ TransformContext) pcommon.Map { + return testCache + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + accessor, err := pathExpressionParser(cacheGetter)(tt.path) + assert.NoError(t, err) + + got, err := accessor.Get(t.Context(), ctx) + assert.NoError(t, err) + assert.Equal(t, tt.expected, got) + }) + } +} + +func createProfileLocationTelemetry() pprofile.Location { + loc := pprofile.NewLocation() + loc.SetAddress(42) + return loc +}