
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 , 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 - 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
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.

Interfaces

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 , 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.

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

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:
Fonte: habr.com
