# Registro

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

Puedes configurar Sisk para que escriba automáticamente registros de acceso y de errores. Es posible definir la rotación de logs, extensiones y frecuencia.

La clase [LogStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) proporciona una forma asíncrona de escribir logs y mantenerlos en una cola de escritura awaitable. La clase `LogStream` implementa `IAsyncDisposable`, garantizando que todos los logs pendientes se escriban antes de que el stream se cierre.

En este artículo te mostraremos cómo configurar el registro para tu aplicación.

## Registros de acceso basados en archivos

Los logs a archivos abren el archivo, escriben la línea de texto y luego cierran el archivo por cada línea escrita. Este procedimiento se adoptó para mantener la capacidad de respuesta de escritura en los 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();
    }
}
```

El código anterior escribirá todas las solicitudes entrantes en el archivo `logs/access.log`. Ten en cuenta que el archivo se crea automáticamente si no existe, sin embargo la carpeta que lo contiene no. No es necesario crear el directorio `logs/` ya que la clase LogStream lo crea automáticamente.

## Registro basado en flujos

Puedes escribir archivos de log en instancias de objetos `TextWriter`, como `Console.Out`, pasando un objeto `TextWriter` en el constructor:

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

Por cada mensaje escrito en el log basado en flujo, se llama al método `TextWriter.Flush()`.

## Formato del registro de acceso

Puedes personalizar el formato del registro de acceso mediante variables predefinidas. Considera la siguiente línea:

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

Escribirá un mensaje 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]

Puedes formatear tu archivo de log según el formato descrito en la tabla:

| Valor | Qué representa | Ejemplo |
|-------|----------------|---------|
| %dd | Día del mes (formateado con dos dígitos) | 05 |
| %dmmm | Nombre completo del mes | July |
| %dmm | Nombre abreviado del mes (tres letras) | Jul |
| %dm | Número del mes (formateado con dos dígitos) | 07 |
| %dy | Año (formateado con cuatro dígitos) | 2023 |
| %th | Hora en formato de 12 horas | 03 |
| %tH | Hora en formato de 24 horas (HH) | 15 |
| %ti | Minutos (formateado con dos dígitos) | 30 |
| %ts | Segundos (formateado con dos dígitos) | 45 |
| %tm | Milisegundos (formateado con tres dígitos) | 123 |
| %tz | Desplazamiento de zona horaria (total de horas en UTC) | +03:00 |
| %ri | Dirección IP remota del cliente, resuelta al inicio de la solicitud | 192.168.1.100 |
| %rm | Método HTTP (mayúsculas) | GET |
| %rs | Esquema URI (http/https) | https |
| %ra | Autoridad URI (dominio) | example.com |
| %rh | Host de la solicitud | www.example.com |
| %rp | Puerto de la solicitud | 443 |
| %rz | Ruta de la solicitud | /path/to/resource |
| %rq | Cadena de consulta | ?key=value&another=123 |
| %sc | Código de estado de la respuesta HTTP | 200 |
| %sd | Descripción del estado de la respuesta HTTP | OK |
| %lin | Tamaño legible de la solicitud | 1.2 KB |
| %linr | Tamaño bruto de la solicitud (bytes) | 1234 |
| %lou | Tamaño legible de la respuesta | 2.5 KB |
| %lour | Tamaño bruto de la respuesta (bytes) | 2560 |
| %lms | Tiempo transcurrido en milisegundos | 120 |
| %ls | Estado de ejecución | Executed |
| %{header-name} | Representa el encabezado `header-name` de la solicitud. | `Mozilla/5.0 (platform; rv:gecko [...]` |
| %{:header-name} | Representa el encabezado `header-name` de la respuesta. | `application/json` |
| %{$key} | Representa el valor almacenado bajo `key` en [HttpContext.RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md), convertido con `ToString()`. Vacío cuando la clave no está establecida. | `42` |

También puedes usar `HttpServerConfiguration.DefaultAccessLogFormat` para emplear el formato de registro de acceso predeterminado.

El término `%{$key}` permite que los manejadores de solicitudes y rutas añadan sus propios valores al registro de acceso:

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

## Rotación de registros

Puedes configurar el servidor HTTP para rotar los archivos de log a un archivo comprimido .gz cuando alcancen un cierto tamaño. El tamaño se verifica periódicamente según el umbral que definas.

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

El código anterior comprobará cada seis horas si el archivo del `LogStream` ha alcanzado su límite de 64 MB. Si es así, el archivo se comprime a .gz y luego se limpia `access.log`.

Durante este proceso, la escritura en el archivo está bloqueada hasta que el archivo se comprime y se limpia. Todas las líneas que ingresen para ser escritas en este período quedarán en una cola esperando el fin de la compresión.

Esta función solo funciona con `LogStream` basados en archivos.

## Registro de errores

Cuando un servidor no lanza errores al depurador, reenvía los errores a la escritura de logs cuando existen. Puedes configurar la escritura de errores con:

```cs
config.ThrowExceptions = false;
config.ErrorsLogsStream = new LogStream("error.log");
```

Esta propiedad solo escribirá algo en el log si el error no es capturado por el callback o la propiedad [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md).

El error escrito por el servidor siempre incluye la fecha y hora, los encabezados de la solicitud (no el cuerpo), la traza del error y la traza de la excepción interna, si existe alguna.

## Otras instancias de registro

Tu aplicación puede tener cero o múltiples `LogStream`; no hay límite en la cantidad de canales de log que puede tener. Por lo tanto, es posible dirigir el log de tu aplicación a un archivo distinto del `AccessLog` o `ErrorLog` predeterminados.

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

## Escritura segura de archivos

Para archivos que no son `LogStream`, como informes o volcados de estado, [LogStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) ofrece ayudantes estáticos que crean el directorio padre faltante, serializan escrituras concurrentes al mismo archivo dentro del proceso y devuelven `false` en lugar de lanzar una excepción cuando la operación falla:

- [LogStream.SafeWriteToFile](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeWriteToFile.md) y [LogStream.SafeWriteToFileAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeWriteToFileAsync.md) reemplazan el contenido del archivo.
- [LogStream.SafeAppendToFile](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeAppendToFile.md) y [LogStream.SafeAppendToFileAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeAppendToFileAsync.md) añaden contenido al archivo.

Cada método acepta una cadena o un arreglo de líneas y tiene sobrecargas que reciben un `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.");
}
```

El bloqueo es por proceso: las escrituras de otros procesos al mismo archivo no están coordinadas. Si solo necesitas asegurarte de que un directorio exista, usa [PathHelper.EnsureDirectoryExistance](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.PathHelper.EnsureDirectoryExistance.md) o [PathHelper.EnsureFileDirectoryExistance](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.PathHelper.EnsureFileDirectoryExistance.md), que devuelven `true` cuando el directorio ya existía y `false` cuando se creó.

## Extender LogStream

Puedes extender la clase `LogStream` para escribir formatos personalizados, compatibles con el motor de logs actual de Sisk. El ejemplo a continuación permite escribir mensajes coloridos en la consola mediante la biblioteca Spectre.Console:

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

Otra forma de escribir automáticamente logs personalizados para cada solicitud/respuesta es crear un [HttpServerHandler](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md). El ejemplo a continuación es un poco más completo. Escribe el cuerpo de la solicitud y la respuesta en JSON a la consola. Puede ser útil para depurar solicitudes en general. Este ejemplo hace uso de `ContextBag` y `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)
        {
            // En este punto, la conexión está abierta y el cliente ha enviado el encabezado que indica
            // que el contenido es JSON. La línea siguiente lee el contenido y lo deja almacenado en la solicitud.
            //
            // Si el contenido no se lee en la acción de la solicitud, es probable que el GC recoja el contenido
            // después de enviar la respuesta al cliente, por lo que el contenido podría no estar disponible después de que la respuesta se cierre.
            //
            _ = request.RawBody;

            // Añade una pista en el contexto para indicar que esta solicitud tiene un cuerpo 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"))
        {
            // Reformatea el JSON usando la 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 si la respuesta es 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
        {
            // Obtiene el estado interno del manejo del 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());
    }
}
```
