# Monitor requests

> How-to guide: 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](https://www.traeger.de/docs/development/opcua/server/request-monitoring)

Types used: `OpcServer`, `OpcRequestValidatingEventArgs`, `OpcRequestValidatingEventHandler`, `OpcRequestProcessingEventArgs`, `OpcRequestProcessingEventHandler`, `OpcRequestProcessedEventArgs`, `OpcRequestProcessedEventHandler`, `OpcRequestValidatedEventArgs`, `OpcRequestValidatedEventHandler`, `IOpcServiceRequest`, `IOpcServiceResponse`

## 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)
1. Deserializing the raw data into a request (message layer of the SDK)
1. Preprocessing the request: **`RequestProcessing` event**
1. Dispatching the request to the responsible service (service layer of the SDK)
1. Validating the request
   1. Standard validations (session, identity, and so on)
   1. Custom validation: **`RequestValidating` event**
   1. Final validation (check of the custom validation)
   1. Custom completion of the validation: **`RequestValidated` event**
1. Processing the request (application layer of the SDK)
1. Creating the response through the responsible service (service layer of the SDK)
1. Postprocessing the request and its response: **`RequestProcessed` event**
1. Serializing the response into raw data (message layer of the SDK)
1. 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 used: `OpcServer`, `OpcRequestJudgementEventArgs`, `OpcRequestImpeachment`

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 used: `OpcServer`, `OpcSession`, `OpcSessionEventArgs`, `OpcSessionActivationFailedEventArgs`, `OpcSubscriptionEventArgs`

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](https://www.traeger.de/en/docs/development/opcua/security/server#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 used: `OpcServer`, `OpcNodeManager`, `OpcReadVariableValueCallback`, `OpcWriteVariableValueCallback`, `IOpcNodeHistoryProvider`, `OpcNodeHistorian`

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](https://www.traeger.de/en/docs/development/opcua/server/node-types#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](https://www.traeger.de/en/docs/development/opcua/server/historical-data#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 used: `OpcServer`, `OpcServerDiagnostics`, `OpcSession`, `OpcSessionDiagnostics`, `OpcServiceCounter`, `OpcSubscription`, `OpcSubscriptionDiagnostics`

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](https://www.traeger.de/en/docs/development/opcua/security/server) 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()
```
