diff --git a/.chloggen/27897-is-bool-converter.yaml b/.chloggen/27897-is-bool-converter.yaml new file mode 100644 index 000000000000..6f90f9f7ef47 --- /dev/null +++ b/.chloggen/27897-is-bool-converter.yaml @@ -0,0 +1,26 @@ +# 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: The IsBool function is now available to OTTL + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [27897] + +# (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. + +# 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/README.md b/pkg/ottl/README.md index 82d97db70495..d71fbd2f3834 100644 --- a/pkg/ottl/README.md +++ b/pkg/ottl/README.md @@ -71,6 +71,7 @@ The following types are supported for single-value parameters in OTTL functions: - `GetSetter` - `Getter` - `PMapGetter` +- `BoolGetter` - `FloatGetter` - `FloatLikeGetter` - `StringGetter` @@ -87,6 +88,7 @@ For slice parameters, the following types are supported: - `Getter` - `PMapGetter` +- `BoolGetter` - `FloatGetter` - `FloatLikeGetter` - `StringGetter` @@ -148,7 +150,7 @@ Contexts will have an implementation of `PathExpressionParser` that decides how The context's implementation will need to make decisions like what a dot (`.`) represents or which paths allow indexing (`["key"]`) and how many indexes. [There are OpenTelemetry-specific contexts provided for each signal here.](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/ottl/contexts) -When using OTTL it is recommended to use these contexts unless you have a specific need. Check out each context to view the paths it supports. +When using OTTL it is recommended to use these contexts unless you have a specific need. Check out each context to view the paths it supports. ### Lists @@ -193,10 +195,10 @@ When defining an OTTL function, if the function needs to take an Enum then the f Math Expressions represent arithmetic calculations. They support `+`, `-`, `*`, and `/`, along with `()` for grouping. -Math Expressions currently support `int64`, `float64`, `time.Time` and `time.Duration`. -For `time.Time` and `time.Duration`, only `+` and `-` are supported with the following rules: +Math Expressions currently support `int64`, `float64`, `time.Time` and `time.Duration`. +For `time.Time` and `time.Duration`, only `+` and `-` are supported with the following rules: - A `time.Time` `-` a `time.Time` yields a `time.Duration`. - - A `time.Duration` `+` a `time.Time` yields a `time.Time`. + - A `time.Duration` `+` a `time.Time` yields a `time.Time`. - A `time.Time` `+` a `time.Duration` yields a `time.Time`. - A `time.Time` `-` a `time.Duration` yields a `time.Time`. - A `time.Duration` `+` a `time.Duration` yields a `time.Duration`. @@ -204,7 +206,7 @@ For `time.Time` and `time.Duration`, only `+` and `-` are supported with the fol Math Expressions support `Paths` and `Editors` that return supported types. Note that `*` and `/` take precedence over `+` and `-`. -Also note that `time.Time` and `time.Duration` can only be used with `+` and `-`. +Also note that `time.Time` and `time.Duration` can only be used with `+` and `-`. Operations that share the same level of precedence will be executed in the order that they appear in the Math Expression. Math Expressions can be grouped with parentheses to override evaluation precedence. Math Expressions that mix `int64` and `float64` will result in an error. diff --git a/pkg/ottl/expression.go b/pkg/ottl/expression.go index e826d4324055..842c9f807282 100644 --- a/pkg/ottl/expression.go +++ b/pkg/ottl/expression.go @@ -142,6 +142,43 @@ func (t TypeError) Error() string { return string(t) } +// BoolGetter is a Getter than must return a bool. +type BoolGetter[K any] interface { + // Get retrieves a bool value. + Get(ctx context.Context, tCtx K) (bool, error) +} + +// StandardBoolGetter is a basic implementation of BoolGetter +type StandardBoolGetter[K any] struct { + Getter func(ctx context.Context, tCtx K) (interface{}, error) +} + +const boolExpectMsg = "expected bool but got" + +// Get retrieves a bool value. +// If the value is not a bool a new TypeError is returned. +// If there is an error getting the value it will be returned. +func (g StandardBoolGetter[K]) Get(ctx context.Context, tCtx K) (bool, error) { + val, err := g.Getter(ctx, tCtx) + if err != nil { + return false, fmt.Errorf("error getting value in %T: %w", g, err) + } + if val == nil { + return false, TypeError(boolExpectMsg + " nil") + } + switch v := val.(type) { + case bool: + return v, nil + case pcommon.Value: + if v.Type() == pcommon.ValueTypeBool { + return v.Bool(), nil + } + return false, TypeError(fmt.Sprintf("%s %v", boolExpectMsg, v.Type())) + default: + return false, TypeError(fmt.Sprintf("%s %T", boolExpectMsg, val)) + } +} + // StringGetter is a Getter that must return a string. type StringGetter[K any] interface { // Get retrieves a string value. diff --git a/pkg/ottl/expression_test.go b/pkg/ottl/expression_test.go index 7b57713fb2c7..d76aded8eaae 100644 --- a/pkg/ottl/expression_test.go +++ b/pkg/ottl/expression_test.go @@ -614,6 +614,70 @@ func Test_exprGetter_Get_Invalid(t *testing.T) { } } +func Test_StandardBoolGetter(t *testing.T) { + tests := []struct { + name string + getter StandardBoolGetter[interface{}] + want interface{} + valid bool + expectedErrorMsg string + }{ + { + name: "bool type", + getter: StandardBoolGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return true, nil + }, + }, + want: true, + valid: true, + }, + { + name: "ValueTypeBool type", + getter: StandardBoolGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return pcommon.NewValueBool(true), nil + }, + }, + want: true, + valid: true, + }, + { + name: "Incorrect type", + getter: StandardBoolGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return "str", nil + }, + }, + valid: false, + expectedErrorMsg: boolExpectMsg + " string", + }, + { + name: "nil", + getter: StandardBoolGetter[interface{}]{ + Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) { + return nil, nil + }, + }, + valid: false, + expectedErrorMsg: boolExpectMsg + " nil", + }, + } + + 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) + assert.Equal(t, tt.want, val) + } else { + assert.IsType(t, TypeError(""), err) + assert.EqualError(t, err, tt.expectedErrorMsg) + } + }) + } +} + func Test_StandardStringGetter(t *testing.T) { tests := []struct { name string diff --git a/pkg/ottl/ottlfuncs/func_is_bool.go b/pkg/ottl/ottlfuncs/func_is_bool.go new file mode 100644 index 000000000000..f232e47b92df --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_is_bool.go @@ -0,0 +1,46 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs" + +import ( + "context" + "fmt" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +type IsBoolArguments[K any] struct { + Target ottl.BoolGetter[K] +} + +func NewIsBoolFactory[K any]() ottl.Factory[K] { + return ottl.NewFactory("IsBool", &IsBoolArguments[K]{}, createIsBoolFunction[K]) +} + +func createIsBoolFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { + args, ok := oArgs.(*IsBoolArguments[K]) + + if !ok { + return nil, fmt.Errorf("IsBoolFactory args must be of type *IsBoolArguments[K]") + } + + return isBool(args.Target), nil +} + +// nolint:errorlint +func isBool[K any](target ottl.BoolGetter[K]) ottl.ExprFunc[K] { + return func(ctx context.Context, tCtx K) (interface{}, error) { + _, err := target.Get(ctx, tCtx) + + // Use type assertion because we don't want to check wrapped errors + switch err.(type) { + case ottl.TypeError: + return false, nil + case nil: + return true, nil + default: + return false, err + } + } +} diff --git a/pkg/ottl/ottlfuncs/func_is_bool_test.go b/pkg/ottl/ottlfuncs/func_is_bool_test.go new file mode 100644 index 000000000000..fb14d0e7f75e --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_is_bool_test.go @@ -0,0 +1,74 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +func Test_IsBool(t *testing.T) { + tests := []struct { + name string + value interface{} + expected bool + }{ + { + name: "bool", + value: true, + expected: true, + }, + { + name: "ValueTypeBool", + value: pcommon.NewValueBool(true), + expected: true, + }, + { + name: "not bool", + value: 1, + expected: false, + }, + { + name: "ValueTypeSlice", + value: pcommon.NewValueSlice(), + expected: false, + }, + { + name: "nil", + value: nil, + expected: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exprFunc := isBool[any](&ottl.StandardBoolGetter[any]{ + Getter: func(context.Context, interface{}) (interface{}, error) { + return tt.value, nil + }, + }) + result, err := exprFunc(context.Background(), nil) + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} + +// nolint:errorlint +func Test_IsBool_Error(t *testing.T) { + exprFunc := isBool[any](&ottl.StandardBoolGetter[any]{ + Getter: func(context.Context, interface{}) (interface{}, error) { + return nil, ottl.TypeError("") + }, + }) + result, err := exprFunc(context.Background(), nil) + assert.Equal(t, false, result) + assert.Error(t, err) + _, ok := err.(ottl.TypeError) + assert.False(t, ok) +} diff --git a/pkg/ottl/ottlfuncs/functions.go b/pkg/ottl/ottlfuncs/functions.go index e43507192392..e892135c45e8 100644 --- a/pkg/ottl/ottlfuncs/functions.go +++ b/pkg/ottl/ottlfuncs/functions.go @@ -42,6 +42,7 @@ func converters[K any]() []ottl.Factory[K] { NewFnvFactory[K](), NewHoursFactory[K](), NewIntFactory[K](), + NewIsBoolFactory[K](), NewIsMapFactory[K](), NewIsMatchFactory[K](), NewIsStringFactory[K](),