How-to guide
Monitor requests
Monitor the request and response messages of the server, for example for logging or your own checks, and query server metrics.
How-to guide
Monitor requests
Types usedOpcServerOpcRequestValidatingEventArgsOpcRequestValidatingEventHandlerOpcRequestProcessingEventArgsOpcRequestProcessingEventHandlerOpcRequestProcessedEventArgsOpcRequestProcessedEventHandlerOpcRequestValidatedEventArgsOpcRequestValidatedEventHandlerIOpcServiceRequestIOpcServiceResponse
Request processing flow
The server processes and validates the requests of the clients as instances of the IOpcServiceRequest interface. It answers them with instances of the IOpcServiceResponse interface. With the RequestProcessing, RequestValidating, RequestValidated, and RequestProcessed events, you can additionally monitor, log, route, or reject the received requests with your own methods. This is especially useful if the mechanisms of the OPC UA .NET SDK do not meet the requirements of the project, above all for specific restrictions. From request to response, processing goes through these steps:
- Receiving the raw data of a request (protocol layer of the SDK)
- Deserializing the raw data into a request (message layer of the SDK)
- Preprocessing the request:
RequestProcessingevent - Dispatching the request to the responsible service (service layer of the SDK)
- Validating the request
- Standard validations (session, identity, and so on)
- Custom validation:
RequestValidatingevent - Final validation (check of the custom validation)
- Custom completion of the validation:
RequestValidatedevent
- Processing the request (application layer of the SDK)
- Creating the response through the responsible service (service layer of the SDK)
- Postprocessing the request and its response:
RequestProcessedevent - Serializing the response into raw data (message layer of the SDK)
- Sending the raw data of the response (protocol layer of the SDK)
Preprocess requests
With the events from steps 3, 5.2, 5.4, and 8, you use your own code to monitor or influence the request processing at the respective point. Right after the server has received the payload and prepared it as an instance of IOpcServiceRequest, it runs the code of the RequestProcessing event. The information of this event mainly serves to diagnose the message traffic between client and server.
An event handler of this event should not throw an exception:
private static void HandleRequestProcessing(object sender, OpcRequestProcessingEventArgs e)
{
Console.Write("Processing: " + e.Request.ToString());
}
// ...
server.RequestProcessing += HandleRequestProcessing;Private Shared Sub HandleRequestProcessing(sender As Object, e As OpcRequestProcessingEventArgs)
Console.Write("Processing: " & e.Request.ToString())
End Sub
' ...
AddHandler server.RequestProcessing, AddressOf HandleRequestProcessingValidate requests
The context in the OpcRequestProcessingEventArgs is always an instance of the OpcContext class. It describes only the general environment of the message processing. In the subsequent RequestValidating event, the server adds details about the session and the identity to this information. For requests that require a session, the OpcContext provided is the specialization OpcOperationContext.
With the OpcOperationContext, you can then also validate per session:
server.RequestValidating += HandleRequestValidating;
...
private static readonly Dictionary<OpcNodeId, int> nodesPerSession
= new Dictionary<OpcNodeId, int>();
private static void HandleRequestValidating(
object sender,
OpcRequestValidatingEventArgs e)
{
Console.Write(" -> Validating: " + e.Request.ToString());
if (e.RequestType == OpcRequestType.AddNodes) {
var sessionId = e.Context.SessionId;
var request = (OpcAddNodesRequest)e.Request;
lock (nodesPerSession) {
int count;
nodesPerSession.TryGetValue(sessionId, out count);
count += request.Commands.Count;
// Reject the request in case it would exceed 100 AddNode commands per session.
if (count > 100)
e.Cancel = true;
else
nodesPerSession[sessionId] = count;
}
}
}AddHandler server.RequestValidating, AddressOf HandleRequestValidating
...
Private Shared ReadOnly nodesPerSession As New Dictionary(Of OpcNodeId, Integer)()
Private Shared Sub HandleRequestValidating( _
ByVal sender As Object, _
ByVal e As OpcRequestValidatingEventArgs)
Console.Write(" -> Validating: " & e.Request.ToString())
If e.RequestType = OpcRequestType.AddNodes Then
Dim sessionId = e.Context.SessionId
Dim request = CType(e.Request, OpcAddNodesRequest)
SyncLock nodesPerSession
Dim count As Integer
nodesPerSession.TryGetValue(sessionId, count)
count += request.Commands.Count
'Reject the request in case it would exceed 100 AddNode commands per session.
If count > 100 Then
e.Cancel = True
Else
nodesPerSession(sessionId) = count
End If
End SyncLock
End If
End SubThe example limits the number of AddNode calls per session to 100. The event handler rejects a request that would exceed this limit. For this, it sets the Cancel property of the event arguments to true. The SDK then automatically sets the code of the Result property of the event arguments to BadNotSupported. You can also cancel the request by setting a "Bad" code (in addition). An event handler of the RequestValidating event may throw an exception.
If the event handler throws an exception, sets Cancel to true, or sets Result to a "Bad" code, the server does not run the event handlers of the RequestValidated event. This follows the Validating/Validated pattern of the .NET Framework. If no event handler cancels the request, the server runs the event handlers of the RequestValidated event:
void HandleRequestValidated(object sender, OpcRequestValidatedEventArgs e)
{
Console.Write(" -> Validated");
}
// ...
server.RequestValidated += HandleRequestValidated;Sub HandleRequestValidated(sender As Object, e As OpcRequestValidatedEventArgs)
Console.Write(" -> Validated")
End Sub
' ...
AddHandler server.RequestValidated, AddressOf HandleRequestValidatedAs with the RequestProcessing event, the information mainly serves to diagnose the message traffic between client and server. The event is useful because the server tries to process and answer the request only after it has raised the event. An event handler of this event should not throw an exception. After the server has processed the request, it answers it with the results.
Postprocess requests
You evaluate the response created in this step together with the request in the RequestProcessed event:
private static void HandleRequestProcessed(object sender, OpcRequestProcessedEventArgs e)
{
if (e.Response.Success)
Console.WriteLine(" -> Processed!");
else
Console.WriteLine(" -> FAILED: {0}!", e.Exception?.Message ?? e.Response.ToString());
}
// ...
server.RequestProcessed += HandleRequestProcessed;Private Shared Sub HandleRequestProcessed(sender As Object, e As OpcRequestProcessedEventArgs)
If e.Response.Success Then
Console.WriteLine(" -> Processed!")
Else
Console.WriteLine(" -> FAILED: {0}!", If(e.Exception?.Message, e.Response.ToString()))
End If
End Sub
' ...
AddHandler server.RequestProcessed, AddressOf HandleRequestProcessedAs the example shows, the event arguments also provide information about an exception, if one occurred. This event handler should not throw an exception either. The RequestType property states which service the request called, even for services the server does not support. You recognize such requests by the IsUnsupportedService property.
Handle outdated and suspicious requests
Types usedOpcServerOpcRequestJudgementEventArgsOpcRequestImpeachment
Before the server processes a request, it checks it for anomalies (OpcRequestImpeachment), for example whether it is outdated because its timestamp lies too far in the past. By default, the server rejects such a request. With the RequestJudgement event, you can override this judgment: if you set the Dismiss property of the arguments to true, the server processes the request anyway. This helps, for example, if the clocks of client and server differ:
server.RequestJudgement += HandleRequestJudgement;
...
private static void HandleRequestJudgement(
object sender,
OpcRequestJudgementEventArgs e)
{
// Accept outdated requests, e.g. in case the clocks of client and server differ.
if (e.Impeachment == OpcRequestImpeachment.Outdated)
e.Dismiss = true;
}AddHandler server.RequestJudgement, AddressOf HandleRequestJudgement
...
Private Shared Sub HandleRequestJudgement( _
ByVal sender As Object, _
ByVal e As OpcRequestJudgementEventArgs)
'Accept outdated requests, e.g. in case the clocks of client and server differ.
If e.Impeachment = OpcRequestImpeachment.Outdated Then
e.Dismiss = True
End If
End SubMonitor sessions
Types usedOpcServerOpcSessionOpcSessionEventArgsOpcSessionActivationFailedEventArgsOpcSubscriptionEventArgs
Besides the requests, you can also monitor the sessions and subscriptions of the clients. For this, the server provides the SessionCreated, SessionActivated, SessionActivationFailed, SessionClosing, SubscriptionCreated, and SubscriptionDeleted events, among others. SessionActivationFailed occurs for each rejected logon (see Audit events). The GetSessions method of the server also returns the sessions that currently exist:
server.SessionCreated += (sender, e) => Console.WriteLine("Session created: {0}", e.Session.Name);
server.SessionClosing += (sender, e) => Console.WriteLine("Session closing: {0}", e.Session.Name);
server.SubscriptionCreated += (sender, e) => Console.WriteLine(
"Subscription {0} created in session {1}",
e.Subscription.Id,
e.Subscription.Session.Name);AddHandler server.SessionCreated, Sub(sender, e) Console.WriteLine("Session created: {0}", e.Session.Name)
AddHandler server.SessionClosing, Sub(sender, e) Console.WriteLine("Session closing: {0}", e.Session.Name)
AddHandler server.SubscriptionCreated, Sub(sender, e) Console.WriteLine( _
"Subscription {0} created in session {1}", _
e.Subscription.Id, _
e.Subscription.Session.Name)Handle concurrent requests
Types usedOpcServerOpcNodeManagerOpcReadVariableValueCallbackOpcWriteVariableValueCallbackIOpcNodeHistoryProviderOpcNodeHistorian
The server processes the requests of its clients in parallel, each request in its own task. The events RequestProcessing, RequestValidating, RequestValidated, and RequestProcessed occur in the thread of the respective request. Their handlers therefore run for several requests at the same time. Protect data that the handlers share against concurrent changes.
For the callbacks of your application:
- Read and write: The callbacks
ReadVariableValueCallbackandWriteVariableValueCallbackalso run in parallel. The server locks the individual node for each access: accesses to the same node run one after another, accesses to different nodes at the same time. A request across several nodes is therefore not atomic. If the values of several nodes are only valid together, synchronize access to the data source yourself. - Methods: The SDK sets no lock for the callbacks of method nodes. Calls from different requests therefore run at the same time, even for the same method. Only within a request does the server run synchronous methods one after another. With
UseAsyncMethodCalls, it runs each call there in its own task as well (see Asynchronous methods). Make the callbacks thread-safe and protect data that a method shares with other callbacks yourself. - Historical data: The server calls a history provider without a lock of the node manager.
OpcNodeHistorianandOpcNodeHistory<T>lock themselves. Your ownIOpcNodeHistoryProvidermust protect concurrent calls itself (see Answer read requests).
The server does not use the SyncRoot property of the node manager when reading and writing. Your own lock on SyncRoot therefore does not protect callbacks against concurrent requests.
Diagnostics
Types usedOpcServerOpcServerDiagnosticsOpcSessionOpcSessionDiagnosticsOpcServiceCounterOpcSubscriptionOpcSubscriptionDiagnostics
For monitoring, health checks, or an operations log, the server provides metrics that it records during operation anyway. Before the server starts, all values are 0.
Server metrics
The Diagnostics property of the server returns the number of current sessions and subscriptions and the total number created since the start. It also returns the number of rejected sessions and requests and the number of aborted and timed-out sessions:
OpcServerDiagnostics diagnostics = server.Diagnostics;
Console.WriteLine(
"Sessions: {0} (total {1})",
diagnostics.CurrentSessionCount,
diagnostics.CumulatedSessionCount);
Console.WriteLine(
"Subscriptions: {0}",
diagnostics.CurrentSubscriptionCount);
Console.WriteLine(
"Rejected sessions: {0}, by security: {1}",
diagnostics.RejectedSessionCount,
diagnostics.SecurityRejectedSessionCount);
Console.WriteLine(
"Rejected requests: {0}, by security: {1}",
diagnostics.RejectedRequestsCount,
diagnostics.SecurityRejectedRequestsCount);
Console.WriteLine(
"Session timeouts: {0}, aborts: {1}",
diagnostics.SessionTimeoutCount,
diagnostics.SessionAbortCount);Dim diagnostics As OpcServerDiagnostics = server.Diagnostics
Console.WriteLine( _
"Sessions: {0} (total {1})", _
diagnostics.CurrentSessionCount, _
diagnostics.CumulatedSessionCount)
Console.WriteLine( _
"Subscriptions: {0}", _
diagnostics.CurrentSubscriptionCount)
Console.WriteLine( _
"Rejected sessions: {0}, by security: {1}", _
diagnostics.RejectedSessionCount, _
diagnostics.SecurityRejectedSessionCount)
Console.WriteLine( _
"Rejected requests: {0}, by security: {1}", _
diagnostics.RejectedRequestsCount, _
diagnostics.SecurityRejectedRequestsCount)
Console.WriteLine( _
"Session timeouts: {0}, aborts: {1}", _
diagnostics.SessionTimeoutCount, _
diagnostics.SessionAbortCount)If the values SecurityRejectedSessionCount and SecurityRejectedRequestsCount rise, the server rejects sessions or requests for security reasons. Causes include an untrusted certificate or a failed logon. This can point to a misconfigured client or to an attack. The Secure servers page describes the protection mechanisms of the server.
Inspect sessions and subscriptions
The GetSessions method returns the existing sessions. Each session states, among other things, its name, the endpoint used (EndpointUrl), the authenticated identity (UsedIdentity), and the time the connection was established. Its Diagnostics property counts the calls and the failed calls per service (for example ReadCounter, WriteCounter, CallCounter, and TotalRequestCounter). It also counts unauthorized requests (UnauthorizedRequestsCount). The Diagnostics property of a subscription counts, among other things, the notifications sent and the overflows of its queues. Both properties can be null:
foreach (OpcSession session in server.GetSessions()) {
Console.WriteLine(
"{0} {1} {2} since {3:u}",
session.Name,
session.EndpointUrl,
session.UsedIdentity,
session.ConnectTime);
OpcSessionDiagnostics sessionDiagnostics = session.Diagnostics;
if (sessionDiagnostics != null) {
Console.WriteLine(
" requests: {0}, failed: {1}",
sessionDiagnostics.TotalRequestCounter.TotalCalls,
sessionDiagnostics.TotalRequestCounter.FailedCalls);
Console.WriteLine(
" unauthorized: {0}, writes: {1}",
sessionDiagnostics.UnauthorizedRequestsCount,
sessionDiagnostics.WriteCounter.TotalCalls);
}
foreach (OpcSubscription subscription in session.GetSubscriptions()) {
OpcSubscriptionDiagnostics subscriptionDiagnostics = subscription.Diagnostics;
if (subscriptionDiagnostics != null) {
Console.WriteLine(
" subscription {0}: queue overflows {1}",
subscription.Id,
subscriptionDiagnostics.MonitoringQueueOverflowCount);
}
}
}For Each session As OpcSession In server.GetSessions()
Console.WriteLine( _
"{0} {1} {2} since {3:u}", _
session.Name, _
session.EndpointUrl, _
session.UsedIdentity, _
session.ConnectTime)
Dim sessionDiagnostics As OpcSessionDiagnostics = session.Diagnostics
If sessionDiagnostics IsNot Nothing Then
Console.WriteLine( _
" requests: {0}, failed: {1}", _
sessionDiagnostics.TotalRequestCounter.TotalCalls, _
sessionDiagnostics.TotalRequestCounter.FailedCalls)
Console.WriteLine( _
" unauthorized: {0}, writes: {1}", _
sessionDiagnostics.UnauthorizedRequestsCount, _
sessionDiagnostics.WriteCounter.TotalCalls)
End If
For Each subscription As OpcSubscription In session.GetSubscriptions()
Dim subscriptionDiagnostics As OpcSubscriptionDiagnostics = subscription.Diagnostics
If subscriptionDiagnostics IsNot Nothing Then
Console.WriteLine( _
" subscription {0}: queue overflows {1}", _
subscription.Id, _
subscriptionDiagnostics.MonitoringQueueOverflowCount)
End If
Next
NextDiagnostics nodes in the address space
Clients read the diagnostics of a server through the diagnostics nodes in its address space, for example ServerDiagnosticsSummary and the diagnostics objects per session and per subscription. The server maintains these nodes only if the Diagnostics.IsEnabled property is true (false by default). The setting does not affect the metrics and the Diagnostics properties of sessions and subscriptions in the sections above. Set the property before the server starts:
server.Diagnostics.IsEnabled = true;
server.Start();server.Diagnostics.IsEnabled = True
server.Start()
