Skip to content

Commit

Permalink
[pkg/ottl] Add StringLikeGetter for turning types into string (#19782)
Browse files Browse the repository at this point in the history
* Add StringLikeGetter

* changelog

* Add godoc comments

* Update concat to use StringLikeGetter

* Update pkg/ottl/ottlfuncs/func_concat_test.go

Co-authored-by: Evan Bradley <11745660+evan-bradley@users.noreply.github.com>

* Add more stringify support

* Simplify switch

* Update changelog

* Fix lint

* Update changelog

---------

Co-authored-by: Evan Bradley <11745660+evan-bradley@users.noreply.github.com>
  • Loading branch information
TylerHelmuth and evan-bradley authored Mar 22, 2023
1 parent 0c68819 commit d238d50
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 100 deletions.
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 {
// 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)
}

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

0 comments on commit d238d50

Please sign in to comment.