fix(inputs.opcua): Handle node array values (#16594)

This commit is contained in:
Alexander Stark 2025-05-01 20:48:25 +02:00 committed by GitHub
parent b490846d24
commit b49810d337
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 122 additions and 7 deletions

View File

@ -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))

View File

@ -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
@ -815,14 +817,60 @@ func TestMetricForNode(t *testing.T) {
MetricTags: map[string]string{"t1": "v1"},
},
},
v: 16,
time: time.Date(2022, 03, 17, 8, 55, 00, 00, &time.Location{}),
status: ua.StatusOK,
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 {
@ -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())

View File

@ -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

View File

@ -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