Skip to content

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.

This content was machine translated and may contain errors. Read in German

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:

  1. Receiving the raw data of a request (protocol layer of the SDK)
  2. Deserializing the raw data into a request (message layer of the SDK)
  3. Preprocessing the request: RequestProcessing event
  4. Dispatching the request to the responsible service (service layer of the SDK)
  5. Validating the request
    1. Standard validations (session, identity, and so on)
    2. Custom validation: RequestValidating event
    3. Final validation (check of the custom validation)
    4. Custom completion of the validation: RequestValidated event
  6. Processing the request (application layer of the SDK)
  7. Creating the response through the responsible service (service layer of the SDK)
  8. Postprocessing the request and its response: RequestProcessed event
  9. Serializing the response into raw data (message layer of the SDK)
  10. 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:

csharp
private static void HandleRequestProcessing(object sender, OpcRequestProcessingEventArgs e)
{
    Console.Write("Processing: " + e.Request.ToString());
}

// ...

server.RequestProcessing += HandleRequestProcessing;
vbnet
Private Shared Sub HandleRequestProcessing(sender As Object, e As OpcRequestProcessingEventArgs)
    Console.Write("Processing: " & e.Request.ToString())
End Sub

' ...

AddHandler server.RequestProcessing, AddressOf HandleRequestProcessing

Validate 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:

csharp
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;
        }
    }
}
vbnet
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 Sub

The 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:

csharp
void HandleRequestValidated(object sender, OpcRequestValidatedEventArgs e)
{
    Console.Write(" -> Validated");
}

// ...

server.RequestValidated += HandleRequestValidated;
vbnet
Sub HandleRequestValidated(sender As Object, e As OpcRequestValidatedEventArgs)
    Console.Write(" -> Validated")
End Sub

' ...

AddHandler server.RequestValidated, AddressOf HandleRequestValidated

As 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:

csharp
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;
vbnet
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 HandleRequestProcessed

As 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:

csharp
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;
}
vbnet
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 Sub

Monitor 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:

csharp
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);
vbnet
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 ReadVariableValueCallback and WriteVariableValueCallback also 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. OpcNodeHistorian and OpcNodeHistory<T> lock themselves. Your own IOpcNodeHistoryProvider must 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:

csharp
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);
vbnet
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:

csharp
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);
        }
    }
}
vbnet
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
Next

Diagnostics 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:

csharp
server.Diagnostics.IsEnabled = true;
server.Start();
vbnet
server.Diagnostics.IsEnabled = True
server.Start()