Skip to content

Commit

Permalink
[pkg/ottl] support top level array values in ParseJSON function (#3…
Browse files Browse the repository at this point in the history
…3908)

**Description:** This PR enables the `ParseJSON` function to also handle
top level arrays.

**Link to tracking Issue:** #33535 

**Testing:** Added unit and e2e tests

**Documentation:** Adapted the documentation of the `ParseJSON` function
to indicate that this can either return a `pcommon.Map` or
`pcommon.Slice` value

---------

Signed-off-by: Florian Bacher <florian.bacher@dynatrace.com>
  • Loading branch information
bacherfl authored Jul 25, 2024
1 parent 59c0c54 commit f85b0e3
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 30 deletions.
27 changes: 27 additions & 0 deletions .chloggen/ottl-parse-json-array.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: bug_fix

# 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: Handle JSON array provided to ParseJSON function

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

# (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: []
8 changes: 8 additions & 0 deletions pkg/ottl/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,14 @@ func Test_e2e_converters(t *testing.T) {
m.PutDouble("id", 1)
},
},
{
statement: `set(attributes["test"], ParseJSON("[\"value1\",\"value2\"]"))`,
want: func(tCtx ottllog.TransformContext) {
m := tCtx.GetLogRecord().Attributes().PutEmptySlice("test")
m.AppendEmpty().SetStr("value1")
m.AppendEmpty().SetStr("value2")
},
},
{
statement: `set(attributes["test"], ParseKeyValue("k1=v1 k2=v2"))`,
want: func(tCtx ottllog.TransformContext) {
Expand Down
5 changes: 4 additions & 1 deletion pkg/ottl/ottlfuncs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1010,7 +1010,7 @@ Examples:

`ParseJSON(target)`

The `ParseJSON` Converter returns a `pcommon.Map` struct that is a result of parsing the target string as JSON
The `ParseJSON` Converter returns a `pcommon.Map` or `pcommon.Slice` struct that is a result of parsing the target string as JSON

`target` is a Getter that returns a string. This string should be in json format.
If `target` is not a string, nil, or cannot be parsed as JSON, `ParseJSON` will return an error.
Expand All @@ -1032,6 +1032,9 @@ Examples:
- `ParseJSON("{\"attr\":true}")`


- `ParseJSON("[\"attr1\",\"attr2\"]")`


- `ParseJSON(attributes["kubernetes"])`


Expand Down
19 changes: 14 additions & 5 deletions pkg/ottl/ottlfuncs/func_parse_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func createParseJSONFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments
return parseJSON(args.Target), nil
}

// parseJSON returns a `pcommon.Map` struct that is a result of parsing the target string as JSON
// parseJSON returns a `pcommon.Map` or `pcommon.Slice` struct that is a result of parsing the target string as JSON
// Each JSON type is converted into a `pdata.Value` using the following map:
//
// JSON boolean -> bool
Expand All @@ -46,13 +46,22 @@ func parseJSON[K any](target ottl.StringGetter[K]) ottl.ExprFunc[K] {
if err != nil {
return nil, err
}
var parsedValue map[string]any
var parsedValue any
err = jsoniter.UnmarshalFromString(targetVal, &parsedValue)
if err != nil {
return nil, err
}
result := pcommon.NewMap()
err = result.FromRaw(parsedValue)
return result, err
switch v := parsedValue.(type) {
case []any:
result := pcommon.NewSlice()
err = result.FromRaw(v)
return result, err
case map[string]any:
result := pcommon.NewMap()
err = result.FromRaw(v)
return result, err
default:
return nil, fmt.Errorf("could not convert parsed value of type %T to JSON object", v)
}
}
}
81 changes: 57 additions & 24 deletions pkg/ottl/ottlfuncs/func_parse_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import (

func Test_ParseJSON(t *testing.T) {
tests := []struct {
name string
target ottl.StringGetter[any]
want func(pcommon.Map)
name string
target ottl.StringGetter[any]
wantMap func(pcommon.Map)
wantSlice func(pcommon.Slice)
}{
{
name: "handle string",
Expand All @@ -27,7 +28,7 @@ func Test_ParseJSON(t *testing.T) {
return `{"test":"string value"}`, nil
},
},
want: func(expectedMap pcommon.Map) {
wantMap: func(expectedMap pcommon.Map) {
expectedMap.PutStr("test", "string value")
},
},
Expand All @@ -38,7 +39,7 @@ func Test_ParseJSON(t *testing.T) {
return `{"test":true}`, nil
},
},
want: func(expectedMap pcommon.Map) {
wantMap: func(expectedMap pcommon.Map) {
expectedMap.PutBool("test", true)
},
},
Expand All @@ -49,7 +50,7 @@ func Test_ParseJSON(t *testing.T) {
return `{"test":1}`, nil
},
},
want: func(expectedMap pcommon.Map) {
wantMap: func(expectedMap pcommon.Map) {
expectedMap.PutDouble("test", 1)
},
},
Expand All @@ -60,7 +61,7 @@ func Test_ParseJSON(t *testing.T) {
return `{"test":1.1}`, nil
},
},
want: func(expectedMap pcommon.Map) {
wantMap: func(expectedMap pcommon.Map) {
expectedMap.PutDouble("test", 1.1)
},
},
Expand All @@ -71,7 +72,7 @@ func Test_ParseJSON(t *testing.T) {
return `{"test":null}`, nil
},
},
want: func(expectedMap pcommon.Map) {
wantMap: func(expectedMap pcommon.Map) {
expectedMap.PutEmpty("test")
},
},
Expand All @@ -82,20 +83,45 @@ func Test_ParseJSON(t *testing.T) {
return `{"test":["string","value"]}`, nil
},
},
want: func(expectedMap pcommon.Map) {
wantMap: func(expectedMap pcommon.Map) {
emptySlice := expectedMap.PutEmptySlice("test")
emptySlice.AppendEmpty().SetStr("string")
emptySlice.AppendEmpty().SetStr("value")
},
},
{
name: "handle top level array",
target: ottl.StandardStringGetter[any]{
Getter: func(_ context.Context, _ any) (any, error) {
return `["string","value"]`, nil
},
},
wantSlice: func(expectedSlice pcommon.Slice) {
expectedSlice.AppendEmpty().SetStr("string")
expectedSlice.AppendEmpty().SetStr("value")
},
},
{
name: "handle top level array of objects",
target: ottl.StandardStringGetter[any]{
Getter: func(_ context.Context, _ any) (any, error) {
return `[{"test":"value"},{"test":"value"}]`, nil
},
},
wantSlice: func(expectedSlice pcommon.Slice) {

expectedSlice.AppendEmpty().SetEmptyMap().PutStr("test", "value")
expectedSlice.AppendEmpty().SetEmptyMap().PutStr("test", "value")
},
},
{
name: "handle nested object",
target: ottl.StandardStringGetter[any]{
Getter: func(_ context.Context, _ any) (any, error) {
return `{"test":{"nested":"true"}}`, nil
},
},
want: func(expectedMap pcommon.Map) {
wantMap: func(expectedMap pcommon.Map) {
newMap := expectedMap.PutEmptyMap("test")
newMap.PutStr("nested", "true")
},
Expand All @@ -107,7 +133,7 @@ func Test_ParseJSON(t *testing.T) {
return `{"existing":"pass"}`, nil
},
},
want: func(expectedMap pcommon.Map) {
wantMap: func(expectedMap pcommon.Map) {
expectedMap.PutStr("existing", "pass")
},
},
Expand All @@ -118,7 +144,7 @@ func Test_ParseJSON(t *testing.T) {
return `{"test1":{"nested":"true"},"test2":"string","test3":1,"test4":1.1,"test5":[[1], [2, 3],[]],"test6":null}`, nil
},
},
want: func(expectedMap pcommon.Map) {
wantMap: func(expectedMap pcommon.Map) {
newMap := expectedMap.PutEmptyMap("test1")
newMap.PutStr("nested", "true")
expectedMap.PutStr("test2", "string")
Expand All @@ -141,19 +167,26 @@ func Test_ParseJSON(t *testing.T) {
result, err := exprFunc(context.Background(), nil)
assert.NoError(t, err)

resultMap, ok := result.(pcommon.Map)
require.True(t, ok)

expected := pcommon.NewMap()
tt.want(expected)
if tt.wantMap != nil {
resultMap, ok := result.(pcommon.Map)
require.True(t, ok)
expected := pcommon.NewMap()
tt.wantMap(expected)
assert.Equal(t, expected.Len(), resultMap.Len())
expected.Range(func(k string, _ pcommon.Value) bool {
ev, _ := expected.Get(k)
av, _ := resultMap.Get(k)
assert.Equal(t, ev, av)
return true
})
} else if tt.wantSlice != nil {
resultSlice, ok := result.(pcommon.Slice)
require.True(t, ok)
expected := pcommon.NewSlice()
tt.wantSlice(expected)
assert.Equal(t, expected, resultSlice)
}

assert.Equal(t, expected.Len(), resultMap.Len())
expected.Range(func(k string, _ pcommon.Value) bool {
ev, _ := expected.Get(k)
av, _ := resultMap.Get(k)
assert.Equal(t, ev, av)
return true
})
})
}
}
Expand Down

0 comments on commit f85b0e3

Please sign in to comment.