feat: do not error if no nodes found for current config with xpath parser (#11102)

This commit is contained in:
Thomas Casteleyn 2022-05-19 17:00:23 +02:00 committed by GitHub
parent ab04f3a1c7
commit 9a6816782b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 92 additions and 29 deletions

View File

@ -188,6 +188,7 @@ type Config struct {
XPathProtobufFile string `toml:"xpath_protobuf_file"` XPathProtobufFile string `toml:"xpath_protobuf_file"`
XPathProtobufType string `toml:"xpath_protobuf_type"` XPathProtobufType string `toml:"xpath_protobuf_type"`
XPathProtobufImportPaths []string `toml:"xpath_protobuf_import_paths"` XPathProtobufImportPaths []string `toml:"xpath_protobuf_import_paths"`
XPathAllowEmptySelection bool `toml:"xpath_allow_empty_selection"`
XPathConfig []XPathConfig XPathConfig []XPathConfig
// JSONPath configuration // JSONPath configuration
@ -287,6 +288,7 @@ func NewParser(config *Config) (Parser, error) {
ProtobufImportPaths: config.XPathProtobufImportPaths, ProtobufImportPaths: config.XPathProtobufImportPaths,
PrintDocument: config.XPathPrintDocument, PrintDocument: config.XPathPrintDocument,
DefaultTags: config.DefaultTags, DefaultTags: config.DefaultTags,
AllowEmptySelection: config.XPathAllowEmptySelection,
Configs: NewXPathParserConfigs(config.MetricName, config.XPathConfig), Configs: NewXPathParserConfigs(config.MetricName, config.XPathConfig),
} }
case "json_v2": case "json_v2":

View File

@ -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. ## to get an idea on the expression necessary to derive fields etc.
# xpath_print_document = false # 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 ## Multiple parsing sections are allowed
[[inputs.file.xpath]] [[inputs.file.xpath]]
## Optional: XPath-query to select a subset of nodes from the XML document. ## Optional: XPath-query to select a subset of nodes from the XML document.
@ -152,6 +156,10 @@ metric.
## to get an idea on the expression necessary to derive fields etc. ## to get an idea on the expression necessary to derive fields etc.
# xpath_print_document = false # 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 ## Multiple parsing sections are allowed
[[inputs.file.xpath]] [[inputs.file.xpath]]
## Optional: XPath-query to select a subset of nodes from the XML document. ## Optional: XPath-query to select a subset of nodes from the XML document.
@ -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. It is also possible to specify a mixture of the two alternative ways of specifying fields.

View File

@ -29,6 +29,7 @@ type Parser struct {
ProtobufMessageType string ProtobufMessageType string
ProtobufImportPaths []string ProtobufImportPaths []string
PrintDocument bool PrintDocument bool
AllowEmptySelection bool
Configs []Config Configs []Config
DefaultTags map[string]string DefaultTags map[string]string
Log telegraf.Logger Log telegraf.Logger
@ -108,7 +109,9 @@ func (p *Parser) Parse(buf []byte) ([]telegraf.Metric, error) {
} }
if len(selectedNodes) < 1 || selectedNodes[0] == nil { if len(selectedNodes) < 1 || selectedNodes[0] == nil {
p.debugEmptyQuery("metric selection", doc, config.Selection) 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)) p.Log.Debugf("Number of selected metric nodes: %d", len(selectedNodes))
@ -126,37 +129,20 @@ func (p *Parser) Parse(buf []byte) ([]telegraf.Metric, error) {
} }
func (p *Parser) ParseLine(line string) (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: case 0:
return nil, nil return nil, nil
case 1: case 1:
config := p.Configs[0] return metrics[0], nil
default:
doc, err := p.document.Parse([]byte(line)) return metrics[0], fmt.Errorf("cannot parse line with multiple (%d) metrics", len(metrics))
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 nil, fmt.Errorf("cannot parse line with multiple (%d) configurations", len(p.Configs))
} }
func (p *Parser) SetDefaultTags(tags map[string]string) { func (p *Parser) SetDefaultTags(tags map[string]string) {

View File

@ -1154,7 +1154,74 @@ func TestEmptySelection(t *testing.T) {
_, err := parser.Parse([]byte(tt.input)) _, err := parser.Parse([]byte(tt.input))
require.Error(t, err) 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)
}) })
} }
} }