
Logging is a very important tool for developers, but when creating distributed systems, it becomes a cornerstone that must be laid right into the foundation of your application; otherwise, the complexity of developing microservices will quickly become apparent.
In .Net Core 3, a great , so if your applications use direct HTTP calls for inter-service communication, you can take advantage of this out-of-the-box functionality. However, if your backend architecture involves interaction through a message broker (RabbitMQ, Kafka, etc.), you still need to address the topic of transmitting correlation context through these messages yourself.
In this article, we will take a simple web API application and organize logging that will
maintain end-to-end correlation between the logs of independent services, making it easy to view all activities triggered by a specific client request.
have a single entry point with convenient analysis, so that the logging tool can be used even by Support, who receives questions like, 'I encountered an error in my application with this request ID.'
First, we need to determine the logging provider in our application. The main requirement for modern logging is structure, meaning we should work not with flat text messages but with objects. With such logs, we can easily build views of our messages in different slices and perform analytics.
For our application, we will use the Serilog package, which has excellent support for structured logging and a rich extension system. I will skip the basic setup steps (you can find a lot of articles on this topic) and assume that
Serilog is already configured and is the default logger in your dependency injection provider
with its configuration enabling the enrichment of messages with context properties (Enrich.FromLogContext).
De volgende stap is om te kiezen naar welk gecentraliseerd logsysteem we de berichten van Serilog willen sturen. Misschien is de meest voorkomende optie in open-source software op dit moment de ELK-stack (Elasticsearch, Logstash en Kibana), en die nemen we. Hiervoor maken we gebruik van het aanbod van ā na registratie op het gratis tarief hebben we de volledige kracht van de zoekmachine Lucene in onze handen.
We moeten het pakket toevoegen aan ons project
Install-Package Serilog.Sinks.Logzio
En de bijbehorende enrichers aan de configuratie van onze logger toevoegen, door hem de toegangstoken te geven
LoggerConfiguration loggerConfig = new LoggerConfiguration();
loggerConfig.WriteTo.Logzio(secrets.LogzioToken, 10, TimeSpan.FromSeconds(10), null, LogEventLevel.Debug);
Wanneer we de applicatie starten, kunnen we onze berichten niet alleen in de console zien, maar ook in Kibana.

Interfaces

In een servicegerichte applicatie kunnen we twee hoofdinterfaces van de interactie met de buitenwereld onderscheiden, die we de verticale en horizontale interface noemen. De verticale interface is de web-API, waarmee verzoeken van de clientapplicatie binnenkomen. De horizontale interface is de berichtenbroker, die wordt gebruikt voor het uitwisselen van gegevens met andere interne diensten.
Laten we de stappen van implementatie van correlatie op elk van deze interfaces bekijken.
Correlatie in HTTP-verzoeken
Om zo veel mogelijk informatie te verkrijgen, moeten we een correlatie-ID genereren zo dichtbij het begin van de activiteit als mogelijk, dat wil zeggen op de gateway of direct op de client (mobiel of web). Aangezien we vandaag de dag met een backend-applicatie werken, stellen we eenvoudigweg de verplichting voor van de header 'X-Correlation-ID' in alle verzoeken naar de web-API.
Voeg het pakket toe , wiens functie bestaat uit het ophalen van de waarde van de header die we nodig hebben
Install-Package CorrelationID
Laten we het toevoegen aan de aanvraagverwerkingspipeline
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application
.UseCorrelationId(new CorrelationIdOptions
{
Header = "X-Correlation-ID",
IncludeInResponse = false,
UpdateTraceIdentifier = false,
UseGuidForCorrelationId = false
});
}
}
Laten we nu met behulp daarvan een eenvoudige action-filter maken:
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();
}
}
En we voegen deze toe aan de controller
[Route("[controller]")]
[ApiController]
[ServiceFilter(typeof(ApiRequestFilter))]
public class CarsController : ControllerBase
{
}
Als resultaat zal de controller 400 Bad Request teruggeven voor alle verzoeken zonder de bijbehorende identificatiekop.
Nadat we de identificatie van de client hebben ontvangen, moeten we deze toevoegen aan de logcontext. Laten we hiervoor een omhulsel maken:
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);
}
}
}
In onze applicatie gebruiken we de standaard ILogger uit het Microsoft.Extensions.Logging.Abstractions pakket, dus we zullen de waarde toevoegen met behulp van een eenvoudige uitbreiding ervan.
public static IDisposable BeginScopeWith(this ILogger logger, params (string key, object value)[] keys)
{
return logger.BeginScope(keys.ToDictionary(x => x.key, x => x.value));
}
We voegen de omhulsel toe aan de verzoekverwerkingspijplijn en verkrijgen het gewenste resultaat.
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application.UseMiddleware();
}
}
Nu bevatten alle activiteiten die voortkomen uit verzoeken naar onze web-API de correlatie-identificatie waarmee ze gemakkelijk kunnen worden gekoppeld.

Correlatie in de berichtenbroker
De volgende stap is om de overdracht en ontvangst van de correlatie-id via een berichtenbroker in te stellen. In ons voorbeeld gebruiken we RabbitMQ en als client maken we gebruik van het MassTransit-framework. Laten we de initiƫle configuratie van MassTransit overslaan en meteen doorgaan naar de logging-instellingen.
Om te beginnen kunnen we de logging van MassTransit inschakelen; hiervoor voegen we het pakket toe aan onze applicatie.
Install-Package MassTransit.SerilogIntegration
Nu, na het toevoegen van de logger aan de MassTransit-instellingen, kunnen we de logs van het framework bekijken.
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
});
});
Laat onze applicatie als reactie op een POST-verzoek een SomethingDoneMessage-evenement versturen met de waarde "done". Het contract voor zo'n bericht kan als volgt worden beschreven:
namespace MbMessages
{
public interface ISomethingDoneMessageV1
{
string Value { get; }
}
}
Berichten van MassTransit zijn feitelijk een envelop waarin de berichten van de broker zijn verpakt. De envelop ziet er ongeveer zo uit:
{
"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"
}
}
In het bericht zijn de systeemvelden zichtbaar die nodig zijn voor de werking van het framework zelf, maar we hebben de mogelijkheid om ook onze eigen extra eigenschappen aan deze envelop toe te voegen. Bovendien heeft MassTransit ingebouwde middelen voor het omgaan met enkele optionele velden, waarvan we het meest geĆÆnteresseerd zijn in de correlatie-id CorrelationId.
Voeg de interface CorrelatedBy toe aan het berichtcontract:
namespace MbMessages
{
public interface ISomethingDoneMessageV1 : CorrelatedBy
{
string Value { get; }
}
}
Laten we dit implementeren en de waarde toewijzen aan de eigenschap CorrelationId bij het aanmaken van het bericht:
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; }
}
Als we naar het bijgewerkte bericht kijken, zien we dat de correlatie-ID niet alleen deel uitmaakt van ons bericht, maar ook van de envelop ā deze ID wordt nu ook in alle logs van MassTransit gebruikt, wat betekent dat het veel gemakkelijker voor ons zal zijn om problemen op het niveau van de berichtenbroker op te lossen.
{
"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": "Hallo"
}
}
We moeten nu de logging van deze systeemkenmerken van het bericht instellen, daarvoor voegen we het pakket toe aan het project. . Het pakket voegt een filter toe aan de berichtverwerkingspijplijn van MassTransit, die de context van het bericht in een thread-veilige stack plaatst. Serilog leest de context uit de stack en voegt deze extra eigenschappen toe aan onze logobjecten.
Install-Package Serilog.Enrichers.MassTransitMessage
In MassTransit voegen we de filter toe.
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
cfg.UseSerilogMessagePropertiesEnricher();
});
});
En in de configuratie van Serilog voegen we de enricher toe.
Log.Logger = new LoggerConfiguration()
.Enrich.FromMassTransitMessage()
.CreateLogger();
Aangezien de applicatie die een bericht uit de RabbitMQ-queue ontvangt toegang heeft tot alle eigenschappen van de MassTransit-envelop, kunnen we de verkregen correlatie-ID binnen de consumentenapplicatie gebruiken en deze ook verder doorgeven door de hele aanroepketen.
Als gevolg hiervan bevatten onze logs de CorrelationId niet alleen binnen ƩƩn service, maar ook tijdens interactie met andere applicaties.

Dus, het verkregen logging-systeem in .Net-applicaties stelt ons in staat om logs van totaal verschillende microservices zonder veel problemen te correleren ā zelfs die welke via een berichtenbroker werken. Met Elasticsearch kunnen we snel en gemakkelijk loganalyses uitvoeren en de benodigde dashboards in Kibana opstellen (een voorbeeld is weergegeven op de afbeelding bij de post).
Natuurlijk dekt logging in deze vorm niet de complexe interacties tussen uw services en verschillende externe systemen, maar het aanbrengen van zo'n ordening in het begin van de projectontwikkeling is iets waar u uzelf vaak dankbaar voor zult zijn.
U kunt de broncode van het resulterende systeem bekijken in het project:
Bron: habr.com
