# Tracing

> How-to guide: Turn on tracing of the OPC UA stack in the SDK, choose masks and output, write entries to your own target, and treat the logs as sensitive data.

> This content was machine translated and may contain errors. [Read in German](https://www.traeger.de/docs/development/opcua/tracing)

Types used: `Opc.Ua.Utils`

Tracing writes entries about what happens inside clients and servers, for example to troubleshoot connections and certificates. The OPC UA .NET SDK has no API of its own for this. It contains the OPC UA stack of the OPC Foundation, and you control the stack's tracing through the static class `Opc.Ua.Utils`. The settings apply to the whole process, that is, to all clients and servers of the application.

## Default behavior

In the NuGet package, tracing is off: no mask is set and no file is defined. This depends on the build of the SDK, not on the build of your application. Even in a debug build of your application, the SDK therefore writes nothing until you turn tracing on yourself.

## Turn on tracing

Types used: `Opc.Ua.Utils`

To write the entries to a file, set three things:

1. **File:** `Utils.SetTraceLog` sets the file. The second parameter (`deleteExisting`) defines whether the SDK deletes an existing file first. Otherwise, it appends the entries.
2. **Masks:** `Utils.SetTraceMask` defines which entries the SDK writes (section [Masks](https://www.traeger.de/en/docs/development/opcua/tracing#masks)).
3. **Output:** `Utils.SetTraceOutput` defines where the SDK writes (section [Output](https://www.traeger.de/en/docs/development/opcua/tracing#output)).

To turn on tracing:

```csharp
string logFilePath = Path.Combine(AppContext.BaseDirectory, "opcua-trace.log");

Utils.SetTraceLog(logFilePath, deleteExisting: true);
Utils.SetTraceMask(
        Utils.TraceMasks.Error
        | Utils.TraceMasks.Information
        | Utils.TraceMasks.Security
        | Utils.TraceMasks.StartStop);

Utils.SetTraceOutput(Utils.TraceOutput.FileOnly);
```

```vbnet
Dim logFilePath As String = Path.Combine(AppContext.BaseDirectory, "opcua-trace.log")

Utils.SetTraceLog(logFilePath, deleteExisting:=True)
Utils.SetTraceMask( _
        Utils.TraceMasks.Error _
        Or Utils.TraceMasks.Information _
        Or Utils.TraceMasks.Security _
        Or Utils.TraceMasks.StartStop)

Utils.SetTraceOutput(Utils.TraceOutput.FileOnly)
```

Use an absolute path for the file, for example in the application directory (`AppContext.BaseDirectory`). The SDK resolves a relative path against the temporary directory (`Path.GetTempPath()`), not against the application directory.

When the file grows beyond 10 MB, the SDK deletes it and starts a new file. Older entries are lost. If you must keep entries longer, write them to your own target (section [Write entries to your own target](https://www.traeger.de/en/docs/development/opcua/tracing#write-entries-to-your-own-target)).

## Masks

The masks of `Utils.TraceMasks` are flags. The SDK writes an entry only if its mask is contained in the mask you set. Combine several masks with a bitwise OR.

| Mask | Value | Entries |
|---|---|---|
| `None` | `0x0` | no entries in the file |
| `Error` | `0x1` | all exceptions and errors, for example rejected certificates, channel errors, and errors of `OpcClient` and `OpcServer` |
| `Information` | `0x2` | general messages, for example closed or reconnected channels and operations in the certificate stores. Entries without a mask of their own get this mask. |
| `StackTrace` | `0x4` | no category of its own: appends the stack trace to error messages and logs every `ServiceResultException` |
| `Service` | `0x8` | call and completion of every service call |
| `ServiceDetail` | `0x10` | number of nodes per Read and RegisterNodes call in the server |
| `Operation` | `0x20` | not used |
| `OperationDetail` | `0x40` | only debug messages of a debug build of the stack and the wrappers for OPC Classic |
| `StartStop` | `0x80` | only start and stop of the central node manager in the server |
| `ExternalSystem` | `0x100` | not used |
| `Security` | `0x200` | audit messages on secure channels and certificates, for example newly created and automatically accepted certificates |
| `All` | `0x7FFFFFFF` | all entries |

For troubleshooting, start with `Error` and `Information`. For problems with connections and certificates, add `Security`; for the sequence of service calls, add `Service`. You only need `StackTrace` to find the origin of an error. `Service` and `StackTrace` produce many entries.

## Output

The values of `Utils.TraceOutput` define where the SDK writes the entries:

| Output | Target |
|---|---|
| `Off` | no output |
| `FileOnly` | the file only |
| `DebugAndFile` | the file and also the debug output (`Debug.WriteLine`) |
| `StdOutAndFile` | same as `FileOnly`: the SDK does not write to the console |

If you need the entries on the console or in your own log, write them yourself.

## Write entries to your own target

Types used: `Opc.Ua.Utils`

To use your own target, for example the log of your application, register a handler for the event `Utils.Tracing.TraceEventHandler`. The event data contains the mask of the entry (`TraceMask`), the format text (`Format`), its arguments (`Arguments`), and for entries about exceptions the exception itself (`Exception`). Build the text of the entry from `Format` and `Arguments`, for example with `string.Format`. Entries about exceptions have no arguments. Their text is already complete in `Format`, so call `string.Format` only if there are arguments. The text already contains the type and message of the exception; you need `Exception` only if you want to evaluate the exception as an object.

```csharp
Utils.Tracing.TraceEventHandler += HandleTraceEvent;

private static void HandleTraceEvent(
        object sender,
        TraceEventArgs e)
{
    if ((e.TraceMask & (Utils.TraceMasks.Error | Utils.TraceMasks.Security)) == 0)
        return;

    Console.WriteLine(FormatTraceText(e));
}

private static string FormatTraceText(TraceEventArgs e)
{
    // Messages without arguments, e.g. those of exceptions, arrive formatted already.
    if (e.Arguments == null || e.Arguments.Length == 0)
        return e.Format;

    return string.Format(CultureInfo.InvariantCulture, e.Format, e.Arguments);
}
```

```vbnet
AddHandler Utils.Tracing.TraceEventHandler, AddressOf HandleTraceEvent

Private Shared Sub HandleTraceEvent( _
        ByVal sender As Object, _
        ByVal e As TraceEventArgs)
    If (e.TraceMask And (Utils.TraceMasks.Error Or Utils.TraceMasks.Security)) = 0 Then Return

    Console.WriteLine(FormatTraceText(e))
End Sub

Private Shared Function FormatTraceText(ByVal e As TraceEventArgs) As String
    'Messages without arguments, e.g. those of exceptions, arrive formatted already.
    If e.Arguments Is Nothing OrElse e.Arguments.Length = 0 Then Return e.Format

    Return String.Format(CultureInfo.InvariantCulture, e.Format, e.Arguments)
End Function

```

Keep two points in mind:

- **Filter:** The SDK raises the event for every entry, including entries that the mask you set excludes. Check the mask of the entry in the handler and discard what you do not need.
- **Buffer:** The SDK calls the handler synchronously in the thread that writes the entry, for example the thread of a request or a timer. A slow handler therefore slows down clients and servers. In the handler, only put the entry into a queue and write it out in a separate thread. The [SDK internals](https://www.traeger.de/en/docs/development/opcua/internals) page shows this pattern.

In the following example, the handler only puts the entries into a queue, and a separate thread writes them out:

```csharp
var traceMessages = new BlockingCollection<string>();

// The traced code waits for the handler, so the handler only queues the text.
Utils.Tracing.TraceEventHandler += (sender, e) => traceMessages.Add(FormatTraceText(e));

var traceWriter = new Thread(() => {
    foreach (string message in traceMessages.GetConsumingEnumerable()) {
        Console.WriteLine(message);
    }
});

traceWriter.IsBackground = true;
traceWriter.Start();
```

```vbnet
Dim traceMessages = New BlockingCollection(Of String)()

'The traced code waits for the handler, so the handler only queues the text.
AddHandler Utils.Tracing.TraceEventHandler, Sub(sender, e) traceMessages.Add(FormatTraceText(e))

Dim traceWriter = New Thread(Sub()
    For Each message As String In traceMessages.GetConsumingEnumerable()
        Console.WriteLine(message)
    Next
End Sub)

traceWriter.IsBackground = True
traceWriter.Start()
```

## Turn on tracing in the XML configuration

You can also set up tracing in the XML configuration, with the element `TraceConfiguration` and its elements `OutputFilePath` (file), `DeleteOnLoad` (delete an existing file on load), and `TraceMasks` (masks as a number). The SDK applies these settings only if you load the configuration with `LoadClientConfigFile` or `LoadServerConfigFile` (see [Configuration through an XML file](https://www.traeger.de/en/docs/development/opcua/client/configuration#configuration-through-an-xml-file)). The default configuration of the SDK contains no such element.

The element `TraceConfiguration` is a direct child of the root element. It comes after `ServerConfiguration`, `ClientConfiguration`, `DiscoveryServerConfiguration`, and `Extensions`, and before `DisableHiResClock`. Placed elsewhere, the file does not load, or the SDK ignores the setting without a message. The path in `OutputFilePath` may contain environment variables such as `%LocalApplicationData%`. The stack resolves them to an absolute path.

```xml
  <TraceConfiguration>
    <OutputFilePath>%LocalApplicationData%\MyClientAppName\Logs\opcua-trace.log</OutputFilePath>
    <DeleteOnLoad>true</DeleteOnLoad>
    <TraceMasks>643</TraceMasks>
  </TraceConfiguration>
```

## Protect the content of the entries

The entries can contain details that reveal information about your plant and its users, for example:

- endpoint addresses
- subject and thumbprint of certificates
- paths of the certificate stores
- session details including the user name (server, mask `Information`)
- values that failed to convert

> **Logs are sensitive**
> Treat trace files and your own logs like other confidential operating data: restrict access to the file, delete it when it is no longer needed, and pass it on only after review, for example to support. In production, turn on extensive masks only for troubleshooting.

For an operating log of a client, the state changes of the connection are often enough, see [Log state changes](https://www.traeger.de/en/docs/development/opcua/client/long-running-clients#log-state-changes). A server can log requests through its own events, see [Monitor requests](https://www.traeger.de/en/docs/development/opcua/server/request-monitoring).
