Skip to content

Commit

Permalink
feat: do not error if no nodes found for current config with xpath pa…
Browse files Browse the repository at this point in the history
…rser (#11102)
  • Loading branch information
Hipska authored May 19, 2022
1 parent ab04f3a commit 9a68167
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 29 deletions.
2 changes: 2 additions & 0 deletions plugins/parsers/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ type Config struct {
XPathProtobufFile string `toml:"xpath_protobuf_file"`
XPathProtobufType string `toml:"xpath_protobuf_type"`
XPathProtobufImportPaths []string `toml:"xpath_protobuf_import_paths"`
XPathAllowEmptySelection bool `toml:"xpath_allow_empty_selection"`
XPathConfig []XPathConfig

// JSONPath configuration
Expand Down Expand Up @@ -287,6 +288,7 @@ func NewParser(config *Config) (Parser, error) {
ProtobufImportPaths: config.XPathProtobufImportPaths,
PrintDocument: config.XPathPrintDocument,
DefaultTags: config.DefaultTags,
AllowEmptySelection: config.XPathAllowEmptySelection,
Configs: NewXPathParserConfigs(config.MetricName, config.XPathConfig),
}
case "json_v2":
Expand Down
10 changes: 9 additions & 1 deletion plugins/parsers/xpath/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ In this configuration mode, you explicitly specify the field and tags you want t
## to get an idea on the expression necessary to derive fields etc.
# xpath_print_document = false

## Allow the results of one of the parsing sections to be empty.
## Useful when not all selected files have the exact same structure.
# xpath_allow_empty_selection = false

## Multiple parsing sections are allowed
[[inputs.file.xpath]]
## Optional: XPath-query to select a subset of nodes from the XML document.
Expand Down Expand Up @@ -152,6 +156,10 @@ metric.
## to get an idea on the expression necessary to derive fields etc.
# xpath_print_document = false

## Allow the results of one of the parsing sections to be empty.
## Useful when not all selected files have the exact same structure.
# xpath_allow_empty_selection = false

## Multiple parsing sections are allowed
[[inputs.file.xpath]]
## Optional: XPath-query to select a subset of nodes from the XML document.
Expand Down Expand Up @@ -201,7 +209,7 @@ metric.

```

*Please note*: The resulting fields are _always_ of type string!
**Please note**: The resulting fields are *always* of type string!

It is also possible to specify a mixture of the two alternative ways of specifying fields.

Expand Down
40 changes: 13 additions & 27 deletions plugins/parsers/xpath/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Parser struct {
ProtobufMessageType string
ProtobufImportPaths []string
PrintDocument bool
AllowEmptySelection bool
Configs []Config
DefaultTags map[string]string
Log telegraf.Logger
Expand Down Expand Up @@ -108,7 +109,9 @@ func (p *Parser) Parse(buf []byte) ([]telegraf.Metric, error) {
}
if len(selectedNodes) < 1 || selectedNodes[0] == nil {
p.debugEmptyQuery("metric selection", doc, config.Selection)
return nil, fmt.Errorf("cannot parse with empty selection node")
if !p.AllowEmptySelection {
return metrics, fmt.Errorf("cannot parse with empty selection node")
}
}
p.Log.Debugf("Number of selected metric nodes: %d", len(selectedNodes))

Expand All @@ -126,37 +129,20 @@ func (p *Parser) Parse(buf []byte) ([]telegraf.Metric, error) {
}

func (p *Parser) ParseLine(line string) (telegraf.Metric, error) {
t := time.Now()

switch len(p.Configs) {
metrics, err := p.Parse([]byte(line))
if err != nil {
return nil, err
}

switch len(metrics) {
case 0:
return nil, nil
case 1:
config := p.Configs[0]

doc, err := p.document.Parse([]byte(line))
if err != nil {
return nil, err
}

selected := doc
if len(config.Selection) > 0 {
selectedNodes, err := p.document.QueryAll(doc, config.Selection)
if err != nil {
return nil, err
}
if len(selectedNodes) < 1 || selectedNodes[0] == nil {
p.debugEmptyQuery("metric selection", doc, config.Selection)
return nil, fmt.Errorf("cannot parse line with empty selection")
} else if len(selectedNodes) != 1 {
return nil, fmt.Errorf("cannot parse line with multiple selected nodes (%d)", len(selectedNodes))
}
selected = selectedNodes[0]
}

return p.parseQuery(t, doc, selected, config)
return metrics[0], nil
default:
return metrics[0], fmt.Errorf("cannot parse line with multiple (%d) metrics", len(metrics))
}
return nil, fmt.Errorf("cannot parse line with multiple (%d) configurations", len(p.Configs))
}

func (p *Parser) SetDefaultTags(tags map[string]string) {
Expand Down
69 changes: 68 additions & 1 deletion plugins/parsers/xpath/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1154,7 +1154,74 @@ func TestEmptySelection(t *testing.T) {

_, err := parser.Parse([]byte(tt.input))
require.Error(t, err)
require.Equal(t, err.Error(), "cannot parse with empty selection node")
require.Equal(t, "cannot parse with empty selection node", err.Error())
})
}
}

func TestEmptySelectionAllowed(t *testing.T) {
var tests = []struct {
name string
input string
configs []Config
}{
{
name: "empty path",
input: multipleNodesXML,
configs: []Config{
{
Selection: "/Device/NonExisting",
Fields: map[string]string{"value": "number(Value)"},
FieldsInt: map[string]string{"mode": "Value/@mode"},
Tags: map[string]string{},
},
},
},
{
name: "empty pattern",
input: multipleNodesXML,
configs: []Config{
{
Selection: "//NonExisting",
Fields: map[string]string{"value": "number(Value)"},
FieldsInt: map[string]string{"mode": "Value/@mode"},
Tags: map[string]string{},
},
},
},
{
name: "empty axis",
input: multipleNodesXML,
configs: []Config{
{
Selection: "/Device/child::NonExisting",
Fields: map[string]string{"value": "number(Value)"},
FieldsInt: map[string]string{"mode": "Value/@mode"},
Tags: map[string]string{},
},
},
},
{
name: "empty predicate",
input: multipleNodesXML,
configs: []Config{
{
Selection: "/Device[@NonExisting=true]",
Fields: map[string]string{"value": "number(Value)"},
FieldsInt: map[string]string{"mode": "Value/@mode"},
Tags: map[string]string{},
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := &Parser{Configs: tt.configs, AllowEmptySelection: true, DefaultTags: map[string]string{}, Log: testutil.Logger{Name: "parsers.xml"}}
require.NoError(t, parser.Init())

_, err := parser.Parse([]byte(tt.input))
require.NoError(t, err)
})
}
}
Expand Down

0 comments on commit 9a68167

Please sign in to comment.