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

[pkg/ottl] Add ParseJSON factory function #16444

Merged
merged 17 commits into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
2 changes: 1 addition & 1 deletion pkg/ottl/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/alecthomas/participle/v2 v2.0.0-beta.5
github.com/gobwas/glob v0.2.3
github.com/iancoleman/strcase v0.2.0
github.com/json-iterator/go v1.1.12
github.com/stretchr/testify v1.8.1
go.opentelemetry.io/collector/component v0.65.0
go.opentelemetry.io/collector/pdata v0.65.0
Expand All @@ -20,7 +21,6 @@ require (
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/knadh/koanf v1.4.4 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
Expand Down
31 changes: 31 additions & 0 deletions pkg/ottl/ottlfuncs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ List of available Factory Functions:
- [ConvertCase](#convertcase)
- [Int](#int)
- [IsMatch](#ismatch)
- [ParseJSON](#ParseJSON)
- [SpanID](#spanid)
- [Split](#split)
- [TraceID](#traceid)
Expand Down Expand Up @@ -126,6 +127,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
89 changes: 89 additions & 0 deletions pkg/ottl/ottlfuncs/func_parse_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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"
)

func ParseJSON[K any](target ottl.Getter[K]) (ottl.ExprFunc[K], error) {
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
return func(ctx context.Context, tCtx K) (interface{}, error) {
targetVal, err := target.Get(ctx, tCtx)
if err != nil {
return nil, err
}
if jsonStr, ok := targetVal.(string); ok {
bogdandrutu marked this conversation as resolved.
Show resolved Hide resolved
var parsedValue map[string]interface{}
err := jsoniter.UnmarshalFromString(jsonStr, &parsedValue)
if err != nil {
return pcommon.Map{}, err
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
}
result := pcommon.NewMap()
for k, v := range parsedValue {
attrVal := pcommon.NewValueEmpty()
err = setValue(attrVal, v)
if err != nil {
return pcommon.Map{}, err
}
attrVal.CopyTo(result.PutEmpty(k))
}
return result, nil
}
return nil, nil
}, nil
}

func setValue(value pcommon.Value, val interface{}) error {
TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
switch v := val.(type) {
case string:
value.SetStr(v)
case bool:
value.SetBool(v)
case float64:
value.SetDouble(v)
case nil:
case []interface{}:
emptySlice := value.SetEmptySlice()
err := setSlice(emptySlice, v)
if err != nil {
return err
}
case map[string]interface{}:
err := value.SetEmptyMap().FromRaw(v)
if err != nil {
return err
}
default:
return fmt.Errorf("unknown type, %T", v)
}
return nil
}

func setSlice(slice pcommon.Slice, value []interface{}) error {
for _, item := range value {
emptyValue := slice.AppendEmpty()
err := setValue(emptyValue, item)
if err != nil {
return err
}
}
return nil
}
173 changes: 173 additions & 0 deletions pkg/ottl/ottlfuncs/func_parse_json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// 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
})
})
}
}