Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
76 changes: 46 additions & 30 deletions plugins/parsers/xpath/json_document.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
package xpath

import (
"reflect"
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/antchfx/jsonquery"
path "github.com/antchfx/xpath"
"github.com/fxamacker/cbor/v2"
"github.com/srebhan/cborquery"
)

type jsonDocument struct{}

func (*jsonDocument) Parse(buf []byte) (dataNode, error) {
return jsonquery.Parse(strings.NewReader(string(buf)))
// First parse JSON to an interface{}
var data interface{}
if err := json.Unmarshal(buf, &data); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}

// Convert to CBOR to leverage cborquery's correct array handling
cborData, err := cbor.Marshal(data)
if err != nil {
return nil, fmt.Errorf("failed to convert JSON to CBOR: %w", err)
}

// Parse with cborquery which handles arrays correctly
return cborquery.Parse(bytes.NewReader(cborData))
}

func (*jsonDocument) QueryAll(node dataNode, expr string) ([]dataNode, error) {
// If this panics it's a programming error as we changed the document type while processing
native, err := jsonquery.QueryAll(node.(*jsonquery.Node), expr)
native, err := cborquery.QueryAll(node.(*cborquery.Node), expr)
if err != nil {
return nil, err
}
Expand All @@ -31,15 +46,15 @@ func (*jsonDocument) QueryAll(node dataNode, expr string) ([]dataNode, error) {

func (*jsonDocument) CreateXPathNavigator(node dataNode) path.NodeNavigator {
// If this panics it's a programming error as we changed the document type while processing
return jsonquery.CreateXPathNavigator(node.(*jsonquery.Node))
return cborquery.CreateXPathNavigator(node.(*cborquery.Node))
}

func (d *jsonDocument) GetNodePath(node, relativeTo dataNode, sep string) string {
names := make([]string, 0)

// If these panic it's a programming error as we changed the document type while processing
nativeNode := node.(*jsonquery.Node)
nativeRelativeTo := relativeTo.(*jsonquery.Node)
nativeNode := node.(*cborquery.Node)
nativeRelativeTo := relativeTo.(*cborquery.Node)

// Climb up the tree and collect the node names
n := nativeNode.Parent
Expand All @@ -64,40 +79,41 @@ func (d *jsonDocument) GetNodePath(node, relativeTo dataNode, sep string) string

func (d *jsonDocument) GetNodeName(node dataNode, sep string, withParent bool) string {
// If this panics it's a programming error as we changed the document type while processing
nativeNode := node.(*jsonquery.Node)
nativeNode := node.(*cborquery.Node)

name := nativeNode.Data
name := nativeNode.Name

// Check if the node is part of an array. If so, determine the index and
// concatenate the parent name and the index.
kind := reflect.Invalid
if nativeNode.Parent != nil && nativeNode.Parent.Value() != nil {
kind = reflect.TypeOf(nativeNode.Parent.Value()).Kind()
}

switch kind {
case reflect.Slice, reflect.Array:
// Determine the index for array elements
if name == "" && nativeNode.Parent != nil && withParent {
name = nativeNode.Parent.Data + sep
// In cborquery, array elements appear as siblings with the same name.
// Check if this node is part of an array by looking for siblings with the same name.
if nativeNode.Parent != nil && name != "" {
idx, count := d.siblingIndex(nativeNode)
if count > 1 {
// This is an array element, append the index
return name + sep + strconv.Itoa(idx)
}
return name + d.index(nativeNode)
}

return name
}

func (*jsonDocument) OutputXML(node dataNode) string {
native := node.(*jsonquery.Node)
native := node.(*cborquery.Node)
return native.OutputXML()
}

func (*jsonDocument) index(node *jsonquery.Node) string {
idx := 0

for n := node; n.PrevSibling != nil; n = n.PrevSibling {
idx++
func (*jsonDocument) siblingIndex(node *cborquery.Node) (idx, count int) {
if node.Parent == nil {
return 0, 1
}

return strconv.Itoa(idx)
// Count siblings with the same name and find our index among them
for sibling := node.Parent.FirstChild; sibling != nil; sibling = sibling.NextSibling {
if sibling.Name == node.Name {
if sibling == node {
idx = count
}
count++
}
}
return idx, count
}
26 changes: 21 additions & 5 deletions plugins/parsers/xpath/msgpack_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,39 @@ package xpath

import (
"bytes"
"encoding/json"
"fmt"

"github.com/antchfx/jsonquery"
path "github.com/antchfx/xpath"
"github.com/fxamacker/cbor/v2"
"github.com/srebhan/cborquery"
"github.com/tinylib/msgp/msgp"
)

type msgpackDocument jsonDocument

func (*msgpackDocument) Parse(buf []byte) (dataNode, error) {
var json bytes.Buffer
var jsonBuf bytes.Buffer

// Unmarshal the message-pack binary message to JSON and proceed with the jsonquery class
if _, err := msgp.UnmarshalAsJSON(&json, buf); err != nil {
// Unmarshal the message-pack binary message to JSON
if _, err := msgp.UnmarshalAsJSON(&jsonBuf, buf); err != nil {
return nil, fmt.Errorf("unmarshalling to json failed: %w", err)
}
return jsonquery.Parse(&json)

// Parse JSON to interface{}
var data interface{}
if err := json.Unmarshal(jsonBuf.Bytes(), &data); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}

// Convert to CBOR to leverage cborquery's correct array handling
cborData, err := cbor.Marshal(data)
if err != nil {
return nil, fmt.Errorf("failed to convert JSON to CBOR: %w", err)
}

// Parse with cborquery which handles arrays correctly
return cborquery.Parse(bytes.NewReader(cborData))
}

func (d *msgpackDocument) QueryAll(node dataNode, expr string) ([]dataNode, error) {
Expand Down
15 changes: 10 additions & 5 deletions plugins/parsers/xpath/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"strings"
"time"

"github.com/antchfx/jsonquery"
path "github.com/antchfx/xpath"
"github.com/srebhan/cborquery"
"github.com/srebhan/protobufquery"
Expand Down Expand Up @@ -493,8 +492,6 @@ func (p *Parser) executeQuery(doc, selected dataNode, query string) (r interface
switch nn := current.(type) {
case *cborquery.NodeNavigator:
return nn.GetValue(), nil
case *jsonquery.NodeNavigator:
return nn.GetValue(), nil
case *protobufquery.NodeNavigator:
return nn.GetValue(), nil
}
Expand Down Expand Up @@ -562,10 +559,18 @@ func (p *Parser) constructFieldName(root, node dataNode, name string, expand boo

// In case the name is empty we should determine the current node's name.
// This involves array index expansion in case the parent of the node is
// and array. If we expanded here, we should skip our parent as this is
// already encoded in the name
// an array. If we expanded here, we should skip our parent as this is
// already encoded in the name.
if name == "" {
name = p.document.GetNodeName(node, "_", !expand)
} else {
// For non-empty names, check if this is an array element and append index.
// GetNodeName returns the name with array index for array elements.
nodeName := p.document.GetNodeName(node, "_", false)
if nodeName != name && strings.Contains(nodeName, name+"_") {
// The node name includes an array index (e.g., "cpus_0" vs "cpus")
name = nodeName
}
}

// If name expansion is requested, construct a path between the current
Expand Down
2 changes: 1 addition & 1 deletion plugins/parsers/xpath/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1604,7 +1604,7 @@ const benchmarkDataJSON = `
`

var benchmarkConfigJSON = Config{
Selection: "data/*",
Selection: "//data",
Tags: map[string]string{
"tags_host": "tags_host",
"tags_sdkver": "tags_sdkver",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nvme,device=/dev/nvme1,model_name=Samsung\ SSD,serial_number=ABC123 ns1_capacity=960197124096i,ns1_id=1i,ns1_utilization=86638583808i,ns2_capacity=500107862016i,ns2_id=2i
21 changes: 21 additions & 0 deletions plugins/parsers/xpath/testcases/json_array_indexing/telegraf.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[[inputs.file]]
files = ["./testcases/json_array_indexing/test.json"]
data_format = "xpath_json"

xpath_native_types = true

[[inputs.file.xpath]]
metric_name = "'nvme'"

[inputs.file.xpath.tags]
device = "string(/device/name)"
model_name = "string(/model_name)"
serial_number = "string(/serial_number)"

[inputs.file.xpath.fields_int]
# Test accessing array elements by index - this is the fix for issue #18145
ns1_id = "number(//nvme_namespaces[1]/id)"
ns1_capacity = "number(//nvme_namespaces[1]/capacity/bytes)"
ns1_utilization = "number(//nvme_namespaces[1]/utilization/bytes)"
ns2_id = "number(//nvme_namespaces[2]/id)"
ns2_capacity = "number(//nvme_namespaces[2]/capacity/bytes)"
27 changes: 27 additions & 0 deletions plugins/parsers/xpath/testcases/json_array_indexing/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"device": {
"name": "/dev/nvme1"
},
"model_name": "Samsung SSD",
"serial_number": "ABC123",
"nvme_namespaces": [
{
"id": 1,
"capacity": {
"bytes": 960197124096
},
"utilization": {
"bytes": 86638583808
}
},
{
"id": 2,
"capacity": {
"bytes": 500107862016
},
"utilization": {
"bytes": 42949672960
}
}
]
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
foo a="a string",b=3.1415,c=true,d="{\"d1\":1,\"d2\":\"foo\",\"d3\":true,\"d4\":null}",e="[\"master\",42,true]",timestamp=1690193829 1690193829000000000
foo a="a string",b=3.1415,c=true,d="map[d1:1 d2:foo d3:true d4:<nil>]",e="master",e_0="master",e_1=42,e_2=true,timestamp=1690193829 1690193829000000000
Original file line number Diff line number Diff line change
@@ -1 +1 @@
foo a="a string",b=3.1415,c=true,d="map[d1:1 d2:foo d3:true d4:<nil>]",e="[master 42 true]",timestamp=1690193829 1690193829000000000
foo a="a string",b=3.1415,c=true,d="map[d1:1 d2:foo d3:true d4:<nil>]",e_0="master",e_1=42,e_2=true,timestamp=1690193829 1690193829000000000
Original file line number Diff line number Diff line change
@@ -1 +1 @@
foo a="a string",b="3.1415",c="true",d="{\"d1\":1,\"d2\":\"foo\",\"d3\":true,\"d4\":null}",e="[\"master\",42,true]",timestamp="1690193829" 1690193829000000000
foo a="a string",b="3.1415",c="true",d="map[d1:1 d2:foo d3:true d4:<nil>]",e_0="master",e_1="42",e_2="true",timestamp="1.690193829e+09" 1690193829000000000
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@

[[inputs.file.xpath]]
metric_name = "'devices'"
metric_selection = "/devices/*"
metric_selection = "//devices"
field_selection = "descendant::*[not(*)]"
field_name_expansion = true
2 changes: 1 addition & 1 deletion plugins/parsers/xpath/testcases/openweathermap_json.conf
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#

metric_name = "'weather'"
metric_selection = "//list/*"
metric_selection = "//list"
timestamp = "dt"
timestamp_format = "unix"

Expand Down
2 changes: 1 addition & 1 deletion plugins/parsers/xpath/testcases/string_join/telegraf.conf
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
timestamp_format = "unix"

[inputs.file.xpath.fields]
cpus = "string-join(//cpus/*, ';')"
cpus = "string-join(//cpus, ';')"
4 changes: 2 additions & 2 deletions plugins/parsers/xpath/testcases/tracker_msgpack.conf
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ timestamp_format = "unix"

[fields]
serial = "info/serial_number"
lat = "number(/geo/*[1])"
lon = "number(/geo/*[2])"
lat = "number(//geo[1])"
lon = "number(//geo[2])"
Loading