From a5384d91f2a1928be46ec83ead8a2ec0bc82df6e Mon Sep 17 00:00:00 2001 From: odubajDT Date: Thu, 23 Oct 2025 11:08:57 +0200 Subject: [PATCH] [pkg/ottl] Support taking match patterns from runtime data in the replace_all_matches() and replace_match() functions Signed-off-by: odubajDT --- .chloggen/ottl-dynamic-replace-match.yaml | 27 +++++++++++++ pkg/ottl/e2e/e2e_test.go | 13 ++++++ pkg/ottl/functions.go | 2 + .../ottlfuncs/func_replace_all_matches.go | 28 ++++++++++--- .../func_replace_all_matches_test.go | 40 ++++++++++++++++--- pkg/ottl/ottlfuncs/func_replace_match.go | 28 ++++++++++--- pkg/ottl/ottlfuncs/func_replace_match_test.go | 40 ++++++++++++++++--- 7 files changed, 156 insertions(+), 22 deletions(-) create mode 100644 .chloggen/ottl-dynamic-replace-match.yaml diff --git a/.chloggen/ottl-dynamic-replace-match.yaml b/.chloggen/ottl-dynamic-replace-match.yaml new file mode 100644 index 0000000000000..f851906041896 --- /dev/null +++ b/.chloggen/ottl-dynamic-replace-match.yaml @@ -0,0 +1,27 @@ +# 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. receiver/filelog) +component: pkg/ottl + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: "Support taking match patterns from runtime data in the `replace_all_matches` and `replace_match` functions." + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [43555] + +# (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: + +# 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: [] \ No newline at end of file diff --git a/pkg/ottl/e2e/e2e_test.go b/pkg/ottl/e2e/e2e_test.go index a119a13998ae8..9c14c79748d4a 100644 --- a/pkg/ottl/e2e/e2e_test.go +++ b/pkg/ottl/e2e/e2e_test.go @@ -241,6 +241,13 @@ func Test_e2e_editors(t *testing.T) { tCtx.GetLogRecord().Attributes().PutStr("http.url", "test") }, }, + { + statement: `replace_all_matches(attributes, Concat(["*","/","*"],""), "test")`, + want: func(tCtx ottllog.TransformContext) { + tCtx.GetLogRecord().Attributes().PutStr("http.path", "test") + tCtx.GetLogRecord().Attributes().PutStr("http.url", "test") + }, + }, { statement: `replace_all_patterns(attributes, "key", "^http", "test")`, want: func(tCtx ottllog.TransformContext) { @@ -259,6 +266,12 @@ func Test_e2e_editors(t *testing.T) { tCtx.GetLogRecord().Attributes().PutStr("http.url", "http:@@localhost@health") }, }, + { + statement: `replace_match(attributes["http.path"], Concat(["*","/","*"],""), "test")`, + want: func(tCtx ottllog.TransformContext) { + tCtx.GetLogRecord().Attributes().PutStr("http.path", "test") + }, + }, { statement: `replace_match(attributes["http.path"], "*/*", "test")`, want: func(tCtx ottllog.TransformContext) { diff --git a/pkg/ottl/functions.go b/pkg/ottl/functions.go index ce3b24339e85b..0e73dc19bea41 100644 --- a/pkg/ottl/functions.go +++ b/pkg/ottl/functions.go @@ -26,6 +26,8 @@ type Enum int64 // EnumSymbol is how OTTL represents an enum's string value. type EnumSymbol string +const InvalidRegexErrMsg = "the regex pattern supplied to %s '%q' is not a valid pattern: %w" + func buildOriginalText(path *path) string { var builder strings.Builder if path.Context != "" { diff --git a/pkg/ottl/ottlfuncs/func_replace_all_matches.go b/pkg/ottl/ottlfuncs/func_replace_all_matches.go index 8639e62015ba6..0ba0c40771f58 100644 --- a/pkg/ottl/ottlfuncs/func_replace_all_matches.go +++ b/pkg/ottl/ottlfuncs/func_replace_all_matches.go @@ -16,7 +16,7 @@ import ( type ReplaceAllMatchesArguments[K any] struct { Target ottl.PMapGetSetter[K] - Pattern string + Pattern ottl.StringGetter[K] Replacement ottl.StringGetter[K] Function ottl.Optional[ottl.FunctionGetter[K]] ReplacementFormat ottl.Optional[ottl.StringGetter[K]] @@ -40,12 +40,28 @@ func createReplaceAllMatchesFunction[K any](_ ottl.FunctionContext, oArgs ottl.A return replaceAllMatches(args.Target, args.Pattern, args.Replacement, args.Function, args.ReplacementFormat) } -func replaceAllMatches[K any](target ottl.PMapGetSetter[K], pattern string, replacement ottl.StringGetter[K], fn ottl.Optional[ottl.FunctionGetter[K]], replacementFormat ottl.Optional[ottl.StringGetter[K]]) (ottl.ExprFunc[K], error) { - glob, err := glob.Compile(pattern) - if err != nil { - return nil, fmt.Errorf("the pattern supplied to replace_match is not a valid pattern: %w", err) +func replaceAllMatches[K any](target ottl.PMapGetSetter[K], pattern, replacement ottl.StringGetter[K], fn ottl.Optional[ottl.FunctionGetter[K]], replacementFormat ottl.Optional[ottl.StringGetter[K]]) (ottl.ExprFunc[K], error) { + literalPattern, ok := ottl.GetLiteralValue(pattern) + var compiledPattern glob.Glob + var err error + if ok { + compiledPattern, err = glob.Compile(literalPattern) + if err != nil { + return nil, fmt.Errorf(ottl.InvalidRegexErrMsg, "replace_all_matches", literalPattern, err) + } } return func(ctx context.Context, tCtx K) (any, error) { + cp := compiledPattern + if cp == nil { + patternVal, err := pattern.Get(ctx, tCtx) + if err != nil { + return nil, err + } + cp, err = glob.Compile(patternVal) + if err != nil { + return nil, fmt.Errorf(ottl.InvalidRegexErrMsg, "replace_all_matches", patternVal, err) + } + } val, err := target.Get(ctx, tCtx) if err != nil { return nil, err @@ -78,7 +94,7 @@ func replaceAllMatches[K any](target ottl.PMapGetSetter[K], pattern string, repl } for _, value := range val.All() { - if value.Type() == pcommon.ValueTypeStr && glob.Match(value.Str()) { + if value.Type() == pcommon.ValueTypeStr && cp.Match(value.Str()) { value.SetStr(replacementVal) } } diff --git a/pkg/ottl/ottlfuncs/func_replace_all_matches_test.go b/pkg/ottl/ottlfuncs/func_replace_all_matches_test.go index 173c4336554b3..6e48e50f1b041 100644 --- a/pkg/ottl/ottlfuncs/func_replace_all_matches_test.go +++ b/pkg/ottl/ottlfuncs/func_replace_all_matches_test.go @@ -138,7 +138,13 @@ func Test_replaceAllMatches(t *testing.T) { }, } - exprFunc, err := replaceAllMatches(target, tt.pattern, tt.replacement, tt.function, tt.replacementFormat) + pattern := ottl.StandardStringGetter[pcommon.Map]{ + Getter: func(_ context.Context, _ pcommon.Map) (any, error) { + return tt.pattern, nil + }, + } + + exprFunc, err := replaceAllMatches(target, pattern, tt.replacement, tt.function, tt.replacementFormat) assert.NoError(t, err) _, err = exprFunc(nil, scenarioMap) @@ -171,7 +177,13 @@ func Test_replaceAllMatches_bad_input(t *testing.T) { function := ottl.Optional[ottl.FunctionGetter[any]]{} replacementFormat := ottl.Optional[ottl.StringGetter[any]]{} - exprFunc, err := replaceAllMatches[any](target, "*", replacement, function, replacementFormat) + pattern := ottl.StandardStringGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return "*", nil + }, + } + + exprFunc, err := replaceAllMatches[any](target, pattern, replacement, function, replacementFormat) assert.NoError(t, err) _, err = exprFunc(nil, input) assert.Error(t, err) @@ -195,7 +207,13 @@ func Test_replaceAllMatches_bad_function_input(t *testing.T) { function := ottl.Optional[ottl.FunctionGetter[any]]{} replacementFormat := ottl.Optional[ottl.StringGetter[any]]{} - exprFunc, err := replaceAllMatches[any](target, "regexp", replacement, function, replacementFormat) + pattern := ottl.StandardStringGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return "regexp", nil + }, + } + + exprFunc, err := replaceAllMatches[any](target, pattern, replacement, function, replacementFormat) assert.NoError(t, err) result, err := exprFunc(nil, input) @@ -228,7 +246,13 @@ func Test_replaceAllMatches_bad_function_result(t *testing.T) { function := ottl.NewTestingOptional[ottl.FunctionGetter[any]](ottlValue) replacementFormat := ottl.Optional[ottl.StringGetter[any]]{} - exprFunc, err := replaceAllMatches[any](target, "regexp", replacement, function, replacementFormat) + pattern := ottl.StandardStringGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return "regexp", nil + }, + } + + exprFunc, err := replaceAllMatches[any](target, pattern, replacement, function, replacementFormat) assert.NoError(t, err) result, err := exprFunc(nil, input) @@ -253,7 +277,13 @@ func Test_replaceAllMatches_get_nil(t *testing.T) { function := ottl.Optional[ottl.FunctionGetter[any]]{} replacementFormat := ottl.Optional[ottl.StringGetter[any]]{} - exprFunc, err := replaceAllMatches[any](target, "*", replacement, function, replacementFormat) + pattern := ottl.StandardStringGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return "*", nil + }, + } + + exprFunc, err := replaceAllMatches[any](target, pattern, replacement, function, replacementFormat) assert.NoError(t, err) _, err = exprFunc(nil, nil) assert.Error(t, err) diff --git a/pkg/ottl/ottlfuncs/func_replace_match.go b/pkg/ottl/ottlfuncs/func_replace_match.go index 8deb7ba4f506e..023f4566d0a7b 100644 --- a/pkg/ottl/ottlfuncs/func_replace_match.go +++ b/pkg/ottl/ottlfuncs/func_replace_match.go @@ -15,7 +15,7 @@ import ( type ReplaceMatchArguments[K any] struct { Target ottl.GetSetter[K] - Pattern string + Pattern ottl.StringGetter[K] Replacement ottl.StringGetter[K] Function ottl.Optional[ottl.FunctionGetter[K]] ReplacementFormat ottl.Optional[ottl.StringGetter[K]] @@ -39,12 +39,28 @@ func createReplaceMatchFunction[K any](_ ottl.FunctionContext, oArgs ottl.Argume return replaceMatch(args.Target, args.Pattern, args.Replacement, args.Function, args.ReplacementFormat) } -func replaceMatch[K any](target ottl.GetSetter[K], pattern string, replacement ottl.StringGetter[K], fn ottl.Optional[ottl.FunctionGetter[K]], replacementFormat ottl.Optional[ottl.StringGetter[K]]) (ottl.ExprFunc[K], error) { - glob, err := glob.Compile(pattern) - if err != nil { - return nil, fmt.Errorf("the pattern supplied to replace_match is not a valid pattern: %w", err) +func replaceMatch[K any](target ottl.GetSetter[K], pattern, replacement ottl.StringGetter[K], fn ottl.Optional[ottl.FunctionGetter[K]], replacementFormat ottl.Optional[ottl.StringGetter[K]]) (ottl.ExprFunc[K], error) { + literalPattern, ok := ottl.GetLiteralValue(pattern) + var compiledPattern glob.Glob + var err error + if ok { + compiledPattern, err = glob.Compile(literalPattern) + if err != nil { + return nil, fmt.Errorf(ottl.InvalidRegexErrMsg, "replace_match", literalPattern, err) + } } return func(ctx context.Context, tCtx K) (any, error) { + cp := compiledPattern + if cp == nil { + patternVal, err := pattern.Get(ctx, tCtx) + if err != nil { + return nil, err + } + cp, err = glob.Compile(patternVal) + if err != nil { + return nil, fmt.Errorf(ottl.InvalidRegexErrMsg, "replace_all_matches", patternVal, err) + } + } val, err := target.Get(ctx, tCtx) var replacementVal string if err != nil { @@ -81,7 +97,7 @@ func replaceMatch[K any](target ottl.GetSetter[K], pattern string, replacement o return nil, nil } if valStr, ok := val.(string); ok { - if glob.Match(valStr) { + if cp.Match(valStr) { err = target.Set(ctx, tCtx, replacementVal) if err != nil { return nil, err diff --git a/pkg/ottl/ottlfuncs/func_replace_match_test.go b/pkg/ottl/ottlfuncs/func_replace_match_test.go index 33801f5c48def..4dba1fc82de27 100644 --- a/pkg/ottl/ottlfuncs/func_replace_match_test.go +++ b/pkg/ottl/ottlfuncs/func_replace_match_test.go @@ -98,7 +98,13 @@ func Test_replaceMatch(t *testing.T) { t.Run(tt.name, func(t *testing.T) { scenarioValue := pcommon.NewValueStr(input.Str()) - exprFunc, err := replaceMatch(tt.target, tt.pattern, tt.replacement, tt.function, tt.replacementFormat) + pattern := ottl.StandardStringGetter[pcommon.Value]{ + Getter: func(_ context.Context, _ pcommon.Value) (any, error) { + return tt.pattern, nil + }, + } + + exprFunc, err := replaceMatch(tt.target, pattern, tt.replacement, tt.function, tt.replacementFormat) assert.NoError(t, err) result, err := exprFunc(nil, scenarioValue) assert.NoError(t, err) @@ -131,7 +137,13 @@ func Test_replaceMatch_bad_input(t *testing.T) { function := ottl.Optional[ottl.FunctionGetter[any]]{} replacementFormat := ottl.Optional[ottl.StringGetter[any]]{} - exprFunc, err := replaceMatch[any](target, "*", replacement, function, replacementFormat) + pattern := ottl.StandardStringGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return "*", nil + }, + } + + exprFunc, err := replaceMatch[any](target, pattern, replacement, function, replacementFormat) assert.NoError(t, err) result, err := exprFunc(nil, input) @@ -160,7 +172,13 @@ func Test_replaceMatch_bad_function_input(t *testing.T) { function := ottl.Optional[ottl.FunctionGetter[any]]{} replacementFormat := ottl.Optional[ottl.StringGetter[any]]{} - exprFunc, err := replaceMatch[any](target, "regexp", replacement, function, replacementFormat) + pattern := ottl.StandardStringGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return "regexp", nil + }, + } + + exprFunc, err := replaceMatch[any](target, pattern, replacement, function, replacementFormat) assert.NoError(t, err) result, err := exprFunc(nil, input) @@ -194,7 +212,13 @@ func Test_replaceMatch_bad_function_result(t *testing.T) { function := ottl.NewTestingOptional[ottl.FunctionGetter[any]](ottlValue) replacementFormat := ottl.Optional[ottl.StringGetter[any]]{} - exprFunc, err := replaceMatch[any](target, "regexp", replacement, function, replacementFormat) + pattern := ottl.StandardStringGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return "regexp", nil + }, + } + + exprFunc, err := replaceMatch[any](target, pattern, replacement, function, replacementFormat) assert.NoError(t, err) result, err := exprFunc(nil, input) @@ -221,7 +245,13 @@ func Test_replaceMatch_get_nil(t *testing.T) { function := ottl.Optional[ottl.FunctionGetter[any]]{} replacementFormat := ottl.Optional[ottl.StringGetter[any]]{} - exprFunc, err := replaceMatch[any](target, "*", replacement, function, replacementFormat) + pattern := ottl.StandardStringGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return "*", nil + }, + } + + exprFunc, err := replaceMatch[any](target, pattern, replacement, function, replacementFormat) assert.NoError(t, err) result, err := exprFunc(nil, nil)