Cuando creamos aplicaciones o sistemas, debemos saber lo que pasa en los mismos.
Para saber lo que pasa en el sistema, la arquitectura o el flujo que une las aplicaciones utilizamos la observabilidad, que veremos en otro vídeo.
Y para saber lo que pasa dentro de una aplicación de manera individual utilizaremos los logs.
Índice
1 - Qué son los logs?
Los logs son la parte de nuestro sistema (comúnmente librerías) que guardan un registro de, normalmente, errores o acciones que suceden en nuestras aplicaciones.
Por ejemplo, si intentamos crear una orden de compra en nuestro sistema, sobre un producto que no existe, el sistema deberá indicarle al usuario que hay un error, pero a su vez, el sistema deberá informarnos del error, con una cantidad mayor de detalles para poder identificar el problema.
Esto quiere decir, que los logs no son visibles al usuario final, únicamente a los administradores del mismo.
1.1 - Qué información debemos almacenar en los logs?
Debemos almacenar dentro de los logs el error, a poder ser con detalles, siempre cumpliendo GDPR, así que ni nombres propios ni números de teléfono, emails, contraseñas, etc.
Además de la ubicación de donde ha pasado dicho error, en .NET tenemos disponible lo que se llama el stacktrace
, lo que nos da la ubicación exacta, y el proceso que ha utilizado el código para llegar hasta allí.
1.2 - Cuántos logs debemos almacenar?
Una de las grandes incógnitas cuando desarrollamos es saber si lo que estás haciendo debes registrarlo o no. Por ejemplo, en el patrón productor consumidor, debemos registrar cuando se ejecuta el consumidor pero no hay mensajes, debemos registrarlo cuando sí hay mensajes, debemos registrar al publicar. Este tipo de preguntas son comunes del día a día.
La respuesta es, al principio de la vida de nuestra aplicación si, pero después podemos ir quitando logs. Esto quiere decir que en las primeras versiones de nuestro software vamos a registrar todos los pasos importantes, para una vez ha pasado un tiempo y sabemos que todo funciona como debería, registrar únicamente los errores.
2 - Donde se almacenan los logs
Por defecto cuando utilizamos logs se guardan en el sistema de archivos, pero obviamente lo podemos cambiar, de hecho debemos hacerlo.
Si bien es cierto que cuando estamos desarrollando en local almacenar los logs no es importante si lo es a la hora de tener las aplicaciones desplegadas, ya sea en dev, uat o producción, de hecho dev es muy importante, porque si vemos una alarma en dev que no ha sucedido en producción podemos arreglar dicho bug antes de que un cliente lo sufra.
Por supuesto cuando tenemos aplicaciones en servidores los logs deben estar fuera del servidor, a poder ser en un servicio externo.
En nuestro caso, los vamos a almacenar en Graylog
(a través de docker), pero tenemos otras opciones, como pueden ser datadog, newrelic, o si estamos en un proveedor de servicios como AWS o Azure el propio del servicio.
3 - Creación de logs en .NET
Dentro de .NET tenemos varias opciones para la creación de logs, las dos más comunes son la librería de logs propia de Microsoft, y la librería SeriLog
, la cual es, en mi opinión, la mas completa.
Microsoft cuando desarrolló su versión la creó pensando en librerías externas y permite configurar su librería para que por detrás utilice serilog.
Y esto es muy muy potente, porque serilog nos provee de lo que se llaman “Sinks” que nos permiten configurar múltiples destinos para nuestros logs.
Esto quiere decir que si estamos migrando de un servicio de logs a otro, podemos loguear en ambos durante un tiempo sin tener que afectar el código, lo mismo aplica para la consola, o para un log de fichero.
Y luego podemos utilizar inyección de dependencias del log de microsoft en nuestro servicios para así no propagar la dependencia de serilog en todas partes.
En mi opinión ha sido una decisión brillante por parte de Microsoft el crear el logger así.
4 - Implementación de un logger en .NET
Lo primero que debemos hacer es incluir Graylog a nuestro docker compose, debido a su funcionamiento debemos incluir también mongo y elasticsearch, pero no trabajaremos con ellos directamente, graylog lo hará:
#########################
# Graylog configuration #
#########################
# mongo should be called mongo
mongo:
container_name: mongo_graylog
image: mongo:4.2
elasticsearch:
container_name: elasticserach_graylog
image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2
environment:
- http.host=0.0.0.0
- transport.host=localhost
- network.host=0.0.0.0
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
deploy:
resources:
limits:
memory: 1g
graylog:
image: graylog/graylog:4.0
environment:
# at least 16 characters
- GRAYLOG_PASSWORD_SECRET=thispassshouldbeatleast16characters
# Password: admin
- GRAYLOG_ROOT_PASSWORD_SHA2=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918
- GRAYLOG_HTTP_EXTERNAL_URI=http://localhost:9000/
entrypoint: /usr/bin/tini -- wait-for-it elasticsearch:9200 -- /docker-entrypoint.sh
restart: always
depends_on:
- mongo
- elasticsearch
ports:
- 9000:9000 # Graylog web interface and REST API
- 1514:1514
- 1514:1514/udp
- 12201:12201
- 12201:12201/udp
#############################
# Graylog configuration end #
#############################
Una vez efectuado con docker-compose up -d
podemos ir a graylog en el navegador (http:\\localhost:9000
).
Nota: usuario y contraseña son admin
- admin
.
Antes de pasar al código debemos de tener en cuenta una cosa, debemos actualizar manualmente dentro de graylog los inputs al sistema, para ello debemos crear uno (o varios si tenemos multiples servidores enviando información) que reciba información glef, el cual es un formato de mensaje creado por graylog (el que se manda por defecto).
Toda la información es por defecto a excepción del nombre, y nos saldrá lo siguiente en la UI:
Hay que tener en cuenta que en entornos de producción el sistema de logs se crea una vez y no se desmonta nunca, así que esto no sería un problema.
Pero si alguien sabe como crear esta configuración en docker, por favor contactame por privado y la añadiré al post.
4.1 - Abstracción de un logger con C# y serilog
Ahora lo que debemos hacer es configurar nuestra aplicación, como en todos los vídeos de la serie, vamos a utilizar el código del repositorio de Distribt en github para ello podemos crear un nuevo proyecto o utilizar el de setup
. En mi caso voy a crear un proyecto llamado Distribt.Shared.Logging
, en la que vamos a instalar el paquete de serilog
así como dos sinks, el de la consola, el de graylog, y finalmente la configuración con aspnetcore.
Con el paquete Serilog.Sinks.Graylog
nos podremos comunicar con .Graylog
y con el paquete de .Console
con la consola que está ejecutando nuestra aplicación, y así ver los mensajes.
- Nota: recomiendo no habilitar la función de console en entornos de desarrollo/uat/producción a no ser que estés debugueando en dicho entorno.
Y vamos a crear las clases que contendrán las configuraciones, para ello creamos dos clases:
public record ConsoleLoggerConfiguration
{
}
public record GraylogLoggerConfiguration
{
}
Las cuales van a pasar a contener la información.
4.1.1- Configurar un sink de consola con Serilog
En el caso del sink de la consola es muy sencillo, únicamente debemos indicar si está habilitado o no, y el nivel mínimo que queremos loguear.
public class ConsoleLoggerConfiguration
{
public bool Enabled { get; set; } = false;
public LogEventLevel MinimumLevel { get; set; }
}
Cuando configuramos el log lo haremos a través del tipo LoggerConfiguration
así que debemos enviar esta información a dicho tipo.
Tenemos la opción de crear un método en la propia clase de la configuración o crear un método extensible que nos lo añada la serilog a través de su opción WriteTo
:
public static class LoggerConfigurationExtensions
{
public static LoggerConfiguration AddConsoleLogger(this LoggerConfiguration loggerConfiguration,
ConsoleLoggerConfiguration consoleLoggerConfiguration)
{
return consoleLoggerConfiguration.Enabled
? loggerConfiguration.WriteTo.Console(consoleLoggerConfiguration.MinimumLevel)
: loggerConfiguration;
}
}
La configuración la recibiremos desde los ficheros de configuración y será como la siguiente:
"Logging": {
...
"Console": {
"Enabled": true,
"MinimumLevel": "Error"
}
...
}
- Nota: podemos especificar unos valores por defecto en caso de que esta sección no este indicada.
4.1.2 - Configurar un sink de Graylog con Serilog
Ahora debemos hacer lo mismo para comunicarnos con serilog, pero en este caso la configuración necesitará algo más de información ya que debemos especificar el host
al que vamos a llamar así como el puerto
.
public class GraylogLoggerConfiguration
{
public bool Enabled { get; set; } = false;
public string Host { get; set; } = "";
public int Port { get; set; }
public LogEventLevel MinimumLevel { get; set; }
}
E igual que antes, debemos ser capaces de configurar LoggerConfiguration
:
public static LoggerConfiguration AddGraylogLogger(this LoggerConfiguration loggerConfiguration,
GraylogLoggerConfiguration graylogLoggerConfiguration)
{
return graylogLoggerConfiguration.Enabled
? loggerConfiguration.WriteTo.Graylog(graylogLoggerConfiguration.Host, graylogLoggerConfiguration.Port,
TransportType.Udp, graylogLoggerConfiguration.MinimumLevel)
: loggerConfiguration;
}
Sin olvidarnos de como quedaría la configuración:
"Logging": {
.....
"Graylog": {
"Enabled": true,
"Host": "localhost",
"Port": "12201",
"MinimumLevel": "Error"
},
.....
}
4.2 - Configurar serilog en .NET
Ahora que tenemos la configuración especificada debemos unir todos los puntos para ser capaces de loguear la información correctamente.
En el caso de serilog no lo vamos a añadir al contenedor de dependencias, sino que lo vamos a añadir al host donde se está ejecutando la aplicación.
En las versiones anteriores a .NET6 lo haciamos desde el IWebHostBuilder
desde .NET 6 lo tenemos que hacer desde IHostBuilder
, por lo que creamos un extension method del mismo para configurar dicha información:
public static class ConfigureLogger
{
public static IHostBuilder ConfigureSerilog(this IHostBuilder builder)
=> builder.UseSerilog((context, loggerConfiguration)
=> ConfigureSerilogLogger(loggerConfiguration, context.Configuration));
private static LoggerConfiguration ConfigureSerilogLogger(LoggerConfiguration loggerConfiguration,
IConfiguration configuration)
{
GraylogLoggerConfiguration graylogLogger = new GraylogLoggerConfiguration();
configuration.GetSection("Logging:Graylog").Bind(graylogLogger);
ConsoleLoggerConfiguration consoleLogger = new ConsoleLoggerConfiguration();
configuration.GetSection("Logging:Console").Bind(consoleLogger);
return loggerConfiguration.AddConsoleLogger(consoleLogger).AddGraylogLogger(graylogLogger);
}
}
como vemos hemos utilizado el binding de la configuración para leer la información de dentro del appsettings
.
Ahora debemos invocarlo desde nuestro WebApplicationBuilder
:
public static WebApplication Create(Action<WebApplicationBuilder>? webappBuilder = null)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.Host.ConfigureSerilog();
...
return builder.Build();
}
Y finalmente para conectar SeriLog con el login de microsoft únicamente debemos especificarlo a la hora de incluir el logger en el contenedor de dependencias:
builder.Services.AddLogging(logger => logger.AddSerilog());
Ahora solo nos queda probarlo.
Para inyectar el log, lo que tenemos que hacer es invocar Ilogger<T>
donde T
es el servicio donde estamos, técnicamente podemos poner cualquier cosa en el generic T
, pero por convención y claridad deberemos poner el servicio actual.
public class OrderController
{
private readonly IDomainMessagePublisher _domainMessagePublisher;
private readonly ILogger<OrderController> _logger;
public OrderController(ILogger<OrderController> logger, IDomainMessagePublisher domainMessagePublisher)
{
_domainMessagePublisher = domainMessagePublisher;
_logger = logger;
}
[HttpGet("{orderId}")]
public Task<OrderDto> GetOrder(Guid orderId)
{
_logger.LogError($"esto es un mensaje de ejemplo con el order {orderId}");
//Todo, change for a real one as this one is only to test the logger.
return Task.FromResult(new OrderDto(orderId, new OrderAddress("stree1", "postalCode"),
new PersonalDetails("name", "surname"), new List<ProductDto>()));
}
}
Como vemos podemos ver el mensaje tanto en la consola como en Graylog:
En el futuro veremos más en detalle tanto serilog como graylog.
Conclusión
- En este post hemos visto que es el logging dentro de la programación.
- Cómo funciona la librería de serilog
- Que es Graylog y como configurarlo junto a serilog.