Logging in een microservices-omgeving met .Net in de praktijk

Logging in een microservices-omgeving met .Net in de praktijk

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 ability to pass correlation context in HTTP headers, 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 Logz.IO — 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 Serilog.Sinks.Logzio

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.

Logging in een microservices-omgeving met .Net in de praktijk

Interfaces

Logging in een microservices-omgeving met .Net in de praktijk

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

Logging in een microservices-omgeving met .Net in de praktijk

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

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. Serilog.Enrichers.MassTransitMessage. 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.

Logging in een microservices-omgeving met .Net in de praktijk

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: github.com/a-postx/YA.ServiceTemplate

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers šŸ”„ Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster