# Protokollierung

Source: https://docs.sisk-framework.org/de/docs/features/logging.html

Sie können Sisk so konfigurieren, dass Zugriffs- und Fehlermeldungen automatisch protokolliert werden. Es ist möglich, Log‑Rotation, Erweiterungen und Frequenz zu definieren.

Die Klasse [LogStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) bietet eine asynchrone Methode zum Schreiben von Logs und hält sie in einer abfragbaren Schreibwarteschlange. Die `LogStream`‑Klasse implementiert `IAsyncDisposable` und stellt sicher, dass alle ausstehenden Logs geschrieben werden, bevor der Stream geschlossen wird.

In diesem Artikel zeigen wir Ihnen, wie Sie das Logging für Ihre Anwendung konfigurieren.

## Dateibasierte Zugriffslogs

Logs zu Dateien öffnen die Datei, schreiben den Zeilentext und schließen die Datei anschließend für jede geschriebene Zeile. Dieses Verfahren wurde gewählt, um die Schreib‑Reaktionsfähigkeit der Logs zu erhalten.

```cs {title="Program.cs"}
class Program
{
    static async Task Main(string[] args)
    {
        using var app = HttpServer.CreateBuilder()
            .UseConfiguration(config => {
                config.AccessLogsStream = new LogStream("logs/access.log");
            })
            .Build();
        
        ...
        
        await app.StartAsync();
    }
}
```

Der obige Code schreibt alle eingehenden Anfragen in die Datei `logs/access.log`. Beachten Sie, dass die Datei automatisch erstellt wird, falls sie nicht existiert, das übergeordnete Verzeichnis jedoch nicht. Es ist nicht nötig, das Verzeichnis `logs/` manuell anzulegen, da die `LogStream`‑Klasse es automatisch erzeugt.

## Stream‑basiertes Logging

Sie können Log‑Dateien in `TextWriter`‑Objekte schreiben, z. B. `Console.Out`, indem Sie ein `TextWriter`‑Objekt im Konstruktor übergeben:

```cs {title="Program.cs"}
using var app = HttpServer.CreateBuilder()
    .UseConfiguration(config => {
        config.AccessLogsStream = new LogStream(Console.Out);
    })
    .Build();
```

Für jede im stream‑basierten Log geschriebene Nachricht wird die Methode `TextWriter.Flush()` aufgerufen.

## Formatierung des Zugriffslogs

Sie können das Zugriffslog‑Format mit vordefinierten Variablen anpassen. Betrachten Sie die folgende Zeile:

```cs
config.AccessLogsFormat = "%dd/%dmm/%dy %tH:%ti:%ts %tz %ls %ri %rs://%ra%rz%rq [%sc %sd] %lin -> %lou in %lmsms [%{user-agent}]";
```

Sie erzeugt eine Meldung wie:

    29/mar./2023 15:21:47 -0300 Executed ::1 http://localhost:5555/ [200 OK] 689B -> 707B in 84ms [Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/111.0.0.0 Safari/537.36]

Sie können Ihre Log‑Datei nach dem in der Tabelle beschriebenen Format formatieren:

| Wert   | Was es darstellt                                                                 | Beispiel                               |
|--------|-----------------------------------------------------------------------------------|----------------------------------------|
| %dd    | Tag des Monats (als zweistellige Zahl formatiert)                                 | 05                                     |
| %dmmm  | Vollständiger Name des Monats                                                     | July                                   |
| %dmm   | Abgekürzter Name des Monats (drei Buchstaben)                                    | Jul                                    |
| %dm    | Monatszahl (als zweistellige Zahl formatiert)                                    | 07                                     |
| %dy    | Jahr (vierstellig)                                                               | 2023                                   |
| %th    | Stunde im 12‑Stunden‑Format                                                      | 03                                     |
| %tH    | Stunde im 24‑Stunden‑Format (HH)                                                 | 15                                     |
| %ti    | Minuten (zweistellig)                                                            | 30                                     |
| %ts    | Sekunden (zweistellig)                                                            | 45                                     |
| %tm    | Millisekunden (dreistellig)                                                      | 123                                    |
| %tz    | Zeitzonenoffset (Gesamtstunden in UTC)                                           | +03:00                                 |
| %ri    | Remote‑IP‑Adresse des Clients, ermittelt zu Beginn der Anfrage                   | 192.168.1.100                          |
| %rm    | HTTP‑Methode (Großbuchstaben)                                                    | GET                                    |
| %rs    | URI‑Schema (http/https)                                                          | https                                  |
| %ra    | URI‑Authority (Domain)                                                          | example.com                            |
| %rh    | Host der Anfrage                                                                  | www.example.com                        |
| %rp    | Port der Anfrage                                                                  | 443                                    |
| %rz    | Pfad der Anfrage                                                                  | /path/to/resource                      |
| %rq    | Abfragezeichenfolge                                                               | ?key=value&another=123                 |
| %sc    | HTTP‑Antwortstatuscode                                                            | 200                                    |
| %sd    | Beschreibung des HTTP‑Antwortstatus                                              | OK                                     |
| %lin   | Menschlich lesbare Größe der Anfrage                                             | 1.2 KB                                 |
| %linr  | Rohgröße der Anfrage (Bytes)                                                     | 1234                                   |
| %lou   | Menschlich lesbare Größe der Antwort                                             | 2.5 KB                                 |
| %lour  | Rohgröße der Antwort (Bytes)                                                     | 2560                                   |
| %lms   | Verstrichene Zeit in Millisekunden                                               | 120                                    |
| %ls    | Ausführungsstatus                                                                 | Executed                               |
| %{header-name}    | Stellt den Header `header-name` der Anfrage dar.                                                            | `Mozilla/5.0 (platform; rv:gecko [...]` |
| %{:header-name}    | Stellt den Header `header-name` der Antwort dar.                                                            | `application/json`                     |
| %{$key}    | Stellt den unter `key` gespeicherten Wert in [HttpContext.RequestBag] dar, konvertiert mit `ToString()`. Leer, wenn der Schlüssel nicht gesetzt ist. | `42`                                   |

Sie können außerdem `HttpServerConfiguration.DefaultAccessLogFormat` verwenden, um das Standard‑Zugriffslog‑Format zu nutzen.

Der Ausdruck `%{$key}` ermöglicht es Request‑Handlern und Routen, eigene Werte zum Zugriffslog hinzuzufügen:

```cs
config.AccessLogsFormat = "%dd/%dmm/%dy %tH:%ti:%ts %rm %rz [%sc] user=%{$UserId}";

router.MapGet("/profile", request =>
{
    request.Bag["UserId"] = "42";
    return new HttpResponse("ok");
});
```

## Rotierende Logs

Sie können den HTTP‑Server so konfigurieren, dass Log‑Dateien in eine komprimierte *.gz*-Datei rotiert werden, sobald sie eine bestimmte Größe erreichen. Die Größe wird periodisch anhand des von Ihnen definierten Schwellenwerts geprüft.

```cs
LogStream errorLog = new LogStream("logs/error.log")
    .ConfigureRotatingPolicy(
        maximumSize: 64 * SizeHelper.UnitMb,
        dueTime: TimeSpan.FromHours(6));
```

Der obige Code prüft alle sechs Stunden, ob die Datei des `LogStream` die Grenze von 64 MiB erreicht hat. Falls ja, wird die Datei zu einer *.gz*-Datei komprimiert und anschließend `access.log` bereinigt.

Während dieses Vorgangs ist das Schreiben in die Datei gesperrt, bis die Kompression abgeschlossen und die Datei bereinigt ist. Alle Zeilen, die in diesem Zeitraum geschrieben werden sollen, landen in einer Warteschlange, die auf das Ende der Kompression wartet.

Diese Funktion arbeitet ausschließlich mit dateibasierten `LogStream`s.

## Fehler‑Logging

Wenn ein Server keine Ausnahmen an den Debugger weitergibt, leitet er die Fehler zum Log‑Schreiben weiter, sofern welche vorhanden sind. Sie können das Fehler‑Logging folgendermaßen konfigurieren:

```cs
config.ThrowExceptions = false;
config.ErrorsLogsStream = new LogStream("error.log");
```

Diese Einstellung schreibt nur dann etwas ins Log, wenn der Fehler nicht durch den Callback oder die Eigenschaft [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) abgefangen wurde.

Der vom Server geschriebene Fehler enthält stets Datum und Uhrzeit, die Request‑Header (nicht den Body), den Fehler‑Stacktrace und, falls vorhanden, den Stacktrace der inneren Ausnahme.

## Weitere Logging‑Instanzen

Ihre Anwendung kann null oder mehrere `LogStream`s besitzen; es gibt keine Begrenzung, wie viele Log‑Kanäle sie haben darf. Daher ist es möglich, das Log Ihrer Anwendung in eine andere Datei als das Standard‑AccessLog oder ErrorLog zu leiten.

```cs
LogStream appMessages = new LogStream("messages.log");
appMessages.WriteLine("Application started at {0}", DateTime.Now);
```

## Dateien sicher schreiben

Für Dateien, die keine Log‑Streams sind, wie Berichte oder Zustands‑Dumps, stellt [LogStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) statische Hilfsmethoden bereit, die das fehlende übergeordnete Verzeichnis anlegen, gleichzeitige Schreibvorgänge auf dieselbe Datei innerhalb des Prozesses serialisieren und `false` zurückgeben, anstatt eine Ausnahme zu werfen, wenn die Operation fehlschlägt:

- [LogStream.SafeWriteToFile](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeWriteToFile.md) und [LogStream.SafeWriteToFileAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeWriteToFileAsync.md) ersetzen den Dateiinhalt.
- [LogStream.SafeAppendToFile](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeAppendToFile.md) und [LogStream.SafeAppendToFileAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeAppendToFileAsync.md) hängen an die Datei an.

Jede Methode akzeptiert einen String oder ein Array von Zeilen und hat Überladungen, die ein `Encoding` entgegennehmen.

```cs
bool written = await LogStream.SafeAppendToFileAsync("logs/audit.log", $"{DateTime.Now:s} user signed in\n");

if (!written)
{
    Console.WriteLine("Could not write the audit entry.");
}
```

Die Sperre gilt pro Prozess: Schreibvorgänge anderer Prozesse auf dieselbe Datei werden nicht koordiniert. Wenn Sie lediglich sicherstellen wollen, dass ein Verzeichnis existiert, verwenden Sie [PathHelper.EnsureDirectoryExistance](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.PathHelper.EnsureDirectoryExistance.md) oder [PathHelper.EnsureFileDirectoryExistance](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.PathHelper.EnsureFileDirectoryExistance.md), die `true` zurückgeben, wenn das Verzeichnis bereits existierte, und `false`, wenn es neu erstellt wurde.

## Erweiterung von LogStream

Sie können die Klasse `LogStream` erweitern, um eigene Formate zu schreiben, die mit der aktuellen Sisk‑Log‑Engine kompatibel sind. Das nachfolgende Beispiel ermöglicht das Schreiben farbiger Meldungen in die Konsole über die Bibliothek Spectre.Console:

```cs {title="CustomLogStream.cs"}
public class CustomLogStream : LogStream
{
    protected override void WriteLineInternal(string line)
    {
        base.WriteLineInternal($"[{DateTime.Now:g}] {line}");
    }
}
```

Eine weitere Möglichkeit, automatisch benutzerdefinierte Logs für jede Anfrage/Antwort zu schreiben, besteht darin, einen [HttpServerHandler](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md) zu erstellen. Das folgende Beispiel ist etwas umfangreicher. Es schreibt den Body von Anfrage und Antwort als JSON in die Konsole. Es kann für das allgemeine Debuggen von Anfragen nützlich sein. Dieses Beispiel nutzt `ContextBag` und `HttpServerHandler`.

```cs {title="Program.cs"}
class Program
{
    static async Task Main(string[] args)
    {
        var app = HttpServer.CreateBuilder(host =>
        {
            host.UseListeningPort(5555);
            host.UseHandler<JsonMessageHandler>();
        });

        app.Router.MapAny("/json", request =>
        {
            return new HttpResponse()
                .WithContent(JsonContent.Create(new
                {
                    method = request.Method.Method,
                    path = request.Path,
                    specialMessage = "Hello, world!!"
                }));
        });

        await app.StartAsync();
    }
}
```

```cs {title="JsonMessageHandler.cs"}
class JsonMessageHandler : HttpServerHandler
{
    protected override void OnHttpRequestOpen(HttpRequest request)
    {
        if (request.Method != HttpMethod.Get && request.Headers["Content-Type"]?.Contains("json", StringComparison.InvariantCultureIgnoreCase) == true)
        {
            // An diesem Punkt ist die Verbindung geöffnet und der Client hat den Header gesendet,
            // der angibt, dass der Inhalt JSON ist. Die nachfolgende Zeile liest den Inhalt
            // und lässt ihn im Request gespeichert.
            //
            // Wird der Inhalt nicht in der Request‑Aktion gelesen, kann die Garbage‑Collection
            // den Inhalt nach dem Senden der Antwort an den Client freigeben, sodass der
            // Inhalt nach dem Schließen der Antwort nicht mehr verfügbar ist.
            //
            _ = request.RawBody;

            // Hinweis im Kontext hinzufügen, dass diese Anfrage einen JSON‑Body besitzt
            request.Bag.Add("IsJsonRequest", true);
        }
    }

    protected override async void OnHttpRequestClose(HttpServerExecutionResult result)
    {
        string? requestJson = null,
                responseJson = null,
                responseMessage;

        if (result.Request.Bag.ContainsKey("IsJsonRequest"))
        {
            // Formatiert das JSON mit der Bibliothek CypherPotato.LightJson neu
            var content = result.Request.Body;
            requestJson = JsonValue.Deserialize(content, new JsonOptions() { WriteIndented = true }).ToString();
        }
        
        if (result.Response is { } response)
        {
            var content = response.Content;
            responseMessage = $"{(int)response.Status} {HttpStatusInformation.GetStatusCodeDescription(response.Status)}";
            
            if (content is HttpContent httpContent &&
                // Prüfen, ob die Antwort JSON ist
                httpContent.Headers.ContentType?.MediaType?.Contains("json", StringComparison.InvariantCultureIgnoreCase) == true)
            {
                string json = await httpContent.ReadAsStringAsync();
                responseJson = JsonValue.Deserialize(json, new JsonOptions() { WriteIndented = true }).ToString();
            }
        }
        else
        {
            // Interner Server‑Verarbeitungsstatus ermitteln
            responseMessage = result.Status.ToString();
        }
        
        StringBuilder outputMessage = new StringBuilder();

        if (requestJson != null)
        {
            outputMessage.AppendLine("-----");
            outputMessage.AppendLine($">>> {result.Request.Method} {result.Request.Path}");

            if (requestJson is not null)
                outputMessage.AppendLine(requestJson);
        }

        outputMessage.AppendLine($"<<< {responseMessage}");

        if (responseJson is not null)
            outputMessage.AppendLine(responseJson);

        outputMessage.AppendLine("-----");

        await Console.Out.WriteLineAsync(outputMessage.ToString());
    }
}
```
