Fazendo login em um ambiente de microsserviço .Net na prática

Fazendo login em um ambiente de microsserviço .Net na prática

O log é uma ferramenta muito importante para um desenvolvedor, mas ao construir sistemas distribuídos, ele se torna uma pedra que precisa ser colocada na base do seu aplicativo, caso contrário, a complexidade do desenvolvimento de microsserviços se fará sentir muito rapidamente.

.Net Core 3 adicionou um ótimo capacidade de passar contexto de correlação em cabeçalhos HTTP, portanto, se seus aplicativos usarem chamadas HTTP diretas para comunicação entre serviços, você poderá aproveitar essa funcionalidade in a box. No entanto, se a arquitetura do seu back-end envolve interação por meio de um corretor de mensagens (RabbitMQ, Kafka, etc.), você ainda precisa cuidar do tópico de passar o contexto de correlação por meio dessas mensagens.

Neste artigo, pegaremos um aplicativo de API da web simples e organizaremos o registro, que irá

  • mantenha a correlação ponta a ponta entre logs de serviços independentes para que você possa ver facilmente todas as atividades causadas por uma solicitação específica do cliente

  • ter um único ponto de entrada com análise conveniente para que até mesmo o Suporte possa usar a ferramenta de registro, para a qual perguntas como “Recebi um erro com tal e tal ID de solicitação” apareceram no aplicativo

Primeiro, precisamos decidir sobre o provedor de registro em nosso aplicativo. O principal requisito para a exploração madeireira moderna é a estrutura, ou seja, deveríamos trabalhar não com mensagens de texto simples, mas com objetos. Graças a esses registros, podemos facilmente criar visualizações de nossas mensagens em diferentes seções e realizar análises.

Para nossa aplicação, usaremos o pacote Serilog (Serilog), que possui excelente suporte para registro estrutural e um rico sistema complementar. Omitirei as etapas básicas para configurá-lo (você pode encontrar um grande número de artigos sobre este tópico) e presumirei que

  • O serilog já está configurado e é o criador de logs padrão do seu provedor de injeção de dependência

  • O enriquecimento de mensagens com propriedades de contexto está habilitado em sua configuração (Enrich.FromLogContext)

A próxima etapa é escolher para qual sistema de registro centralizado enviar mensagens do Serilog. Talvez a opção de código aberto mais comum hoje seja a pilha ELK (Elasticsearch, Logstash e Kibana), e vamos lá. Para isso, utilizamos a proposta de Logz.IO - após se cadastrar em um plano gratuito, todo o poder do mecanismo de busca Lucene está em nossas mãos.

Resta-nos adicionar um pacote ao nosso projeto Serilog.Sinks.Logzio

Install-Package Serilog.Sinks.Logzio

E adicione o Enricher apropriado à nossa configuração do logger, alimentando-o com um token de acesso

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

Ao executar a aplicação poderemos observar nossas mensagens não só no console, mas também no Kibane.

Fazendo login em um ambiente de microsserviço .Net na prática

Interfaces

Fazendo login em um ambiente de microsserviço .Net na prática

Numa aplicação do tipo serviço, podem ser distinguidas duas interfaces principais de sua interação com o mundo exterior, iremos denotá-las como verticais e horizontais. Uma interface vertical é uma API da web por meio da qual chegam chamadas de um aplicativo cliente. Horizontal é um corretor de mensagens usado para trocar dados com outros serviços internos.

Consideremos os estágios de implementação da correlação em cada uma dessas interfaces.

Correlação em solicitações HTTP

Para obter o máximo de informações possível, precisamos gerar um ID de correlação o mais próximo possível do início da atividade, ou seja, no gateway ou diretamente no cliente (mobile ou web). Como hoje estamos lidando com uma aplicação back-end, simplesmente indicaremos nela a exigência do cabeçalho “X-Correlation-ID” obrigatório em todas as solicitações à API web.

Adicionando um pacote ID de correlação, cuja função é obter o valor do cabeçalho que precisamos

Install-Package CorrelationID

Adicione-o ao pipeline de processamento de solicitações

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

Agora vamos fazer um filtro de ação simples com ele:

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

E adicione-o ao controlador

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

}

Como resultado, o controlador exibirá 400 solicitações incorretas para todas as solicitações sem cabeçalho com o identificador correspondente.

Após começarmos a receber um identificador do cliente, devemos adicioná-lo ao contexto de logging, faremos uma camada de enquadramento para isso:

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

Em nossa aplicação, usamos o ILogger padrão do pacote Microsoft.Extensions.Logging.Abstractions, portanto adicionaremos um valor usando uma extensão simples a ele.

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

Adicionamos uma camada ao pipeline de processamento de solicitações e obtemos o resultado desejado.

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

Agora, todas as atividades geradas por solicitações à nossa API web contêm um identificador de correlação pelo qual podem ser facilmente vinculadas.

Fazendo login em um ambiente de microsserviço .Net na prática

Correlação em mensagens do corretor

A próxima etapa é configurar a transmissão e recepção do identificador de correlação através do agente de mensagens. Em nosso exemplo usaremos RabbitMQ, e como cliente usaremos o framework MassTransit (MassTranzit). Novamente, vamos pular a configuração inicial para trabalhar com o MassTransit e ir direto para a configuração do registro.

Para começar podemos habilitar os logs do próprio MassTransit, para isso adicionaremos um pacote à nossa aplicação MassTransit.SerilogIntegration

Install-Package MassTransit.SerilogIntegration

Agora, depois de adicionar o logger às configurações do MassTransit, poderemos ver os logs da estrutura.

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

Deixe nosso aplicativo enviar o evento SomethingDoneMessage com o valor “done” como resposta à solicitação POST. O contrato de tal mensagem pode ser descrito da seguinte forma:

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

As mensagens do MassTransit são essencialmente um envelope contendo mensagens do corretor. O envelope fica assim:

{
  "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"
  }
}

A mensagem mostra os campos de serviço necessários para o funcionamento da estrutura, mas temos a capacidade de adicionar nossas próprias propriedades adicionais a esse envelope. Além disso, MassTransit possui ferramentas integradas para trabalhar com alguns campos opcionais, o mais interessante dos quais nos interessa é o CorrelationId.

Adicione a interface CorrelatedBy ao contrato de mensagem:

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

Vamos implementá-lo e atribuir um valor à propriedade CorrelationId ao criar uma mensagem:

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

Se olharmos para a mensagem atualizada, veremos que o identificador de correlação se tornou não apenas parte de nossa mensagem, mas também parte do envelope - esse identificador agora também será usado em todos os logs do MassTransit, o que significa que será muito mais fácil para lidarmos com problemas no nível do corretor de mensagens.

{
  "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"
  }
}

Resta-nos configurar o log dessas propriedades do serviço da mensagem, para isso adicionaremos um pacote ao projeto Serilog.Enrichers.MassTransitMessage. O pacote adiciona um filtro ao pipeline de processamento de mensagens do MassTransit que envia o contexto da mensagem para uma pilha thread-safe. O serilog lê o contexto da pilha e adiciona essas propriedades adicionais aos nossos objetos de log.

Install-Package Serilog.Enrichers.MassTransitMessage

Insira um filtro no MassTransit

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

E na configuração do Serilog, adicione um Enricher

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

Como o aplicativo que recebe a mensagem da fila RabbitMQ tem acesso a todas as propriedades do envelope MassTransit, podemos usar o identificador de correlação recebido dentro do aplicativo consumidor e também passá-lo adiante na cadeia de chamadas.

Como resultado, nossos logs passaram a conter CorrelationId não apenas dentro do mesmo serviço, mas também ao interagir com outros aplicativos.

Fazendo login em um ambiente de microsserviço .Net na prática

Portanto, o sistema de log resultante em aplicativos .Net nos permite correlacionar logs de microsserviços completamente diferentes sem problemas - mesmo aqueles que funcionam por meio de um corretor de mensagens. E com a ajuda do Elasticsearch, podemos analisar logs de forma rápida e conveniente, construindo os painéis que precisamos no Kibana (um exemplo é mostrado na imagem do post).

É claro que o login neste formulário não cobrirá opções complexas para a interação de seus serviços e vários sistemas externos, mas estabelecer tal ordem logo no início do desenvolvimento do projeto é uma daquelas coisas pelas quais você se agradecerá mais de uma vez.

Você pode entender o código-fonte do sistema resultante no projeto: github.com/a-postx/YA.ServiceTemplate

Fonte: habr.com

Compre hospedagem confiável para sites com proteção DDoS, servidores VPS VDS 🔥 Compre hospedagem de sites confiável com proteção contra DDoS, servidores VPS/VDS | ProHoster