Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(inputs.opcua): Implement browsing tags on initial connect #15064

Closed

Conversation

supergrover
Copy link

@supergrover supergrover commented Mar 26, 2024

Summary

This is a proposed implementation in order to implement tag browsing for OPC-UA tags on initial connect as described in #15001.
This pull request is for review purposes, some tests are still failing on this because a connection to an OPC server is now required in order initialize all NodeIDs. I'm still working on that part.

Checklist

  • No AI generated code was used in this PR

Related issues

resolves #15001

Introduce a `browse` flag in opcua and opcua_listener configurations
which causes the plugin to browse the tag using the OPC servers' browse
feature.
@srebhan srebhan changed the title Implement browsing opcua tags on initial connect feat(inputs.opcua): Implement browsing tags on initial connect Mar 26, 2024
@telegraf-tiger telegraf-tiger bot added area/opcua feat Improvement on an existing feature such as adding a new setting/mode to an existing plugin plugin/input 1. Request for new input plugins 2. Issues/PRs that are related to input plugins labels Mar 26, 2024
Copy link
Contributor

@srebhan srebhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@supergrover first of all thank you for your pull-request! I do have some comments in the code and additionally the following more high-level comments:

  1. Please run make lint to see the linter issues in your code. Please fix them!
  2. How does this code relate to gopcua's BrowseRequest? Can we make use of the upstream functionality instead of coding this by hand?

@@ -57,6 +58,7 @@ type NodeSettings struct {
TagsSlice [][]string `toml:"tags" deprecated:"1.25.0;use 'default_tags' instead"`
DefaultTags map[string]string `toml:"default_tags"`
MonitoringParams MonitoringParameters `toml:"monitoring_params"`
Browse bool `toml:"browse"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about making this a pointer like

Suggested change
Browse bool `toml:"browse"`
Browse *bool `toml:"browse"`

as this allows you to decide whether the value was actually set by the user or not.

Comment on lines +358 to +360
if node.Browse == false {
node.Browse = group.Browse
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the above change you can do

Suggested change
if node.Browse == false {
node.Browse = group.Browse
}
if node.Browse == nil {
node.Browse = &group.Browse
}

Comment on lines +380 to 403
var nid *ua.NodeID = nil

if node.Tag.Browse && node.Tag.IdentifierType == "s" {
browsedNodeID, err := o.browseNode(ctx, node.Tag.Namespace, node.Tag.Identifier)
if err != nil {
o.Log.Errorf("Error browsing node %s: %s", node.Tag.FieldName, err)
// continue, other nodes may be browseable
// also, add the node as a string identifier. The server will not be able to return data
// but we must add a node to the o.NodeIDs array in order for it to match the o.NodeMetricMappings
parsedNodeID, err := ua.ParseNodeID(node.Tag.NodeID())
if err != nil {
return err
}
nid = parsedNodeID
} else {
nid = browsedNodeID
}
} else {
parsedNodeID, err := ua.ParseNodeID(node.Tag.NodeID())
if err != nil {
return err
}
nid = parsedNodeID
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about

Suggested change
var nid *ua.NodeID = nil
if node.Tag.Browse && node.Tag.IdentifierType == "s" {
browsedNodeID, err := o.browseNode(ctx, node.Tag.Namespace, node.Tag.Identifier)
if err != nil {
o.Log.Errorf("Error browsing node %s: %s", node.Tag.FieldName, err)
// continue, other nodes may be browseable
// also, add the node as a string identifier. The server will not be able to return data
// but we must add a node to the o.NodeIDs array in order for it to match the o.NodeMetricMappings
parsedNodeID, err := ua.ParseNodeID(node.Tag.NodeID())
if err != nil {
return err
}
nid = parsedNodeID
} else {
nid = browsedNodeID
}
} else {
parsedNodeID, err := ua.ParseNodeID(node.Tag.NodeID())
if err != nil {
return err
}
nid = parsedNodeID
}
var nid *ua.NodeID = nil
if node.Tag.Browse && node.Tag.IdentifierType == "s" {
browsedNodeID, err := o.browseNode(ctx, node.Tag.Namespace, node.Tag.Identifier)
if err == nil {
o.NodeIDs = append(o.NodeIDs, browsedNodeID)
continue
}
// Continue as other nodes might still be browseable.
// Add the node as a string identifier, the server will not be able to return data
// but we must add a node to the o.NodeIDs array in order for it to match the
// o.NodeMetricMappings
o.Log.Errorf("Browsing node %q failed: %v", node.Tag.FieldName, err)
}
parsedNodeID, err := ua.ParseNodeID(node.Tag.NodeID())
if err != nil {
return err
}
o.NodeIDs = append(o.NodeIDs, parsedNodeID)

Comment on lines +411 to +418
func parseNamespaceId(ns string) (uint16, error) {
nsId, err := strconv.ParseUint(ns, 10, 16)
if err != nil {
return 0, fmt.Errorf("invalid namespace number: %s", err)
}

return uint16(nsId), nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please merge this into the respective calling places as this is not worth a wrapper.

}

func (o *OpcUAInputClient) browseNode(ctx context.Context, ns string, browsePath string) (*ua.NodeID, error) {
o.Log.Infof("Browsing node %s", browsePath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should at most be a debug log.

@@ -800,6 +800,7 @@ func TestSubscribeClientConfigValidMonitoringParams(t *testing.T) {
})

subClient, err := subscribeConfig.CreateSubscribeClient(testutil.Logger{})
subClient.Connect()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to modify this test?

@@ -124,6 +110,22 @@ func (o *SubscribeClient) Connect() error {
return err
}

// Make sure we setup the node-ids correctly after reconnect
// as the server might be restarted and IDs changed
o.Log.Debugf("Creating monitored items on first connection")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This log line does not help anyone. ;-)

@srebhan srebhan self-assigned this Mar 26, 2024
@supergrover
Copy link
Author

Thanks for the suggestions! I'll work on them and get back to this.

1. Please run `make lint` to see the linter issues in your code. Please fix them!

Sure, I did not know about the linter until Github started screaming at me :)

2. How does this code relate to [gopcua's BrowseRequest](https://pkg.go.dev/github.com/gopcua/opcua@v0.5.3/ua#BrowseRequest)? Can we make use of the upstream functionality instead of coding this by hand?

The current code basically parses the string formatted browse path and passes it to the opcua.Node.TranslateBrowsePathsToNodeIDs method on the gopcua client, so basically this already is an upstream function we're calling. Although the parsing of the browse paths could be a suggestion for upstream, if that's what you mean.
The BrowseRequest you mentioned confused me for a while, but as far as I understand it's intended for actual browsing (i.e. listing all subnodes of a node which satisfy some criteria) instead of asking the server for the Node which corresponds to a BrowsePath.

Why do you need to modify this test?

That's part of my work on seeing what needs to be done to satisfy the tests which appears to have slipped through. Basically, the previous code parsed all the node configurations and created the monitored item requests on creation of the subscribe client in https://github.com/influxdata/telegraf/blob/master/plugins/inputs/opcua_listener/subscribe_client.go#L111.
Because nodes may need to be browsed, we cannot know the NodeID's them until we have connected to an OPC server in order to ask it for the NodeID's belonging to the BrowsePath.

@srebhan
Copy link
Contributor

srebhan commented Mar 27, 2024

Sure, I did not know about the linter until Github started screaming at me :)

No worries! It would be good to fix them before pushing so we save some CircleCI credits. ;-)

  1. How does this code relate to gopcua's BrowseRequest? Can we make use of the upstream functionality instead of coding this by hand?

The current code basically parses the string formatted browse path and passes it to the opcua.Node.TranslateBrowsePathsToNodeIDs method on the gopcua client, so basically this already is an upstream function we're calling. Although the parsing of the browse paths could be a suggestion for upstream, if that's what you mean. The BrowseRequest you mentioned confused me for a while, but as far as I understand it's intended for actual browsing (i.e. listing all subnodes of a node which satisfy some criteria) instead of asking the server for the Node which corresponds to a BrowsePath.

Thanks for your explanation! Yeah I wondered why you manually need to mangle the path and suspected an upstream function for doing this. :-) I'm OK with doing it in the plugin but if you want to maybe suggesting this upstream is also a good thing. We can still switch over to it later...

Why do you need to modify this test?

That's part of my work on seeing what needs to be done to satisfy the tests which appears to have slipped through. Basically, the previous code parsed all the node configurations and created the monitored item requests on creation of the subscribe client in https://github.com/influxdata/telegraf/blob/master/plugins/inputs/opcua_listener/subscribe_client.go#L111. Because nodes may need to be browsed, we cannot know the NodeID's them until we have connected to an OPC server in order to ask it for the NodeID's belonging to the BrowsePath.

I see, then please check all errors before continuing! I think the Connect() needs to be further down and also the error returned by Connect() need to be checked in the test!

@srebhan srebhan added the waiting for response waiting for response from contributor label Apr 3, 2024
@telegraf-tiger
Copy link
Contributor

Hello! I am closing this issue due to inactivity. I hope you were able to resolve your problem, if not please try posting this question in our Community Slack or Community Forums or provide additional details in this issue and reqeust that it be re-opened. Thank you!

@telegraf-tiger telegraf-tiger bot closed this Apr 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/opcua feat Improvement on an existing feature such as adding a new setting/mode to an existing plugin plugin/input 1. Request for new input plugins 2. Issues/PRs that are related to input plugins waiting for response waiting for response from contributor
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add a way to resolve OPC-UA tags by name when the server only permits numeric identifiers
2 participants