Sisk

Registro

Esta página fue traducida automáticamente del inglés. Leer el original

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 proporciona una forma asíncrona de escribir logs y mantenerlos en una cola de escritura esperable. La clase LogStream implementa IAsyncDisposable, asegurando que todos los logs pendientes se escriban antes de que el flujo 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.

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

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 anterior 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 registro en instancias de objetos TextWriter, como Console.Out, pasando un objeto TextWriter en el constructor:

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

Por cada mensaje escrito en el registro 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:

C#
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 registro con el formato descrito en la tabla:

ValorQué representaEjemplo
%ddDía del mes (formateado con dos dígitos)05
%dmmmNombre completo del mesJuly
%dmmNombre abreviado del mes (tres letras)Jul
%dmNúmero del mes (formateado con dos dígitos)07
%dyAño (formateado con cuatro dígitos)2023
%thHora en formato de 12 horas03
%tHHora en formato de 24 horas (HH)15
%tiMinutos (formateado con dos dígitos)30
%tsSegundos (formateado con dos dígitos)45
%tmMilisegundos (formateado con tres dígitos)123
%tzDesplazamiento de zona horaria (horas totales en UTC)+03:00
%riDirección IP remota del cliente192.168.1.100
%rmMétodo HTTP (mayúsculas)GET
%rsEsquema URI (http/https)https
%raAutoridad URI (dominio)example.com
%rhHost de la solicitudwww.example.com
%rpPuerto de la solicitud443
%rzRuta de la solicitud/path/to/resource
%rqCadena de consulta?key=value&another=123
%scCódigo de estado de la respuesta HTTP200
%sdDescripción del estado de la respuesta HTTPOK
%linTamaño legible por humanos de la solicitud1.2 KB
%linrTamaño bruto de la solicitud (bytes)1234
%louTamaño legible por humanos de la respuesta2.5 KB
%lourTamaño bruto de la respuesta (bytes)2560
%lmsTiempo transcurrido en milisegundos120
%lsEstado de ejecuciónExecuted
%{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

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

Rotación de logs #

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

C#
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. De ser 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 intenten escribirse en este período quedarán en una cola esperando el final 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:

C#
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.

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 registro que puede tener. Por lo tanto, es posible dirigir el registro de tu aplicación a un archivo distinto del AccessLog o ErrorLog predeterminados.

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

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:

CustomLogStream.csC#
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. 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.

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)
        {
            // En este punto, la conexión está abierta y el cliente ha enviado el encabezado que especifica
            // 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, el GC probablemente 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;

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

Escribe para buscar en la documentación y en la referencia de la API.