Logning i en mikrotjeneste-omgivelse med .Net i praksis

Logning i en mikrotjeneste-omgivelse med .Net i praksis

Logging er et meget vigtigt værktøj for udvikleren, men når man skaber distribuerede systemer, bliver det en grundsten, der skal lægges direkte i fundamentet af din applikation; ellers vil kompleksiteten ved udviklingen af mikrotjenester hurtigt give sig til kende.

I .Net Core 3 er der tilføjet en fremragende mulighed for at overføre korrelationskontext i HTTP-overskrifter, så hvis dine applikationer bruger direkte HTTP-anrop til inter-service kommunikation, kan du drage fordel af denne indbyggede funktionalitet. Men hvis arkitekturen af din backend indebærer interaktion via en meddelelsesbroker (RabbitMQ, Kafka osv.), skal du stadig selv sørge for at overføre den korrelationsmæssige kontekst gennem disse meddelelser.

I denne artikel vil vi tage en simpel web-API-applikation og organisere logging, som vil

  • bevare transaktionskorrelationen mellem logs fra uafhængige tjenester, så det bliver let at se alle aktiviteter, der er blevet udløst af en bestemt forespørgsel fra klienten.

  • have et enkelt indgangspunkt med en nem analyse, så loggingværktøjet kan bruges selv af support, der får spørgsmål som "jeg har fået en fejl i appen med denne anmodnings-ID".

For det første skal vi beslutte os for en logging-leverandør i vores applikation. Hovedkravet til moderne logging er struktur, dvs. vi skal arbejde med objekter, ikke flade tekstbeskeder. Med sådanne logs kan vi nemt konstruere præsentationer af vores beskeder i forskellige perspektiver og udføre analyser.

Til vores applikation vil vi bruge Serilog-pakken, som har fremragende støtte til strukturel logging og et rigt system af udvidelser. Jeg vil springe de grundlæggende trin i opsætningen over (du kan finde et væld af artikler om emnet) og antage at

  • Serilog allerede er konfigureret og er loggeren som standard i din DI-leverandør.

  • i dens konfiguration er berigelse af meddelelser med kontekst-egenskaber (Enrich.FromLogContext) inkluderet.

Det næste skridt er at vælge, hvilken centraliseret logindsamlingssystem vi skal sende beskeder fra Serilog til. Den mest udbredte open-source løsning i dag er ELK-stakken (Elasticsearch, Logstash og Kibana), som vi vil benytte. Til dette formål vil vi bruge tilbuddet fra Logz.IO — efter tilmelding til den gratis plan har vi al kraften fra søgemaskinen Lucene i vores hænder.

Vi skal tilføje pakken Serilog.Sinks.Logzio

Install-Package Serilog.Sinks.Logzio

Og tilføje den relevante enricher til konfigurationen af vores logger ved at give den adgangstokenet

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

Når vi kører applikationen, vil vi se vores beskeder ikke kun i konsollen, men også i Kibana.

Logning i en mikrotjeneste-omgivelse med .Net i praksis

Grænseflader

Logning i en mikrotjeneste-omgivelse med .Net i praksis

I en servicebaseret applikation kan vi identificere to hovedinterfacer, som skaber interaktion med omverdenen; lad os betegne dem som vertikal og horisontal. Den vertikale grænseflade er web-API'en, hvorigennem kald kommer fra klientapplikationen. Den horisontale er meddelelsesbrokeren, der bruges til datadeling med andre interne services.

Lad os gennemgå trin til implementering af korrelation på hver af disse interfaces.

Korrelation i HTTP-anmodninger

For at indsamle så meget information som muligt skal vi generere en korrelationsidentifikator så tidligt som muligt i aktiviteten, dvs. ved gatewayen eller direkte på klienten (mobil eller web). Da vi i dag arbejder med en backend-applikation, vil vi blot angive krav om den obligatoriske header «X-Correlation-ID» i alle anmodninger til web-API'en.

Lad os tilføje pakken CorrelationID, hvis funktion er at hente værdien fra den header, vi har brug for.

Install-Package CorrelationID

Lad os tilføje den i forespørgselsbehandlingspipen

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

Nu laver vi en simpel action-filter ved hjælp af den:

offentlig sealed class ApiRequestFilter : ActionFilterAttribute
{
    offentlig ApiRequestFilter(IApiRequestTracker apiRequestTracker, ICorrelationContextAccessor correlationContextAccessor)
    {
        _correlationContextAccessor = correlationContextAccessor ?? throw new ArgumentNullException(nameof(correlationContextAccessor));
    }
    
    private readonly ICorrelationContextAccessor _correlationContextAccessor;
    
    offentlig 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();
    }
    
    offentlig override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        await next.Invoke();
    }
}

Og vi tilføjer den til controlleren

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

}

Som et resultat vil controlleren returnere 400 Bad request på alle anmodninger uden header med det relevante identifikationsnummer.

Når vi begyndte at modtage identifikationen fra klienten, skal vi tilføje den til logningskonteksten; vi laver derfor et indpakningslag:

offentlig class CorrelationIdContextLogger
{
    offentlig CorrelationIdContextLogger(RequestDelegate next)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
    }
    
    readonly RequestDelegate _next;
    
    offentlig 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(httpContext);
            }
        }
        else
        {
            await _next(httpContext);
        }
    }
}

I vores applikation bruger vi den standard ILogger fra pakken Microsoft.Extensions.Logging.Abstractions, så værdien vil blive tilføjet ved hjælp af en simpel udvidelse til det.

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

Vi tilføjer et lag i forespørgselsbehandlingspipeline og får det ønskede resultat.

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

Nu indeholder alle aktiviteter, der stammer fra anmodninger til vores web-API, en korrelationsidentifikation, som de nemt kan knyttes til.

Logning i en mikrotjeneste-omgivelse med .Net i praksis

Korrelation i meddelelserne fra brokeren

Det næste skridt er at etablere overførsel og modtagelse af korrelationsidentifikatoren via en meddelelsesbroker. I vores eksempel vil vi bruge RabbitMQ, og som klient vil vi tage MassTransit-frameworket. Igen vil vi springe den indledende opsætning af MassTransit over og gå direkte til opsætning af logføring.

For at starte kan vi aktivere loggene for selve MassTransit ved at tilføje pakken til vores applikation. MassTransit.SerilogIntegration

Install-Package MassTransit.SerilogIntegration

Nu, efter at have tilføjet loggeren til MassTransit-indstillingerne, vil vi kunne se loggene fra frameworket.

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

Lad vores applikation som reaktion på en POST-anmodning sende en begivenhed SomethingDoneMessage med værdien "done". Kontrakten for en sådan meddelelse kan beskrives som følger:

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

Meddelelserne i MassTransit fungerer i bund og grund som en konvolut, hvori meddelelser fra brokeren er indlejret. Konvolutten ser cirka sådan ud:

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

I meddelelsen ses tjenestefelterne, der er nødvendige for selve frameworkets funktion, men vi har mulighed for at tilføje vores egne yderligere egenskaber til denne konvolut. Desuden har MassTransit indbyggede værktøjer til håndtering af visse valgfri felter, hvoraf den mest interessante for os er korrelationsidentifikatoren CorrelationId.

Lad os tilføje CorrelatedBy-interface til meddelelseskontrakten:

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

Vi implementerer det og vil tildele værdien til CorrelationId-egenskaben ved oprettelse af meddelelsen:

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

Når vi ser på den opdaterede meddelelse, kan vi se, at korrelations-id'et ikke kun er blevet en del af vores meddelelse, men også en del af konvolutten — dette id vil nu også blive brugt i alle MassTransit-logs, hvilket gør det meget lettere for os at tackle problemer på meddelelsesbroker-niveau.

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

Vi skal nu konfigurere logningen af disse meddelelsesegenskaber, for det tilføjer vi en pakke til projektet Serilog.Enrichers.MassTransitMessage. Pakken tilføjer et filter til MassTransit-meddelelsesbehandlingsrøret, som stakker meddelelses-konteksten i en tråd-sikker stak. Serilog læser konteksten fra stakken og tilføjer disse ekstra egenskaber til vores logobjekter.

Install-Package Serilog.Enrichers.MassTransitMessage

I MassTransit indsætter vi filteret

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

Og i Serilog-konfigurationen tilføjer vi enricheren

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

Da applikationen, der modtager meddelelsen fra RabbitMQ-køen, har adgang til alle egenskaberne i MassTransit-konvolutten, kan vi bruge det modtagne korrelations-id inde i forbrugerapplikationen samt videregive det gennem hele kaldkæden.

Som følge heraf indeholder vores logs CorrelationId ikke kun inden for én service, men også ved interaktion med andre applikationer.

Logning i en mikrotjeneste-omgivelse med .Net i praksis

Således gør det opnåede logningssystem i .Net-applikationer det muligt for os at uden problemer korrelere logs fra helt forskellige mikrotjenester — selv dem, der arbejder gennem en meddelelsesbroker. Og med Elasticsearch kan vi hurtigt og bekvemt analysere logs ved at opbygge de ønskede dashboards i Kibana (eksemplet er vist på billedet i indlægget).

Selvfølgelig, i denne form vil logningen ikke dække komplekse interaktionsformer mellem dine tjenester og forskellige eksterne systemer, men at indføre en sådan orden i starten af projektets udvikling er én af de ting, du vil takke dig selv for senere.

Du kan dykke ned i kildekoden til det resulterende system i projektet: github.com/a-postx/YA.ServiceTemplate

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster