Skip to content

Commit

Permalink
[pkg/ottl] Add ParseJSON factory function (#16444)
Browse files Browse the repository at this point in the history
* Add ParseToMap factory function

* add changelog entry

* run go mod tidy

* Switch to json-specific parser

* Update .chloggen/ottl-parse-to-map.yaml

Co-authored-by: Evan Bradley <github@evanbradley.org>

* Update example

* Apply feedback

* Reduce further

* Update pkg/ottl/ottlfuncs/func_parse_json.go

Co-authored-by: Bogdan Drutu <lazy@splunk.com>

* Fix lint

* Return error

* Update pkg/ottl/ottlfuncs/func_parse_json.go

Co-authored-by: Bogdan Drutu <lazy@splunk.com>

* Fix

Co-authored-by: Evan Bradley <github@evanbradley.org>
Co-authored-by: Bogdan Drutu <lazy@splunk.com>
  • Loading branch information
3 people authored Nov 29, 2022
1 parent 44c00b9 commit a743302
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .chloggen/ottl-parse-to-map.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# 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 new `ParseJSON` function that can convert a json string into `pcommon.Map`.

# One or more tracking issues related to the change
issues: [16444]

# (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:
31 changes: 31 additions & 0 deletions pkg/ottl/ottlfuncs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ List of available Factory Functions:
- [ConvertCase](#convertcase)
- [Int](#int)
- [IsMatch](#ismatch)
- [ParseJSON](#ParseJSON)
- [SpanID](#spanid)
- [Split](#split)
- [TraceID](#traceid)
Expand Down Expand Up @@ -129,6 +130,36 @@ Examples:

- `IsMatch("string", ".*ring")`

### ParseJSON

`ParseJSON(target)`

The `ParseJSON` factory function returns a `pcommon.Map` 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.

Unmarshalling is done using [jsoniter](https://github.com/json-iterator/go).
Each JSON type is converted into a `pdata.Value` using the following map:

```
JSON boolean -> bool
JSON number -> float64
JSON string -> string
JSON null -> nil
JSON arrays -> pdata.SliceValue
JSON objects -> map[string]any
```

Examples:

- `ParseJSON("{\"attr\":true}")`


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


- `ParseJSON(body)`

### SpanID

`SpanID(bytes)`
Expand Down
55 changes: 55 additions & 0 deletions pkg/ottl/ottlfuncs/func_parse_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs"

import (
"context"
"fmt"

jsoniter "github.com/json-iterator/go"
"go.opentelemetry.io/collector/pdata/pcommon"

"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
)

// ParseJSON factory function returns a `pcommon.Map` 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
// JSON number -> float64
// JSON string -> string
// JSON null -> nil
// JSON arrays -> pdata.SliceValue
// JSON objects -> map[string]any
func ParseJSON[K any](target ottl.Getter[K]) (ottl.ExprFunc[K], error) {
return func(ctx context.Context, tCtx K) (interface{}, error) {
targetVal, err := target.Get(ctx, tCtx)
if err != nil {
return nil, err
}
jsonStr, ok := targetVal.(string)
if !ok {
return nil, fmt.Errorf("target must be a string but got %T", targetVal)
}
var parsedValue map[string]interface{}
err = jsoniter.UnmarshalFromString(jsonStr, &parsedValue)
if err != nil {
return nil, err
}
result := pcommon.NewMap()
err = result.FromRaw(parsedValue)
return result, err
}, nil
}
185 changes: 185 additions & 0 deletions pkg/ottl/ottlfuncs/func_parse_json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs"

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"go.opentelemetry.io/collector/pdata/pcommon"

"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
)

func Test_ParseJSON(t *testing.T) {
tests := []struct {
name string
target ottl.Getter[any]
want func(pcommon.Map)
}{
{
name: "handle string",
target: ottl.StandardGetSetter[any]{
Getter: func(ctx context.Context, tCtx any) (interface{}, error) {
return `{"test":"string value"}`, nil
},
},
want: func(expectedMap pcommon.Map) {
expectedMap.PutStr("test", "string value")
},
},
{
name: "handle bool",
target: ottl.StandardGetSetter[any]{
Getter: func(ctx context.Context, tCtx any) (interface{}, error) {
return `{"test":true}`, nil
},
},
want: func(expectedMap pcommon.Map) {
expectedMap.PutBool("test", true)
},
},
{
name: "handle int",
target: ottl.StandardGetSetter[any]{
Getter: func(ctx context.Context, tCtx any) (interface{}, error) {
return `{"test":1}`, nil
},
},
want: func(expectedMap pcommon.Map) {
expectedMap.PutDouble("test", 1)
},
},
{
name: "handle float",
target: ottl.StandardGetSetter[any]{
Getter: func(ctx context.Context, tCtx any) (interface{}, error) {
return `{"test":1.1}`, nil
},
},
want: func(expectedMap pcommon.Map) {
expectedMap.PutDouble("test", 1.1)
},
},
{
name: "handle nil",
target: ottl.StandardGetSetter[any]{
Getter: func(ctx context.Context, tCtx any) (interface{}, error) {
return `{"test":null}`, nil
},
},
want: func(expectedMap pcommon.Map) {
expectedMap.PutEmpty("test")
},
},
{
name: "handle array",
target: ottl.StandardGetSetter[any]{
Getter: func(ctx context.Context, tCtx any) (interface{}, error) {
return `{"test":["string","value"]}`, nil
},
},
want: func(expectedMap pcommon.Map) {
emptySlice := expectedMap.PutEmptySlice("test")
emptySlice.AppendEmpty().SetStr("string")
emptySlice.AppendEmpty().SetStr("value")
},
},
{
name: "handle nested object",
target: ottl.StandardGetSetter[any]{
Getter: func(ctx context.Context, tCtx any) (interface{}, error) {
return `{"test":{"nested":"true"}}`, nil
},
},
want: func(expectedMap pcommon.Map) {
newMap := expectedMap.PutEmptyMap("test")
newMap.PutStr("nested", "true")
},
},
{
name: "updates existing",
target: ottl.StandardGetSetter[any]{
Getter: func(ctx context.Context, tCtx any) (interface{}, error) {
return `{"existing":"pass"}`, nil
},
},
want: func(expectedMap pcommon.Map) {
expectedMap.PutStr("existing", "pass")
},
},
{
name: "complex",
target: ottl.StandardGetSetter[any]{
Getter: func(ctx context.Context, tCtx any) (interface{}, error) {
return `{"test1":{"nested":"true"},"test2":"string","test3":1,"test4":1.1,"test5":[[1], [2, 3],[]],"test6":null}`, nil
},
},
want: func(expectedMap pcommon.Map) {
newMap := expectedMap.PutEmptyMap("test1")
newMap.PutStr("nested", "true")
expectedMap.PutStr("test2", "string")
expectedMap.PutDouble("test3", 1)
expectedMap.PutDouble("test4", 1.1)
slice := expectedMap.PutEmptySlice("test5")
slice0 := slice.AppendEmpty().SetEmptySlice()
slice0.AppendEmpty().SetDouble(1)
slice1 := slice.AppendEmpty().SetEmptySlice()
slice1.AppendEmpty().SetDouble(2)
slice1.AppendEmpty().SetDouble(3)
slice.AppendEmpty().SetEmptySlice()
expectedMap.PutEmpty("test6")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
exprFunc, err := ParseJSON(tt.target)
assert.NoError(t, err)

result, err := exprFunc(context.Background(), nil)
assert.NoError(t, err)

resultMap, ok := result.(pcommon.Map)
if !ok {
assert.Fail(t, "pcommon.Map not returned")
}

expected := pcommon.NewMap()
tt.want(expected)

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

func Test_ParseJSON_Error(t *testing.T) {
target := &ottl.StandardGetSetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return 1, nil
},
}
exprFunc, err := ParseJSON[interface{}](target)
assert.NoError(t, err)
_, err = exprFunc(context.Background(), nil)
assert.Error(t, err)
}

0 comments on commit a743302

Please sign in to comment.