Repository: ozontech/file.d (go)

Issue:
Feature: Add check type operation in do_if
Sometimes we need to discard logs which are not json objects or arrays. It would be great to add an operation for checking types of the log root or its nodes.

Pull request description:
Add do_if check_type op
# Description

This PR adds operation in do_if for checking types of JSON nodes (including root node). Allowed values are `obj`, `arr`, `number`, `string`, `null`, `nil` (nil means the node is absent).

Example:

```yaml
- type: "discard"
  do_if:
    op: "not"
    operands:
      - op: "check_type"
        field: "log"
        values: ["obj", "arr"]
```

That will discard all logs where `log` field is not an object or an array.

Fixes #694 

Patch:
diff --git a/fd/util.go b/fd/util.go
index 41159c7..768b785 100644
--- a/fd/util.go
+++ b/fd/util.go
@@ -221,10 +221,14 @@ var (
 	doIfTimestampCmpOpNodes = map[string]struct{}{
 		"ts_cmp": {},
 	}
+	doIfСheckTypeOpNode = "check_type"
 )
 
 func extractFieldOpVals(jsonNode *simplejson.Json) [][]byte {
-	values := jsonNode.Get("values")
+	values, has := jsonNode.CheckGet("values")
+	if !has {
+		return nil
+	}
 	vals := make([][]byte, 0)
 	iFaceVal := values.Interface()
 	if iFaceVal == nil {
@@ -400,6 +404,19 @@ func extractTsCmpOpNode(_ string, jsonNode *simplejson.Json) (doif.Node, error)
 	return doif.NewTsCmpOpNode(fieldPath, format, cmpOp, cmpMode, cmpValue, cmpValueShift, updateInterval)
 }
 
+func extractCheckTypeOpNode(_ string, jsonNode *simplejson.Json) (doif.Node, error) {
+	fieldPath, err := requiredString(jsonNode, "field")
+	if err != nil {
+		return nil, err
+	}
+	vals := extractFieldOpVals(jsonNode)
+	result, err := doif.NewCheckTypeOpNode(fieldPath, vals)
+	if err != nil {
+		return nil, fmt.Errorf("failed to init check_type op: %w", err)
+	}
+	return result, nil
+}
+
 func extractLogicalOpNode(opName string, jsonNode *simplejson.Json) (doif.Node, error) {
 	var result, operand doif.Node
 	var err error
@@ -434,6 +451,8 @@ func extractDoIfNode(jsonNode *simplejson.Json) (doif.Node, error) {
 		return extractLengthCmpOpNode(opName, jsonNode)
 	} else if _, has := doIfTimestampCmpOpNodes[opName]; has {
 		return extractTsCmpOpNode(opName, jsonNode)
+	} else if opName == doIfСheckTypeOpNode {
+		return extractCheckTypeOpNode(opName, jsonNode)
 	} else {
 		return nil, fmt.Errorf("unknown op %q", opName)
 	}
diff --git a/pipeline/doif/README.idoc.md b/pipeline/doif/README.idoc.md
index 72d18bf..666b809 100644
--- a/pipeline/doif/README.idoc.md
+++ b/pipeline/doif/README.idoc.md
@@ -25,3 +25,6 @@ the chain of Match func calls are performed across the whole tree.
 
 ### Timestamp comparison op node
 @do-if-ts-cmp-op-node
+
+### Check type op node
+@do-if-check-type-op-node
diff --git a/pipeline/doif/README.md b/pipeline/doif/README.md
index 1054c42..28ac558 100755
--- a/pipeline/doif/README.md
+++ b/pipeline/doif/README.md
@@ -18,6 +18,10 @@ the chain of Match func calls are performed across the whole tree.
 
 <br>
 
+**`CheckTypeOp`** Type of node where matching rules for check types are stored.
+
+<br>
+
 **`LogicalOp`** Type of node where logical rules for applying other rules are stored.
 
 <br>
@@ -399,4 +403,34 @@ Result:
 {"timestamp":"2011-01-01T00:00:00Z"}  # not discarded (condition is not met)
 ```
 
+### Check type op node
+DoIf check type op node checks whether the type of the field node is the one from the list.
+
+Params:
+  - `op` - for that node the value is `check_type`
+  - `field` - path to JSON node, can be empty string meaning root node, can be nested field `field.subfield` (if the field consists of `.` in the name, it must be shielded e.g. `exception\.type`)
+  - `values` - list of types to check against. Allowed values are `object` (or `obj`), `array` (or `arr`), `number` (or `num`, matches both ints or floats), `string` (or `str`), `null`, `nil` (for the abscent fields). Values are deduplicated with respect to aliases on initialization to prevent redundant checks.
+
+Example:
+
+```yaml
+- type: discard
+  do_if:
+    op: not
+    operands:
+      - op: check_type
+        field: log
+        values: [obj, arr]
+```
+
+result:
+```
+{"log":{"message":"test"}}   # not discarded
+{"log":[{"message":"test"}]} # not discarded
+{"log":"test"}               # discarded
+{"log":123}                  # discarded
+{"log":null}                 # discarded
+{"not_log":{"test":"test"}}  # discarded
+```
+
 <br>*Generated using [__insane-doc__](https://github.com/vitkovskii/insane-doc)*
\ No newline at end of file
diff --git a/pipeline/doif/check_type_op.go b/pipeline/doif/check_type_op.go
new file mode 100644
index 0000000..e1ea66f
--- /dev/null
+++ b/pipeline/doif/check_type_op.go
@@ -0,0 +1,203 @@
+package doif
+
+import (
+	"errors"
+	"fmt"
+	"slices"
+
+	"github.com/ozontech/file.d/cfg"
+	insaneJSON "github.com/vitkovskii/insane-json"
+)
+
+/*{ do-if-check-type-op-node
+DoIf check type op node checks whether the type of the field node is the one from the list.
+
+Params:
+  - `op` - for that node the value is `check_type`
+  - `field` - path to JSON node, can be empty string meaning root node, can be nested field `field.subfield` (if the field consists of `.` in the name, it must be shielded e.g. `exception\.type`)
+  - `values` - list of types to check against. Allowed values are `object` (or `obj`), `array` (or `arr`), `number` (or `num`, matches both ints or floats), `string` (or `str`), `null`, `nil` (for the abscent fields). Values are deduplicated with respect to aliases on initialization to prevent redundant checks.
+
+Example:
+
+```yaml
+- type: discard
+  do_if:
+    op: not
+    operands:
+      - op: check_type
+        field: log
+        values: [obj, arr]
+```
+
+result:
+```
+{"log":{"message":"test"}}   # not discarded
+{"log":[{"message":"test"}]} # not discarded
+{"log":"test"}               # discarded
+{"log":123}                  # discarded
+{"log":null}                 # discarded
+{"not_log":{"test":"test"}}  # discarded
+```
+}*/
+
+type checkTypeVal int
+
+const (
+	checkTypeUnknown checkTypeVal = iota
+	checkTypeObj
+	checkTypeArr
+	checkTypeNumber
+	checkTypeString
+	checkTypeNull
+	checkTypeNil
+)
+
+func (t checkTypeVal) String() string {
+	switch t {
+	case checkTypeObj:
+		return "object"
+	case checkTypeArr:
+		return "array"
+	case checkTypeNumber:
+		return "number"
+	case checkTypeString:
+		return "string"
+	case checkTypeNull:
+		return "null"
+	case checkTypeNil:
+		return "nil"
+	default:
+		return "unknown" // nolint:goconst
+	}
+}
+
+type checkTypeFn func(*insaneJSON.Node) bool
+
+type checkTypeOpNode struct {
+	fieldPath    []string
+	fieldPathStr string
+	checkTypeFns []checkTypeFn
+}
+
+func NewCheckTypeOpNode(field string, values [][]byte) (Node, error) {
+	if len(values) == 0 {
+		return nil, errors.New("values are not provided")
+	}
+	fieldPath := cfg.ParseFieldSelector(field)
+	usedTypesMap := make(map[checkTypeVal]struct{})
+	checkTypeFns := make([]checkTypeFn, 0)
+	for _, val := range values {
+		switch string(val) {
+		case "obj", "object":
+			if _, ok := usedTypesMap[checkTypeObj]; ok {
+				break
+			}
+			usedTypesMap[checkTypeObj] = struct{}{}
+			checkTypeFns = append(checkTypeFns, func(n *insaneJSON.Node) bool {
+				return n.IsObject()
+			})
+		case "arr", "array":
+			if _, ok := usedTypesMap[checkTypeArr]; ok {
+				break
+			}
+			usedTypesMap[checkTypeArr] = struct{}{}
+			checkTypeFns = append(checkTypeFns, func(n *insaneJSON.Node) bool {
+				return n.IsArray()
+			})
+		case "num", "number":
+			if _, ok := usedTypesMap[checkTypeNumber]; ok {
+				break
+			}
+			usedTypesMap[checkTypeNumber] = struct{}{}
+			checkTypeFns = append(checkTypeFns, func(n *insaneJSON.Node) bool {
+				return n.IsNumber()
+			})
+		case "str", "string":
+			if _, ok := usedTypesMap[checkTypeString]; ok {
+				break
+			}
+			usedTypesMap[checkTypeString] = struct{}{}
+			checkTypeFns = append(checkTypeFns, func(n *insaneJSON.Node) bool {
+				return n.IsString()
+			})
+		case "null":
+			if _, ok := usedTypesMap[checkTypeNull]; ok {
+				break
+			}
+			usedTypesMap[checkTypeNull] = struct{}{}
+			checkTypeFns = append(checkTypeFns, func(n *insaneJSON.Node) bool {
+				return n.IsNull()
+			})
+		case "nil":
+			if _, ok := usedTypesMap[checkTypeNil]; ok {
+				break
+			}
+			usedTypesMap[checkTypeNil] = struct{}{}
+			checkTypeFns = append(checkTypeFns, func(n *insaneJSON.Node) bool {
+				return n.IsNil()
+			})
+		default:
+			return nil, fmt.Errorf(
+				"invalid value for check_type: %q. Allowed values are: 'obj','arr','number','string','null','nil'",
+				val,
+			)
+		}
+	}
+	return &checkTypeOpNode{
+		fieldPath:    fieldPath,
+		fieldPathStr: field,
+		checkTypeFns: checkTypeFns,
+	}, nil
+}
+
+func (n *checkTypeOpNode) Type() NodeType {
+	return NodeCheckTypeOp
+}
+
+func (n *checkTypeOpNode) Check(eventRoot *insaneJSON.Root) bool {
+	node := eventRoot.Dig(n.fieldPath...)
+	for _, checkFn := range n.checkTypeFns {
+		if checkFn(node) {
+			return true
+		}
+	}
+	return false
+}
+
+func (n *checkTypeOpNode) isEqualTo(n2 Node, _ int) error {
+	n2f, ok := n2.(*checkTypeOpNode)
+	if !ok {
+		return errors.New("nodes have different types expected: checkTypeOpNode")
+	}
+	if n.fieldPathStr != n2f.fieldPathStr || slices.Compare[[]string](n.fieldPath, n2f.fieldPath) != 0 {
+		return fmt.Errorf("nodes have different fieldPathStr expected: fieldPathStr=%q fieldPath=%v",
+			n.fieldPathStr, n.fieldPath,
+		)
+	}
+	if len(n.checkTypeFns) != len(n2f.checkTypeFns) {
+		return fmt.Errorf("nodes have different checkTypes slices len expected: %d", len(n.checkTypeFns))
+	}
+	data := []byte(`{"obj":{"subfield":"test"},"arr":["test","test"],"number":123,"string":"test","null":null}`)
+	root := insaneJSON.Spawn()
+	if err := root.DecodeBytes(data); err != nil {
+		panic(err)
+	}
+	nodes := map[string]*insaneJSON.Node{
+		"obj":    root.Dig("obj"),
+		"arr":    root.Dig("arr"),
+		"number": root.Dig("number"),
+		"string": root.Dig("string"),
+		"null":   root.Dig("null"),
+		"nil":    root.Dig("nil"),
+	}
+	for i := 0; i < len(n.checkTypeFns); i++ {
+		for key, node := range nodes {
+			res1 := n.checkTypeFns[i](node)
+			res2 := n2f.checkTypeFns[i](node)
+			if res1 != res2 {
+				return fmt.Errorf("nodes have different check functions, different results for %q for funcs on position %d", key, i)
+			}
+		}
+	}
+	return nil
+}
diff --git a/pipeline/doif/do_if.go b/pipeline/doif/do_if.go
index 770b8e9..847c6b9 100644
--- a/pipeline/doif/do_if.go
+++ b/pipeline/doif/do_if.go
@@ -21,6 +21,9 @@ const (
 	// > Type of node where matching rules for timestamps are stored.
 	NodeTimestampCmpOp // *
 
+	// > Type of node where matching rules for check types are stored.
+	NodeCheckTypeOp // *
+
 	// > Type of node where logical rules for applying other rules are stored.
 	NodeLogicalOp // *
 )
diff --git a/pipeline/doif/field_op.go b/pipeline/doif/field_op.go
index abd68c5..0b2904d 100644
--- a/pipeline/doif/field_op.go
+++ b/pipeline/doif/field_op.go
@@ -202,9 +202,6 @@ type fieldOpNode struct {
 }
 
 func NewFieldOpNode(op string, field string, caseSensitive bool, values [][]byte) (Node, error) {
-	if field == "" {
-		return nil, errors.New("field is not specified")
-	}
 	if len(values) == 0 {
 		return nil, errors.New("values are not provided")
 	}


Test patch:
diff --git a/fd/util_test.go b/fd/util_test.go
index 2126606..c69f29f 100644
--- a/fd/util_test.go
+++ b/fd/util_test.go
@@ -59,6 +59,8 @@ type doIfTreeNode struct {
 	tsCmpValue         time.Time
 	tsCmpValueShift    time.Duration
 	tsUpdateInterval   time.Duration
+
+	checkTypeOp bool
 }
 
 // nolint:gocritic
@@ -96,6 +98,11 @@ func buildDoIfTree(node *doIfTreeNode) (doif.Node, error) {
 			node.tsCmpValueShift,
 			node.tsUpdateInterval,
 		)
+	case node.checkTypeOp:
+		return doif.NewCheckTypeOpNode(
+			node.fieldName,
+			node.values,
+		)
 	default:
 		return nil, errors.New("unknown type of node")
 	}
@@ -321,6 +328,24 @@ func Test_extractDoIfChecker(t *testing.T) {
 				tsUpdateInterval:   defaultTsCmpValUpdateInterval,
 			},
 		},
+		{
+			name: "ok_check_type",
+			args: args{
+				cfgStr: `{
+					"op": "check_type",
+					"field": "log",
+					"values": ["obj","arr"]
+				}`,
+			},
+			want: &doIfTreeNode{
+				checkTypeOp: true,
+				fieldName:   "log",
+				values: [][]byte{
+					[]byte("obj"),
+					[]byte("arr"),
+				},
+			},
+		},
 		{
 			name: "ok_single_val",
 			args: args{
@@ -535,6 +560,38 @@ func Test_extractDoIfChecker(t *testing.T) {
 			},
 			wantErr: true,
 		},
+		{
+			name: "error_check_type_op_empty_values",
+			args: args{
+				cfgStr: `{
+					"op": "check_type",
+					"field": "log",
+					"values": []
+				}`,
+			},
+			wantErr: true,
+		},
+		{
+			name: "error_check_type_op_invalid_value",
+			args: args{
+				cfgStr: `{
+					"op": "check_type",
+					"field": "log",
+					"values": ["uknown_type"]
+				}`,
+			},
+			wantErr: true,
+		},
+		{
+			name: "error_check_type_op_no_field",
+			args: args{
+				cfgStr: `{
+					"op": "check_type",
+					"values": ["obj"]
+				}`,
+			},
+			wantErr: true,
+		},
 	}
 	for _, tt := range tests {
 		tt := tt
diff --git a/pipeline/doif/check_type_test.go b/pipeline/doif/check_type_test.go
new file mode 100644
index 0000000..8ab0fb3
--- /dev/null
+++ b/pipeline/doif/check_type_test.go
@@ -0,0 +1,374 @@
+package doif
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	insaneJSON "github.com/vitkovskii/insane-json"
+)
+
+type testCheckTypeNode struct {
+	field  string
+	values [][]byte
+}
+
+func TestCheckType(t *testing.T) {
+	t.Parallel()
+	type argsResp struct {
+		eventStr string
+		want     bool
+	}
+
+	tests := []struct {
+		name string
+		node testCheckTypeNode
+		data []argsResp
+	}{
+		{
+			name: "ok_type_obj",
+			node: testCheckTypeNode{
+				field:  "log",
+				values: [][]byte{[]byte("obj")},
+			},
+			data: []argsResp{
+				{
+					eventStr: `{"log":{"sublog":"test"}}`,
+					want:     true,
+				},
+				{
+					eventStr: `{"log":"test"}`,
+					want:     false,
+				},
+				{
+					eventStr: `"test"`,
+					want:     false,
+				},
+			},
+		},
+		{
+			name: "ok_type_arr",
+			node: testCheckTypeNode{
+				field:  "log",
+				values: [][]byte{[]byte("arr")},
+			},
+			data: []argsResp{
+				{
+					eventStr: `{"log":[{"sublog":"test"}]}`,
+					want:     true,
+				},
+				{
+					eventStr: `{"log":"test"}`,
+					want:     false,
+				},
+				{
+					eventStr: `"test"`,
+					want:     false,
+				},
+			},
+		},
+		{
+			name: "ok_type_number",
+			node: testCheckTypeNode{
+				field:  "log",
+				values: [][]byte{[]byte("number")},
+			},
+			data: []argsResp{
+				{
+					eventStr: `{"log":123}`,
+					want:     true,
+				},
+				{
+					eventStr: `{"log":"test"}`,
+					want:     false,
+				},
+				{
+					eventStr: `"test"`,
+					want:     false,
+				},
+			},
+		},
+		{
+			name: "ok_type_string",
+			node: testCheckTypeNode{
+				field:  "log",
+				values: [][]byte{[]byte("string")},
+			},
+			data: []argsResp{
+				{
+					eventStr: `{"log":"test"}`,
+					want:     true,
+				},
+				{
+					eventStr: `{"log":{"subfield":"test"}}`,
+					want:     false,
+				},
+				{
+					eventStr: `"test"`,
+					want:     false,
+				},
+			},
+		},
+		{
+			name: "ok_type_null",
+			node: testCheckTypeNode{
+				field:  "log",
+				values: [][]byte{[]byte("null")},
+			},
+			data: []argsResp{
+				{
+					eventStr: `{"log":null}`,
+					want:     true,
+				},
+				{
+					eventStr: `{"log":"test"}`,
+					want:     false,
+				},
+				{
+					eventStr: `"test"`,
+					want:     false,
+				},
+			},
+		},
+		{
+			name: "ok_type_nil",
+			node: testCheckTypeNode{
+				field:  "log",
+				values: [][]byte{[]byte("nil")},
+			},
+			data: []argsResp{
+				{
+					eventStr: `{"not_log":null}`,
+					want:     true,
+				},
+				{
+					eventStr: `{"log":"test"}`,
+					want:     false,
+				},
+				{
+					eventStr: `"test"`,
+					want:     true,
+				},
+			},
+		},
+		{
+			name: "ok_multiple_types",
+			node: testCheckTypeNode{
+				field:  "log",
+				values: [][]byte{[]byte("obj"), []byte("arr")},
+			},
+			data: []argsResp{
+				{
+					eventStr: `{"log":{"subfield":"test"}}`,
+					want:     true,
+				},
+				{
+					eventStr: `{"log":[{"subfield":"test"}]}`,
+					want:     true,
+				},
+				{
+					eventStr: `{"log":"test"}`,
+					want:     false,
+				},
+				{
+					eventStr: `"test"`,
+					want:     false,
+				},
+			},
+		},
+		{
+			name: "ok_root_is_obj_or_arr",
+			node: testCheckTypeNode{
+				field:  "",
+				values: [][]byte{[]byte("obj"), []byte("arr")},
+			},
+			data: []argsResp{
+				{
+					eventStr: `{"log":{"subfield":"test"}}`,
+					want:     true,
+				},
+				{
+					eventStr: `[{"log":{"subfield":"test"}}]`,
+					want:     true,
+				},
+				{
+					eventStr: `"test"`,
+					want:     false,
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			var eventRoot *insaneJSON.Root
+			node, err := NewCheckTypeOpNode(tt.node.field, tt.node.values)
+			require.NoError(t, err)
+			for _, d := range tt.data {
+				if d.eventStr == "" {
+					eventRoot = nil
+				} else {
+					eventRoot, err = insaneJSON.DecodeString(d.eventStr)
+					require.NoError(t, err)
+				}
+				got := node.Check(eventRoot)
+				assert.Equal(t, d.want, got, "invalid result for event %q", d.eventStr)
+			}
+		})
+	}
+}
+
+func TestCheckTypeDuplicateValues(t *testing.T) {
+	t.Parallel()
+
+	logsMap := map[checkTypeVal]string{
+		checkTypeObj:    `{"log":{"sublog":"test"}}`,
+		checkTypeArr:    `{"log":[{"sublog":"test"}]}`,
+		checkTypeNumber: `{"log":123}`,
+		checkTypeString: `{"log":"test"}`,
+		checkTypeNull:   `{"log":null}`,
+		checkTypeNil:    `{"notlog":"test"}`,
+	}
+
+	type argsResp struct {
+		checkType checkTypeVal
+		want      bool
+	}
+
+	tests := []struct {
+		name         string
+		node         testCheckTypeNode
+		expectedVals int
+		data         []argsResp
+	}{
+		{
+			name: "ok_multi_same_dup",
+			node: testCheckTypeNode{
+				field: "log",
+				values: [][]byte{
+					[]byte("obj"), []byte("obj"), []byte("obj"), []byte("obj"), []byte("obj"), []byte("obj"),
+				},
+			},
+			expectedVals: 1,
+			data: []argsResp{
+				{checkType: checkTypeObj, want: true},
+				{checkType: checkTypeArr, want: false},
+				{checkType: checkTypeNumber, want: false},
+				{checkType: checkTypeString, want: false},
+				{checkType: checkTypeNull, want: false},
+				{checkType: checkTypeNil, want: false},
+			},
+		},
+		{
+			name: "ok_dup_obj_alias",
+			node: testCheckTypeNode{
+				field: "log",
+				values: [][]byte{
+					[]byte("object"), []byte("obj"),
+				},
+			},
+			expectedVals: 1,
+			data: []argsResp{
+				{checkType: checkTypeObj, want: true},
+				{checkType: checkTypeArr, want: false},
+				{checkType: checkTypeNumber, want: false},
+				{checkType: checkTypeString, want: false},
+				{checkType: checkTypeNull, want: false},
+				{checkType: checkTypeNil, want: false},
+			},
+		},
+		{
+			name: "ok_dup_arr_alias",
+			node: testCheckTypeNode{
+				field: "log",
+				values: [][]byte{
+					[]byte("array"), []byte("arr"),
+				},
+			},
+			expectedVals: 1,
+			data: []argsResp{
+				{checkType: checkTypeObj, want: false},
+				{checkType: checkTypeArr, want: true},
+				{checkType: checkTypeNumber, want: false},
+				{checkType: checkTypeString, want: false},
+				{checkType: checkTypeNull, want: false},
+				{checkType: checkTypeNil, want: false},
+			},
+		},
+		{
+			name: "ok_dup_number_alias",
+			node: testCheckTypeNode{
+				field: "log",
+				values: [][]byte{
+					[]byte("num"), []byte("number"),
+				},
+			},
+			expectedVals: 1,
+			data: []argsResp{
+				{checkType: checkTypeObj, want: false},
+				{checkType: checkTypeArr, want: false},
+				{checkType: checkTypeNumber, want: true},
+				{checkType: checkTypeString, want: false},
+				{checkType: checkTypeNull, want: false},
+				{checkType: checkTypeNil, want: false},
+			},
+		},
+		{
+			name: "ok_dup_str_alias",
+			node: testCheckTypeNode{
+				field: "log",
+				values: [][]byte{
+					[]byte("str"), []byte("string"),
+				},
+			},
+			expectedVals: 1,
+			data: []argsResp{
+				{checkType: checkTypeObj, want: false},
+				{checkType: checkTypeArr, want: false},
+				{checkType: checkTypeNumber, want: false},
+				{checkType: checkTypeString, want: true},
+				{checkType: checkTypeNull, want: false},
+				{checkType: checkTypeNil, want: false},
+			},
+		},
+		{
+			name: "ok_multi_dup_with_alias",
+			node: testCheckTypeNode{
+				field: "log",
+				values: [][]byte{
+					[]byte("null"), []byte("nil"), []byte("null"), []byte("nil"), []byte("obj"), []byte("object"),
+				},
+			},
+			expectedVals: 3,
+			data: []argsResp{
+				{checkType: checkTypeObj, want: true},
+				{checkType: checkTypeArr, want: false},
+				{checkType: checkTypeNumber, want: false},
+				{checkType: checkTypeString, want: false},
+				{checkType: checkTypeNull, want: true},
+				{checkType: checkTypeNil, want: true},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			node, err := NewCheckTypeOpNode(tt.node.field, tt.node.values)
+			require.NoError(t, err, "must be no error on NewCheckTypeOpNode")
+			ctnode, ok := node.(*checkTypeOpNode)
+			require.True(t, ok, "must be *checkTypeOpNode type")
+			assert.Equal(t, tt.expectedVals, len(ctnode.checkTypeFns))
+			for i, d := range tt.data {
+				eventStr := logsMap[d.checkType]
+				eventRoot, err := insaneJSON.DecodeString(eventStr)
+				require.NoError(t, err, "must be no error on decode checkEvent")
+				got := ctnode.Check(eventRoot)
+				assert.Equal(t, d.want, got, "invalid result for check %d of type %q", i, d.checkType)
+			}
+		})
+	}
+}