Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pkg/ottl] Add StringLikeGetter for turning types into string #19782

Merged
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
16 changes: 16 additions & 0 deletions .chloggen/ottl-use-stringlikegetter.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: breaking

# 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: Adds new StringLikeGetter for converting values to string for use.

# One or more tracking issues related to the change
issues: [19782]

# (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: Concat now converts more types to string instead of ignoring them. IsMatch now converts []byte to string using `hex.EncodeToString(v)`.
52 changes: 52 additions & 0 deletions pkg/ottl/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ package ottl // import "github.com/open-telemetry/opentelemetry-collector-contri

import (
"context"
"encoding/hex"
"fmt"

jsoniter "github.com/json-iterator/go"
"go.opentelemetry.io/collector/pdata/pcommon"
)

Expand Down Expand Up @@ -91,7 +93,9 @@ func (l *listGetter[K]) Get(ctx context.Context, tCtx K) (interface{}, error) {
return evaluated, nil
}

// StringGetter is a Getter that must return a string.
type StringGetter[K any] interface {
// Get retrieves a string value. If the value is not a string, an error is returned.
Get(ctx context.Context, tCtx K) (string, error)
}

Expand Down Expand Up @@ -123,6 +127,54 @@ func (g StandardTypeGetter[K, T]) Get(ctx context.Context, tCtx K) (T, error) {
return v, nil
}

// StringLikeGetter is a Getter that returns a string by converting the underlying value to a string if necessary.
type StringLikeGetter[K any] interface {
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
// Get retrieves a string value.
// Unlike `StringGetter`, the expectation is that the underlying value is converted to a string if possible.
// If the value cannot be converted to a string, nil and an error are returned.
// If the value is nil, nil is returned without an error.
Get(ctx context.Context, tCtx K) (*string, error)
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
}

type StandardStringLikeGetter[K any] struct {
Getter func(ctx context.Context, tCtx K) (interface{}, error)
}

func (g StandardStringLikeGetter[K]) Get(ctx context.Context, tCtx K) (*string, error) {
val, err := g.Getter(ctx, tCtx)
if err != nil {
return nil, err
}
if val == nil {
return nil, nil
}
var result string
switch v := val.(type) {
case string:
result = v
case []byte:
result = hex.EncodeToString(v)
case pcommon.Map:
result, err = jsoniter.MarshalToString(v.AsRaw())
if err != nil {
return nil, err
}
case pcommon.Slice:
result, err = jsoniter.MarshalToString(v.AsRaw())
if err != nil {
return nil, err
}
case pcommon.Value:
result = v.AsString()
default:
result, err = jsoniter.MarshalToString(v)
if err != nil {
return nil, fmt.Errorf("unsupported type: %T", v)
}
}
return &result, nil
}

func (p *Parser[K]) newGetter(val value) (Getter[K], error) {
if val.IsNil != nil && *val.IsNil {
return &literal[K]{value: nil}, nil
Expand Down
134 changes: 134 additions & 0 deletions pkg/ottl/expression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/stretchr/testify/assert"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/pdata/pcommon"

"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottltest"
)
Expand Down Expand Up @@ -371,3 +372,136 @@ func Test_StandardTypeGetter(t *testing.T) {
})
}
}

func Test_StandardStringLikeGetter(t *testing.T) {
tests := []struct {
name string
getter StringLikeGetter[interface{}]
want interface{}
valid bool
expectedErrorMsg string
}{
{
name: "string type",
getter: StandardStringLikeGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "str", nil
},
},
want: "str",
valid: true,
},
{
name: "bool type",
getter: StandardStringLikeGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return true, nil
},
},
want: "true",
valid: true,
},
{
name: "int64 type",
getter: StandardStringLikeGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return int64(1), nil
},
},
want: "1",
valid: true,
},
{
name: "float64 type",
getter: StandardStringLikeGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return 1.1, nil
},
},
want: "1.1",
valid: true,
},
{
name: "byte[] type",
getter: StandardStringLikeGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return []byte{0}, nil
},
},
want: "00",
valid: true,
},
{
name: "pcommon.map type",
getter: StandardStringLikeGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
m := pcommon.NewMap()
m.PutStr("test", "passed")
return m, nil
},
},
want: `{"test":"passed"}`,
valid: true,
},
{
name: "pcommon.slice type",
getter: StandardStringLikeGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
s := pcommon.NewSlice()
v := s.AppendEmpty()
v.SetStr("test")
return s, nil
},
},
want: `["test"]`,
valid: true,
},
{
name: "pcommon.value type",
getter: StandardStringLikeGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
v := pcommon.NewValueInt(int64(100))
return v, nil
},
},
want: "100",
valid: true,
},
{
name: "nil",
getter: StandardStringLikeGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return nil, nil
},
},
want: nil,
valid: true,
},
{
name: "invalid type",
getter: StandardStringLikeGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return make(chan int), nil
},
},
valid: false,
expectedErrorMsg: "unsupported type: chan int",
},
}

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.EqualError(t, err, tt.expectedErrorMsg)
}
})
}
}
12 changes: 12 additions & 0 deletions pkg/ottl/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ func (p *Parser[K]) buildSliceArg(argVal value, argType reflect.Type) (any, erro
return nil, err
}
return arg, nil
case strings.HasPrefix(name, "StringLikeGetter"):
arg, err := buildSlice[StringLikeGetter[K]](argVal, argType, p.buildArg, name)
if err != nil {
return nil, err
}
return arg, nil
default:
return nil, fmt.Errorf("unsupported slice type '%s' for function", argType.Elem().Name())
}
Expand Down Expand Up @@ -171,6 +177,12 @@ func (p *Parser[K]) buildArg(argVal value, argType reflect.Type) (any, error) {
return nil, err
}
return StandardTypeGetter[K, string]{Getter: arg.Get}, nil
case strings.HasPrefix(name, "StringLikeGetter"):
arg, err := p.newGetter(argVal)
if err != nil {
return nil, err
}
return StandardStringLikeGetter[K]{Getter: arg.Get}, nil
case strings.HasPrefix(name, "IntGetter"):
arg, err := p.newGetter(argVal)
if err != nil {
Expand Down
49 changes: 49 additions & 0 deletions pkg/ottl/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,29 @@ func Test_NewFunctionCall(t *testing.T) {
},
want: 2,
},
{
name: "stringlikegetter slice arg",
inv: invocation{
Function: "testing_stringlikegetter_slice",
Arguments: []value{
{
List: &list{
Values: []value{
{
String: ottltest.Strp("test"),
},
{
Literal: &mathExprLiteral{
Int: ottltest.Intp(1),
},
},
},
},
},
},
},
want: 2,
},
{
name: "setter arg",
inv: invocation{
Expand Down Expand Up @@ -694,6 +717,18 @@ func Test_NewFunctionCall(t *testing.T) {
},
want: nil,
},
{
name: "stringlikegetter arg",
inv: invocation{
Function: "testing_stringlikegetter",
Arguments: []value{
{
Bool: (*boolean)(ottltest.Boolp(false)),
},
},
},
want: nil,
},
{
name: "intgetter arg",
inv: invocation{
Expand Down Expand Up @@ -971,6 +1006,12 @@ func functionWithPMapGetterSlice(getters []PMapGetter[interface{}]) (ExprFunc[in
}, nil
}

func functionWithStringLikeGetterSlice(getters []StringLikeGetter[interface{}]) (ExprFunc[interface{}], error) {
return func(context.Context, interface{}) (interface{}, error) {
return len(getters), nil
}, nil
}

func functionWithSetter(Setter[interface{}]) (ExprFunc[interface{}], error) {
return func(context.Context, interface{}) (interface{}, error) {
return "anything", nil
Expand All @@ -995,6 +1036,12 @@ func functionWithStringGetter(StringGetter[interface{}]) (ExprFunc[interface{}],
}, nil
}

func functionWithStringLikeGetter(StringLikeGetter[interface{}]) (ExprFunc[interface{}], error) {
return func(context.Context, interface{}) (interface{}, error) {
return "anything", nil
}, nil
}

func functionWithIntGetter(IntGetter[interface{}]) (ExprFunc[interface{}], error) {
return func(context.Context, interface{}) (interface{}, error) {
return "anything", nil
Expand Down Expand Up @@ -1077,10 +1124,12 @@ func defaultFunctionsForTests() map[string]interface{} {
functions["testing_getter_slice"] = functionWithGetterSlice
functions["testing_stringgetter_slice"] = functionWithStringGetterSlice
functions["testing_pmapgetter_slice"] = functionWithPMapGetterSlice
functions["testing_stringlikegetter_slice"] = functionWithStringLikeGetterSlice
functions["testing_setter"] = functionWithSetter
functions["testing_getsetter"] = functionWithGetSetter
functions["testing_getter"] = functionWithGetter
functions["testing_stringgetter"] = functionWithStringGetter
functions["testing_stringlikegetter"] = functionWithStringLikeGetter
functions["testing_intgetter"] = functionWithIntGetter
functions["testing_pmapgetter"] = functionWithPMapGetter
functions["testing_string"] = functionWithString
Expand Down
20 changes: 5 additions & 15 deletions pkg/ottl/ottlfuncs/func_concat.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,19 @@ import (
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
)

func Concat[K any](vals []ottl.Getter[K], delimiter string) (ottl.ExprFunc[K], error) {
func Concat[K any](vals []ottl.StringLikeGetter[K], delimiter string) (ottl.ExprFunc[K], error) {
return func(ctx context.Context, tCtx K) (interface{}, error) {
builder := strings.Builder{}
for i, rv := range vals {
val, err := rv.Get(ctx, tCtx)
if err != nil {
return nil, err
}
switch v := val.(type) {
case string:
builder.WriteString(v)
case []byte:
builder.WriteString(fmt.Sprintf("%x", v))
case int64:
builder.WriteString(fmt.Sprint(v))
case float64:
builder.WriteString(fmt.Sprint(v))
case bool:
builder.WriteString(fmt.Sprint(v))
case nil:
builder.WriteString(fmt.Sprint(v))
if val == nil {
builder.WriteString(fmt.Sprint(val))
} else {
builder.WriteString(*val)
}

if i != len(vals)-1 {
builder.WriteString(delimiter)
}
Expand Down
Loading