Skip to content

Commit ede04c6

Browse files
authored
feat(inputs.opcua): Add namespace URI support (#17906)
1 parent dee58fd commit ede04c6

File tree

10 files changed

+562
-33
lines changed

10 files changed

+562
-33
lines changed

plugins/common/opcua/client.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package opcua
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"log" //nolint:depguard // just for debug
78
"net/url"
@@ -184,7 +185,8 @@ type OpcUAClient struct {
184185
Config *OpcUAClientConfig
185186
Log telegraf.Logger
186187

187-
Client *opcua.Client
188+
Client *opcua.Client
189+
namespaceArray []string
188190

189191
opts []opcua.Option
190192
codes []ua.StatusCode
@@ -327,3 +329,53 @@ func (o *OpcUAClient) State() ConnectionState {
327329
}
328330
return ConnectionState(o.Client.State())
329331
}
332+
333+
// UpdateNamespaceArray fetches the namespace array from the OPC UA server
334+
// The namespace array is stored at the well-known node ns=0;i=2255
335+
func (o *OpcUAClient) UpdateNamespaceArray(ctx context.Context) error {
336+
if o.Client == nil {
337+
return errors.New("client not connected")
338+
}
339+
340+
nodeID := ua.NewNumericNodeID(0, 2255)
341+
req := &ua.ReadRequest{
342+
MaxAge: 2000,
343+
NodesToRead: []*ua.ReadValueID{
344+
{NodeID: nodeID},
345+
},
346+
TimestampsToReturn: ua.TimestampsToReturnBoth,
347+
}
348+
349+
resp, err := o.Client.Read(ctx, req)
350+
if err != nil {
351+
return fmt.Errorf("failed to read namespace array: %w", err)
352+
}
353+
354+
if len(resp.Results) == 0 {
355+
return errors.New("no results returned when reading namespace array")
356+
}
357+
358+
result := resp.Results[0]
359+
if result.Status != ua.StatusOK {
360+
return fmt.Errorf("failed to read namespace array, status: %w", result.Status)
361+
}
362+
363+
if result.Value == nil {
364+
return errors.New("namespace array value is nil")
365+
}
366+
367+
// The namespace array is an array of strings
368+
namespaces, ok := result.Value.Value().([]string)
369+
if !ok {
370+
return fmt.Errorf("namespace array is not a string array, got type: %T", result.Value.Value())
371+
}
372+
373+
o.namespaceArray = namespaces
374+
o.Log.Debugf("Fetched namespace array with %d entries", len(namespaces))
375+
return nil
376+
}
377+
378+
// NamespaceArray returns the cached namespace array
379+
func (o *OpcUAClient) NamespaceArray() []string {
380+
return o.namespaceArray
381+
}

plugins/common/opcua/input/input_client.go

Lines changed: 114 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type MonitoringParameters struct {
5151
type NodeSettings struct {
5252
FieldName string `toml:"name"`
5353
Namespace string `toml:"namespace"`
54+
NamespaceURI string `toml:"namespace_uri"`
5455
IdentifierType string `toml:"identifier_type"`
5556
Identifier string `toml:"identifier"`
5657
DefaultTags map[string]string `toml:"default_tags"`
@@ -59,13 +60,17 @@ type NodeSettings struct {
5960

6061
// NodeID returns the OPC UA node id
6162
func (tag *NodeSettings) NodeID() string {
63+
if tag.NamespaceURI != "" {
64+
return "nsu=" + tag.NamespaceURI + ";" + tag.IdentifierType + "=" + tag.Identifier
65+
}
6266
return "ns=" + tag.Namespace + ";" + tag.IdentifierType + "=" + tag.Identifier
6367
}
6468

6569
// NodeGroupSettings describes a mapping of group of nodes to Metrics
6670
type NodeGroupSettings struct {
6771
MetricName string `toml:"name"` // Overrides plugin's setting
6872
Namespace string `toml:"namespace"` // Can be overridden by node setting
73+
NamespaceURI string `toml:"namespace_uri"` // Can be overridden by node setting
6974
IdentifierType string `toml:"identifier_type"` // Can be overridden by node setting
7075
Nodes []NodeSettings `toml:"nodes"`
7176
DefaultTags map[string]string `toml:"default_tags"`
@@ -74,11 +79,15 @@ type NodeGroupSettings struct {
7479

7580
type EventNodeSettings struct {
7681
Namespace string `toml:"namespace"`
82+
NamespaceURI string `toml:"namespace_uri"`
7783
IdentifierType string `toml:"identifier_type"`
7884
Identifier string `toml:"identifier"`
7985
}
8086

8187
func (e *EventNodeSettings) NodeID() string {
88+
if e.NamespaceURI != "" {
89+
return "nsu=" + e.NamespaceURI + ";" + e.IdentifierType + "=" + e.Identifier
90+
}
8291
return "ns=" + e.Namespace + ";" + e.IdentifierType + "=" + e.Identifier
8392
}
8493

@@ -87,6 +96,7 @@ type EventGroupSettings struct {
8796
QueueSize uint32 `toml:"queue_size"`
8897
EventTypeNode EventNodeSettings `toml:"event_type_node"`
8998
Namespace string `toml:"namespace"`
99+
NamespaceURI string `toml:"namespace_uri"`
90100
IdentifierType string `toml:"identifier_type"`
91101
NodeIDSettings []EventNodeSettings `toml:"node_ids"`
92102
SourceNames []string `toml:"source_names"`
@@ -99,6 +109,9 @@ func (e *EventGroupSettings) UpdateNodeIDSettings() {
99109
if n.Namespace == "" {
100110
n.Namespace = e.Namespace
101111
}
112+
if n.NamespaceURI == "" {
113+
n.NamespaceURI = e.NamespaceURI
114+
}
102115
if n.IdentifierType == "" {
103116
n.IdentifierType = e.IdentifierType
104117
}
@@ -138,11 +151,23 @@ func (e EventNodeSettings) validateEventNodeSettings() error {
138151
}
139152
if e.Identifier == "" {
140153
return errors.New("identifier must be set")
141-
} else if e.IdentifierType == "" {
154+
}
155+
if e.IdentifierType == "" {
142156
return errors.New("identifier_type must be set")
143-
} else if e.Namespace == "" {
144-
return errors.New("namespace must be set")
145157
}
158+
159+
// Validate namespace configuration
160+
hasNamespace := len(e.Namespace) > 0
161+
hasNamespaceURI := len(e.NamespaceURI) > 0
162+
163+
if hasNamespace && hasNamespaceURI {
164+
return errors.New("cannot specify both 'namespace' and 'namespace_uri', use only one")
165+
}
166+
167+
if !hasNamespace && !hasNamespaceURI {
168+
return errors.New("must specify either 'namespace' or 'namespace_uri'")
169+
}
170+
146171
return nil
147172
}
148173

@@ -334,8 +359,16 @@ func validateNodeToAdd(existing map[metricParts]struct{}, nmm *NodeMetricMapping
334359
return fmt.Errorf("empty name in %q", nmm.Tag.FieldName)
335360
}
336361

337-
if len(nmm.Tag.Namespace) == 0 {
338-
return errors.New("empty node namespace not allowed")
362+
// Validate namespace configuration
363+
hasNamespace := len(nmm.Tag.Namespace) > 0
364+
hasNamespaceURI := len(nmm.Tag.NamespaceURI) > 0
365+
366+
if hasNamespace && hasNamespaceURI {
367+
return fmt.Errorf("node %q: cannot specify both 'namespace' and 'namespace_uri', use only one", nmm.Tag.FieldName)
368+
}
369+
370+
if !hasNamespace && !hasNamespaceURI {
371+
return fmt.Errorf("node %q: must specify either 'namespace' or 'namespace_uri'", nmm.Tag.FieldName)
339372
}
340373

341374
if len(nmm.Tag.Identifier) == 0 {
@@ -396,6 +429,9 @@ func (o *OpcUAInputClient) InitNodeMetricMapping() error {
396429
if node.Namespace == "" {
397430
node.Namespace = group.Namespace
398431
}
432+
if node.NamespaceURI == "" {
433+
node.NamespaceURI = group.NamespaceURI
434+
}
399435
if node.IdentifierType == "" {
400436
node.IdentifierType = group.IdentifierType
401437
}
@@ -420,29 +456,91 @@ func (o *OpcUAInputClient) InitNodeMetricMapping() error {
420456

421457
func (o *OpcUAInputClient) InitNodeIDs() error {
422458
o.NodeIDs = make([]*ua.NodeID, 0, len(o.NodeMetricMapping))
459+
namespaceArray := o.NamespaceArray()
460+
423461
for _, node := range o.NodeMetricMapping {
424-
nid, err := ua.ParseNodeID(node.Tag.NodeID())
425-
if err != nil {
426-
return err
462+
nodeIDStr := node.Tag.NodeID()
463+
464+
// Check if this uses namespace URI (nsu=) format
465+
if strings.HasPrefix(nodeIDStr, "nsu=") {
466+
// Namespace URI format requires namespace array
467+
if len(namespaceArray) == 0 {
468+
return fmt.Errorf("node ID %q uses namespace URI (nsu=) but namespace array is not available - connection to server may be required", nodeIDStr)
469+
}
470+
// Use ParseExpandedNodeID for namespace URI support
471+
expandedNodeID, err := ua.ParseExpandedNodeID(nodeIDStr, namespaceArray)
472+
if err != nil {
473+
return fmt.Errorf("failed to parse node ID %q: %w", nodeIDStr, err)
474+
}
475+
o.NodeIDs = append(o.NodeIDs, expandedNodeID.NodeID)
476+
} else {
477+
// Use ParseNodeID for namespace index (ns=) format
478+
nid, err := ua.ParseNodeID(nodeIDStr)
479+
if err != nil {
480+
return fmt.Errorf("failed to parse node ID %q: %w", nodeIDStr, err)
481+
}
482+
o.NodeIDs = append(o.NodeIDs, nid)
427483
}
428-
o.NodeIDs = append(o.NodeIDs, nid)
429484
}
430485

431486
return nil
432487
}
433488

434489
func (o *OpcUAInputClient) InitEventNodeIDs() error {
490+
namespaceArray := o.NamespaceArray()
491+
435492
for _, eventSetting := range o.EventGroups {
436-
eid, err := ua.ParseNodeID(eventSetting.EventTypeNode.NodeID())
437-
if err != nil {
438-
return err
493+
eventTypeNodeIDStr := eventSetting.EventTypeNode.NodeID()
494+
var eid *ua.NodeID
495+
496+
// Parse event type node ID
497+
if strings.HasPrefix(eventTypeNodeIDStr, "nsu=") {
498+
if len(namespaceArray) == 0 {
499+
return fmt.Errorf(
500+
"event type node ID %q uses namespace URI (nsu=) but namespace array is not available - "+
501+
"connection to server may be required",
502+
eventTypeNodeIDStr,
503+
)
504+
}
505+
expandedNodeID, err := ua.ParseExpandedNodeID(eventTypeNodeIDStr, namespaceArray)
506+
if err != nil {
507+
return fmt.Errorf("failed to parse event type node ID %q: %w", eventTypeNodeIDStr, err)
508+
}
509+
eid = expandedNodeID.NodeID
510+
} else {
511+
parsedID, err := ua.ParseNodeID(eventTypeNodeIDStr)
512+
if err != nil {
513+
return fmt.Errorf("failed to parse event type node ID %q: %w", eventTypeNodeIDStr, err)
514+
}
515+
eid = parsedID
439516
}
440-
for _, node := range eventSetting.NodeIDSettings {
441-
nid, err := ua.ParseNodeID(node.NodeID())
442517

443-
if err != nil {
444-
return err
518+
for _, node := range eventSetting.NodeIDSettings {
519+
nodeIDStr := node.NodeID()
520+
var nid *ua.NodeID
521+
522+
// Parse node ID
523+
if strings.HasPrefix(nodeIDStr, "nsu=") {
524+
if len(namespaceArray) == 0 {
525+
return fmt.Errorf(
526+
"event node ID %q uses namespace URI (nsu=) but namespace array is not available - "+
527+
"connection to server may be required",
528+
nodeIDStr,
529+
)
530+
}
531+
expandedNodeID, err := ua.ParseExpandedNodeID(nodeIDStr, namespaceArray)
532+
if err != nil {
533+
return fmt.Errorf("failed to parse node ID %q: %w", nodeIDStr, err)
534+
}
535+
nid = expandedNodeID.NodeID
536+
} else {
537+
parsedID, err := ua.ParseNodeID(nodeIDStr)
538+
if err != nil {
539+
return fmt.Errorf("failed to parse node ID %q: %w", nodeIDStr, err)
540+
}
541+
nid = parsedID
445542
}
543+
446544
nmm := EventNodeMetricMapping{
447545
NodeID: nid,
448546
SamplingInterval: &eventSetting.SamplingInterval,

0 commit comments

Comments
 (0)