Skip to content

Commit

Permalink
Adding an optional replacementFormat argument to the replace_pattern …
Browse files Browse the repository at this point in the history
…editors that specified the format of the replacement string (#30837)

Description: Adding an optional replacementFormat argument to the
replace_pattern editors that specified the format of the replacement
string

Testing: Unit tests were added for this new optional argument

Documentation:

https://github.com/rnishtala-sumo/opentelemetry-collector-contrib/blob/ottl-replace-pattern/pkg/ottl/ottlfuncs/README.md#replace_pattern
  • Loading branch information
rnishtala-sumo authored Feb 14, 2024
1 parent 37c5073 commit 69b2d51
Show file tree
Hide file tree
Showing 10 changed files with 350 additions and 111 deletions.
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

0 comments on commit 69b2d51

Please sign in to comment.