Логирование
Эта страница переведена с английского автоматически. Читать оригинал
Вы можете настроить Sisk для автоматической записи журналов доступа и ошибок. Можно задать ротацию журналов, расширения и частоту.
Класс LogStream предоставляет асинхронный способ записи журналов и хранит их в очереди записи, которую можно ожидать. Класс LogStream реализует IAsyncDisposable, гарантируя, что все ожидающие записи будут записаны до закрытия потока.
В этой статье мы покажем, как настроить журналирование для вашего приложения.
Файловые журналы доступа #
Запись журналов в файлы открывает файл, записывает строку текста и затем закрывает файл после каждой записи. Эта процедура была принята для поддержания отзывчивости записи в журналах.
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();
}
}Приведённый код будет записывать все входящие запросы в файл logs/access.log. Обратите внимание, что файл создаётся автоматически, если его нет, однако папка перед ним не создаётся. Не требуется создавать каталог logs/, так как класс LogStream создаёт его автоматически.
Потоковое журналирование #
Вы можете записывать журналы в объекты TextWriter, такие как Console.Out, передавая объект TextWriter в конструктор:
using var app = HttpServer.CreateBuilder()
.UseConfiguration(config => {
config.AccessLogsStream = new LogStream(Console.Out);
})
.Build();Для каждого сообщения, записываемого в потоковый журнал, вызывается метод TextWriter.Flush().
Форматирование журнала доступа #
Вы можете настроить формат журнала доступа с помощью предопределённых переменных. Рассмотрите следующую строку:
config.AccessLogsFormat = "%dd/%dmm/%dy %tH:%ti:%ts %tz %ls %ri %rs://%ra%rz%rq [%sc %sd] %lin -> %lou in %lmsms [%{user-agent}]";Он запишет сообщение, похожее на:
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]
Вы можете форматировать ваш журнал, используя формат, описанный в таблице:
| Значение | Что представляет | Пример |
|---|---|---|
| %dd | День месяца (в виде двух цифр) | 05 |
| %dmmm | Полное название месяца | July |
| %dmm | Сокращённое название месяца (три буквы) | Jul |
| %dm | Номер месяца (в виде двух цифр) | 07 |
| %dy | Год (в виде четырёх цифр) | 2023 |
| %th | Час в 12-часовом формате | 03 |
| %tH | Час в 24-часовом формате (HH) | 15 |
| %ti | Минуты (в виде двух цифр) | 30 |
| %ts | Секунды (в виде двух цифр) | 45 |
| %tm | Миллисекунды (в виде трёх цифр) | 123 |
| %tz | Смещение часового пояса (общее количество часов в UTC) | +03:00 |
| %ri | Удалённый IP-адрес клиента, определённый в начале запроса | 192.168.1.100 |
| %rm | HTTP-метод (в верхнем регистре) | GET |
| %rs | Схема URI (http/https) | https |
| %ra | Авторитет URI (домен) | example.com |
| %rh | Хост запроса | www.example.com |
| %rp | Порт запроса | 443 |
| %rz | Путь запроса | /path/to/resource |
| %rq | Строка запроса | ?key=value&another=123 |
| %sc | Код статуса HTTP-ответа | 200 |
| %sd | Описание статуса HTTP-ответа | OK |
| %lin | Размер запроса в человекочитаемом виде | 1.2 KB |
| %linr | Необработанный размер запроса (байты) | 1234 |
| %lou | Размер ответа в человекочитаемом виде | 2.5 KB |
| %lour | Необработанный размер ответа (байты) | 2560 |
| %lms | Прошедшее время в миллисекундах | 120 |
| %ls | Статус выполнения | Executed |
| %{header-name} | Представляет заголовок header-name запроса. | Mozilla/5.0 (platform; rv:gecko [...] |
| %{:header-name} | Представляет заголовок header-name ответа. | application/json |
| %{$key} | Представляет значение, хранящееся под key в HttpContext.RequestBag, преобразованное с помощью ToString(). Пусто, если ключ не установлен. | 42 |
Вы также можете использовать HttpServerConfiguration.DefaultAccessLogFormat для применения формата журнала доступа по умолчанию.
Термин %{$key} позволяет обработчикам запросов и маршрутам добавлять свои собственные значения в журнал доступа:
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");
});Ротация журналов #
Вы можете настроить HTTP‑сервер так, чтобы он вращал файлы журналов в сжатый .gz файл, когда они достигают определённого размера. Размер проверяется периодически согласно заданному порогу.
LogStream errorLog = new LogStream("logs/error.log")
.ConfigureRotatingPolicy(
maximumSize: 64 * SizeHelper.UnitMb,
dueTime: TimeSpan.FromHours(6));Приведённый код будет каждые шесть часов проверять, достиг ли файл LogStream лимита в 64 МБ. Если да, файл сжимается в .gz и затем access.log очищается.
Во время этого процесса запись в файл блокируется до завершения сжатия и очистки. Все строки, поступающие на запись в этот период, будут помещены в очередь, ожидая завершения сжатия.
Эта функция работает только с файловыми LogStream.
Журналирование ошибок #
Когда сервер не бросает ошибки в отладчик, он перенаправляет их в журнал, если они есть. Вы можете настроить запись ошибок с помощью:
config.ThrowExceptions = false;
config.ErrorsLogsStream = new LogStream("error.log");Это свойство будет записывать в журнал только в том случае, если ошибка не перехвачена обратным вызовом или свойством Router.CallbackErrorHandler.
Ошибка, записываемая сервером, всегда включает дату и время, заголовки запроса (не тело), трассировку ошибки и трассировку внутреннего исключения, если они есть.
Другие экземпляры журналов #
Ваше приложение может иметь ноль или несколько LogStream, нет ограничения на количество каналов журналов. Поэтому можно направлять журнал вашего приложения в файл, отличный от стандартного AccessLog или ErrorLog.
LogStream appMessages = new LogStream("messages.log");
appMessages.WriteLine("Application started at {0}", DateTime.Now);Безопасная запись файлов #
Для файлов, которые не являются потоками журналов, например отчётов или дампов состояния, LogStream предоставляет статические вспомогательные методы, которые создают недостающий родительский каталог, сериализуют конкурентные записи в один и тот же файл внутри процесса и возвращают false вместо выброса исключения при неудачной операции:
- LogStream.SafeWriteToFile и LogStream.SafeWriteToFileAsync заменяют содержимое файла.
- LogStream.SafeAppendToFile и LogStream.SafeAppendToFileAsync добавляют данные в конец файла.
Каждый метод принимает строку или массив строк и имеет перегрузки, принимающие Encoding.
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.");
}Блокировка действует в пределах процесса: записи из других процессов в тот же файл не координируются. Если вам нужно лишь убедиться, что каталог существует, используйте PathHelper.EnsureDirectoryExistance или PathHelper.EnsureFileDirectoryExistance, которые возвращают true, если каталог уже существовал, и false, если он был создан.
Расширение LogStream #
Вы можете расширить класс LogStream для записи пользовательских форматов, совместимых с текущим движком журналов Sisk. Пример ниже позволяет выводить цветные сообщения в консоль с помощью библиотеки Spectre.Console:
public class CustomLogStream : LogStream
{
protected override void WriteLineInternal(string line)
{
base.WriteLineInternal($"[{DateTime.Now:g}] {line}");
}
}Другой способ автоматически записывать пользовательские журналы для каждого запроса/ответа — создать HttpServerHandler. Пример ниже более полный. Он записывает тело запроса и ответа в формате JSON в консоль. Это может быть полезно для отладки запросов в целом. В примере используется ContextBag и 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)
{
// На данном этапе соединение открыто, и клиент отправил заголовок, указывающий
// что содержимое является JSON. Следующая строка читает содержимое и сохраняет его в запросе.
//
// Если содержимое не будет прочитано в обработчике запроса, сборщик мусора, вероятно, соберёт его
// после отправки ответа клиенту, поэтому содержимое может быть недоступно после закрытия ответа.
//
_ = request.RawBody;
// добавить подсказку в контекст, указывающую, что у этого запроса есть тело JSON
request.Bag.Add("IsJsonRequest", true);
}
}
protected override async void OnHttpRequestClose(HttpServerExecutionResult result)
{
string? requestJson = null,
responseJson = null,
responseMessage;
if (result.Request.Bag.ContainsKey("IsJsonRequest"))
{
// переформатировать JSON с использованием библиотеки CypherPotato.LightJson
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 &&
// проверить, является ли ответ JSON
httpContent.Headers.ContentType?.MediaType?.Contains("json", StringComparison.InvariantCultureIgnoreCase) == true)
{
string json = await httpContent.ReadAsStringAsync();
responseJson = JsonValue.Deserialize(json, new JsonOptions() { WriteIndented = true }).ToString();
}
}
else
{
// получить статус внутренней обработки сервером
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 != null)
outputMessage.AppendLine(responseJson);
outputMessage.AppendLine("-----");
await Console.Out.WriteLineAsync(outputMessage.ToString());
}
}