Sisk

Registro de logs

Esta página foi traduzida automaticamente do inglês. Ler o original

Você pode configurar o Sisk para gravar logs de acesso e de erro automaticamente. É possível definir rotação de logs, extensões e frequência.

A classe LogStream fornece uma maneira assíncrona de escrever logs e mantê‑los em uma fila de escrita aguardável. A classe LogStream implementa IAsyncDisposable, garantindo que todos os logs pendentes sejam gravados antes que o stream seja fechado.

Neste artigo mostraremos como configurar o registro de logs para sua aplicação.

Logs de acesso baseados em arquivo #

Logs para arquivos abrem o arquivo, escrevem a linha de texto e então fecham o arquivo para cada linha escrita. Esse procedimento foi adotado para manter a responsividade de escrita nos 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();
    }
}

O código acima gravará todas as requisições recebidas no arquivo logs/access.log. Observe que o arquivo é criado automaticamente se não existir, porém a pasta anterior não é. Não é necessário criar o diretório logs/ pois a classe LogStream o cria automaticamente.

Registro de logs baseado em stream #

Você pode gravar arquivos de log em instâncias de objetos TextWriter, como Console.Out, passando um objeto TextWriter no construtor:

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

Para cada mensagem gravada no log baseado em stream, o método TextWriter.Flush() é chamado.

Formatação do log de acesso #

Você pode personalizar o formato do log de acesso por variáveis predefinidas. Considere a linha a seguir:

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

Ela gravará uma mensagem como:

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]

Você pode formatar seu arquivo de log conforme a tabela descrita abaixo:

ValorO que representaExemplo
%ddDia do mês (formatado com dois dígitos)05
%dmmmNome completo do mêsJulho
%dmmNome abreviado do mês (três letras)Jul
%dmNúmero do mês (formatado com dois dígitos)07
%dyAno (formatado com quatro dígitos)2023
%thHora no formato de 12 horas03
%tHHora no formato de 24 horas (HH)15
%tiMinutos (formatado com dois dígitos)30
%tsSegundos (formatado com dois dígitos)45
%tmMilissegundos (formatado com três dígitos)123
%tzDeslocamento de fuso horário (horas totais em UTC)+03:00
%riEndereço IP remoto do cliente192.168.1.100
%rmMétodo HTTP (maiúsculas)GET
%rsEsquema da URI (http/https)https
%raAutoridade da URI (domínio)example.com
%rhHost da requisiçãowww.example.com
%rpPorta da requisição443
%rzCaminho da requisição/path/to/resource
%rqString de consulta?key=value&another=123
%scCódigo de status da resposta HTTP200
%sdDescrição do status da resposta HTTPOK
%linTamanho da requisição legível por humanos1.2 KB
%linrTamanho bruto da requisição (bytes)1234
%louTamanho da resposta legível por humanos2.5 KB
%lourTamanho bruto da resposta (bytes)2560
%lmsTempo decorrido em milissegundos120
%lsStatus de execuçãoExecutado
%{header-name}Representa o cabeçalho header-name da requisição.Mozilla/5.0 (platform; rv:gecko [...]
%{:header-name}Representa o cabeçalho header-name da resposta.application/json

Você também pode usar HttpServerConfiguration.DefaultAccessLogFormat para utilizar o formato padrão de log de acesso.

Rotação de logs #

Você pode configurar o servidor HTTP para rotacionar os arquivos de log para um arquivo comprimido .gz quando eles atingirem determinado tamanho. O tamanho é verificado periodicamente pelo limiar que você definir.

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

O código acima verificará a cada seis horas se o arquivo do LogStream atingiu o limite de 64 MB. Caso positivo, o arquivo será comprimido para um .gz e então access.log será limpo.

Durante esse processo, a escrita no arquivo fica bloqueada até que a compressão e limpeza terminem. Todas as linhas que chegarem para ser escritas nesse período ficarão em uma fila aguardando o fim da compressão.

Esta função funciona apenas com LogStreams baseados em arquivo.

Registro de erros #

Quando o servidor não lança erros para o depurador, ele encaminha os erros para gravação de log quando houver algum. Você pode configurar a gravação de erros com:

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

Esta propriedade gravará algo no log somente se o erro não for capturado pelo callback ou pela propriedade Router.CallbackErrorHandler.

O erro gravado pelo servidor sempre inclui a data e hora, os cabeçalhos da requisição (não o corpo), o rastreamento do erro e o rastreamento da exceção interna, se houver.

Outras instâncias de registro #

Sua aplicação pode ter zero ou múltiplos LogStreams, não há limite para a quantidade de canais de log que ela pode ter. Portanto, é possível direcionar o log da sua aplicação para um arquivo diferente do AccessLog ou ErrorLog padrão.

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

Estendendo LogStream #

Você pode estender a classe LogStream para gravar formatos personalizados, compatíveis com o mecanismo de logs atual do Sisk. O exemplo abaixo permite escrever mensagens coloridas no Console através da biblioteca Spectre.Console:

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

Outra forma de gravar automaticamente logs personalizados para cada requisição/resposta é criar um HttpServerHandler. O exemplo abaixo é um pouco mais completo. Ele grava o corpo da requisição e da resposta em JSON no Console. Pode ser útil para depurar requisições em geral. Este exemplo faz uso de ContextBag e 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)
        {
            // Neste ponto, a conexão está aberta e o cliente enviou o cabeçalho especificando
            // que o conteúdo é JSON. A linha abaixo lê o conteúdo e o deixa armazenado na requisição.
            //
            // Se o conteúdo não for lido na ação da requisição, o GC provavelmente coletará o conteúdo
            // após o envio da resposta ao cliente, portanto o conteúdo pode não estar disponível após a resposta ser fechada.
            //
            _ = request.RawBody;

            // adiciona uma dica no contexto para indicar que esta requisição possui um corpo 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"))
        {
            // reformata o JSON usando a biblioteca 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 &&
                // verifica se a resposta é 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
        {
            // obtém o status interno de manipulação do servidor
            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());
    }
}

Digite para pesquisar na documentação e na referência da API.