fix(inputs.opcua): Handle node array values (#16594)
This commit is contained in:
parent
b490846d24
commit
b49810d337
|
|
@ -288,6 +288,7 @@ type NodeValue struct {
|
|||
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
|
||||
|
|
@ -527,6 +528,7 @@ func (o *OpcUAInputClient) UpdateNodeValue(nodeIdx int, d *ua.DataValue) {
|
|||
|
||||
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 {
|
||||
|
|
@ -541,7 +543,6 @@ func (o *OpcUAInputClient) UpdateNodeValue(nodeIdx int, d *ua.DataValue) {
|
|||
|
||||
func (o *OpcUAInputClient) MetricForNode(nodeIdx int) telegraf.Metric {
|
||||
nmm := &o.NodeMetricMapping[nodeIdx]
|
||||
fields := make(map[string]interface{})
|
||||
tags := map[string]string{
|
||||
"id": nmm.idStr,
|
||||
}
|
||||
|
|
@ -549,7 +550,47 @@ func (o *OpcUAInputClient) MetricForNode(nodeIdx int) telegraf.Metric {
|
|||
tags[k] = v
|
||||
}
|
||||
|
||||
fields[nmm.Tag.FieldName] = o.LastReceivedData[nodeIdx].Value
|
||||
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)
|
||||
|
|
@ -573,6 +614,15 @@ func (o *OpcUAInputClient) MetricForNode(nodeIdx int) telegraf.Metric {
|
|||
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
|
||||
}
|
||||
|
||||
func (o *OpcUAInputClient) MetricForEvent(nodeIdx int, event *ua.EventFieldList) telegraf.Metric {
|
||||
node := o.EventNodeMetricMapping[nodeIdx]
|
||||
fields := make(map[string]interface{}, len(event.EventFields))
|
||||
|
|
|
|||
|
|
@ -799,6 +799,8 @@ func TestMetricForNode(t *testing.T) {
|
|||
testname string
|
||||
nmm []NodeMetricMapping
|
||||
v interface{}
|
||||
isArray bool
|
||||
dataType ua.TypeID
|
||||
time time.Time
|
||||
status ua.StatusCode
|
||||
expected telegraf.Metric
|
||||
|
|
@ -816,6 +818,8 @@ func TestMetricForNode(t *testing.T) {
|
|||
},
|
||||
},
|
||||
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",
|
||||
|
|
@ -823,6 +827,50 @@ func TestMetricForNode(t *testing.T) {
|
|||
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 {
|
||||
|
|
@ -831,6 +879,8 @@ func TestMetricForNode(t *testing.T) {
|
|||
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())
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
The `opcua` plugin retrieves data from OPC UA Server devices.
|
||||
|
||||
Telegraf minimum version: Telegraf 1.16
|
||||
Plugin minimum tested version: 1.16
|
||||
⭐ Telegraf v1.16.0
|
||||
🏷️ network
|
||||
💻 linux, windows
|
||||
|
||||
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
||||
|
||||
|
|
@ -202,6 +203,13 @@ produces a metric like this:
|
|||
opcua,id=ns\=3;s\=Temperature temp=79.0,Quality="OK (0x0)",DataType="Float" 1597820490000000000
|
||||
```
|
||||
|
||||
If the value is an array, each element is unpacked into a field
|
||||
using indexed keys. For example:
|
||||
|
||||
```text
|
||||
opcua,id=ns\=3;s\=Temperature temp[0]=79.0,temp[1]=38.9,Quality="OK (0x0)",DataType="Float" 1597820490000000000
|
||||
```
|
||||
|
||||
## Group Configuration
|
||||
|
||||
Groups can set default values for the namespace, identifier type, and
|
||||
|
|
|
|||
|
|
@ -315,6 +315,13 @@ produces a metric like this:
|
|||
opcua,id=ns\=3;s\=Temperature temp=79.0,Quality="OK (0x0)",DataType="Float" 1597820490000000000
|
||||
```
|
||||
|
||||
If the value is an array, each element is unpacked into a field
|
||||
using indexed keys. For example:
|
||||
|
||||
```text
|
||||
opcua,id=ns\=3;s\=Temperature temp[0]=79.0,temp[1]=38.9,Quality="OK (0x0)",DataType="Float" 1597820490000000000
|
||||
```
|
||||
|
||||
#### Group Configuration
|
||||
|
||||
Groups can set default values for the namespace, identifier type, tags
|
||||
|
|
|
|||
Loading…
Reference in New Issue