Protokollierung
Diese Seite wurde automatisch aus dem Englischen übersetzt. Original lesen
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 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.
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:
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:
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:
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.
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 LogStreams.
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:
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 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 LogStreams 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.
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 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 und LogStream.SafeWriteToFileAsync ersetzen den Dateiinhalt.
- LogStream.SafeAppendToFile und LogStream.SafeAppendToFileAsync hängen an die Datei an.
Jede Methode akzeptiert einen String oder ein Array von Zeilen und hat Überladungen, die ein Encoding entgegennehmen.
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 oder PathHelper.EnsureFileDirectoryExistance, 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:
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 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.
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();
}
}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());
}
}