Skip to content

Commit f85b0e3

Browse files
authored
[pkg/ottl] support top level array values in ParseJSON function (#33908)
**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 <[email protected]>
1 parent 59c0c54 commit f85b0e3

File tree

5 files changed

+110
-30
lines changed

5 files changed

+110
-30
lines changed

.chloggen/ottl-parse-json-array.yaml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: bug_fix
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: pkg/ottl
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Handle JSON array provided to ParseJSON function
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [33535]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: []

pkg/ottl/e2e/e2e_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,14 @@ func Test_e2e_converters(t *testing.T) {
547547
m.PutDouble("id", 1)
548548
},
549549
},
550+
{
551+
statement: `set(attributes["test"], ParseJSON("[\"value1\",\"value2\"]"))`,
552+
want: func(tCtx ottllog.TransformContext) {
553+
m := tCtx.GetLogRecord().Attributes().PutEmptySlice("test")
554+
m.AppendEmpty().SetStr("value1")
555+
m.AppendEmpty().SetStr("value2")
556+
},
557+
},
550558
{
551559
statement: `set(attributes["test"], ParseKeyValue("k1=v1 k2=v2"))`,
552560
want: func(tCtx ottllog.TransformContext) {

pkg/ottl/ottlfuncs/README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,7 @@ Examples:
10101010

10111011
`ParseJSON(target)`
10121012

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

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

10341034

1035+
- `ParseJSON("[\"attr1\",\"attr2\"]")`
1036+
1037+
10351038
- `ParseJSON(attributes["kubernetes"])`
10361039

10371040

pkg/ottl/ottlfuncs/func_parse_json.go

+14-5
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func createParseJSONFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments
3131
return parseJSON(args.Target), nil
3232
}
3333

34-
// parseJSON returns a `pcommon.Map` struct that is a result of parsing the target string as JSON
34+
// parseJSON returns a `pcommon.Map` or `pcommon.Slice` struct that is a result of parsing the target string as JSON
3535
// Each JSON type is converted into a `pdata.Value` using the following map:
3636
//
3737
// JSON boolean -> bool
@@ -46,13 +46,22 @@ func parseJSON[K any](target ottl.StringGetter[K]) ottl.ExprFunc[K] {
4646
if err != nil {
4747
return nil, err
4848
}
49-
var parsedValue map[string]any
49+
var parsedValue any
5050
err = jsoniter.UnmarshalFromString(targetVal, &parsedValue)
5151
if err != nil {
5252
return nil, err
5353
}
54-
result := pcommon.NewMap()
55-
err = result.FromRaw(parsedValue)
56-
return result, err
54+
switch v := parsedValue.(type) {
55+
case []any:
56+
result := pcommon.NewSlice()
57+
err = result.FromRaw(v)
58+
return result, err
59+
case map[string]any:
60+
result := pcommon.NewMap()
61+
err = result.FromRaw(v)
62+
return result, err
63+
default:
64+
return nil, fmt.Errorf("could not convert parsed value of type %T to JSON object", v)
65+
}
5766
}
5867
}

pkg/ottl/ottlfuncs/func_parse_json_test.go

+57-24
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ import (
1616

1717
func Test_ParseJSON(t *testing.T) {
1818
tests := []struct {
19-
name string
20-
target ottl.StringGetter[any]
21-
want func(pcommon.Map)
19+
name string
20+
target ottl.StringGetter[any]
21+
wantMap func(pcommon.Map)
22+
wantSlice func(pcommon.Slice)
2223
}{
2324
{
2425
name: "handle string",
@@ -27,7 +28,7 @@ func Test_ParseJSON(t *testing.T) {
2728
return `{"test":"string value"}`, nil
2829
},
2930
},
30-
want: func(expectedMap pcommon.Map) {
31+
wantMap: func(expectedMap pcommon.Map) {
3132
expectedMap.PutStr("test", "string value")
3233
},
3334
},
@@ -38,7 +39,7 @@ func Test_ParseJSON(t *testing.T) {
3839
return `{"test":true}`, nil
3940
},
4041
},
41-
want: func(expectedMap pcommon.Map) {
42+
wantMap: func(expectedMap pcommon.Map) {
4243
expectedMap.PutBool("test", true)
4344
},
4445
},
@@ -49,7 +50,7 @@ func Test_ParseJSON(t *testing.T) {
4950
return `{"test":1}`, nil
5051
},
5152
},
52-
want: func(expectedMap pcommon.Map) {
53+
wantMap: func(expectedMap pcommon.Map) {
5354
expectedMap.PutDouble("test", 1)
5455
},
5556
},
@@ -60,7 +61,7 @@ func Test_ParseJSON(t *testing.T) {
6061
return `{"test":1.1}`, nil
6162
},
6263
},
63-
want: func(expectedMap pcommon.Map) {
64+
wantMap: func(expectedMap pcommon.Map) {
6465
expectedMap.PutDouble("test", 1.1)
6566
},
6667
},
@@ -71,7 +72,7 @@ func Test_ParseJSON(t *testing.T) {
7172
return `{"test":null}`, nil
7273
},
7374
},
74-
want: func(expectedMap pcommon.Map) {
75+
wantMap: func(expectedMap pcommon.Map) {
7576
expectedMap.PutEmpty("test")
7677
},
7778
},
@@ -82,20 +83,45 @@ func Test_ParseJSON(t *testing.T) {
8283
return `{"test":["string","value"]}`, nil
8384
},
8485
},
85-
want: func(expectedMap pcommon.Map) {
86+
wantMap: func(expectedMap pcommon.Map) {
8687
emptySlice := expectedMap.PutEmptySlice("test")
8788
emptySlice.AppendEmpty().SetStr("string")
8889
emptySlice.AppendEmpty().SetStr("value")
8990
},
9091
},
92+
{
93+
name: "handle top level array",
94+
target: ottl.StandardStringGetter[any]{
95+
Getter: func(_ context.Context, _ any) (any, error) {
96+
return `["string","value"]`, nil
97+
},
98+
},
99+
wantSlice: func(expectedSlice pcommon.Slice) {
100+
expectedSlice.AppendEmpty().SetStr("string")
101+
expectedSlice.AppendEmpty().SetStr("value")
102+
},
103+
},
104+
{
105+
name: "handle top level array of objects",
106+
target: ottl.StandardStringGetter[any]{
107+
Getter: func(_ context.Context, _ any) (any, error) {
108+
return `[{"test":"value"},{"test":"value"}]`, nil
109+
},
110+
},
111+
wantSlice: func(expectedSlice pcommon.Slice) {
112+
113+
expectedSlice.AppendEmpty().SetEmptyMap().PutStr("test", "value")
114+
expectedSlice.AppendEmpty().SetEmptyMap().PutStr("test", "value")
115+
},
116+
},
91117
{
92118
name: "handle nested object",
93119
target: ottl.StandardStringGetter[any]{
94120
Getter: func(_ context.Context, _ any) (any, error) {
95121
return `{"test":{"nested":"true"}}`, nil
96122
},
97123
},
98-
want: func(expectedMap pcommon.Map) {
124+
wantMap: func(expectedMap pcommon.Map) {
99125
newMap := expectedMap.PutEmptyMap("test")
100126
newMap.PutStr("nested", "true")
101127
},
@@ -107,7 +133,7 @@ func Test_ParseJSON(t *testing.T) {
107133
return `{"existing":"pass"}`, nil
108134
},
109135
},
110-
want: func(expectedMap pcommon.Map) {
136+
wantMap: func(expectedMap pcommon.Map) {
111137
expectedMap.PutStr("existing", "pass")
112138
},
113139
},
@@ -118,7 +144,7 @@ func Test_ParseJSON(t *testing.T) {
118144
return `{"test1":{"nested":"true"},"test2":"string","test3":1,"test4":1.1,"test5":[[1], [2, 3],[]],"test6":null}`, nil
119145
},
120146
},
121-
want: func(expectedMap pcommon.Map) {
147+
wantMap: func(expectedMap pcommon.Map) {
122148
newMap := expectedMap.PutEmptyMap("test1")
123149
newMap.PutStr("nested", "true")
124150
expectedMap.PutStr("test2", "string")
@@ -141,19 +167,26 @@ func Test_ParseJSON(t *testing.T) {
141167
result, err := exprFunc(context.Background(), nil)
142168
assert.NoError(t, err)
143169

144-
resultMap, ok := result.(pcommon.Map)
145-
require.True(t, ok)
146-
147-
expected := pcommon.NewMap()
148-
tt.want(expected)
170+
if tt.wantMap != nil {
171+
resultMap, ok := result.(pcommon.Map)
172+
require.True(t, ok)
173+
expected := pcommon.NewMap()
174+
tt.wantMap(expected)
175+
assert.Equal(t, expected.Len(), resultMap.Len())
176+
expected.Range(func(k string, _ pcommon.Value) bool {
177+
ev, _ := expected.Get(k)
178+
av, _ := resultMap.Get(k)
179+
assert.Equal(t, ev, av)
180+
return true
181+
})
182+
} else if tt.wantSlice != nil {
183+
resultSlice, ok := result.(pcommon.Slice)
184+
require.True(t, ok)
185+
expected := pcommon.NewSlice()
186+
tt.wantSlice(expected)
187+
assert.Equal(t, expected, resultSlice)
188+
}
149189

150-
assert.Equal(t, expected.Len(), resultMap.Len())
151-
expected.Range(func(k string, _ pcommon.Value) bool {
152-
ev, _ := expected.Get(k)
153-
av, _ := resultMap.Get(k)
154-
assert.Equal(t, ev, av)
155-
return true
156-
})
157190
})
158191
}
159192
}

0 commit comments

Comments
 (0)