# Registro de Log

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

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](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) 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 escritos antes que o stream seja fechado.

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

## Logs de acesso baseados em arquivos

Logs em arquivos abrem o arquivo, escrevem o texto da linha e então fecham o arquivo para cada linha escrita. Esse procedimento foi adotado para manter a responsividade de escrita nos logs.

```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();
    }
}
```

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.

## Log baseado em stream

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

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

Para cada mensagem escrita 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:

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

Ela gerará 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:

| Valor | O que representa | Exemplo |
|-------|------------------|---------|
| %dd | Dia do mês (formatado com dois dígitos) | 05 |
| %dmmm | Nome completo do mês | July |
| %dmm | Nome abreviado do mês (três letras) | Jul |
| %dm | Número do mês (formatado com dois dígitos) | 07 |
| %dy | Ano (formatado com quatro dígitos) | 2023 |
| %th | Hora no formato de 12 horas | 03 |
| %tH | Hora no formato de 24 horas (HH) | 15 |
| %ti | Minutos (formatado com dois dígitos) | 30 |
| %ts | Segundos (formatado com dois dígitos) | 45 |
| %tm | Milissegundos (formatado com três dígitos) | 123 |
| %tz | Deslocamento do fuso horário (total de horas em UTC) | +03:00 |
| %ri | Endereço IP remoto do cliente, resolvido no início da requisição | 192.168.1.100 |
| %rm | Método HTTP (maiúsculas) | GET |
| %rs | Esquema da URI (http/https) | https |
| %ra | Autoridade da URI (domínio) | example.com |
| %rh | Host da requisição | www.example.com |
| %rp | Porta da requisição | 443 |
| %rz | Caminho da requisição | /path/to/resource |
| %rq | String de consulta | ?key=value&another=123 |
| %sc | Código de status da resposta HTTP | 200 |
| %sd | Descrição do status da resposta HTTP | OK |
| %lin | Tamanho da requisição legível por humanos | 1.2 KB |
| %linr | Tamanho bruto da requisição (bytes) | 1234 |
| %lou | Tamanho da resposta legível por humanos | 2.5 KB |
| %lour | Tamanho bruto da resposta (bytes) | 2560 |
| %lms | Tempo decorrido em milissegundos | 120 |
| %ls | Status de execução | Executed |
| %{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` |
| %{$key} | Representa o valor armazenado sob `key` em [HttpContext.RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md), convertido com `ToString()`. Vazio quando a chave não está definida. | `42` |

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

O termo `%{$key}` permite que manipuladores de requisição e rotas adicionem seus próprios valores ao log de acesso:

```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");
});
```

## Rotação de logs

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

```cs
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á compactado para um .gz e então o `access.log` será limpo.

Durante esse processo, a escrita no arquivo fica bloqueada até que o arquivo seja compactado e limpo. Todas as linhas que entrarem para serem escritas nesse período ficarão em uma fila aguardando o fim da compactação.

Esta função funciona apenas com `LogStream`s baseados em arquivo.

## Registro de erros

Quando um 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:

```cs
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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md).

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 log

Sua aplicação pode ter zero ou múltiplos `LogStream`s, 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.

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

## Gravando arquivos com segurança

Para arquivos que não são streams de log, como relatórios ou dumps de estado, [LogStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) fornece auxiliares estáticos que criam o diretório pai ausente, serializam gravações concorrentes no mesmo arquivo dentro do processo e retornam `false` em vez de lançar exceção quando a operação falha:

- [LogStream.SafeWriteToFile](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeWriteToFile.md) e [LogStream.SafeWriteToFileAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeWriteToFileAsync.md) substituem o conteúdo do arquivo.
- [LogStream.SafeAppendToFile](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeAppendToFile.md) e [LogStream.SafeAppendToFileAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeAppendToFileAsync.md) adicionam conteúdo ao final do arquivo.

Cada método aceita uma string ou um array de linhas e possui sobrecargas que recebem um `Encoding`.

```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.");
}
```

O bloqueio é por processo: gravações de outros processos no mesmo arquivo não são coordenadas. Se você precisar apenas garantir que um diretório exista, use [PathHelper.EnsureDirectoryExistance](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.PathHelper.EnsureDirectoryExistance.md) ou [PathHelper.EnsureFileDirectoryExistance](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.PathHelper.EnsureFileDirectoryExistance.md), que retornam `true` quando o diretório já existia e `false` quando foi criado.

## Extendendo LogStream

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

```cs {title="CustomLogStream.cs"}
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](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md). 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`.

```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)
        {
            // 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 o fechamento da resposta.
            //
            _ = 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());
    }
}
```
