Registro en un entorno de microservicios .Net en la práctica

Registro en un entorno de microservicios .Net en la práctica

La registración es una herramienta muy importante para los desarrolladores, pero al crear sistemas distribuidos, se convierte en una piedra angular que debe ser incrustada directamente en los cimientos de su aplicación; de lo contrario, la complejidad del desarrollo de microservicios se manifestará rápidamente.

En .Net Core 3 se ha añadido una excelente capacidad para transmitir el contexto de correlación en los encabezados HTTP, por lo que si sus aplicaciones utilizan llamadas HTTP directas para la interacción entre servicios, puede aprovechar esta funcionalidad lista para usar. Sin embargo, si la arquitectura de su backend implica interacción a través de un broker de mensajes (RabbitMQ, Kafka, etc.), todavía deberá preocuparse por la transmisión del contexto de correlación a través de estos mensajes por su cuenta.

En este artículo, tomaremos una simple aplicación web API y organizaremos un registro que

  • mantendrá la correlación entre los logs de servicios independientes para que se puedan ver fácilmente todas las actividades provocadas por una solicitud específica desde el cliente.

  • Tener un único punto de entrada con un análisis conveniente para que la herramienta de registro pueda ser utilizada incluso por Soporte, que recibe preguntas como «Tuve un error en mi aplicación con el ID de solicitud tal».

Primero, debemos decidir sobre el proveedor de registro en nuestra aplicación. El requisito principal para el registro moderno es la estructura, es decir, debemos trabajar no con mensajes de texto planos, sino con objetos. Gracias a tales registros, podemos construir fácilmente vistas de nuestros mensajes desde diferentes perspectivas y realizar análisis.

Para nuestra aplicación, utilizaremos el paquete Serilog, que tiene un excelente soporte para el registro estructurado y un rico sistema de extensiones. Omitiré las etapas básicas de su configuración (puede encontrar una gran cantidad de artículos sobre este tema) y asumiré que

  • Serilog ya está configurado y es el registrador por defecto de su proveedor de inyección de dependencias

  • en su configuración se incluye el enriquecimiento de mensajes con propiedades del contexto (Enrich.FromLogContext)

El siguiente paso es elegir a qué sistema de recolección centralizada de logs enviar los mensajes desde Serilog. Tal vez, la opción más común hoy en día entre el software de código abierto sea la pila ELK (Elasticsearch, Logstash y Kibana), así que la utilizaremos. Para ello, aprovecharemos la oferta de Logz.IO — tras registrarnos en el plan gratuito, tendremos en nuestras manos todo el poder del motor de búsqueda Lucene.

Solo queda agregar al nuestro proyecto el paquete Serilog.Sinks.Logzio

Install-Package Serilog.Sinks.Logzio

Y agregar el enriquecedor correspondiente en la configuración de nuestro logger, proporcionando el token de acceso

LoggerConfiguration loggerConfig = new LoggerConfiguration();
loggerConfig.WriteTo.Logzio(secrets.LogzioToken, 10, TimeSpan.FromSeconds(10), null, LogEventLevel.Debug);

Al iniciar la aplicación, podremos ver nuestros mensajes no solo en la consola, sino también en Kibana.

Registro en un entorno de microservicios .Net en la práctica

Interfaces

Registro en un entorno de microservicios .Net en la práctica

En una aplicación tipo servicio, podemos destacar dos interfaces principales de su interacción con el mundo exterior, que llamaremos vertical y horizontal. La interfaz vertical es la API web, a través de la cual llegan las llamadas desde la aplicación cliente. La horizontal es el corredor de mensajes, que se utiliza para intercambiar datos con otros servicios internos.

Examinemos las etapas de implementación de la correlación en cada una de estas interfaces.

Correlación en solicitudes HTTP

Para obtener la mayor cantidad de información posible, necesitamos generar un identificador de correlación lo más cerca posible del inicio de la actividad, es decir, en el gateway o directamente en el cliente (móvil o web). Dado que hoy tratamos con una aplicación backend, simplemente señalaremos la necesidad del encabezado obligatorio "X-Correlation-ID" en todas las solicitudes a la API web.

Agregamos el paquete CorrelationID, cuya función es obtener el valor del encabezado que necesitamos

Install-Package CorrelationID

Lo añadiremos al pipeline de procesamiento de solicitudes

public class Startup
{
    public void Configure(IApplicationBuilder application)
    {
        application
	    .UseCorrelationId(new CorrelationIdOptions
        {
            Header = "X-Correlation-ID",
            IncludeInResponse = false,
            UpdateTraceIdentifier = false,
            UseGuidForCorrelationId = false
        });
    }
}

Ahora, con su ayuda, haremos un filtro de acción simple:

clase pública sellada ApiRequestFilter : ActionFilterAttribute
{
    public ApiRequestFilter(IApiRequestTracker apiRequestTracker, ICorrelationContextAccessor correlationContextAccessor)
    {
        _correlationContextAccessor = correlationContextAccessor ?? throw new ArgumentNullException(nameof(correlationContextAccessor));
    }
    
    private readonly ICorrelationContextAccessor _correlationContextAccessor;
    
    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        if (!Guid.TryParse(_correlationContextAccessor.CorrelationContext.CorrelationId, out Guid correlationId))
        {
            context.Result = new BadRequestResult();
            return;
        }
    
        await next.Invoke();
    }
    
    public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        await next.Invoke();
    }
}

Y lo agregaremos al controlador

[Route("[controller]")]
[ApiController]
[ServiceFilter(typeof(ApiRequestFilter))]
public class CarsController : ControllerBase
{

}

Como resultado, el controlador devolverá un 400 Bad Request para todas las solicitudes sin el encabezado correspondiente con el identificador.

Después de recibir el identificador del cliente, debemos agregarlo al contexto de registro; para ello, crearemos una capa envolvente:

public class CorrelationIdContextLogger
{
    public CorrelationIdContextLogger(RequestDelegate next)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
    }
    
    readonly RequestDelegate _next;
    
    public async Task InvokeAsync(HttpContext httpContext, ILogger logger, ICorrelationContextAccessor correlationContextAccessor)
    {
        if (Guid.TryParse(correlationContextAccessor.CorrelationContext.CorrelationId, out Guid correlationId))
        {
            using (logger.BeginScopeWith(("CorrelationId", correlationId)))
            {
                await _next(context);
            }
        }
        else
        {
            await _next(context);
        }
    }
}

En nuestra aplicación utilizamos el ILogger estándar del paquete Microsoft.Extensions.Logging.Abstractions, por lo que agregaremos el valor mediante una sencilla extensión.

public static IDisposable BeginScopeWith(this ILogger logger, params (string key, object value)[] keys)
{
    return logger.BeginScope(keys.ToDictionary(x => x.key, x => x.value));
}

Agregamos la capa al pipeline de procesamiento de solicitudes y obtenemos el resultado deseado.

public class Startup
{
    public void Configure(IApplicationBuilder application)
    {
        application.UseMiddleware();
    }
}

Ahora todas las actividades generadas por las solicitudes a nuestra API web contienen el identificador de correlación que permite vincularlas fácilmente.

Registro en un entorno de microservicios .Net en la práctica

Correlación en los mensajes del corredor

El siguiente paso es establecer la transmisión y recepción del identificador de correlación a través de un broker de mensajes. En nuestro ejemplo, utilizaremos RabbitMQ y, como cliente, tomaremos el framework MassTransit. Nuevamente, omitiremos la configuración inicial del trabajo con MassTransit y pasaremos directamente a la configuración del registro.

Para empezar, podemos habilitar los registros del propio MassTransit, para ello agregaremos el paquete a nuestra aplicación. MassTransit.SerilogIntegration

Install-Package MassTransit.SerilogIntegration

Ahora, después de agregar el registrador a la configuración de MassTransit, podremos ver los registros del framework.

services
    .AddSingleton(provider =>
        {
            return Bus.Factory.CreateUsingRabbitMq(cfg =>
            {
                cfg.UseSerilog();
            });
        });

Hagamos que nuestra aplicación, como reacción a una solicitud POST, envíe un evento SomethingDoneMessage con el valor 'done'. El contrato de dicho mensaje se puede describir así:

namespace MbMessages
{
    public interface ISomethingDoneMessageV1
    {
        string Value { get; }
    }
}

Los mensajes de MassTransit son esencialmente un sobre, dentro del cual se encuentran los mensajes del broker. El sobre se ve aproximadamente así:

{
  "messageId": "59020000-5dba-0015-10b8-08d77ec28593",
  "requestId": "59020000-5dba-0015-5674-08d77ec28592",
  "conversationId": "59020000-5dba-0015-bca8-08d77ec28594",
  "destinationAddress": "rabbitmq://bear.rmq.cloudamqp.com/aelzlsta/ya.servicetemplate.receiveendpoint",
  "headers": {},
  "messageType": [
    "urn:message:MbMessages:ISomethingDoneMessageV1"
  ],
  "message": {
    "value": "done"
  }
}

En el mensaje se pueden ver los campos de servicio que son necesarios para el funcionamiento del framework, pero también tenemos la posibilidad de agregar nuestras propias propiedades adicionales a este sobre. Además, MassTransit tiene herramientas integradas para trabajar con algunos campos opcionales, de los cuales el que más nos interesa es el identificador de correlación CorrelationId.

Agreguemos a el contrato del mensaje la interfaz CorrelatedBy:

namespace MbMessages
{
    public interface ISomethingDoneMessageV1 : CorrelatedBy
    {
        string Value { get; }
    }
}

Lo implementamos y le asignaremos un valor a la propiedad CorrelationId al crear el mensaje:

internal class SomethingDoneMessageV1 : ISomethingDoneMessageV1
{
    internal SomethingDoneMessageV1(Guid correlationId, string value)
    {
        CorrelationId = correlationId;
        Value = value;
    }
    
    public Guid CorrelationId { get; private set; }
    public string Value { get; private set; }
}

Si miramos el mensaje actualizado, veremos que el identificador de correlación se ha convertido no solo en parte de nuestro mensaje, sino también en parte del sobre; este identificador ahora se utilizará también en todos los registros de MassTransit, lo que significa que será mucho más fácil resolver problemas a nivel del broker de mensajes.

{
  "messageId": "59020000-5dba-0015-10b8-08d77ec28593",
  "requestId": "59020000-5dba-0015-5674-08d77ec28592",
  "conversationId": "59020000-5dba-0015-bca8-08d77ec28594",
  "correlationId": "c7ff562a-b639-415b-9add-c9e524a727cc",
  "destinationAddress": "rabbitmq://bear.rmq.cloudamqp.com/aelzlsta/ya.servicetemplate.receiveendpoint",
  "headers": {},
  "messageType": [
    "urn:message:MbMessages:ISomethingDoneMessageV1"
  ],
  "message": {
    "correlationId": "c7ff562a-b639-415b-9add-c9e524a727cc",
    "value": "Hello"
  }
}

Nos queda configurar el registro de estas propiedades del mensaje; para esto, añadiremos el paquete al proyecto Serilog.Enrichers.MassTransitMessage. El paquete añade un filtro al pipeline de procesamiento de mensajes de MassTransit, que apila el contexto del mensaje en una pila segura para subprocesos. Serilog lee el contexto de la pila y añade estas propiedades adicionales a nuestros objetos de registro.

Install-Package Serilog.Enrichers.MassTransitMessage

En MassTransit insertamos el filtro

services
    .AddSingleton(provider =>
        {
            return Bus.Factory.CreateUsingRabbitMq(cfg =>
            {
                cfg.UseSerilog();
                cfg.UseSerilogMessagePropertiesEnricher();
            });
        });

Y en la configuración de Serilog añadimos el enriquecedor

Log.Logger = new LoggerConfiguration()
    .Enrich.FromMassTransitMessage()
    .CreateLogger();

Dado que la aplicación que recibe el mensaje de la cola RabbitMQ tiene acceso a todas las propiedades del sobre de MassTransit, podemos utilizar el identificador de correlación obtenido dentro de la aplicación consumidora, así como pasarlo a lo largo de toda la cadena de llamadas.

Como resultado, nuestros registros comenzaron a contener CorrelationId no solo dentro de un servicio, sino también al interactuar con otras aplicaciones.

Registro en un entorno de microservicios .Net en la práctica

Así, el sistema de registro obtenido en aplicaciones .Net nos permite correlacionar registros de microservicios completamente diferentes sin problemas, incluso aquellos que funcionan a través de un broker de mensajes. Y con Elasticsearch podemos analizar rápidamente los registros, creando en Kibana los dashboards que necesitamos (un ejemplo se muestra en la imagen del post).

Por supuesto, en esta forma, el registro no cubre los casos complejos de interacción entre sus servicios y diferentes sistemas externos, pero establecer este tipo de orden desde el inicio del desarrollo del proyecto es una de esas cosas por las que se lo agradecerán a ustedes mismos en múltiples ocasiones.

Para entender el código fuente del sistema resultante, pueden consultar el proyecto: github.com/a-postx/YA.ServiceTemplate

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster