# Ablaufverfolgung

> Anleitung: Die Ablaufverfolgung des OPC UA Stacks im SDK einschalten, Masken und Ausgabe wählen, Einträge selbst ausgeben und die Protokolle als sensibel behandeln.

Beteiligte Typen: `Opc.Ua.Utils`

Die Ablaufverfolgung (engl. Trace) schreibt Einträge über die Abläufe in Client und Server, zum Beispiel zur Fehlersuche bei Verbindungen und Zertifikaten. Das OPC UA .NET SDK hat dafür keine eigene API. Es enthält den OPC UA Stack der OPC Foundation, und dessen Ablaufverfolgung steuern Sie über die statische Klasse `Opc.Ua.Utils`. Die Einstellungen gelten für den ganzen Prozess, also für alle Clients und Server der Anwendung.

## Standardverhalten

Im NuGet-Paket ist die Ablaufverfolgung ausgeschaltet: Keine Maske ist gesetzt, und es ist keine Datei festgelegt. Das hängt vom Build des SDK ab, nicht vom Build Ihrer Anwendung. Auch in einem Debug-Build Ihrer Anwendung schreibt das SDK deshalb nichts, solange Sie die Ablaufverfolgung nicht selbst einschalten.

## Ablaufverfolgung einschalten

Beteiligte Typen: `Opc.Ua.Utils`

Um die Einträge in eine Datei zu schreiben, legen Sie drei Dinge fest:

1. **Datei:** `Utils.SetTraceLog` legt die Datei fest. Der zweite Parameter (`deleteExisting`) legt fest, ob das SDK eine vorhandene Datei zuerst löscht. Sonst hängt es die Einträge an.
2. **Masken:** `Utils.SetTraceMask` legt fest, welche Einträge das SDK schreibt (Abschnitt [Masken](https://www.traeger.de/docs/development/opcua/tracing#masken)).
3. **Ausgabe:** `Utils.SetTraceOutput` legt fest, wohin das SDK schreibt (Abschnitt [Ausgabe](https://www.traeger.de/docs/development/opcua/tracing#ausgabe)).

So schalten Sie die Ablaufverfolgung ein:

```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)
```

Verwenden Sie für die Datei einen absoluten Pfad, zum Beispiel im Verzeichnis der Anwendung (`AppContext.BaseDirectory`). Einen relativen Pfad löst das SDK gegen das temporäre Verzeichnis auf (`Path.GetTempPath()`), nicht gegen das Verzeichnis der Anwendung.

Wenn die Datei größer als 10 MB wird, löscht das SDK sie und beginnt eine neue Datei. Ältere Einträge gehen dabei verloren. Wenn Sie Einträge länger aufbewahren müssen, geben Sie sie selbst aus (Abschnitt [Einträge selbst ausgeben](https://www.traeger.de/docs/development/opcua/tracing#eintrage-selbst-ausgeben)).

## Masken

Die Masken von `Utils.TraceMasks` sind Flags. Das SDK schreibt einen Eintrag nur, wenn seine Maske in der gesetzten Maske enthalten ist. Mehrere Masken kombinieren Sie mit dem bitweisen Oder.

| Maske | Wert | Einträge |
|---|---|---|
| `None` | `0x0` | keine Einträge in der Datei |
| `Error` | `0x1` | alle Ausnahmen und Fehler, zum Beispiel abgelehnte Zertifikate, Fehler im Kanal und Fehler von `OpcClient` und `OpcServer` |
| `Information` | `0x2` | allgemeine Meldungen, zum Beispiel geschlossene oder neu verbundene Kanäle und Vorgänge in den Zertifikatspeichern. Einträge ohne eigene Maske erhalten diese Maske. |
| `StackTrace` | `0x4` | keine eigene Kategorie: hängt an Fehlermeldungen den Stack-Trace an und protokolliert jede `ServiceResultException` |
| `Service` | `0x8` | Aufruf und Abschluss jedes Service-Aufrufs |
| `ServiceDetail` | `0x10` | Anzahl der Nodes je Read- und RegisterNodes-Aufruf im Server |
| `Operation` | `0x20` | nicht verwendet |
| `OperationDetail` | `0x40` | nur Debug-Meldungen eines Debug-Builds des Stacks und die Wrapper für OPC Classic |
| `StartStop` | `0x80` | nur Start und Stopp des zentralen Node-Managers im Server |
| `ExternalSystem` | `0x100` | nicht verwendet |
| `Security` | `0x200` | Audit-Meldungen zu sicheren Kanälen (Secure Channels) und Zertifikaten, zum Beispiel neu erzeugte und automatisch akzeptierte Zertifikate |
| `All` | `0x7FFFFFFF` | alle Einträge |

Für die Fehlersuche beginnen Sie mit `Error` und `Information`. Bei Problemen mit Verbindungen und Zertifikaten nehmen Sie `Security` hinzu, für die Abfolge der Service-Aufrufe `Service`. `StackTrace` brauchen Sie nur, wenn Sie den Ursprung eines Fehlers suchen. `Service` und `StackTrace` erzeugen viele Einträge.

## Ausgabe

Die Werte von `Utils.TraceOutput` legen fest, wohin das SDK die Einträge schreibt:

| Ausgabe | Ziel |
|---|---|
| `Off` | keine Ausgabe |
| `FileOnly` | nur in die Datei |
| `DebugAndFile` | in die Datei und zusätzlich in die Debug-Ausgabe (`Debug.WriteLine`) |
| `StdOutAndFile` | wie `FileOnly`: Das SDK schreibt dabei nicht auf die Konsole |

Wenn Sie die Einträge auf der Konsole oder in einem eigenen Protokoll brauchen, geben Sie sie selbst aus.

## Einträge selbst ausgeben

Beteiligte Typen: `Opc.Ua.Utils`

Für ein eigenes Ziel, zum Beispiel das Protokoll Ihrer Anwendung, registrieren Sie einen Handler für das Ereignis `Utils.Tracing.TraceEventHandler`. Die Ereignisdaten enthalten die Maske des Eintrags (`TraceMask`), den Formattext (`Format`), seine Argumente (`Arguments`) und bei Einträgen zu Ausnahmen die Ausnahme selbst (`Exception`). Den Text des Eintrags bilden Sie aus `Format` und `Arguments`, zum Beispiel mit `string.Format`. Einträge zu Ausnahmen haben keine Argumente, ihr Text steht schon vollständig in `Format`; rufen Sie `string.Format` deshalb nur auf, wenn Argumente vorhanden sind. Typ und Meldung der Ausnahme enthält der Text bereits; `Exception` brauchen Sie nur, wenn Sie die Ausnahme als Objekt auswerten wollen.

```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

```

Beachten Sie dabei zwei Punkte:

- **Filtern:** Das SDK löst das Ereignis für jeden Eintrag aus, auch für Einträge, die die gesetzte Maske ausschließt. Prüfen Sie deshalb im Handler selbst die Maske des Eintrags und verwerfen Sie, was Sie nicht brauchen.
- **Puffern:** Das SDK ruft den Handler synchron im Thread auf, der den Eintrag schreibt, zum Beispiel im Thread einer Anfrage oder eines Timers. Ein langsamer Handler bremst deshalb Client und Server. Legen Sie den Eintrag im Handler nur in eine Warteschlange und geben Sie ihn in einem eigenen Thread aus. Das Muster dafür zeigt die Seite [Abläufe im SDK](https://www.traeger.de/docs/development/opcua/internals).

So legt der Handler die Einträge nur in eine Warteschlange, und ein eigener Thread gibt sie aus:

```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()
```

## Per XML-Konfiguration einschalten

Die Ablaufverfolgung lässt sich auch in der XML-Konfiguration einstellen, mit dem Element `TraceConfiguration` und seinen Elementen `OutputFilePath` (Datei), `DeleteOnLoad` (vorhandene Datei beim Laden löschen) und `TraceMasks` (Masken als Zahl). Das SDK wendet diese Einstellungen nur an, wenn Sie die Konfiguration mit `LoadClientConfigFile` beziehungsweise `LoadServerConfigFile` laden (siehe [Konfiguration per XML-Datei](https://www.traeger.de/docs/development/opcua/client/configuration#konfiguration-per-xml-datei)). Die Standardkonfiguration des SDK enthält kein solches Element.

Das Element `TraceConfiguration` ist ein direktes Kind des Wurzelelements. Es steht nach `ServerConfiguration`, `ClientConfiguration`, `DiscoveryServerConfiguration` und `Extensions` und vor `DisableHiResClock`. An einer anderen Stelle lädt die Datei nicht, oder das SDK ignoriert die Einstellung ohne Meldung. Der Pfad in `OutputFilePath` darf Umgebungsvariablen wie `%LocalApplicationData%` enthalten. Der Stack löst sie zu einem absoluten Pfad auf.

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

## Inhalt der Einträge schützen

Die Einträge können Angaben enthalten, die Rückschlüsse auf Ihre Anlage und ihre Benutzer erlauben, zum Beispiel:

- Adressen von Endpunkten
- Subject und Thumbprint von Zertifikaten
- Pfade der Zertifikatspeicher
- Angaben zu Sitzungen mit dem Benutzernamen (Server, Maske `Information`)
- Werte, bei denen eine Umwandlung fehlgeschlagen ist

> **Protokolle sind sensibel**
> Behandeln Sie Trace-Dateien und eigene Protokolle wie andere vertrauliche Betriebsdaten: Beschränken Sie den Zugriff auf die Datei, löschen Sie sie, wenn sie nicht mehr gebraucht wird, und geben Sie sie nur geprüft weiter, zum Beispiel an den Support. Schalten Sie umfangreiche Masken im Betrieb nur zur Fehlersuche ein.

Für ein Betriebsprotokoll eines Clients genügen oft die Zustandswechsel der Verbindung, siehe [Zustandswechsel protokollieren](https://www.traeger.de/docs/development/opcua/client/long-running-clients#zustandswechsel-protokollieren). Ein Server kann Anfragen über eigene Ereignisse protokollieren, siehe [Anfragen überwachen](https://www.traeger.de/docs/development/opcua/server/request-monitoring).
