diff --git a/.chloggen/profile-attributes-accessor.yaml b/.chloggen/profile-attributes-accessor.yaml new file mode 100644 index 0000000000000..ebb90593ba0f4 --- /dev/null +++ b/.chloggen/profile-attributes-accessor.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 accessors for profile attributes + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [39681] + +# (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: [user] diff --git a/pkg/ottl/contexts/internal/ctxprofile/context.go b/pkg/ottl/contexts/internal/ctxprofile/context.go index 619f5cccc5641..806f9ce8d422c 100644 --- a/pkg/ottl/contexts/internal/ctxprofile/context.go +++ b/pkg/ottl/contexts/internal/ctxprofile/context.go @@ -13,4 +13,5 @@ const ( type Context interface { GetProfile() pprofile.Profile + GetProfilesDictionary() pprofile.ProfilesDictionary } diff --git a/pkg/ottl/contexts/internal/ctxprofile/profile.go b/pkg/ottl/contexts/internal/ctxprofile/profile.go index 2262403f762e2..aa184b59dc8bd 100644 --- a/pkg/ottl/contexts/internal/ctxprofile/profile.go +++ b/pkg/ottl/contexts/internal/ctxprofile/profile.go @@ -20,6 +20,7 @@ import ( type ProfileContext interface { GetProfile() pprofile.Profile + GetProfilesDictionary() pprofile.ProfilesDictionary } func PathGetSetter[K ProfileContext](path ottl.Path[K]) (ottl.GetSetter[K], error) { @@ -66,6 +67,11 @@ func PathGetSetter[K ProfileContext](path ottl.Path[K]) (ottl.GetSetter[K], erro return accessOriginalPayloadFormat[K](), nil case "original_payload": return accessOriginalPayload[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) } @@ -310,3 +316,43 @@ func accessOriginalPayload[K ProfileContext]() ottl.StandardGetSetter[K] { }, } } + +func accessAttributes[K ProfileContext]() ottl.StandardGetSetter[K] { + return ottl.StandardGetSetter[K]{ + Getter: func(_ context.Context, tCtx K) (any, error) { + return pprofile.FromAttributeIndices(tCtx.GetProfilesDictionary().AttributeTable(), tCtx.GetProfile()), nil + }, + Setter: func(_ context.Context, tCtx K, val any) error { + m, err := ctxutil.GetMap(val) + if err != nil { + return err + } + tCtx.GetProfile().AttributeIndices().FromRaw([]int32{}) + for k, v := range m.All() { + if err := pprofile.PutAttribute(tCtx.GetProfilesDictionary().AttributeTable(), tCtx.GetProfile(), 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.GetProfile()), 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 := pcommon.NewValueEmpty() + if err = ctxutil.SetIndexableValue[K](ctx, tCtx, v, val, key[1:]); err != nil { + return err + } + return pprofile.PutAttribute(tCtx.GetProfilesDictionary().AttributeTable(), tCtx.GetProfile(), *newKey, v) + }, + } +} diff --git a/pkg/ottl/contexts/internal/ctxprofile/profile_test.go b/pkg/ottl/contexts/internal/ctxprofile/profile_test.go index a6bf7c1d322f1..d463185504484 100644 --- a/pkg/ottl/contexts/internal/ctxprofile/profile_test.go +++ b/pkg/ottl/contexts/internal/ctxprofile/profile_test.go @@ -11,9 +11,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "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/pathtest" + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottltest" ) func TestPathGetSetter(t *testing.T) { @@ -21,6 +24,7 @@ func TestPathGetSetter(t *testing.T) { tests := []struct { path string val any + keys []ottl.Key[*profileContext] setFails bool }{ { @@ -109,29 +113,78 @@ func TestPathGetSetter(t *testing.T) { path: "original_payload", val: []byte{1, 2, 3}, }, + { + path: "attributes", + val: func() pcommon.Map { + m := pcommon.NewMap() + m.PutStr("akey", "val") + return m + }(), + }, + { + path: "attributes", + keys: []ottl.Key[*profileContext]{ + &pathtest.Key[*profileContext]{ + S: ottltest.Strp("akey"), + }, + }, + val: "val", + }, + { + path: "attributes", + keys: []ottl.Key[*profileContext]{ + &pathtest.Key[*profileContext]{ + S: ottltest.Strp("akey"), + }, + &pathtest.Key[*profileContext]{ + S: ottltest.Strp("bkey"), + }, + }, + val: "val", + }, + { + path: "attributes", + keys: []ottl.Key[*profileContext]{ + &pathtest.Key[*profileContext]{ + G: &ottl.StandardGetSetter[*profileContext]{ + Getter: func(context.Context, *profileContext) (any, error) { + return "", nil + }, + Setter: func(context.Context, *profileContext, any) error { + return nil + }, + }, + }, + }, + val: "val", + }, } for _, tt := range tests { t.Run(tt.path, func(t *testing.T) { pathParts := strings.Split(tt.path, " ") path := &pathtest.Path[*profileContext]{N: pathParts[0]} + if tt.keys != nil { + path.KeySlice = tt.keys + } if len(pathParts) > 1 { path.NextPath = &pathtest.Path[*profileContext]{N: pathParts[1]} } profile := pprofile.NewProfile() + dictionary := pprofile.NewProfilesDictionary() - accessor, err := PathGetSetter[*profileContext](path) + accessor, err := PathGetSetter(path) require.NoError(t, err) - err = accessor.Set(context.Background(), newProfileContext(profile), tt.val) + err = accessor.Set(context.Background(), newProfileContext(profile, dictionary), tt.val) if tt.setFails { require.Error(t, err) return } require.NoError(t, err) - got, err := accessor.Get(context.Background(), newProfileContext(profile)) + got, err := accessor.Get(context.Background(), newProfileContext(profile, dictionary)) require.NoError(t, err) assert.Equal(t, tt.val, got) @@ -140,15 +193,20 @@ func TestPathGetSetter(t *testing.T) { } type profileContext struct { - profile pprofile.Profile + profile pprofile.Profile + dictionary pprofile.ProfilesDictionary +} + +func (p *profileContext) GetProfilesDictionary() pprofile.ProfilesDictionary { + return p.dictionary } func (p *profileContext) GetProfile() pprofile.Profile { return p.profile } -func newProfileContext(profile pprofile.Profile) *profileContext { - return &profileContext{profile: profile} +func newProfileContext(profile pprofile.Profile, dictionary pprofile.ProfilesDictionary) *profileContext { + return &profileContext{profile: profile, dictionary: dictionary} } func createValueTypeSlice() pprofile.ValueTypeSlice { diff --git a/pkg/ottl/contexts/ottlprofile/README.md b/pkg/ottl/contexts/ottlprofile/README.md index 05495adc9fd33..2c65213b02ee7 100644 --- a/pkg/ottl/contexts/ottlprofile/README.md +++ b/pkg/ottl/contexts/ottlprofile/README.md @@ -22,6 +22,8 @@ The following paths are supported. | 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 | +| profile.attributes | attributes of the profile being processed | pcommon.Map | +| profile.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 | | profile.sample_type | the sample types of the profile being processed | pprofile.ValueTypeSlice | | profile.sample | the samples of the profile being processed | pprofile.SampleSlice | | profile.location_indices | the location indices of the profile being processed | []int64 | diff --git a/pkg/ottl/contexts/ottlprofile/profile.go b/pkg/ottl/contexts/ottlprofile/profile.go index 32e54a15516b3..4feee5d87d7cb 100644 --- a/pkg/ottl/contexts/ottlprofile/profile.go +++ b/pkg/ottl/contexts/ottlprofile/profile.go @@ -88,6 +88,11 @@ func (tCtx TransformContext) GetProfile() pprofile.Profile { return tCtx.profile } +// 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