Sisk

Logging

Эта страница переведена с английского автоматически. Читать оригинал

Вы можете настроить Sisk для автоматической записи журналов доступа и ошибок. Возможна настройка ротации журналов, расширений и частоты.

Класс LogStream предоставляет асинхронный способ записи журналов и хранит их в очереди записи, которую можно ожидать. Класс LogStream реализует IAsyncDisposable, гарантируя, что все ожидающие записи будут записаны до закрытия потока.

В этой статье мы покажем, как настроить журналирование для вашего приложения.

File based access logs #

Журналы в файлы открывают файл, записывают строку текста, а затем закрывают файл для каждой записанной строки. Такая процедура была принята для поддержания отзывчивости записи в журналах.

Program.csC#
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 создаёт его автоматически.

Stream based logging #

Вы можете записывать журналы в объекты TextWriter, такие как Console.Out, передавая объект TextWriter в конструктор:

Program.csC#
using var app = HttpServer.CreateBuilder()
    .UseConfiguration(config => {
        config.AccessLogsStream = new LogStream(Console.Out);
    })
    .Build();

Для каждого сообщения, записываемого в потоковый журнал, вызывается метод TextWriter.Flush().

Access log formatting #

Вы можете настроить формат журнала доступа с помощью предопределённых переменных. Рассмотрим следующую строку:

C#
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
%rmHTTP‑метод (верхний регистр)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

Вы также можете использовать HttpServerConfiguration.DefaultAccessLogFormat, чтобы применить формат журнала доступа по умолчанию.

Rotating logs #

Вы можете настроить HTTP‑сервер так, чтобы он ротировал файлы журналов в сжатый .gz‑файл, когда они достигают определённого размера. Размер проверяется периодически согласно заданному лимиту.

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

Приведённый код будет каждые шесть часов проверять, достиг ли файл LogStream лимита в 64 МБ. Если да, файл сжимается в .gz, после чего access.log очищается.

Во время этого процесса запись в файл блокируется до завершения сжатия и очистки. Все строки, поступившие на запись в этот период, помещаются в очередь, ожидая окончания сжатия.

Эта функция работает только с файловыми LogStream‑ами.

Error logging #

Когда сервер не бросает ошибки в отладчик, он перенаправляет их в журнал, если они есть. Вы можете настроить запись ошибок так:

C#
config.ThrowExceptions = false;
config.ErrorsLogsStream = new LogStream("error.log");

Это свойство будет записывать в журнал только те ошибки, которые не перехвачены обратным вызовом или свойством Router.CallbackErrorHandler.

Записываемая сервером ошибка всегда содержит дату и время, заголовки запроса (не тело), трассировку ошибки и трассировку внутреннего исключения, если они есть.

Other logging instances #

Ваше приложение может иметь ноль или несколько LogStream‑ов, ограничений на количество каналов журнала нет. Поэтому возможно направить журнал вашего приложения в файл, отличный от журнала доступа или журнала ошибок по умолчанию.

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

Extending LogStream #

Вы можете расширить класс LogStream, чтобы писать пользовательские форматы, совместимые с текущим движком журналов Sisk. Пример ниже позволяет выводить цветные сообщения в консоль через библиотеку Spectre.Console:

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

Другой способ автоматически писать пользовательские журналы для каждого запроса/ответа — создать HttpServerHandler. Пример ниже более полный. Он выводит тело запроса и ответа в JSON в консоль. Может быть полезен для отладки запросов в целом. В примере используется ContextBag и HttpServerHandler.

Program.csC#
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();
    }
}
JsonMessageHandler.csC#
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 is not null)
            outputMessage.AppendLine(responseJson);

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

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

Sisk распространяется с открытым исходным кодом по лицензии MIT.

Начните вводить, чтобы искать по документации и справочнику API.