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

Adding an optional replacementFormat argument to the replace_pattern editors that specified the format of the replacement string #30837

Merged
merged 6 commits into from
Feb 14, 2024
27 changes: 27 additions & 0 deletions .chloggen/ottl-replace-pattern.yaml
Original file line number Diff line number Diff line change
@@ -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. filelogreceiver)
component: pkg/ottl

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add support to specify the format for a replacement string

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [27820]

# (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: []
18 changes: 12 additions & 6 deletions pkg/ottl/ottlfuncs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,17 +251,19 @@ Examples:
The `replace_all_matches` function replaces any matching string value with the replacement string.

`target` is a path expression to a `pcommon.Map` type field. `pattern` is a string following [filepath.Match syntax](https://pkg.go.dev/path/filepath#Match). `replacement` is either a path expression to a string telemetry field or a literal string. `function` is an optional argument that can take in any Converter that accepts a (`replacement`) string and returns a string. An example is a hash function that replaces any matching string with the hash value of `replacement`.
`replacementFormat` is an optional string argument that specifies the format of the replacement. It must contain exactly one `%s` format specifier as shown in the example below. No other format specifiers are supported.

Each string value in `target` that matches `pattern` will get replaced with `replacement`. Non-string values are ignored.

Examples:

- `replace_all_matches(attributes, "/user/*/list/*", "/user/{userId}/list/{listId}")`
- `replace_all_matches(attributes, "/user/*/list/*", "/user/{userId}/list/{listId}", SHA256)`
- `replace_all_matches(attributes, "/user/*/list/*", "/user/{userId}/list/{listId}", SHA256, "/user/%s")`

### replace_all_patterns

`replace_all_patterns(target, mode, regex, replacement, Optional[function])`
`replace_all_patterns(target, mode, regex, replacement, function, replacementFormat)`

The `replace_all_patterns` function replaces any segments in a string value or key that match the regex pattern with the replacement string.

Expand All @@ -271,7 +273,7 @@ The `replace_all_patterns` function replaces any segments in a string value or k

If one or more sections of `target` match `regex` they will get replaced with `replacement`.

The `replacement` string can refer to matched groups using [regexp.Expand syntax](https://pkg.go.dev/regexp#Regexp.Expand).
The `replacement` string can refer to matched groups using [regexp.Expand syntax](https://pkg.go.dev/regexp#Regexp.Expand). `replacementFormat` is an optional string argument that specifies the format of the replacement. It must contain exactly one `%s` format specifier as shown in the example below. No other format specifiers are supported.

The `function` is an optional argument that can take in any Converter that accepts a (`replacement`) string and returns a string. An example is a hash function that replaces any matching regex pattern with the hash value of `replacement`.

Expand All @@ -280,7 +282,8 @@ Examples:
- `replace_all_patterns(attributes, "value", "/account/\\d{4}", "/account/{accountId}")`
- `replace_all_patterns(attributes, "key", "/account/\\d{4}", "/account/{accountId}")`
- `replace_all_patterns(attributes, "key", "^kube_([0-9A-Za-z]+_)", "k8s.$$1.")`
- `replace_all_patterns(attributes, "key", "^kube_([0-9A-Za-z]+_)", "k8s.$$1.", SHA256)`
- `replace_all_patterns(attributes, "key", "^kube_([0-9A-Za-z]+_)", "$$1.")`
- `replace_all_patterns(attributes, "key", "^kube_([0-9A-Za-z]+_)", "$$1.", SHA256, "k8s.%s")`

Note that when using OTTL within the collector's configuration file, `$` must be escaped to `$$` to bypass
environment variable substitution logic. To input a literal `$` from the configuration file, use `$$$`.
Expand All @@ -293,6 +296,7 @@ If using OTTL outside of collector configuration, `$` should not be escaped and
The `replace_match` function allows replacing entire strings if they match a glob pattern.

`target` is a path expression to a telemetry field. `pattern` is a string following [filepath.Match syntax](https://pkg.go.dev/path/filepath#Match). `replacement` is either a path expression to a string telemetry field or a literal string.
`replacementFormat` is an optional string argument that specifies the format of the replacement. It must contain exactly one `%s` format specifier as shown in the example below. No other format specifiers are supported.

If `target` matches `pattern` it will get replaced with `replacement`.

Expand All @@ -301,27 +305,29 @@ The `function` is an optional argument that can take in any Converter that accep
Examples:

- `replace_match(attributes["http.target"], "/user/*/list/*", "/user/{userId}/list/{listId}")`
- `replace_match(attributes["http.target"], "/user/*/list/*", "/user/{userId}/list/{listId}", SHA256)`
- `replace_match(attributes["http.target"], "/user/*/list/*", "/user/{userId}/list/{listId}", SHA256, "/user/%s")`

### replace_pattern

`replace_pattern(target, regex, replacement, Optional[function])`
`replace_pattern(target, regex, replacement, function)`
`replace_pattern(target, regex, replacement, function, replacementFormat)`

The `replace_pattern` function allows replacing all string sections that match a regex pattern with a new value.

`target` is a path expression to a telemetry field. `regex` is a regex string indicating a segment to replace. `replacement` is either a path expression to a string telemetry field or a literal string.

If one or more sections of `target` match `regex` they will get replaced with `replacement`.

The `replacement` string can refer to matched groups using [regexp.Expand syntax](https://pkg.go.dev/regexp#Regexp.Expand).
The `replacement` string can refer to matched groups using [regexp.Expand syntax](https://pkg.go.dev/regexp#Regexp.Expand). `replacementFormat` is an optional string argument that specifies the format of the replacement. It must contain exactly one `%s` format specifier as shown in the example below. No other format specifiers are supported

The `function` is an optional argument that can take in any Converter that accepts a (`replacement`) string and returns a string. An example is a hash function that replaces a matching regex pattern with the hash value of `replacement`.

Examples:

- `replace_pattern(resource.attributes["process.command_line"], "password\\=[^\\s]*(\\s?)", "password=***")`
- `replace_pattern(name, "^kube_([0-9A-Za-z]+_)", "k8s.$$1.")`
- `replace_pattern(name, "^kube_([0-9A-Za-z]+_)", "k8s.$$1.", SHA256)`
- `replace_pattern(name, "^kube_([0-9A-Za-z]+_)", "$$1.", SHA256, "k8s.%s")`

Note that when using OTTL within the collector's configuration file, `$` must be escaped to `$$` to bypass
environment variable substitution logic. To input a literal `$` from the configuration file, use `$$$`.
Expand Down
18 changes: 11 additions & 7 deletions pkg/ottl/ottlfuncs/func_replace_all_matches.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import (
)

type ReplaceAllMatchesArguments[K any] struct {
Target ottl.PMapGetter[K]
Pattern string
Replacement ottl.StringGetter[K]
Function ottl.Optional[ottl.FunctionGetter[K]]
Target ottl.PMapGetter[K]
Pattern string
Replacement ottl.StringGetter[K]
Function ottl.Optional[ottl.FunctionGetter[K]]
ReplacementFormat ottl.Optional[ottl.StringGetter[K]]
}

type replaceAllMatchesFuncArgs[K any] struct {
Expand All @@ -35,10 +36,10 @@ func createReplaceAllMatchesFunction[K any](_ ottl.FunctionContext, oArgs ottl.A
return nil, fmt.Errorf("ReplaceAllMatchesFactory args must be of type *ReplaceAllMatchesArguments[K]")
}

return replaceAllMatches(args.Target, args.Pattern, args.Replacement, args.Function)
return replaceAllMatches(args.Target, args.Pattern, args.Replacement, args.Function, args.ReplacementFormat)
}

func replaceAllMatches[K any](target ottl.PMapGetter[K], pattern string, replacement ottl.StringGetter[K], fn ottl.Optional[ottl.FunctionGetter[K]]) (ottl.ExprFunc[K], error) {
func replaceAllMatches[K any](target ottl.PMapGetter[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)
Expand Down Expand Up @@ -68,7 +69,10 @@ func replaceAllMatches[K any](target ottl.PMapGetter[K], pattern string, replace
if !ok {
return nil, fmt.Errorf("replacement value is not a string")
}
replacementVal = replacementValStr
replacementVal, err = applyReplaceFormat(ctx, tCtx, replacementFormat, replacementValStr)
if err != nil {
return nil, err
}
}
val.Range(func(key string, value pcommon.Value) bool {
if glob.Match(value.Str()) {
Expand Down
45 changes: 29 additions & 16 deletions pkg/ottl/ottlfuncs/func_replace_all_matches_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ func Test_replaceAllMatches(t *testing.T) {
},
Fact: optionalFnTestFactory[pcommon.Map](),
}
prefix := ottl.StandardStringGetter[pcommon.Map]{
Getter: func(context.Context, pcommon.Map) (any, error) {
return "prefix=%s", nil
},
}
optionalArg := ottl.NewTestingOptional[ottl.FunctionGetter[pcommon.Map]](ottlValue)

target := &ottl.StandardPMapGetter[pcommon.Map]{
Expand All @@ -36,12 +41,13 @@ func Test_replaceAllMatches(t *testing.T) {
}

tests := []struct {
name string
target ottl.PMapGetter[pcommon.Map]
pattern string
replacement ottl.StringGetter[pcommon.Map]
function ottl.Optional[ottl.FunctionGetter[pcommon.Map]]
want func(pcommon.Map)
name string
target ottl.PMapGetter[pcommon.Map]
pattern string
replacement ottl.StringGetter[pcommon.Map]
function ottl.Optional[ottl.FunctionGetter[pcommon.Map]]
replacementFormat ottl.Optional[ottl.StringGetter[pcommon.Map]]
want func(pcommon.Map)
}{
{
name: "replace only matches (with hash function)",
Expand All @@ -52,10 +58,11 @@ func Test_replaceAllMatches(t *testing.T) {
return "hello {universe}", nil
},
},
function: optionalArg,
function: optionalArg,
replacementFormat: ottl.NewTestingOptional[ottl.StringGetter[pcommon.Map]](prefix),
want: func(expectedMap pcommon.Map) {
expectedMap.PutStr("test", "hash(hello {universe})")
expectedMap.PutStr("test2", "hash(hello {universe})")
expectedMap.PutStr("test", "prefix=hash(hello {universe})")
expectedMap.PutStr("test2", "prefix=hash(hello {universe})")
expectedMap.PutStr("test3", "goodbye")
},
},
Expand All @@ -68,7 +75,8 @@ func Test_replaceAllMatches(t *testing.T) {
return "hello {universe}", nil
},
},
function: ottl.Optional[ottl.FunctionGetter[pcommon.Map]]{},
function: ottl.Optional[ottl.FunctionGetter[pcommon.Map]]{},
replacementFormat: ottl.Optional[ottl.StringGetter[pcommon.Map]]{},
want: func(expectedMap pcommon.Map) {
expectedMap.PutStr("test", "hello {universe}")
expectedMap.PutStr("test2", "hello {universe}")
Expand All @@ -84,7 +92,8 @@ func Test_replaceAllMatches(t *testing.T) {
return "nothing {matches}", nil
},
},
function: ottl.Optional[ottl.FunctionGetter[pcommon.Map]]{},
function: ottl.Optional[ottl.FunctionGetter[pcommon.Map]]{},
replacementFormat: ottl.Optional[ottl.StringGetter[pcommon.Map]]{},
want: func(expectedMap pcommon.Map) {
expectedMap.PutStr("test", "hello world")
expectedMap.PutStr("test2", "hello")
Expand All @@ -97,7 +106,7 @@ func Test_replaceAllMatches(t *testing.T) {
scenarioMap := pcommon.NewMap()
input.CopyTo(scenarioMap)

exprFunc, err := replaceAllMatches(tt.target, tt.pattern, tt.replacement, tt.function)
exprFunc, err := replaceAllMatches(tt.target, tt.pattern, tt.replacement, tt.function, tt.replacementFormat)
assert.NoError(t, err)

result, err := exprFunc(nil, scenarioMap)
Expand Down Expand Up @@ -125,8 +134,9 @@ 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)
exprFunc, err := replaceAllMatches[any](target, "*", replacement, function, replacementFormat)
assert.NoError(t, err)
_, err = exprFunc(nil, input)
assert.Error(t, err)
Expand All @@ -145,8 +155,9 @@ 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)
exprFunc, err := replaceAllMatches[any](target, "regexp", replacement, function, replacementFormat)
assert.NoError(t, err)

result, err := exprFunc(nil, input)
Expand Down Expand Up @@ -174,8 +185,9 @@ func Test_replaceAllMatches_bad_function_result(t *testing.T) {
Fact: StandardConverters[any]()["IsString"],
}
function := ottl.NewTestingOptional[ottl.FunctionGetter[any]](ottlValue)
replacementFormat := ottl.Optional[ottl.StringGetter[any]]{}

exprFunc, err := replaceAllMatches[any](target, "regexp", replacement, function)
exprFunc, err := replaceAllMatches[any](target, "regexp", replacement, function, replacementFormat)
assert.NoError(t, err)

result, err := exprFunc(nil, input)
Expand All @@ -195,8 +207,9 @@ 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)
exprFunc, err := replaceAllMatches[any](target, "*", replacement, function, replacementFormat)
assert.NoError(t, err)
_, err = exprFunc(nil, nil)
assert.Error(t, err)
Expand Down
19 changes: 10 additions & 9 deletions pkg/ottl/ottlfuncs/func_replace_all_patterns.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ const (
)

type ReplaceAllPatternsArguments[K any] struct {
Target ottl.PMapGetter[K]
Mode string
RegexPattern string
Replacement ottl.StringGetter[K]
Function ottl.Optional[ottl.FunctionGetter[K]]
Target ottl.PMapGetter[K]
Mode string
RegexPattern string
Replacement ottl.StringGetter[K]
Function ottl.Optional[ottl.FunctionGetter[K]]
ReplacementFormat ottl.Optional[ottl.StringGetter[K]]
}

func NewReplaceAllPatternsFactory[K any]() ottl.Factory[K] {
Expand All @@ -37,10 +38,10 @@ func createReplaceAllPatternsFunction[K any](_ ottl.FunctionContext, oArgs ottl.
return nil, fmt.Errorf("ReplaceAllPatternsFactory args must be of type *ReplaceAllPatternsArguments[K]")
}

return replaceAllPatterns(args.Target, args.Mode, args.RegexPattern, args.Replacement, args.Function)
return replaceAllPatterns(args.Target, args.Mode, args.RegexPattern, args.Replacement, args.Function, args.ReplacementFormat)
}

func replaceAllPatterns[K any](target ottl.PMapGetter[K], mode string, regexPattern string, replacement ottl.StringGetter[K], fn ottl.Optional[ottl.FunctionGetter[K]]) (ottl.ExprFunc[K], error) {
func replaceAllPatterns[K any](target ottl.PMapGetter[K], mode string, regexPattern string, replacement ottl.StringGetter[K], fn ottl.Optional[ottl.FunctionGetter[K]], replacementFormat ottl.Optional[ottl.StringGetter[K]]) (ottl.ExprFunc[K], error) {
compiledPattern, err := regexp.Compile(regexPattern)
if err != nil {
return nil, fmt.Errorf("the regex pattern supplied to replace_all_patterns is not a valid pattern: %w", err)
Expand All @@ -66,7 +67,7 @@ func replaceAllPatterns[K any](target ottl.PMapGetter[K], mode string, regexPatt
case modeValue:
if compiledPattern.MatchString(originalValue.Str()) {
if !fn.IsEmpty() {
updatedString, err := applyOptReplaceFunction(ctx, tCtx, compiledPattern, fn, originalValue.Str(), replacementVal)
updatedString, err := applyOptReplaceFunction(ctx, tCtx, compiledPattern, fn, originalValue.Str(), replacementVal, replacementFormat)
if err != nil {
return false
}
Expand All @@ -81,7 +82,7 @@ func replaceAllPatterns[K any](target ottl.PMapGetter[K], mode string, regexPatt
case modeKey:
if compiledPattern.MatchString(key) {
if !fn.IsEmpty() {
updatedString, err := applyOptReplaceFunction(ctx, tCtx, compiledPattern, fn, key, replacementVal)
updatedString, err := applyOptReplaceFunction(ctx, tCtx, compiledPattern, fn, key, replacementVal, replacementFormat)
if err != nil {
return false
}
Expand Down
Loading
Loading