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
[[inputs.opcua]]
## Device name
# name = "localhost"
## Metric name
# name = "opcua"
#
## OPC UA Endpoint URL
# endpoint = "opc.tcp://localhost:4840"
@ -47,34 +47,97 @@ Plugin minimum tested version: 1.16
# password = ""
#
## Node ID configuration
## name - the variable name
## namespace - integer value 0 thru 3
## identifier_type - s=string, i=numeric, g=guid, b=opaque
## identifier - tag as shown in opcua browser
## data_type - boolean, byte, short, int, uint, uint16, int16,
## uint32, int32, float, double, string, datetime, number
## name - field name to use in the output
## namespace - OPC UA namespace of the node (integer value 0 thru 3)
## identifier_type - OPC UA ID type (s=string, i=numeric, g=guid, b=opaque)
## identifier - OPC UA ID (tag as shown in opcua browser)
## tags - extra tags to be added to the output metric (optional)
## 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 = [
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""},
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""},
{name="name", identifier="1001", tags=[["node1_tag", "val2"]]},
{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
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:
It produces metrics like these:
```
{name="LabelName", namespace="3", identifier_type="s", identifier="Temperature", data_type="float", description="Description of node"},
```
### Example Output
```
opcua,host=3c70aee0901e,name=Random,type=double Random=0.018158170305814902 1597820490000000000
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
```

View File

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

View File

@ -7,6 +7,7 @@ import (
"time"
"github.com/influxdata/telegraf/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -15,7 +16,6 @@ type OPCTags struct {
Namespace string
IdentifierType string
Identifier string
DataType string
Want string
}
@ -25,15 +25,15 @@ func TestClient1(t *testing.T) {
}
var testopctags = []OPCTags{
{"ProductName", "0", "i", "2261", "string", "open62541 OPC UA Server"},
{"ProductUri", "0", "i", "2262", "string", "http://open62541.org"},
{"ManufacturerName", "0", "i", "2263", "string", "open62541"},
{"ProductName", "0", "i", "2261", "open62541 OPC UA Server"},
{"ProductUri", "0", "i", "2262", "http://open62541.org"},
{"ManufacturerName", "0", "i", "2263", "open62541"},
}
var o OpcUA
var err error
o.Name = "testing"
o.MetricName = "testing"
o.Endpoint = "opc.tcp://opcua.rocks:4840"
o.AuthMethod = "Anonymous"
o.ConnectTimeout = config.Duration(10 * time.Second)
@ -41,7 +41,7 @@ func TestClient1(t *testing.T) {
o.SecurityPolicy = "None"
o.SecurityMode = "None"
for _, tags := range testopctags {
o.NodeList = append(o.NodeList, MapOPCTag(tags))
o.RootNodes = append(o.RootNodes, MapOPCTag(tags))
}
err = o.Init()
if err != nil {
@ -52,26 +52,25 @@ func TestClient1(t *testing.T) {
t.Fatalf("Connect Error: %s", err)
}
for i, v := range o.NodeData {
for i, v := range o.nodeData {
if v.Value != nil {
types := reflect.TypeOf(v.Value)
value := reflect.ValueOf(v.Value)
compare := fmt.Sprintf("%v", value.Interface())
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 {
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) {
out.Name = tags.Name
func MapOPCTag(tags OPCTags) (out NodeSettings) {
out.FieldName = tags.Name
out.Namespace = tags.Namespace
out.IdentifierType = tags.IdentifierType
out.Identifier = tags.Identifier
out.DataType = tags.DataType
return out
}
@ -90,9 +89,21 @@ auth_method = "Anonymous"
username = ""
password = ""
nodes = [
{name="name", namespace="", identifier_type="", identifier="", data_type="", description=""},
{name="name2", namespace="", identifier_type="", identifier="", data_type="", description=""},
{name="name", namespace="1", identifier_type="s", identifier="one"},
{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()
@ -104,7 +115,143 @@ nodes = [
o, ok := c.Inputs[0].Input.(*OpcUA)
require.True(t, ok)
require.Len(t, o.NodeList, 2)
require.Equal(t, o.NodeList[0].Name, "name")
require.Equal(t, o.NodeList[1].Name, "name2")
require.Len(t, o.RootNodes, 2)
require.Equal(t, o.RootNodes[0].FieldName, "name")
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())
})
}
}