Add node groups to opcua input plugin (#8389)

This commit is contained in:
reimda 2020-12-02 17:06:47 -07:00 committed by GitHub
parent 0ccb134ae4
commit 498a6da75f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 456 additions and 121 deletions

View File

@ -9,8 +9,8 @@ Plugin minimum tested version: 1.16
```toml ```toml
[[inputs.opcua]] [[inputs.opcua]]
## Device name ## Metric name
# name = "localhost" # name = "opcua"
# #
## OPC UA Endpoint URL ## OPC UA Endpoint URL
# endpoint = "opc.tcp://localhost:4840" # endpoint = "opc.tcp://localhost:4840"
@ -47,34 +47,97 @@ Plugin minimum tested version: 1.16
# password = "" # password = ""
# #
## Node ID configuration ## Node ID configuration
## name - the variable name ## name - field name to use in the output
## namespace - integer value 0 thru 3 ## namespace - OPC UA namespace of the node (integer value 0 thru 3)
## identifier_type - s=string, i=numeric, g=guid, b=opaque ## identifier_type - OPC UA ID type (s=string, i=numeric, g=guid, b=opaque)
## identifier - tag as shown in opcua browser ## identifier - OPC UA ID (tag as shown in opcua browser)
## data_type - boolean, byte, short, int, uint, uint16, int16, ## tags - extra tags to be added to the output metric (optional)
## uint32, int32, float, double, string, datetime, number
## Example: ## Example:
## {name="ProductUri", namespace="0", identifier_type="i", identifier="2262", data_type="string", description="http://open62541.org"} ## {name="ProductUri", namespace="0", identifier_type="i", identifier="2262", tags=[["tag1","value1"],["tag2","value2]]}
# nodes = [
# {name="", namespace="", identifier_type="", identifier=""},
# {name="", namespace="", identifier_type="", identifier=""},
#]
#
## Node Group
## Sets defaults for OPC UA namespace and ID type so they aren't required in
## every node. A group can also have a metric name that overrides the main
## plugin metric name.
##
## Multiple node groups are allowed
#[[inputs.opcua.group]]
## Group Metric name. Overrides the top level name. If unset, the
## top level name is used.
# name =
#
## Group default namespace. If a node in the group doesn't set its
## namespace, this is used.
# namespace =
#
## Group default identifier type. If a node in the group doesn't set its
## namespace, this is used.
# identifier_type =
#
## Node ID Configuration. Array of nodes with the same settings as above.
# nodes = [
# {name="", namespace="", identifier_type="", identifier=""},
# {name="", namespace="", identifier_type="", identifier=""},
#]
```
### Node Configuration
An OPC UA node ID may resemble: "n=3;s=Temperature". In this example:
- n=3 is indicating the `namespace` is 3
- s=Temperature is indicting that the `identifier_type` is a string and `identifier` value is 'Temperature'
- This example temperature node has a value of 79.0
To gather data from this node enter the following line into the 'nodes' property above:
```
{field_name="temp", namespace="3", identifier_type="s", identifier="Temperature"},
```
This node configuration produces a metric like this:
```
opcua,id=n\=3;s\=Temperature temp=79.0,quality="OK (0x0)" 1597820490000000000
```
### Group Configuration
Groups can set default values for the namespace, identifier type, and
tags settings. The default values apply to all the nodes in the
group. If a default is set, a node may omit the setting altogether.
This simplifies node configuration, especially when many nodes share
the same namespace or identifier type.
The output metric will include tags set in the group and the node. If
a tag with the same name is set in both places, the tag value from the
node is used.
This example group configuration has two groups with two nodes each:
```
[[inputs.opcua.group]]
name="group1_metric_name"
namespace="3"
identifier_type="i"
tags=[["group1_tag", "val1"]]
nodes = [ nodes = [
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""}, {name="name", identifier="1001", tags=[["node1_tag", "val2"]]},
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""}, {name="name", identifier="1002", tags=[["node1_tag", "val3"]]},
]
[[inputs.opcua.group]]
name="group2_metric_name"
namespace="3"
identifier_type="i"
tags=[["group2_tag", "val3"]]
nodes = [
{name="saw", identifier="1003", tags=[["node2_tag", "val4"]]},
{name="sin", identifier="1004"},
] ]
``` ```
### Example Node Configuration It produces metrics like these:
An OPC UA node ID may resemble: "n=3,s=Temperature". In this example:
- n=3 is indicating the `namespace` is 3
- s=Temperature is indicting that the `identifier_type` is a string and `identifier` value is 'Temperature'
- This example temperature node has a value of 79.0, which makes the `data_type` a 'float'.
To gather data from this node enter the following line into the 'nodes' property above:
``` ```
{name="LabelName", namespace="3", identifier_type="s", identifier="Temperature", data_type="float", description="Description of node"}, group1_metric_name,group1_tag=val1,id=ns\=3;i\=1001,node1_tag=val2 name=0,Quality="OK (0x0)" 1606893246000000000
``` group1_metric_name,group1_tag=val1,id=ns\=3;i\=1002,node1_tag=val3 name=-1.389117,Quality="OK (0x0)" 1606893246000000000
group2_metric_name,group2_tag=val3,id=ns\=3;i\=1003,node2_tag=val4 Quality="OK (0x0)",saw=-1.6 1606893246000000000
group2_metric_name,group2_tag=val3,id=ns\=3;i\=1004 sin=1.902113,Quality="OK (0x0)" 1606893246000000000
### Example Output
```
opcua,host=3c70aee0901e,name=Random,type=double Random=0.018158170305814902 1597820490000000000
``` ```

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/url" "net/url"
"sort"
"strings" "strings"
"time" "time"
@ -13,11 +14,12 @@ import (
"github.com/influxdata/telegraf" "github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/inputs" "github.com/influxdata/telegraf/plugins/inputs"
"github.com/influxdata/telegraf/selfstat"
) )
// OpcUA type // OpcUA type
type OpcUA struct { type OpcUA struct {
Name string `toml:"name"` MetricName string `toml:"name"`
Endpoint string `toml:"endpoint"` Endpoint string `toml:"endpoint"`
SecurityPolicy string `toml:"security_policy"` SecurityPolicy string `toml:"security_policy"`
SecurityMode string `toml:"security_mode"` SecurityMode string `toml:"security_mode"`
@ -28,18 +30,18 @@ type OpcUA struct {
AuthMethod string `toml:"auth_method"` AuthMethod string `toml:"auth_method"`
ConnectTimeout config.Duration `toml:"connect_timeout"` ConnectTimeout config.Duration `toml:"connect_timeout"`
RequestTimeout config.Duration `toml:"request_timeout"` RequestTimeout config.Duration `toml:"request_timeout"`
NodeList []OPCTag `toml:"nodes"` RootNodes []NodeSettings `toml:"nodes"`
Groups []GroupSettings `toml:"group"`
Nodes []string `toml:"-"` nodes []Node
NodeData []OPCData `toml:"-"` nodeData []OPCData
NodeIDs []*ua.NodeID `toml:"-"` nodeIDs []*ua.NodeID
NodeIDerror []error `toml:"-"` nodeIDerror []error
state ConnectionState state ConnectionState
// status // status
ReadSuccess int `toml:"-"` ReadSuccess selfstat.Stat `toml:"-"`
ReadError int `toml:"-"` ReadError selfstat.Stat `toml:"-"`
NumberOfTags int `toml:"-"`
// internal values // internal values
client *opcua.Client client *opcua.Client
@ -48,13 +50,29 @@ type OpcUA struct {
} }
// OPCTag type // OPCTag type
type OPCTag struct { type NodeSettings struct {
Name string `toml:"name"` FieldName string `toml:"name"`
Namespace string `toml:"namespace"` Namespace string `toml:"namespace"`
IdentifierType string `toml:"identifier_type"` IdentifierType string `toml:"identifier_type"`
Identifier string `toml:"identifier"` Identifier string `toml:"identifier"`
DataType string `toml:"data_type"` DataType string `toml:"data_type"` // Kept for backward compatibility but was never used.
Description string `toml:"description"` Description string `toml:"description"` // Kept for backward compatibility but was never used.
TagsSlice [][]string `toml:"tags"`
}
type Node struct {
tag NodeSettings
idStr string
metricName string
metricTags map[string]string
}
type GroupSettings struct {
MetricName string `toml:"name"` // Overrides plugin's setting
Namespace string `toml:"namespace"` // Can be overridden by node setting
IdentifierType string `toml:"identifier_type"` // Can be overridden by node setting
Nodes []NodeSettings `toml:"nodes"`
TagsSlice [][]string `toml:"tags"`
} }
// OPCData type // OPCData type
@ -81,9 +99,8 @@ const (
const description = `Retrieve data from OPCUA devices` const description = `Retrieve data from OPCUA devices`
const sampleConfig = ` const sampleConfig = `
[[inputs.opcua]] ## Metric name
## Device name # name = "opcua"
# name = "localhost"
# #
## OPC UA Endpoint URL ## OPC UA Endpoint URL
# endpoint = "opc.tcp://localhost:4840" # endpoint = "opc.tcp://localhost:4840"
@ -120,18 +137,41 @@ const sampleConfig = `
# password = "" # password = ""
# #
## Node ID configuration ## Node ID configuration
## name - the variable name ## name - field name to use in the output
## namespace - integer value 0 thru 3 ## namespace - OPC UA namespace of the node (integer value 0 thru 3)
## identifier_type - s=string, i=numeric, g=guid, b=opaque ## identifier_type - OPC UA ID type (s=string, i=numeric, g=guid, b=opaque)
## identifier - tag as shown in opcua browser ## identifier - OPC UA ID (tag as shown in opcua browser)
## data_type - boolean, byte, short, int, uint, uint16, int16,
## uint32, int32, float, double, string, datetime, number
## Example: ## Example:
## {name="ProductUri", namespace="0", identifier_type="i", identifier="2262", data_type="string", description="http://open62541.org"} ## {name="ProductUri", namespace="0", identifier_type="i", identifier="2262"}
nodes = [ # nodes = [
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""}, # {name="", namespace="", identifier_type="", identifier=""},
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""}, # {name="", namespace="", identifier_type="", identifier=""},
] #]
#
## Node Group
## Sets defaults for OPC UA namespace and ID type so they aren't required in
## every node. A group can also have a metric name that overrides the main
## plugin metric name.
##
## Multiple node groups are allowed
#[[inputs.opcua.group]]
## Group Metric name. Overrides the top level name. If unset, the
## top level name is used.
# name =
#
## Group default namespace. If a node in the group doesn't set its
## namespace, this is used.
# namespace =
#
## Group default identifier type. If a node in the group doesn't set its
## namespace, this is used.
# identifier_type =
#
## Node ID Configuration. Array of nodes with the same settings as above.
# nodes = [
# {name="", namespace="", identifier_type="", identifier=""},
# {name="", namespace="", identifier_type="", identifier=""},
#]
` `
// Description will appear directly above the plugin definition in the config file // Description will appear directly above the plugin definition in the config file
@ -157,16 +197,21 @@ func (o *OpcUA) Init() error {
if err != nil { if err != nil {
return err return err
} }
o.NumberOfTags = len(o.NodeList)
o.setupOptions() o.setupOptions()
tags := map[string]string{
"endpoint": o.Endpoint,
}
o.ReadError = selfstat.Register("opcua", "read_error", tags)
o.ReadSuccess = selfstat.Register("opcua", "read_success", tags)
return nil return nil
} }
func (o *OpcUA) validateEndpoint() error { func (o *OpcUA) validateEndpoint() error {
if o.Name == "" { if o.MetricName == "" {
return fmt.Errorf("device name is empty") return fmt.Errorf("device name is empty")
} }
@ -184,22 +229,79 @@ func (o *OpcUA) validateEndpoint() error {
case "None", "Basic128Rsa15", "Basic256", "Basic256Sha256", "auto": case "None", "Basic128Rsa15", "Basic256", "Basic256Sha256", "auto":
break break
default: default:
return fmt.Errorf("invalid security type '%s' in '%s'", o.SecurityPolicy, o.Name) return fmt.Errorf("invalid security type '%s' in '%s'", o.SecurityPolicy, o.MetricName)
} }
//search security mode type //search security mode type
switch o.SecurityMode { switch o.SecurityMode {
case "None", "Sign", "SignAndEncrypt", "auto": case "None", "Sign", "SignAndEncrypt", "auto":
break break
default: default:
return fmt.Errorf("invalid security type '%s' in '%s'", o.SecurityMode, o.Name) return fmt.Errorf("invalid security type '%s' in '%s'", o.SecurityMode, o.MetricName)
} }
return nil return nil
} }
func tagsSliceToMap(tags [][]string) (map[string]string, error) {
m := make(map[string]string)
for i, tag := range tags {
if len(tag) != 2 {
return nil, fmt.Errorf("tag %d needs 2 values, has %d: %v", i+1, len(tag), tag)
}
if tag[0] == "" {
return nil, fmt.Errorf("tag %d has empty name", i+1)
}
if tag[1] == "" {
return nil, fmt.Errorf("tag %d has empty value", i+1)
}
if _, ok := m[tag[0]]; ok {
return nil, fmt.Errorf("tag %d has duplicate key: %v", i+1, tag[0])
}
m[tag[0]] = tag[1]
}
return m, nil
}
//InitNodes Method on OpcUA //InitNodes Method on OpcUA
func (o *OpcUA) InitNodes() error { func (o *OpcUA) InitNodes() error {
if len(o.NodeList) == 0 { for _, node := range o.RootNodes {
return nil o.nodes = append(o.nodes, Node{
metricName: o.MetricName,
tag: node,
})
}
for _, group := range o.Groups {
if group.MetricName == "" {
group.MetricName = o.MetricName
}
groupTags, err := tagsSliceToMap(group.TagsSlice)
if err != nil {
return err
}
for _, node := range group.Nodes {
if node.Namespace == "" {
node.Namespace = group.Namespace
}
if node.IdentifierType == "" {
node.IdentifierType = group.IdentifierType
}
nodeTags, err := tagsSliceToMap(node.TagsSlice)
if err != nil {
return err
}
mergedTags := make(map[string]string)
for k, v := range groupTags {
mergedTags[k] = v
}
for k, v := range nodeTags {
mergedTags[k] = v
}
o.nodes = append(o.nodes, Node{
metricName: group.MetricName,
tag: node,
metricTags: mergedTags,
})
}
} }
err := o.validateOPCTags() err := o.validateOPCTags()
@ -210,50 +312,74 @@ func (o *OpcUA) InitNodes() error {
return nil return nil
} }
type metricParts struct {
metricName string
fieldName string
tags string // sorted by tag name and in format tag1=value1, tag2=value2
}
func newMP(n *Node) metricParts {
var keys []string
for key := range n.metricTags {
keys = append(keys, key)
}
sort.Strings(keys)
var sb strings.Builder
for i, key := range keys {
if i != 0 {
sb.WriteString(", ")
}
sb.WriteString(key)
sb.WriteString("=")
sb.WriteString(n.metricTags[key])
}
x := metricParts{
metricName: n.metricName,
fieldName: n.tag.FieldName,
tags: sb.String(),
}
return x
}
func (o *OpcUA) validateOPCTags() error { func (o *OpcUA) validateOPCTags() error {
nameEncountered := map[string]bool{} nameEncountered := map[metricParts]struct{}{}
for i, item := range o.NodeList { for _, node := range o.nodes {
mp := newMP(&node)
//check empty name //check empty name
if item.Name == "" { if node.tag.FieldName == "" {
return fmt.Errorf("empty name in '%s'", item.Name) return fmt.Errorf("empty name in '%s'", node.tag.FieldName)
} }
//search name duplicate //search name duplicate
if nameEncountered[item.Name] { if _, ok := nameEncountered[mp]; ok {
return fmt.Errorf("name '%s' is duplicated in '%s'", item.Name, item.Name) return fmt.Errorf("name '%s' is duplicated (metric name '%s', tags '%s')",
mp.fieldName, mp.metricName, mp.tags)
} else { } else {
nameEncountered[item.Name] = true //add it to the set
nameEncountered[mp] = struct{}{}
} }
//search identifier type //search identifier type
switch item.IdentifierType { switch node.tag.IdentifierType {
case "s", "i", "g", "b": case "s", "i", "g", "b":
break break
default: default:
return fmt.Errorf("invalid identifier type '%s' in '%s'", item.IdentifierType, item.Name) return fmt.Errorf("invalid identifier type '%s' in '%s'", node.tag.IdentifierType, node.tag.FieldName)
}
// search data type
switch item.DataType {
case "boolean", "byte", "short", "int", "uint", "uint16", "int16", "uint32", "int32", "float", "double", "string", "datetime", "number":
break
default:
return fmt.Errorf("invalid data type '%s' in '%s'", item.DataType, item.Name)
} }
// build nodeid node.idStr = BuildNodeID(node.tag)
o.Nodes = append(o.Nodes, BuildNodeID(item))
//parse NodeIds and NodeIds errors //parse NodeIds and NodeIds errors
nid, niderr := ua.ParseNodeID(o.Nodes[i]) nid, niderr := ua.ParseNodeID(node.idStr)
// build NodeIds and Errors // build NodeIds and Errors
o.NodeIDs = append(o.NodeIDs, nid) o.nodeIDs = append(o.nodeIDs, nid)
o.NodeIDerror = append(o.NodeIDerror, niderr) o.nodeIDerror = append(o.nodeIDerror, niderr)
// Grow NodeData for later input // Grow NodeData for later input
o.NodeData = append(o.NodeData, OPCData{}) o.nodeData = append(o.nodeData, OPCData{})
} }
return nil return nil
} }
// BuildNodeID build node ID from OPC tag // BuildNodeID build node ID from OPC tag
func BuildNodeID(tag OPCTag) string { func BuildNodeID(tag NodeSettings) string {
return "ns=" + tag.Namespace + ";" + tag.IdentifierType + "=" + tag.Identifier return "ns=" + tag.Namespace + ";" + tag.IdentifierType + "=" + tag.Identifier
} }
@ -280,7 +406,7 @@ func Connect(o *OpcUA) error {
} }
regResp, err := o.client.RegisterNodes(&ua.RegisterNodesRequest{ regResp, err := o.client.RegisterNodes(&ua.RegisterNodesRequest{
NodesToRegister: o.NodeIDs, NodesToRegister: o.nodeIDs,
}) })
if err != nil { if err != nil {
return fmt.Errorf("RegisterNodes failed: %v", err) return fmt.Errorf("RegisterNodes failed: %v", err)
@ -325,22 +451,22 @@ func (o *OpcUA) setupOptions() error {
func (o *OpcUA) getData() error { func (o *OpcUA) getData() error {
resp, err := o.client.Read(o.req) resp, err := o.client.Read(o.req)
if err != nil { if err != nil {
o.ReadError++ o.ReadError.Incr(1)
return fmt.Errorf("RegisterNodes Read failed: %v", err) return fmt.Errorf("RegisterNodes Read failed: %v", err)
} }
o.ReadSuccess++ o.ReadSuccess.Incr(1)
for i, d := range resp.Results { for i, d := range resp.Results {
if d.Status != ua.StatusOK { if d.Status != ua.StatusOK {
return fmt.Errorf("Status not OK: %v", d.Status) return fmt.Errorf("Status not OK: %v", d.Status)
} }
o.NodeData[i].TagName = o.NodeList[i].Name o.nodeData[i].TagName = o.nodes[i].tag.FieldName
if d.Value != nil { if d.Value != nil {
o.NodeData[i].Value = d.Value.Value() o.nodeData[i].Value = d.Value.Value()
o.NodeData[i].DataType = d.Value.Type() o.nodeData[i].DataType = d.Value.Type()
} }
o.NodeData[i].Quality = d.Status o.nodeData[i].Quality = d.Status
o.NodeData[i].TimeStamp = d.ServerTimestamp.String() o.nodeData[i].TimeStamp = d.ServerTimestamp.String()
o.NodeData[i].Time = d.SourceTimestamp.String() o.nodeData[i].Time = d.SourceTimestamp.String()
} }
return nil return nil
} }
@ -359,9 +485,6 @@ func disconnect(o *OpcUA) error {
return err return err
} }
o.ReadError = 0
o.ReadSuccess = 0
switch u.Scheme { switch u.Scheme {
case "opc.tcp": case "opc.tcp":
o.state = Disconnected o.state = Disconnected
@ -392,16 +515,18 @@ func (o *OpcUA) Gather(acc telegraf.Accumulator) error {
return err return err
} }
for i, n := range o.NodeList { for i, n := range o.nodes {
fields := make(map[string]interface{}) fields := make(map[string]interface{})
tags := map[string]string{ tags := map[string]string{
"name": n.Name, "id": n.idStr,
"id": BuildNodeID(n), }
for k, v := range n.metricTags {
tags[k] = v
} }
fields[o.NodeData[i].TagName] = o.NodeData[i].Value fields[o.nodeData[i].TagName] = o.nodeData[i].Value
fields["Quality"] = strings.TrimSpace(fmt.Sprint(o.NodeData[i].Quality)) fields["Quality"] = strings.TrimSpace(fmt.Sprint(o.nodeData[i].Quality))
acc.AddFields(o.Name, fields, tags) acc.AddFields(n.metricName, fields, tags)
} }
return nil return nil
} }
@ -410,7 +535,7 @@ func (o *OpcUA) Gather(acc telegraf.Accumulator) error {
func init() { func init() {
inputs.Add("opcua", func() telegraf.Input { inputs.Add("opcua", func() telegraf.Input {
return &OpcUA{ return &OpcUA{
Name: "localhost", MetricName: "opcua",
Endpoint: "opc.tcp://localhost:4840", Endpoint: "opc.tcp://localhost:4840",
SecurityPolicy: "auto", SecurityPolicy: "auto",
SecurityMode: "auto", SecurityMode: "auto",

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -15,7 +16,6 @@ type OPCTags struct {
Namespace string Namespace string
IdentifierType string IdentifierType string
Identifier string Identifier string
DataType string
Want string Want string
} }
@ -25,15 +25,15 @@ func TestClient1(t *testing.T) {
} }
var testopctags = []OPCTags{ var testopctags = []OPCTags{
{"ProductName", "0", "i", "2261", "string", "open62541 OPC UA Server"}, {"ProductName", "0", "i", "2261", "open62541 OPC UA Server"},
{"ProductUri", "0", "i", "2262", "string", "http://open62541.org"}, {"ProductUri", "0", "i", "2262", "http://open62541.org"},
{"ManufacturerName", "0", "i", "2263", "string", "open62541"}, {"ManufacturerName", "0", "i", "2263", "open62541"},
} }
var o OpcUA var o OpcUA
var err error var err error
o.Name = "testing" o.MetricName = "testing"
o.Endpoint = "opc.tcp://opcua.rocks:4840" o.Endpoint = "opc.tcp://opcua.rocks:4840"
o.AuthMethod = "Anonymous" o.AuthMethod = "Anonymous"
o.ConnectTimeout = config.Duration(10 * time.Second) o.ConnectTimeout = config.Duration(10 * time.Second)
@ -41,7 +41,7 @@ func TestClient1(t *testing.T) {
o.SecurityPolicy = "None" o.SecurityPolicy = "None"
o.SecurityMode = "None" o.SecurityMode = "None"
for _, tags := range testopctags { for _, tags := range testopctags {
o.NodeList = append(o.NodeList, MapOPCTag(tags)) o.RootNodes = append(o.RootNodes, MapOPCTag(tags))
} }
err = o.Init() err = o.Init()
if err != nil { if err != nil {
@ -52,26 +52,25 @@ func TestClient1(t *testing.T) {
t.Fatalf("Connect Error: %s", err) t.Fatalf("Connect Error: %s", err)
} }
for i, v := range o.NodeData { for i, v := range o.nodeData {
if v.Value != nil { if v.Value != nil {
types := reflect.TypeOf(v.Value) types := reflect.TypeOf(v.Value)
value := reflect.ValueOf(v.Value) value := reflect.ValueOf(v.Value)
compare := fmt.Sprintf("%v", value.Interface()) compare := fmt.Sprintf("%v", value.Interface())
if compare != testopctags[i].Want { if compare != testopctags[i].Want {
t.Errorf("Tag %s: Values %v for type %s does not match record", o.NodeList[i].Name, value.Interface(), types) t.Errorf("Tag %s: Values %v for type %s does not match record", o.nodes[i].tag.FieldName, value.Interface(), types)
} }
} else { } else {
t.Errorf("Tag: %s has value: %v", o.NodeList[i].Name, v.Value) t.Errorf("Tag: %s has value: %v", o.nodes[i].tag.FieldName, v.Value)
} }
} }
} }
func MapOPCTag(tags OPCTags) (out OPCTag) { func MapOPCTag(tags OPCTags) (out NodeSettings) {
out.Name = tags.Name out.FieldName = tags.Name
out.Namespace = tags.Namespace out.Namespace = tags.Namespace
out.IdentifierType = tags.IdentifierType out.IdentifierType = tags.IdentifierType
out.Identifier = tags.Identifier out.Identifier = tags.Identifier
out.DataType = tags.DataType
return out return out
} }
@ -90,9 +89,21 @@ auth_method = "Anonymous"
username = "" username = ""
password = "" password = ""
nodes = [ nodes = [
{name="name", namespace="", identifier_type="", identifier="", data_type="", description=""}, {name="name", namespace="1", identifier_type="s", identifier="one"},
{name="name2", namespace="", identifier_type="", identifier="", data_type="", description=""}, {name="name2", namespace="2", identifier_type="s", identifier="two"},
] ]
[[inputs.opcua.group]]
name = "foo"
namespace = "3"
identifier_type = "i"
tags = [["tag1", "val1"], ["tag2", "val2"]]
nodes = [{name="name3", identifier="3000", tags=[["tag3", "val3"]]}]
[[inputs.opcua.group]]
name = "bar"
namespace = "0"
identifier_type = "i"
tags = [["tag1", "val1"], ["tag2", "val2"]]
nodes = [{name="name4", identifier="4000", tags=[["tag1", "override"]]}]
` `
c := config.NewConfig() c := config.NewConfig()
@ -104,7 +115,143 @@ nodes = [
o, ok := c.Inputs[0].Input.(*OpcUA) o, ok := c.Inputs[0].Input.(*OpcUA)
require.True(t, ok) require.True(t, ok)
require.Len(t, o.NodeList, 2) require.Len(t, o.RootNodes, 2)
require.Equal(t, o.NodeList[0].Name, "name") require.Equal(t, o.RootNodes[0].FieldName, "name")
require.Equal(t, o.NodeList[1].Name, "name2") require.Equal(t, o.RootNodes[1].FieldName, "name2")
require.Len(t, o.Groups, 2)
require.Equal(t, o.Groups[0].MetricName, "foo")
require.Len(t, o.Groups[0].Nodes, 1)
require.Equal(t, o.Groups[0].Nodes[0].Identifier, "3000")
require.NoError(t, o.InitNodes())
require.Len(t, o.nodes, 4)
require.Len(t, o.nodes[2].metricTags, 3)
require.Len(t, o.nodes[3].metricTags, 2)
}
func TestTagsSliceToMap(t *testing.T) {
m, err := tagsSliceToMap([][]string{{"foo", "bar"}, {"baz", "bat"}})
assert.NoError(t, err)
assert.Len(t, m, 2)
assert.Equal(t, m["foo"], "bar")
assert.Equal(t, m["baz"], "bat")
}
func TestTagsSliceToMap_twoStrings(t *testing.T) {
var err error
_, err = tagsSliceToMap([][]string{{"foo", "bar", "baz"}})
assert.Error(t, err)
_, err = tagsSliceToMap([][]string{{"foo"}})
assert.Error(t, err)
}
func TestTagsSliceToMap_dupeKey(t *testing.T) {
_, err := tagsSliceToMap([][]string{{"foo", "bar"}, {"foo", "bat"}})
assert.Error(t, err)
}
func TestTagsSliceToMap_empty(t *testing.T) {
_, err := tagsSliceToMap([][]string{{"foo", ""}})
assert.Equal(t, fmt.Errorf("tag 1 has empty value"), err)
_, err = tagsSliceToMap([][]string{{"", "bar"}})
assert.Equal(t, fmt.Errorf("tag 1 has empty name"), err)
}
func TestValidateOPCTags(t *testing.T) {
tests := []struct {
name string
nodes []Node
err error
}{
{
"same",
[]Node{
{
metricName: "mn",
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
metricTags: map[string]string{"t1": "v1", "t2": "v2"},
},
{
metricName: "mn",
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
metricTags: map[string]string{"t1": "v1", "t2": "v2"},
},
},
fmt.Errorf("name 'fn' is duplicated (metric name 'mn', tags 't1=v1, t2=v2')"),
},
{
"different metric tag names",
[]Node{
{
metricName: "mn",
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
metricTags: map[string]string{"t1": "", "t2": ""},
},
{
metricName: "mn",
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
metricTags: map[string]string{"t1": "", "t3": ""},
},
},
nil,
},
{
"different metric tag values",
[]Node{
{
metricName: "mn",
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
metricTags: map[string]string{"t1": "foo", "t2": ""},
},
{
metricName: "mn",
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
metricTags: map[string]string{"t1": "bar", "t2": ""},
},
},
nil,
},
{
"different metric names",
[]Node{
{
metricName: "mn",
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
metricTags: map[string]string{"t1": "", "t2": ""},
},
{
metricName: "mn2",
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
metricTags: map[string]string{"t1": "", "t2": ""},
},
},
nil,
},
{
"different field names",
[]Node{
{
metricName: "mn",
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
metricTags: map[string]string{"t1": "", "t2": ""},
},
{
metricName: "mn",
tag: NodeSettings{FieldName: "fn2", IdentifierType: "s"},
metricTags: map[string]string{"t1": "", "t2": ""},
},
},
nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := OpcUA{
nodes: tt.nodes,
}
require.Equal(t, tt.err, o.validateOPCTags())
})
}
} }