Skip to content

Commit f9041bd

Browse files
[pkg/ottl]: Add a new Function Getter to the OTTL package, to allow passing Converters as literal parameters (#24356)
**Description:** Add a new Function Getter to the OTTL package, to allow passing Converters as literal parameters **Link to tracking Issue:** [<Issue number if applicable>](#22961) **Testing:** Added a unit test and also tested the following configuration ``` replace_pattern(attributes["message"], "device=*", attributes["device_name"], SHA256) ``` A tentative example of how the literal function gets invoked is ``` type ReplacePatternArguments[K any] struct { Target ottl.GetSetter[K] `ottlarg:"0"` RegexPattern string `ottlarg:"1"` Replacement ottl.StringGetter[K] `ottlarg:"2"` Function ottl.FunctionGetter[K] `ottlarg:"3"` } func replacePattern[K any](target ottl.GetSetter[K], regexPattern string, replacement ottl.StringGetter[K], fn ottl.Expr[K]) (ottl.ExprFunc[K], error) { compiledPattern, err := regexp.Compile(regexPattern) if err != nil { return nil, fmt.Errorf("the regex pattern supplied to replace_pattern is not a valid pattern: %w", err) } return func(ctx context.Context, tCtx K) (interface{}, error) { originalVal, err := target.Get(ctx, tCtx) if err != nil { return nil, err } replacementVal, err := fn.Eval(ctx, tCtx) if err != nil { return nil, err } ....... updatedStr := compiledPattern.ReplaceAllString(originalValStr, replacementValHash.(string)) } ```
1 parent e96ae18 commit f9041bd

File tree

7 files changed

+432
-23
lines changed

7 files changed

+432
-23
lines changed

.chloggen/function-getter.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Use this changelog template to create an entry for release notes.
2+
# If your change doesn't affect end users, such as a test fix or a tooling change,
3+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
4+
5+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
6+
change_type: enhancement
7+
8+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
9+
component: pkg/ottl
10+
11+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
12+
note: Add a new Function Getter to the OTTL package, to allow passing Converters as literal parameters.
13+
14+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
15+
issues: [22961]
16+
17+
# (Optional) One or more lines of additional information to render under the primary note.
18+
# These lines will be padded with 2 spaces and then inserted directly into the document.
19+
# Use pipe (|) for multiline entries.
20+
subtext: |
21+
Currently OTTL provides no way to use any defined Converter within another Editor/Converter.
22+
Although Converters can be passed as a parameter, they are always executed and the result is what is actually passed as the parameter.
23+
This allows OTTL to pass Converters themselves as a parameter so they can be executed within the function.

pkg/ottl/expression.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"encoding/hex"
99
"fmt"
10+
"reflect"
1011
"strconv"
1112

1213
jsoniter "github.com/json-iterator/go"
@@ -245,6 +246,56 @@ func (g StandardFloatGetter[K]) Get(ctx context.Context, tCtx K) (float64, error
245246
}
246247
}
247248

249+
// FunctionGetter uses a function factory to return an instantiated function as an Expr.
250+
type FunctionGetter[K any] interface {
251+
Get(args Arguments) (Expr[K], error)
252+
}
253+
254+
// StandardFunctionGetter is a basic implementation of FunctionGetter.
255+
type StandardFunctionGetter[K any] struct {
256+
fCtx FunctionContext
257+
fact Factory[K]
258+
}
259+
260+
// Get takes an Arguments struct containing arguments the caller wants passed to the
261+
// function and instantiates the function with those arguments.
262+
// If there is a mismatch between the function's signature and the arguments the caller
263+
// wants to pass to the function, an error is returned.
264+
func (g StandardFunctionGetter[K]) Get(args Arguments) (Expr[K], error) {
265+
if g.fact == nil {
266+
return Expr[K]{}, fmt.Errorf("undefined function")
267+
}
268+
fArgs := g.fact.CreateDefaultArguments()
269+
if reflect.TypeOf(fArgs).Kind() != reflect.Pointer {
270+
return Expr[K]{}, fmt.Errorf("factory for %q must return a pointer to an Arguments value in its CreateDefaultArguments method", g.fact.Name())
271+
}
272+
if reflect.TypeOf(args).Kind() != reflect.Pointer {
273+
return Expr[K]{}, fmt.Errorf("%q must be pointer to an Arguments value", reflect.TypeOf(args).Kind())
274+
}
275+
fArgsVal := reflect.ValueOf(fArgs).Elem()
276+
argsVal := reflect.ValueOf(args).Elem()
277+
if fArgsVal.NumField() != argsVal.NumField() {
278+
return Expr[K]{}, fmt.Errorf("incorrect number of arguments. Expected: %d Received: %d", fArgsVal.NumField(), argsVal.NumField())
279+
}
280+
for i := 0; i < fArgsVal.NumField(); i++ {
281+
field := argsVal.Field(i)
282+
argIndex, err := getArgumentIndex(i, argsVal)
283+
if err != nil {
284+
return Expr[K]{}, err
285+
}
286+
fArgIndex, err := getArgumentIndex(argIndex, fArgsVal)
287+
if err != nil {
288+
return Expr[K]{}, err
289+
}
290+
fArgsVal.Field(fArgIndex).Set(field)
291+
}
292+
fn, err := g.fact.CreateFunction(g.fCtx, fArgs)
293+
if err != nil {
294+
return Expr[K]{}, fmt.Errorf("couldn't create function: %w", err)
295+
}
296+
return Expr[K]{exprFunc: fn}, nil
297+
}
298+
248299
// PMapGetter is a Getter that must return a pcommon.Map.
249300
type PMapGetter[K any] interface {
250301
// Get retrieves a pcommon.Map value.
@@ -399,7 +450,7 @@ func (g StandardFloatLikeGetter[K]) Get(ctx context.Context, tCtx K) (*float64,
399450
return &result, nil
400451
}
401452

402-
// IntLikeGetter is a Getter that returns an int by converting the underlying value to an int if necessary.
453+
// IntLikeGetter is a Getter that returns an int by converting the underlying value to an int if necessary
403454
type IntLikeGetter[K any] interface {
404455
// Get retrieves an int value.
405456
// Unlike `IntGetter`, the expectation is that the underlying value is converted to an int if possible.

pkg/ottl/expression_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,126 @@ func Test_StandardStringGetter(t *testing.T) {
676676
}
677677
}
678678

679+
func Test_FunctionGetter(t *testing.T) {
680+
functions := CreateFactoryMap(
681+
createFactory[any](
682+
"SHA256",
683+
&stringGetterArguments{},
684+
functionWithStringGetter,
685+
),
686+
createFactory[any](
687+
"test_arg_mismatch",
688+
&multipleArgsArguments{},
689+
functionWithStringGetter,
690+
),
691+
createFactory[any](
692+
"no_struct_tag",
693+
&noStructTagFunctionArguments{},
694+
functionWithStringGetter,
695+
),
696+
NewFactory(
697+
"cannot_create_function",
698+
&stringGetterArguments{},
699+
func(FunctionContext, Arguments) (ExprFunc[any], error) {
700+
return functionWithErr()
701+
},
702+
),
703+
)
704+
type EditorArguments struct {
705+
Replacement StringGetter[any]
706+
Function FunctionGetter[any]
707+
}
708+
type FuncArgs struct {
709+
Input StringGetter[any] `ottlarg:"0"`
710+
}
711+
tests := []struct {
712+
name string
713+
getter StringGetter[any]
714+
function FunctionGetter[any]
715+
want interface{}
716+
valid bool
717+
expectedErrorMsg string
718+
}{
719+
{
720+
name: "function getter",
721+
getter: StandardStringGetter[interface{}]{
722+
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
723+
return "str", nil
724+
},
725+
},
726+
function: StandardFunctionGetter[any]{fCtx: FunctionContext{Set: componenttest.NewNopTelemetrySettings()}, fact: functions["SHA256"]},
727+
want: "anything",
728+
valid: true,
729+
},
730+
{
731+
name: "function getter nil",
732+
getter: StandardStringGetter[interface{}]{
733+
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
734+
return nil, nil
735+
},
736+
},
737+
function: StandardFunctionGetter[any]{fCtx: FunctionContext{Set: componenttest.NewNopTelemetrySettings()}, fact: functions["SHA250"]},
738+
want: "anything",
739+
valid: false,
740+
expectedErrorMsg: "undefined function",
741+
},
742+
{
743+
name: "function arg mismatch",
744+
getter: StandardStringGetter[interface{}]{
745+
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
746+
return nil, nil
747+
},
748+
},
749+
function: StandardFunctionGetter[any]{fCtx: FunctionContext{Set: componenttest.NewNopTelemetrySettings()}, fact: functions["test_arg_mismatch"]},
750+
want: "anything",
751+
valid: false,
752+
expectedErrorMsg: "incorrect number of arguments. Expected: 4 Received: 1",
753+
},
754+
{
755+
name: "Invalid Arguments struct tag",
756+
getter: StandardStringGetter[interface{}]{
757+
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
758+
return nil, nil
759+
},
760+
},
761+
function: StandardFunctionGetter[any]{fCtx: FunctionContext{Set: componenttest.NewNopTelemetrySettings()}, fact: functions["no_struct_tag"]},
762+
want: "anything",
763+
valid: false,
764+
expectedErrorMsg: "no `ottlarg` struct tag on Arguments field \"StringArg\"",
765+
},
766+
{
767+
name: "Cannot create function",
768+
getter: StandardStringGetter[interface{}]{
769+
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
770+
return nil, nil
771+
},
772+
},
773+
function: StandardFunctionGetter[any]{fCtx: FunctionContext{Set: componenttest.NewNopTelemetrySettings()}, fact: functions["cannot_create_function"]},
774+
want: "anything",
775+
valid: false,
776+
expectedErrorMsg: "couldn't create function: error",
777+
},
778+
}
779+
780+
for _, tt := range tests {
781+
t.Run(tt.name, func(t *testing.T) {
782+
editorArgs := EditorArguments{
783+
Replacement: tt.getter,
784+
Function: tt.function,
785+
}
786+
fn, err := editorArgs.Function.Get(&FuncArgs{Input: editorArgs.Replacement})
787+
if tt.valid {
788+
var result interface{}
789+
result, err = fn.Eval(context.Background(), nil)
790+
assert.NoError(t, err)
791+
assert.Equal(t, tt.want, result.(string))
792+
} else {
793+
assert.EqualError(t, err, tt.expectedErrorMsg)
794+
}
795+
})
796+
}
797+
}
798+
679799
// nolint:errorlint
680800
func Test_StandardStringGetter_WrappedError(t *testing.T) {
681801
getter := StandardStringGetter[interface{}]{

pkg/ottl/functions.go

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,42 +47,57 @@ func (p *Parser[K]) newFunctionCall(ed editor) (Expr[K], error) {
4747
return Expr[K]{exprFunc: fn}, err
4848
}
4949

50+
func getArgumentIndex(index int, args reflect.Value) (int, error) {
51+
argsType := args.Type()
52+
fieldTag, ok := argsType.Field(index).Tag.Lookup("ottlarg")
53+
if !ok {
54+
return 0, fmt.Errorf("no `ottlarg` struct tag on Arguments field %q", argsType.Field(index).Name)
55+
}
56+
argNum, err := strconv.Atoi(fieldTag)
57+
if err != nil {
58+
return 0, fmt.Errorf("ottlarg struct tag on field %q is not a valid integer: %w", argsType.Field(index).Name, err)
59+
}
60+
if argNum < 0 || argNum >= args.NumField() {
61+
return 0, fmt.Errorf("ottlarg struct tag on field %q has value %d, but must be between 0 and %d", argsType.Field(index).Name, argNum, args.NumField())
62+
}
63+
return argNum, nil
64+
}
65+
5066
func (p *Parser[K]) buildArgs(ed editor, argsVal reflect.Value) error {
5167
if len(ed.Arguments) != argsVal.NumField() {
5268
return fmt.Errorf("incorrect number of arguments. Expected: %d Received: %d", argsVal.NumField(), len(ed.Arguments))
5369
}
5470

55-
argsType := argsVal.Type()
56-
5771
for i := 0; i < argsVal.NumField(); i++ {
5872
field := argsVal.Field(i)
5973
fieldType := field.Type()
60-
61-
fieldTag, ok := argsType.Field(i).Tag.Lookup("ottlarg")
62-
63-
if !ok {
64-
return fmt.Errorf("no `ottlarg` struct tag on Arguments field %q", argsType.Field(i).Name)
65-
}
66-
67-
argNum, err := strconv.Atoi(fieldTag)
68-
74+
argNum, err := getArgumentIndex(i, argsVal)
6975
if err != nil {
70-
return fmt.Errorf("ottlarg struct tag on field %q is not a valid integer: %w", argsType.Field(i).Name, err)
76+
return err
7177
}
72-
73-
if argNum < 0 || argNum >= len(ed.Arguments) {
74-
return fmt.Errorf("ottlarg struct tag on field %q has value %d, but must be between 0 and %d", argsType.Field(i).Name, argNum, len(ed.Arguments))
75-
}
76-
7778
argVal := ed.Arguments[argNum]
78-
7979
var val any
80-
if fieldType.Kind() == reflect.Slice {
80+
switch {
81+
case strings.HasPrefix(fieldType.Name(), "FunctionGetter"):
82+
var name string
83+
switch {
84+
case argVal.Enum != nil:
85+
name = string(*argVal.Enum)
86+
case argVal.FunctionName != nil:
87+
name = *argVal.FunctionName
88+
default:
89+
return fmt.Errorf("invalid function name given")
90+
}
91+
f, ok := p.functions[name]
92+
if !ok {
93+
return fmt.Errorf("undefined function %s", name)
94+
}
95+
val = StandardFunctionGetter[K]{fCtx: FunctionContext{Set: p.telemetrySettings}, fact: f}
96+
case fieldType.Kind() == reflect.Slice:
8197
val, err = p.buildSliceArg(argVal, fieldType)
82-
} else {
98+
default:
8399
val, err = p.buildArg(argVal, fieldType)
84100
}
85-
86101
if err != nil {
87102
return fmt.Errorf("invalid argument at position %v: %w", i, err)
88103
}

0 commit comments

Comments
 (0)