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 IsBool Converter #29085

Closed
Closed
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
26 changes: 26 additions & 0 deletions .chloggen/27897-is-bool-converter.yaml
Original file line number Diff line number Diff line change
@@ -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]
12 changes: 7 additions & 5 deletions pkg/ottl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ The following types are supported for single-value parameters in OTTL functions:
- `GetSetter`
- `Getter`
- `PMapGetter`
- `BoolGetter`
- `FloatGetter`
- `FloatLikeGetter`
- `StringGetter`
Expand All @@ -87,6 +88,7 @@ For slice parameters, the following types are supported:

- `Getter`
- `PMapGetter`
- `BoolGetter`
- `FloatGetter`
- `FloatLikeGetter`
- `StringGetter`
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -193,18 +195,18 @@ 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`.
- A `time.Duration` `-` a `time.Duration` yields a `time.Duration`.

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.
Expand Down
37 changes: 37 additions & 0 deletions pkg/ottl/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
64 changes: 64 additions & 0 deletions pkg/ottl/expression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions pkg/ottl/ottlfuncs/func_is_bool.go
Original file line number Diff line number Diff line change
@@ -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
}
}
}
74 changes: 74 additions & 0 deletions pkg/ottl/ottlfuncs/func_is_bool_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions pkg/ottl/ottlfuncs/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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](),
Expand Down