Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
500
plugins/common/opcua/input/input_client.go
Normal file
500
plugins/common/opcua/input/input_client.go
Normal file
|
@ -0,0 +1,500 @@
|
|||
package input
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gopcua/opcua/ua"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/internal/choice"
|
||||
"github.com/influxdata/telegraf/metric"
|
||||
"github.com/influxdata/telegraf/plugins/common/opcua"
|
||||
)
|
||||
|
||||
type Trigger string
|
||||
|
||||
const (
|
||||
Status Trigger = "Status"
|
||||
StatusValue Trigger = "StatusValue"
|
||||
StatusValueTimestamp Trigger = "StatusValueTimestamp"
|
||||
)
|
||||
|
||||
type DeadbandType string
|
||||
|
||||
const (
|
||||
Absolute DeadbandType = "Absolute"
|
||||
Percent DeadbandType = "Percent"
|
||||
)
|
||||
|
||||
type DataChangeFilter struct {
|
||||
Trigger Trigger `toml:"trigger"`
|
||||
DeadbandType DeadbandType `toml:"deadband_type"`
|
||||
DeadbandValue *float64 `toml:"deadband_value"`
|
||||
}
|
||||
|
||||
type MonitoringParameters struct {
|
||||
SamplingInterval config.Duration `toml:"sampling_interval"`
|
||||
QueueSize *uint32 `toml:"queue_size"`
|
||||
DiscardOldest *bool `toml:"discard_oldest"`
|
||||
DataChangeFilter *DataChangeFilter `toml:"data_change_filter"`
|
||||
}
|
||||
|
||||
// NodeSettings describes how to map from a OPC UA node to a Metric
|
||||
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" deprecated:"1.17.0;1.35.0;option is ignored"`
|
||||
Description string `toml:"description" deprecated:"1.17.0;1.35.0;option is ignored"`
|
||||
TagsSlice [][]string `toml:"tags" deprecated:"1.25.0;1.35.0;use 'default_tags' instead"`
|
||||
DefaultTags map[string]string `toml:"default_tags"`
|
||||
MonitoringParams MonitoringParameters `toml:"monitoring_params"`
|
||||
}
|
||||
|
||||
// NodeID returns the OPC UA node id
|
||||
func (tag *NodeSettings) NodeID() string {
|
||||
return "ns=" + tag.Namespace + ";" + tag.IdentifierType + "=" + tag.Identifier
|
||||
}
|
||||
|
||||
// NodeGroupSettings describes a mapping of group of nodes to Metrics
|
||||
type NodeGroupSettings 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" deprecated:"1.26.0;1.35.0;use default_tags"`
|
||||
DefaultTags map[string]string `toml:"default_tags"`
|
||||
SamplingInterval config.Duration `toml:"sampling_interval"` // Can be overridden by monitoring parameters
|
||||
}
|
||||
|
||||
type TimestampSource string
|
||||
|
||||
const (
|
||||
TimestampSourceServer TimestampSource = "server"
|
||||
TimestampSourceSource TimestampSource = "source"
|
||||
TimestampSourceTelegraf TimestampSource = "gather"
|
||||
)
|
||||
|
||||
// InputClientConfig a configuration for the input client
|
||||
type InputClientConfig struct {
|
||||
opcua.OpcUAClientConfig
|
||||
MetricName string `toml:"name"`
|
||||
Timestamp TimestampSource `toml:"timestamp"`
|
||||
TimestampFormat string `toml:"timestamp_format"`
|
||||
RootNodes []NodeSettings `toml:"nodes"`
|
||||
Groups []NodeGroupSettings `toml:"group"`
|
||||
}
|
||||
|
||||
func (o *InputClientConfig) Validate() error {
|
||||
if o.MetricName == "" {
|
||||
return errors.New("metric name is empty")
|
||||
}
|
||||
|
||||
err := choice.Check(string(o.Timestamp), []string{"", "gather", "server", "source"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.TimestampFormat == "" {
|
||||
o.TimestampFormat = time.RFC3339Nano
|
||||
}
|
||||
|
||||
if len(o.Groups) == 0 && len(o.RootNodes) == 0 {
|
||||
return errors.New("no groups or root nodes provided to gather from")
|
||||
}
|
||||
for _, group := range o.Groups {
|
||||
if len(group.Nodes) == 0 {
|
||||
return errors.New("group has no nodes to collect from")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *InputClientConfig) CreateInputClient(log telegraf.Logger) (*OpcUAInputClient, error) {
|
||||
if err := o.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debug("Initialising OpcUAInputClient")
|
||||
opcClient, err := o.OpcUAClientConfig.CreateClient(log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &OpcUAInputClient{
|
||||
OpcUAClient: opcClient,
|
||||
Log: log,
|
||||
Config: *o,
|
||||
}
|
||||
|
||||
log.Debug("Initialising node to metric mapping")
|
||||
if err := c.InitNodeMetricMapping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.initLastReceivedValues()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// NodeMetricMapping mapping from a single node to a metric
|
||||
type NodeMetricMapping struct {
|
||||
Tag NodeSettings
|
||||
idStr string
|
||||
metricName string
|
||||
MetricTags map[string]string
|
||||
}
|
||||
|
||||
// NewNodeMetricMapping builds a new NodeMetricMapping from the given argument
|
||||
func NewNodeMetricMapping(metricName string, node NodeSettings, groupTags map[string]string) (*NodeMetricMapping, error) {
|
||||
mergedTags := make(map[string]string)
|
||||
for n, t := range groupTags {
|
||||
mergedTags[n] = t
|
||||
}
|
||||
|
||||
nodeTags := make(map[string]string)
|
||||
if len(node.DefaultTags) > 0 {
|
||||
nodeTags = node.DefaultTags
|
||||
} else if len(node.TagsSlice) > 0 {
|
||||
// fixme: once the TagsSlice has been removed (after deprecation), remove this if else logic
|
||||
var err error
|
||||
nodeTags, err = tagsSliceToMap(node.TagsSlice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for n, t := range nodeTags {
|
||||
mergedTags[n] = t
|
||||
}
|
||||
|
||||
return &NodeMetricMapping{
|
||||
Tag: node,
|
||||
idStr: node.NodeID(),
|
||||
metricName: metricName,
|
||||
MetricTags: mergedTags,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NodeValue The received value for a node
|
||||
type NodeValue struct {
|
||||
TagName string
|
||||
Value interface{}
|
||||
Quality ua.StatusCode
|
||||
ServerTime time.Time
|
||||
SourceTime time.Time
|
||||
DataType ua.TypeID
|
||||
IsArray bool
|
||||
}
|
||||
|
||||
// OpcUAInputClient can receive data from an OPC UA server and map it to Metrics. This type does not contain
|
||||
// logic for actually retrieving data from the server, but is used by other types like ReadClient and
|
||||
// OpcUAInputSubscribeClient to store data needed to convert node ids to the corresponding metrics.
|
||||
type OpcUAInputClient struct {
|
||||
*opcua.OpcUAClient
|
||||
Config InputClientConfig
|
||||
Log telegraf.Logger
|
||||
|
||||
NodeMetricMapping []NodeMetricMapping
|
||||
NodeIDs []*ua.NodeID
|
||||
LastReceivedData []NodeValue
|
||||
}
|
||||
|
||||
// Stop the connection to the client
|
||||
func (o *OpcUAInputClient) Stop(ctx context.Context) <-chan struct{} {
|
||||
ch := make(chan struct{})
|
||||
defer close(ch)
|
||||
err := o.Disconnect(ctx)
|
||||
if err != nil {
|
||||
o.Log.Warn("Disconnecting from server failed with error ", err)
|
||||
}
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// metricParts is only used to ensure no duplicate metrics are created
|
||||
type metricParts struct {
|
||||
metricName string
|
||||
fieldName string
|
||||
tags string // sorted by tag name and in format tag1=value1, tag2=value2
|
||||
}
|
||||
|
||||
func newMP(n *NodeMetricMapping) metricParts {
|
||||
keys := make([]string, 0, len(n.MetricTags))
|
||||
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
|
||||
}
|
||||
|
||||
// fixme: once the TagsSlice has been removed (after deprecation), remove this
|
||||
// tagsSliceToMap takes an array of pairs of strings and creates a map from it
|
||||
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
|
||||
}
|
||||
|
||||
func validateNodeToAdd(existing map[metricParts]struct{}, nmm *NodeMetricMapping) error {
|
||||
if nmm.Tag.FieldName == "" {
|
||||
return fmt.Errorf("empty name in %q", nmm.Tag.FieldName)
|
||||
}
|
||||
|
||||
if len(nmm.Tag.Namespace) == 0 {
|
||||
return errors.New("empty node namespace not allowed")
|
||||
}
|
||||
|
||||
if len(nmm.Tag.Identifier) == 0 {
|
||||
return errors.New("empty node identifier not allowed")
|
||||
}
|
||||
|
||||
mp := newMP(nmm)
|
||||
if _, exists := existing[mp]; exists {
|
||||
return fmt.Errorf("name %q is duplicated (metric name %q, tags %q)",
|
||||
mp.fieldName, mp.metricName, mp.tags)
|
||||
}
|
||||
|
||||
switch nmm.Tag.IdentifierType {
|
||||
case "i":
|
||||
if _, err := strconv.Atoi(nmm.Tag.Identifier); err != nil {
|
||||
return fmt.Errorf("identifier type %q does not match the type of identifier %q", nmm.Tag.IdentifierType, nmm.Tag.Identifier)
|
||||
}
|
||||
case "s", "g", "b":
|
||||
// Valid identifier type - do nothing.
|
||||
default:
|
||||
return fmt.Errorf("invalid identifier type %q in %q", nmm.Tag.IdentifierType, nmm.Tag.FieldName)
|
||||
}
|
||||
|
||||
existing[mp] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitNodeMetricMapping builds nodes from the configuration
|
||||
func (o *OpcUAInputClient) InitNodeMetricMapping() error {
|
||||
existing := make(map[metricParts]struct{}, len(o.Config.RootNodes))
|
||||
for _, node := range o.Config.RootNodes {
|
||||
nmm, err := NewNodeMetricMapping(o.Config.MetricName, node, make(map[string]string))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateNodeToAdd(existing, nmm); err != nil {
|
||||
return err
|
||||
}
|
||||
o.NodeMetricMapping = append(o.NodeMetricMapping, *nmm)
|
||||
}
|
||||
|
||||
for _, group := range o.Config.Groups {
|
||||
if group.MetricName == "" {
|
||||
group.MetricName = o.Config.MetricName
|
||||
}
|
||||
|
||||
if len(group.DefaultTags) > 0 && len(group.TagsSlice) > 0 {
|
||||
o.Log.Warn("Tags found in both `tags` and `default_tags`, only using tags defined in `default_tags`")
|
||||
}
|
||||
|
||||
groupTags := make(map[string]string)
|
||||
if len(group.DefaultTags) > 0 {
|
||||
groupTags = group.DefaultTags
|
||||
} else if len(group.TagsSlice) > 0 {
|
||||
// fixme: once the TagsSlice has been removed (after deprecation), remove this if else logic
|
||||
var err error
|
||||
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
|
||||
}
|
||||
if node.MonitoringParams.SamplingInterval == 0 {
|
||||
node.MonitoringParams.SamplingInterval = group.SamplingInterval
|
||||
}
|
||||
|
||||
nmm, err := NewNodeMetricMapping(group.MetricName, node, groupTags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateNodeToAdd(existing, nmm); err != nil {
|
||||
return err
|
||||
}
|
||||
o.NodeMetricMapping = append(o.NodeMetricMapping, *nmm)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpcUAInputClient) InitNodeIDs() error {
|
||||
o.NodeIDs = make([]*ua.NodeID, 0, len(o.NodeMetricMapping))
|
||||
for _, node := range o.NodeMetricMapping {
|
||||
nid, err := ua.ParseNodeID(node.Tag.NodeID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.NodeIDs = append(o.NodeIDs, nid)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpcUAInputClient) initLastReceivedValues() {
|
||||
o.LastReceivedData = make([]NodeValue, len(o.NodeMetricMapping))
|
||||
for nodeIdx, nmm := range o.NodeMetricMapping {
|
||||
o.LastReceivedData[nodeIdx].TagName = nmm.Tag.FieldName
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpcUAInputClient) UpdateNodeValue(nodeIdx int, d *ua.DataValue) {
|
||||
o.LastReceivedData[nodeIdx].Quality = d.Status
|
||||
if !o.StatusCodeOK(d.Status) {
|
||||
// Verify NodeIDs array has been built before trying to get item; otherwise show '?' for node id
|
||||
if len(o.NodeIDs) > nodeIdx {
|
||||
o.Log.Errorf("status not OK for node %v (%v): %v", o.NodeMetricMapping[nodeIdx].Tag.FieldName, o.NodeIDs[nodeIdx].String(), d.Status)
|
||||
} else {
|
||||
o.Log.Errorf("status not OK for node %v (%v): %v", o.NodeMetricMapping[nodeIdx].Tag.FieldName, '?', d.Status)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if d.Value != nil {
|
||||
o.LastReceivedData[nodeIdx].DataType = d.Value.Type()
|
||||
o.LastReceivedData[nodeIdx].IsArray = d.Value.Has(ua.VariantArrayValues)
|
||||
|
||||
o.LastReceivedData[nodeIdx].Value = d.Value.Value()
|
||||
if o.LastReceivedData[nodeIdx].DataType == ua.TypeIDDateTime {
|
||||
if t, ok := d.Value.Value().(time.Time); ok {
|
||||
o.LastReceivedData[nodeIdx].Value = t.Format(o.Config.TimestampFormat)
|
||||
}
|
||||
}
|
||||
}
|
||||
o.LastReceivedData[nodeIdx].ServerTime = d.ServerTimestamp
|
||||
o.LastReceivedData[nodeIdx].SourceTime = d.SourceTimestamp
|
||||
}
|
||||
|
||||
func (o *OpcUAInputClient) MetricForNode(nodeIdx int) telegraf.Metric {
|
||||
nmm := &o.NodeMetricMapping[nodeIdx]
|
||||
tags := map[string]string{
|
||||
"id": nmm.idStr,
|
||||
}
|
||||
for k, v := range nmm.MetricTags {
|
||||
tags[k] = v
|
||||
}
|
||||
|
||||
fields := make(map[string]interface{})
|
||||
if o.LastReceivedData[nodeIdx].Value != nil {
|
||||
// Simple scalar types can be stored directly under the field name while
|
||||
// arrays (see 5.2.5) and structures (see 5.2.6) must be unpacked.
|
||||
// Note: Structures and arrays of structures are currently not supported.
|
||||
if o.LastReceivedData[nodeIdx].IsArray {
|
||||
switch typedValue := o.LastReceivedData[nodeIdx].Value.(type) {
|
||||
case []uint8:
|
||||
fields = unpack(nmm.Tag.FieldName, typedValue)
|
||||
case []uint16:
|
||||
fields = unpack(nmm.Tag.FieldName, typedValue)
|
||||
case []uint32:
|
||||
fields = unpack(nmm.Tag.FieldName, typedValue)
|
||||
case []uint64:
|
||||
fields = unpack(nmm.Tag.FieldName, typedValue)
|
||||
case []int8:
|
||||
fields = unpack(nmm.Tag.FieldName, typedValue)
|
||||
case []int16:
|
||||
fields = unpack(nmm.Tag.FieldName, typedValue)
|
||||
case []int32:
|
||||
fields = unpack(nmm.Tag.FieldName, typedValue)
|
||||
case []int64:
|
||||
fields = unpack(nmm.Tag.FieldName, typedValue)
|
||||
case []float32:
|
||||
fields = unpack(nmm.Tag.FieldName, typedValue)
|
||||
case []float64:
|
||||
fields = unpack(nmm.Tag.FieldName, typedValue)
|
||||
case []string:
|
||||
fields = unpack(nmm.Tag.FieldName, typedValue)
|
||||
case []bool:
|
||||
fields = unpack(nmm.Tag.FieldName, typedValue)
|
||||
default:
|
||||
o.Log.Errorf("could not unpack variant array of type: %T", typedValue)
|
||||
}
|
||||
} else {
|
||||
fields = map[string]interface{}{
|
||||
nmm.Tag.FieldName: o.LastReceivedData[nodeIdx].Value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fields["Quality"] = strings.TrimSpace(o.LastReceivedData[nodeIdx].Quality.Error())
|
||||
if choice.Contains("DataType", o.Config.OptionalFields) {
|
||||
fields["DataType"] = strings.Replace(o.LastReceivedData[nodeIdx].DataType.String(), "TypeID", "", 1)
|
||||
}
|
||||
if !o.StatusCodeOK(o.LastReceivedData[nodeIdx].Quality) {
|
||||
mp := newMP(nmm)
|
||||
o.Log.Debugf("status not OK for node %q(metric name %q, tags %q)",
|
||||
mp.fieldName, mp.metricName, mp.tags)
|
||||
}
|
||||
|
||||
var t time.Time
|
||||
switch o.Config.Timestamp {
|
||||
case TimestampSourceServer:
|
||||
t = o.LastReceivedData[nodeIdx].ServerTime
|
||||
case TimestampSourceSource:
|
||||
t = o.LastReceivedData[nodeIdx].SourceTime
|
||||
default:
|
||||
t = time.Now()
|
||||
}
|
||||
|
||||
return metric.New(nmm.metricName, tags, fields, t)
|
||||
}
|
||||
|
||||
func unpack[Slice ~[]E, E any](prefix string, value Slice) map[string]interface{} {
|
||||
fields := make(map[string]interface{}, len(value))
|
||||
for i, v := range value {
|
||||
key := fmt.Sprintf("%s[%d]", prefix, i)
|
||||
fields[key] = v
|
||||
}
|
||||
return fields
|
||||
}
|
890
plugins/common/opcua/input/input_client_test.go
Normal file
890
plugins/common/opcua/input/input_client_test.go
Normal file
|
@ -0,0 +1,890 @@
|
|||
package input
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gopcua/opcua/ua"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/metric"
|
||||
"github.com/influxdata/telegraf/plugins/common/opcua"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
)
|
||||
|
||||
func TestTagsSliceToMap(t *testing.T) {
|
||||
m, err := tagsSliceToMap([][]string{{"foo", "bar"}, {"baz", "bat"}})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 2)
|
||||
require.Equal(t, "bar", m["foo"])
|
||||
require.Equal(t, "bat", m["baz"])
|
||||
}
|
||||
|
||||
func TestTagsSliceToMap_twoStrings(t *testing.T) {
|
||||
var err error
|
||||
_, err = tagsSliceToMap([][]string{{"foo", "bar", "baz"}})
|
||||
require.Error(t, err)
|
||||
_, err = tagsSliceToMap([][]string{{"foo"}})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestTagsSliceToMap_dupeKey(t *testing.T) {
|
||||
_, err := tagsSliceToMap([][]string{{"foo", "bar"}, {"foo", "bat"}})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestTagsSliceToMap_empty(t *testing.T) {
|
||||
_, err := tagsSliceToMap([][]string{{"foo", ""}})
|
||||
require.Equal(t, errors.New("tag 1 has empty value"), err)
|
||||
_, err = tagsSliceToMap([][]string{{"", "bar"}})
|
||||
require.Equal(t, errors.New("tag 1 has empty name"), err)
|
||||
}
|
||||
|
||||
func TestValidateOPCTags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config InputClientConfig
|
||||
err error
|
||||
}{
|
||||
{
|
||||
"duplicates",
|
||||
InputClientConfig{
|
||||
MetricName: "mn",
|
||||
RootNodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "fn",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "i1",
|
||||
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
|
||||
},
|
||||
},
|
||||
Groups: []NodeGroupSettings{
|
||||
{
|
||||
Nodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "fn",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "i1",
|
||||
},
|
||||
},
|
||||
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
errors.New(`name "fn" is duplicated (metric name "mn", tags "t1=v1, t2=v2")`),
|
||||
},
|
||||
{
|
||||
"empty tag value not allowed",
|
||||
InputClientConfig{
|
||||
MetricName: "mn",
|
||||
RootNodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "fn",
|
||||
IdentifierType: "s",
|
||||
TagsSlice: [][]string{{"t1", ""}},
|
||||
},
|
||||
},
|
||||
},
|
||||
errors.New("tag 1 has empty value"),
|
||||
},
|
||||
{
|
||||
"empty tag name not allowed",
|
||||
InputClientConfig{
|
||||
MetricName: "mn",
|
||||
RootNodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "fn",
|
||||
IdentifierType: "s",
|
||||
TagsSlice: [][]string{{"", "1"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
errors.New("tag 1 has empty name"),
|
||||
},
|
||||
{
|
||||
"different metric tag names",
|
||||
InputClientConfig{
|
||||
MetricName: "mn",
|
||||
RootNodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "fn",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "i1",
|
||||
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
|
||||
},
|
||||
{
|
||||
FieldName: "fn",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "i1",
|
||||
TagsSlice: [][]string{{"t1", "v1"}, {"t3", "v2"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"different metric tag values",
|
||||
InputClientConfig{
|
||||
MetricName: "mn",
|
||||
RootNodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "fn",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "i1",
|
||||
TagsSlice: [][]string{{"t1", "foo"}, {"t2", "v2"}},
|
||||
},
|
||||
{
|
||||
FieldName: "fn",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "i1",
|
||||
TagsSlice: [][]string{{"t1", "bar"}, {"t2", "v2"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"different metric names",
|
||||
InputClientConfig{
|
||||
MetricName: "mn",
|
||||
Groups: []NodeGroupSettings{
|
||||
{
|
||||
MetricName: "mn",
|
||||
Namespace: "2",
|
||||
Nodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "fn",
|
||||
IdentifierType: "s",
|
||||
Identifier: "i1",
|
||||
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
MetricName: "mn2",
|
||||
Namespace: "2",
|
||||
Nodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "fn",
|
||||
IdentifierType: "s",
|
||||
Identifier: "i1",
|
||||
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"different field names",
|
||||
InputClientConfig{
|
||||
MetricName: "mn",
|
||||
RootNodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "fn",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "i1",
|
||||
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
|
||||
},
|
||||
{
|
||||
FieldName: "fn2",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "i1",
|
||||
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := OpcUAInputClient{
|
||||
Config: tt.config,
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
require.Equal(t, tt.err, o.InitNodeMetricMapping())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewNodeMetricMappingTags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
settings NodeSettings
|
||||
groupTags map[string]string
|
||||
expectedTags map[string]string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "empty tags",
|
||||
settings: NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "h",
|
||||
},
|
||||
groupTags: map[string]string{},
|
||||
expectedTags: map[string]string{},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "node tags only",
|
||||
settings: NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "h",
|
||||
TagsSlice: [][]string{{"t1", "v1"}},
|
||||
},
|
||||
groupTags: map[string]string{},
|
||||
expectedTags: map[string]string{"t1": "v1"},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "group tags only",
|
||||
settings: NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "h",
|
||||
},
|
||||
groupTags: map[string]string{"t1": "v1"},
|
||||
expectedTags: map[string]string{"t1": "v1"},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "node tag overrides group tags",
|
||||
settings: NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "h",
|
||||
TagsSlice: [][]string{{"t1", "v2"}},
|
||||
},
|
||||
groupTags: map[string]string{"t1": "v1"},
|
||||
expectedTags: map[string]string{"t1": "v2"},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "node tag merged with group tags",
|
||||
settings: NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "h",
|
||||
TagsSlice: [][]string{{"t2", "v2"}},
|
||||
},
|
||||
groupTags: map[string]string{"t1": "v1"},
|
||||
expectedTags: map[string]string{"t1": "v1", "t2": "v2"},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
nmm, err := NewNodeMetricMapping("testmetric", tt.settings, tt.groupTags)
|
||||
require.Equal(t, tt.err, err)
|
||||
require.Equal(t, tt.expectedTags, nmm.MetricTags)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewNodeMetricMappingIdStrInstantiated(t *testing.T) {
|
||||
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "h",
|
||||
}, map[string]string{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "ns=2;s=h", nmm.idStr)
|
||||
}
|
||||
|
||||
func TestValidateNodeToAdd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
existing map[metricParts]struct{}
|
||||
nmm *NodeMetricMapping
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
existing: map[metricParts]struct{}{},
|
||||
nmm: func() *NodeMetricMapping {
|
||||
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "hf",
|
||||
}, map[string]string{})
|
||||
require.NoError(t, err)
|
||||
return nmm
|
||||
}(),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "empty field name not allowed",
|
||||
existing: map[metricParts]struct{}{},
|
||||
nmm: func() *NodeMetricMapping {
|
||||
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
|
||||
FieldName: "",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "hf",
|
||||
}, map[string]string{})
|
||||
require.NoError(t, err)
|
||||
return nmm
|
||||
}(),
|
||||
err: errors.New(`empty name in ""`),
|
||||
},
|
||||
{
|
||||
name: "empty namespace not allowed",
|
||||
existing: map[metricParts]struct{}{},
|
||||
nmm: func() *NodeMetricMapping {
|
||||
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "",
|
||||
IdentifierType: "s",
|
||||
Identifier: "hf",
|
||||
}, map[string]string{})
|
||||
require.NoError(t, err)
|
||||
return nmm
|
||||
}(),
|
||||
err: errors.New("empty node namespace not allowed"),
|
||||
},
|
||||
{
|
||||
name: "empty identifier type not allowed",
|
||||
existing: map[metricParts]struct{}{},
|
||||
nmm: func() *NodeMetricMapping {
|
||||
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "",
|
||||
Identifier: "hf",
|
||||
}, map[string]string{})
|
||||
require.NoError(t, err)
|
||||
return nmm
|
||||
}(),
|
||||
err: errors.New(`invalid identifier type "" in "f"`),
|
||||
},
|
||||
{
|
||||
name: "invalid identifier type not allowed",
|
||||
existing: map[metricParts]struct{}{},
|
||||
nmm: func() *NodeMetricMapping {
|
||||
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "j",
|
||||
Identifier: "hf",
|
||||
}, map[string]string{})
|
||||
require.NoError(t, err)
|
||||
return nmm
|
||||
}(),
|
||||
err: errors.New(`invalid identifier type "j" in "f"`),
|
||||
},
|
||||
{
|
||||
name: "duplicate metric not allowed",
|
||||
existing: map[metricParts]struct{}{
|
||||
{metricName: "testmetric", fieldName: "f", tags: "t1=v1, t2=v2"}: {},
|
||||
},
|
||||
nmm: func() *NodeMetricMapping {
|
||||
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "hf",
|
||||
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
|
||||
}, map[string]string{})
|
||||
require.NoError(t, err)
|
||||
return nmm
|
||||
}(),
|
||||
err: errors.New(`name "f" is duplicated (metric name "testmetric", tags "t1=v1, t2=v2")`),
|
||||
},
|
||||
{
|
||||
name: "identifier type mismatch",
|
||||
existing: map[metricParts]struct{}{},
|
||||
nmm: func() *NodeMetricMapping {
|
||||
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "i",
|
||||
Identifier: "hf",
|
||||
}, map[string]string{})
|
||||
require.NoError(t, err)
|
||||
return nmm
|
||||
}(),
|
||||
err: errors.New(`identifier type "i" does not match the type of identifier "hf"`),
|
||||
},
|
||||
}
|
||||
|
||||
for idT, idV := range map[string]string{
|
||||
"s": "hf",
|
||||
"i": "1",
|
||||
"g": "849683f0-ce92-4fa2-836f-a02cde61d75d",
|
||||
"b": "aGVsbG8gSSBhbSBhIHRlc3QgaWRlbnRpZmllcg=="} {
|
||||
tests = append(tests, struct {
|
||||
name string
|
||||
existing map[metricParts]struct{}
|
||||
nmm *NodeMetricMapping
|
||||
err error
|
||||
}{
|
||||
name: "identifier type " + idT + " allowed",
|
||||
existing: map[metricParts]struct{}{},
|
||||
nmm: func() *NodeMetricMapping {
|
||||
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: idT,
|
||||
Identifier: idV,
|
||||
}, map[string]string{})
|
||||
require.NoError(t, err)
|
||||
return nmm
|
||||
}(),
|
||||
err: nil,
|
||||
})
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateNodeToAdd(tt.existing, tt.nmm)
|
||||
require.Equal(t, tt.err, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitNodeMetricMapping(t *testing.T) {
|
||||
tests := []struct {
|
||||
testname string
|
||||
config InputClientConfig
|
||||
expected []NodeMetricMapping
|
||||
err error
|
||||
}{
|
||||
{
|
||||
testname: "only root node",
|
||||
config: InputClientConfig{
|
||||
MetricName: "testmetric",
|
||||
Timestamp: TimestampSourceTelegraf,
|
||||
RootNodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "id1",
|
||||
TagsSlice: [][]string{{"t1", "v1"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []NodeMetricMapping{
|
||||
{
|
||||
Tag: NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "id1",
|
||||
TagsSlice: [][]string{{"t1", "v1"}},
|
||||
},
|
||||
idStr: "ns=2;s=id1",
|
||||
metricName: "testmetric",
|
||||
MetricTags: map[string]string{"t1": "v1"},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
testname: "root node and group node",
|
||||
config: InputClientConfig{
|
||||
MetricName: "testmetric",
|
||||
Timestamp: TimestampSourceTelegraf,
|
||||
RootNodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "id1",
|
||||
TagsSlice: [][]string{{"t1", "v1"}},
|
||||
},
|
||||
},
|
||||
Groups: []NodeGroupSettings{
|
||||
{
|
||||
MetricName: "groupmetric",
|
||||
Namespace: "3",
|
||||
IdentifierType: "s",
|
||||
Nodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "f",
|
||||
Identifier: "id2",
|
||||
TagsSlice: [][]string{{"t2", "v2"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []NodeMetricMapping{
|
||||
{
|
||||
Tag: NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "id1",
|
||||
TagsSlice: [][]string{{"t1", "v1"}},
|
||||
},
|
||||
idStr: "ns=2;s=id1",
|
||||
metricName: "testmetric",
|
||||
MetricTags: map[string]string{"t1": "v1"},
|
||||
},
|
||||
{
|
||||
Tag: NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "3",
|
||||
IdentifierType: "s",
|
||||
Identifier: "id2",
|
||||
TagsSlice: [][]string{{"t2", "v2"}},
|
||||
},
|
||||
idStr: "ns=3;s=id2",
|
||||
metricName: "groupmetric",
|
||||
MetricTags: map[string]string{"t2": "v2"},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
testname: "only group node",
|
||||
config: InputClientConfig{
|
||||
MetricName: "testmetric",
|
||||
Timestamp: TimestampSourceTelegraf,
|
||||
Groups: []NodeGroupSettings{
|
||||
{
|
||||
MetricName: "groupmetric",
|
||||
Namespace: "3",
|
||||
IdentifierType: "s",
|
||||
Nodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "f",
|
||||
Identifier: "id2",
|
||||
TagsSlice: [][]string{{"t2", "v2"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []NodeMetricMapping{
|
||||
{
|
||||
Tag: NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "3",
|
||||
IdentifierType: "s",
|
||||
Identifier: "id2",
|
||||
TagsSlice: [][]string{{"t2", "v2"}},
|
||||
},
|
||||
idStr: "ns=3;s=id2",
|
||||
metricName: "groupmetric",
|
||||
MetricTags: map[string]string{"t2": "v2"},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
testname: "tags and default only default tags used",
|
||||
config: InputClientConfig{
|
||||
MetricName: "testmetric",
|
||||
Timestamp: TimestampSourceTelegraf,
|
||||
Groups: []NodeGroupSettings{
|
||||
{
|
||||
MetricName: "groupmetric",
|
||||
Namespace: "3",
|
||||
IdentifierType: "s",
|
||||
Nodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "f",
|
||||
Identifier: "id2",
|
||||
TagsSlice: [][]string{{"t2", "v2"}},
|
||||
DefaultTags: map[string]string{"t3": "v3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []NodeMetricMapping{
|
||||
{
|
||||
Tag: NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "3",
|
||||
IdentifierType: "s",
|
||||
Identifier: "id2",
|
||||
TagsSlice: [][]string{{"t2", "v2"}},
|
||||
DefaultTags: map[string]string{"t3": "v3"},
|
||||
},
|
||||
idStr: "ns=3;s=id2",
|
||||
metricName: "groupmetric",
|
||||
MetricTags: map[string]string{"t3": "v3"},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
testname: "only root node default overrides slice",
|
||||
config: InputClientConfig{
|
||||
MetricName: "testmetric",
|
||||
Timestamp: TimestampSourceTelegraf,
|
||||
RootNodes: []NodeSettings{
|
||||
{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "id1",
|
||||
TagsSlice: [][]string{{"t1", "v1"}},
|
||||
DefaultTags: map[string]string{"t3": "v3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []NodeMetricMapping{
|
||||
{
|
||||
Tag: NodeSettings{
|
||||
FieldName: "f",
|
||||
Namespace: "2",
|
||||
IdentifierType: "s",
|
||||
Identifier: "id1",
|
||||
TagsSlice: [][]string{{"t1", "v1"}},
|
||||
DefaultTags: map[string]string{"t3": "v3"},
|
||||
},
|
||||
idStr: "ns=2;s=id1",
|
||||
metricName: "testmetric",
|
||||
MetricTags: map[string]string{"t3": "v3"},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.testname, func(t *testing.T) {
|
||||
o := OpcUAInputClient{Config: tt.config}
|
||||
err := o.InitNodeMetricMapping()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expected, o.NodeMetricMapping)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateNodeValue(t *testing.T) {
|
||||
type testStep struct {
|
||||
nodeIdx int
|
||||
value interface{}
|
||||
status ua.StatusCode
|
||||
expected interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
testname string
|
||||
steps []testStep
|
||||
}{
|
||||
{
|
||||
"value should update when code ok",
|
||||
[]testStep{
|
||||
{
|
||||
0,
|
||||
"Harmony",
|
||||
ua.StatusOK,
|
||||
"Harmony",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"value should not update when code bad",
|
||||
[]testStep{
|
||||
{
|
||||
0,
|
||||
"Harmony",
|
||||
ua.StatusOK,
|
||||
"Harmony",
|
||||
},
|
||||
{
|
||||
0,
|
||||
"Odium",
|
||||
ua.StatusBad,
|
||||
"Harmony",
|
||||
},
|
||||
{
|
||||
0,
|
||||
"Ati",
|
||||
ua.StatusOK,
|
||||
"Ati",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
conf := &opcua.OpcUAClientConfig{
|
||||
Endpoint: "opc.tcp://localhost:4930",
|
||||
SecurityPolicy: "None",
|
||||
SecurityMode: "None",
|
||||
AuthMethod: "",
|
||||
ConnectTimeout: config.Duration(2 * time.Second),
|
||||
RequestTimeout: config.Duration(2 * time.Second),
|
||||
Workarounds: opcua.OpcUAWorkarounds{},
|
||||
}
|
||||
c, err := conf.CreateClient(testutil.Logger{})
|
||||
require.NoError(t, err)
|
||||
o := OpcUAInputClient{
|
||||
OpcUAClient: c,
|
||||
Log: testutil.Logger{},
|
||||
NodeMetricMapping: []NodeMetricMapping{
|
||||
{
|
||||
Tag: NodeSettings{
|
||||
FieldName: "f",
|
||||
},
|
||||
},
|
||||
{
|
||||
Tag: NodeSettings{
|
||||
FieldName: "f2",
|
||||
},
|
||||
},
|
||||
},
|
||||
LastReceivedData: make([]NodeValue, 2),
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.testname, func(t *testing.T) {
|
||||
o.LastReceivedData = make([]NodeValue, 2)
|
||||
for i, step := range tt.steps {
|
||||
v, err := ua.NewVariant(step.value)
|
||||
require.NoError(t, err)
|
||||
o.UpdateNodeValue(0, &ua.DataValue{
|
||||
Value: v,
|
||||
Status: step.status,
|
||||
SourceTimestamp: time.Date(2022, 03, 17, 8, 33, 00, 00, &time.Location{}).Add(time.Duration(i) * time.Second),
|
||||
SourcePicoseconds: 0,
|
||||
ServerTimestamp: time.Date(2022, 03, 17, 8, 33, 00, 500, &time.Location{}).Add(time.Duration(i) * time.Second),
|
||||
ServerPicoseconds: 0,
|
||||
})
|
||||
require.Equal(t, step.expected, o.LastReceivedData[0].Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricForNode(t *testing.T) {
|
||||
conf := &opcua.OpcUAClientConfig{
|
||||
Endpoint: "opc.tcp://localhost:4930",
|
||||
SecurityPolicy: "None",
|
||||
SecurityMode: "None",
|
||||
AuthMethod: "",
|
||||
ConnectTimeout: config.Duration(2 * time.Second),
|
||||
RequestTimeout: config.Duration(2 * time.Second),
|
||||
Workarounds: opcua.OpcUAWorkarounds{},
|
||||
}
|
||||
c, err := conf.CreateClient(testutil.Logger{})
|
||||
require.NoError(t, err)
|
||||
o := OpcUAInputClient{
|
||||
Config: InputClientConfig{
|
||||
Timestamp: TimestampSourceSource,
|
||||
},
|
||||
OpcUAClient: c,
|
||||
Log: testutil.Logger{},
|
||||
LastReceivedData: make([]NodeValue, 2),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
testname string
|
||||
nmm []NodeMetricMapping
|
||||
v interface{}
|
||||
isArray bool
|
||||
dataType ua.TypeID
|
||||
time time.Time
|
||||
status ua.StatusCode
|
||||
expected telegraf.Metric
|
||||
}{
|
||||
{
|
||||
testname: "metric build correctly",
|
||||
nmm: []NodeMetricMapping{
|
||||
{
|
||||
Tag: NodeSettings{
|
||||
FieldName: "fn",
|
||||
},
|
||||
idStr: "ns=3;s=hi",
|
||||
metricName: "testingmetric",
|
||||
MetricTags: map[string]string{"t1": "v1"},
|
||||
},
|
||||
},
|
||||
v: 16,
|
||||
isArray: false,
|
||||
dataType: ua.TypeIDInt32,
|
||||
time: time.Date(2022, 03, 17, 8, 55, 00, 00, &time.Location{}),
|
||||
status: ua.StatusOK,
|
||||
expected: metric.New("testingmetric",
|
||||
map[string]string{"t1": "v1", "id": "ns=3;s=hi"},
|
||||
map[string]interface{}{"Quality": "The operation succeeded. StatusGood (0x0)", "fn": 16},
|
||||
time.Date(2022, 03, 17, 8, 55, 00, 00, &time.Location{})),
|
||||
},
|
||||
{
|
||||
testname: "array-like metric build correctly",
|
||||
nmm: []NodeMetricMapping{
|
||||
{
|
||||
Tag: NodeSettings{
|
||||
FieldName: "fn",
|
||||
},
|
||||
idStr: "ns=3;s=hi",
|
||||
metricName: "testingmetric",
|
||||
MetricTags: map[string]string{"t1": "v1"},
|
||||
},
|
||||
},
|
||||
v: []int32{16, 17},
|
||||
isArray: true,
|
||||
dataType: ua.TypeIDInt32,
|
||||
time: time.Date(2022, 03, 17, 8, 55, 00, 00, &time.Location{}),
|
||||
status: ua.StatusOK,
|
||||
expected: metric.New("testingmetric",
|
||||
map[string]string{"t1": "v1", "id": "ns=3;s=hi"},
|
||||
map[string]interface{}{"Quality": "The operation succeeded. StatusGood (0x0)", "fn[0]": 16, "fn[1]": 17},
|
||||
time.Date(2022, 03, 17, 8, 55, 00, 00, &time.Location{})),
|
||||
},
|
||||
{
|
||||
testname: "nil does not panic",
|
||||
nmm: []NodeMetricMapping{
|
||||
{
|
||||
Tag: NodeSettings{
|
||||
FieldName: "fn",
|
||||
},
|
||||
idStr: "ns=3;s=hi",
|
||||
metricName: "testingmetric",
|
||||
MetricTags: map[string]string{"t1": "v1"},
|
||||
},
|
||||
},
|
||||
v: nil,
|
||||
isArray: false,
|
||||
dataType: ua.TypeIDNull,
|
||||
time: time.Date(2022, 03, 17, 8, 55, 00, 00, &time.Location{}),
|
||||
status: ua.StatusOK,
|
||||
expected: metric.New("testingmetric",
|
||||
map[string]string{"t1": "v1", "id": "ns=3;s=hi"},
|
||||
map[string]interface{}{"Quality": "The operation succeeded. StatusGood (0x0)"},
|
||||
time.Date(2022, 03, 17, 8, 55, 00, 00, &time.Location{})),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.testname, func(t *testing.T) {
|
||||
o.NodeMetricMapping = tt.nmm
|
||||
o.LastReceivedData[0].SourceTime = tt.time
|
||||
o.LastReceivedData[0].Quality = tt.status
|
||||
o.LastReceivedData[0].Value = tt.v
|
||||
o.LastReceivedData[0].DataType = tt.dataType
|
||||
o.LastReceivedData[0].IsArray = tt.isArray
|
||||
actual := o.MetricForNode(0)
|
||||
require.Equal(t, tt.expected.Tags(), actual.Tags())
|
||||
require.Equal(t, tt.expected.Fields(), actual.Fields())
|
||||
require.Equal(t, tt.expected.Time(), actual.Time())
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue