From 0bf1a757072468434af929ed6c314e016e9a42e6 Mon Sep 17 00:00:00 2001 From: odubajDT <93584209+odubajDT@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:12:31 +0200 Subject: [PATCH] [pkg/ottl]: Add ByteSliceLikeGetter interface (#33536) **Description:** Adds ByteSliceLikeGetter interface due to future support of Hex() converter **Link to tracking Issue:** #31929 **Testing:** **Documentation:** --------- Signed-off-by: odubajDT --- .chloggen/add-byteslicelikegetter.yaml | 27 ++++ pkg/ottl/LANGUAGE.md | 1 + pkg/ottl/expression.go | 77 ++++++++++ pkg/ottl/expression_test.go | 199 +++++++++++++++++++++++++ pkg/ottl/functions.go | 6 + pkg/ottl/functions_test.go | 29 ++++ 6 files changed, 339 insertions(+) create mode 100644 .chloggen/add-byteslicelikegetter.yaml diff --git a/.chloggen/add-byteslicelikegetter.yaml b/.chloggen/add-byteslicelikegetter.yaml new file mode 100644 index 000000000000..4c3dcbfb9193 --- /dev/null +++ b/.chloggen/add-byteslicelikegetter.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 ByteSliceLikeGetter interface + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [31929] + +# (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/LANGUAGE.md b/pkg/ottl/LANGUAGE.md index 05df67e0ffef..a1ac77a57dc4 100644 --- a/pkg/ottl/LANGUAGE.md +++ b/pkg/ottl/LANGUAGE.md @@ -69,6 +69,7 @@ The following types are supported for single-value parameters in OTTL functions: - `IntLikeGetter` - `BoolGetter` - `BoolLikeGetter` +- `ByteSliceLikeGetter` - `Enum` - `string` - `float64` diff --git a/pkg/ottl/expression.go b/pkg/ottl/expression.go index 6f6cc18b81c7..c1cb833c66ce 100644 --- a/pkg/ottl/expression.go +++ b/pkg/ottl/expression.go @@ -4,7 +4,9 @@ package ottl // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" import ( + "bytes" "context" + "encoding/binary" "encoding/hex" "fmt" "reflect" @@ -549,6 +551,81 @@ func (g StandardIntLikeGetter[K]) Get(ctx context.Context, tCtx K) (*int64, erro return &result, nil } +// ByteSliceLikeGetter is a Getter that returns []byte by converting the underlying value to an []byte if necessary +type ByteSliceLikeGetter[K any] interface { + // Get retrieves []byte value. + // The expectation is that the underlying value is converted to []byte if possible. + // If the value cannot be converted to []byte, nil and an error are returned. + // If the value is nil, nil is returned without an error. + Get(ctx context.Context, tCtx K) ([]byte, error) +} + +type StandardByteSliceLikeGetter[K any] struct { + Getter func(ctx context.Context, tCtx K) (any, error) +} + +func (g StandardByteSliceLikeGetter[K]) Get(ctx context.Context, tCtx K) ([]byte, error) { + val, err := g.Getter(ctx, tCtx) + if err != nil { + return nil, fmt.Errorf("error getting value in %T: %w", g, err) + } + if val == nil { + return nil, nil + } + var result []byte + switch v := val.(type) { + case []byte: + result = v + case string: + result = []byte(v) + case float64, int64, bool: + result, err = valueToBytes(v) + if err != nil { + return nil, fmt.Errorf("error converting value %f of %T: %w", v, g, err) + } + case pcommon.Value: + switch v.Type() { + case pcommon.ValueTypeBytes: + result = v.Bytes().AsRaw() + case pcommon.ValueTypeInt: + result, err = valueToBytes(v.Int()) + if err != nil { + return nil, fmt.Errorf("error converting value %d of int64: %w", v.Int(), err) + } + case pcommon.ValueTypeDouble: + result, err = valueToBytes(v.Double()) + if err != nil { + return nil, fmt.Errorf("error converting value %f of float64: %w", v.Double(), err) + } + case pcommon.ValueTypeStr: + result = []byte(v.Str()) + case pcommon.ValueTypeBool: + result, err = valueToBytes(v.Bool()) + if err != nil { + return nil, fmt.Errorf("error converting value %s of bool: %w", v.Str(), err) + } + default: + return nil, TypeError(fmt.Sprintf("unsupported value type: %v", v.Type())) + } + default: + return nil, TypeError(fmt.Sprintf("unsupported type: %T", v)) + } + return result, nil +} + +// valueToBytes converts a value to a byte slice of length 8. +func valueToBytes(n any) ([]byte, error) { + // Create a buffer to hold the bytes + buf := new(bytes.Buffer) + // Write the value to the buffer using binary.Write + err := binary.Write(buf, binary.BigEndian, n) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + // BoolLikeGetter is a Getter that returns a bool by converting the underlying value to a bool if necessary. type BoolLikeGetter[K any] interface { // Get retrieves a bool value. diff --git a/pkg/ottl/expression_test.go b/pkg/ottl/expression_test.go index 821c86a8acfe..511d5d47b487 100644 --- a/pkg/ottl/expression_test.go +++ b/pkg/ottl/expression_test.go @@ -1449,6 +1449,205 @@ func Test_StandardIntLikeGetter_WrappedError(t *testing.T) { assert.False(t, ok) } +func Test_StandardByteSliceLikeGetter(t *testing.T) { + tests := []struct { + name string + getter ByteSliceLikeGetter[any] + want any + valid bool + expectedErrorMsg string + }{ + { + name: "string type", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return "1", nil + }, + }, + want: []byte{49}, + valid: true, + }, + { + name: "byte type", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return []byte{49}, nil + }, + }, + want: []byte{49}, + valid: true, + }, + { + name: "int64 type", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return int64(12), nil + }, + }, + want: []byte{0, 0, 0, 0, 0, 0, 0, 12}, + valid: true, + }, + { + name: "float64 type", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return 1.1, nil + }, + }, + want: []byte{63, 241, 153, 153, 153, 153, 153, 154}, + valid: true, + }, + { + name: "primitive bool true", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return true, nil + }, + }, + want: []byte{1}, + valid: true, + }, + { + name: "primitive bool false", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return false, nil + }, + }, + want: []byte{0}, + valid: true, + }, + { + name: "pcommon.value type int", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + v := pcommon.NewValueInt(int64(100)) + return v, nil + }, + }, + want: []byte{0, 0, 0, 0, 0, 0, 0, 100}, + valid: true, + }, + { + name: "pcommon.value type float", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + v := pcommon.NewValueDouble(float64(1.9)) + return v, nil + }, + }, + want: []byte{63, 254, 102, 102, 102, 102, 102, 102}, + valid: true, + }, + { + name: "pcommon.value type string", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + v := pcommon.NewValueStr("1") + return v, nil + }, + }, + want: []byte{49}, + valid: true, + }, + { + name: "pcommon.value type bytes", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + v := pcommon.NewValueBytes() + v.SetEmptyBytes().Append(byte(12)) + return v, nil + }, + }, + want: []byte{12}, + valid: true, + }, + { + name: "pcommon.value type bool true", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + v := pcommon.NewValueBool(true) + return v, nil + }, + }, + want: []byte{1}, + valid: true, + }, + { + name: "pcommon.value type bool false", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + v := pcommon.NewValueBool(false) + return v, nil + }, + }, + want: []byte{0}, + valid: true, + }, + { + name: "nil", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return nil, nil + }, + }, + want: nil, + valid: true, + }, + { + name: "invalid type", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return map[string]string{}, nil + }, + }, + valid: false, + expectedErrorMsg: "unsupported type: map[string]string", + }, + { + name: "invalid pcommon.Value type", + getter: StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + v := pcommon.NewValueMap() + return v, nil + }, + }, + valid: false, + expectedErrorMsg: "unsupported value type: Map", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.getter.Get(context.Background(), nil) + if tt.valid { + assert.NoError(t, err) + if tt.want == nil { + assert.Nil(t, val) + } else { + assert.Equal(t, tt.want, val) + } + } else { + assert.IsType(t, TypeError(""), err) + assert.EqualError(t, err, tt.expectedErrorMsg) + } + }) + } +} + +// nolint:errorlint +func Test_StandardByteSliceLikeGetter_WrappedError(t *testing.T) { + getter := StandardByteSliceLikeGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return nil, TypeError("") + }, + } + _, err := getter.Get(context.Background(), nil) + assert.Error(t, err) + _, ok := err.(TypeError) + assert.False(t, ok) +} + func Test_StandardBoolGetter(t *testing.T) { tests := []struct { name string diff --git a/pkg/ottl/functions.go b/pkg/ottl/functions.go index 98f730a24dc7..9c6ccc226aaf 100644 --- a/pkg/ottl/functions.go +++ b/pkg/ottl/functions.go @@ -493,6 +493,12 @@ func (p *Parser[K]) buildArg(argVal value, argType reflect.Type) (any, error) { return nil, err } return StandardBoolLikeGetter[K]{Getter: arg.Get}, nil + case strings.HasPrefix(name, "ByteSliceLikeGetter"): + arg, err := p.newGetter(argVal) + if err != nil { + return nil, err + } + return StandardByteSliceLikeGetter[K]{Getter: arg.Get}, nil case name == "Enum": arg, err := p.enumParser((*EnumSymbol)(argVal.Enum)) if err != nil { diff --git a/pkg/ottl/functions_test.go b/pkg/ottl/functions_test.go index d94c84f53106..b2210c36654b 100644 --- a/pkg/ottl/functions_test.go +++ b/pkg/ottl/functions_test.go @@ -1207,6 +1207,20 @@ func Test_NewFunctionCall(t *testing.T) { }, want: nil, }, + { + name: "byteslicelikegetter arg", + inv: editor{ + Function: "testing_byte_slice", + Arguments: []argument{ + { + Value: value{ + Bytes: &byteSlice{1}, + }, + }, + }, + }, + want: nil, + }, { name: "pmapgetter arg", inv: editor{ @@ -1864,6 +1878,16 @@ func functionWithIntLikeGetter(IntLikeGetter[any]) (ExprFunc[any], error) { }, nil } +type byteSliceLikeGetterArguments struct { + ByteSliceLikeGetterArg ByteSliceLikeGetter[any] +} + +func functionWithByteSliceLikeGetter(ByteSliceLikeGetter[any]) (ExprFunc[any], error) { + return func(context.Context, any) (any, error) { + return "anything", nil + }, nil +} + type pMapGetterArguments struct { PMapArg PMapGetter[any] } @@ -2150,6 +2174,11 @@ func defaultFunctionsForTests() map[string]Factory[any] { &intLikeGetterArguments{}, functionWithIntLikeGetter, ), + createFactory[any]( + "testing_byteslicelikegetter", + &byteSliceLikeGetterArguments{}, + functionWithByteSliceLikeGetter, + ), createFactory[any]( "testing_pmapgetter", &pMapGetterArguments{},