Il logging in un ambiente microservizi .Net nella pratica

Il logging in un ambiente microservizi .Net nella pratica

Il logging è uno strumento molto importante per lo sviluppatore, ma quando si creano sistemi distribuiti diventa una pietra miliare da integrare direttamente nelle fondamenta della tua applicazione, altrimenti la complessità dello sviluppo di microservizi si farà sentire rapidamente.

In .Net Core 3 è stata aggiunta una fantastica funzionalità per la trasmissione del contesto di correlazione negli header HTTP, quindi se le tue applicazioni utilizzano chiamate HTTP dirette per l'interazione tra servizi, puoi avvalerti di questa funzionalità out-of-the-box. Tuttavia, se l'architettura del tuo backend prevede l'interazione tramite un broker di messaggi (RabbitMQ, Kafka, ecc.), dovrai ancora preoccuparti del passaggio del contesto di correlazione attraverso questi messaggi autonomamente.

In questo articolo prenderemo una semplice applicazione web API e organizzeremo il logging in modo che

  • mantenga una correlazione continua tra i log di servizi indipendenti, in modo da poter facilmente visualizzare tutte le attività che sono state attivate da una richiesta specifica del client

  • avere un'unica punto di accesso con un'analisi comoda, affinché lo strumento di logging possa essere utilizzato anche dal supporto, a cui arrivano domande come «Ho un errore nel mio applicativo con questo identificativo della richiesta»

Innanzitutto, dobbiamo decidere quale fornitore di logging utilizzare nella nostra applicazione. Il requisito principale per un logging moderno è la strutturalità, cioè dobbiamo lavorare non con messaggi di testo piatti, ma con oggetti. Grazie a log di questo tipo possiamo facilmente costruire rappresentazioni dei nostri messaggi in vari tagli e condurre analisi.

Per la nostra applicazione utilizzeremo il pacchetto Serilog, che ha un'ottima supporto per il logging strutturato e un ricco sistema di estensioni. Salterò le fasi di base della sua configurazione (puoi trovare un'ampia quantità di articoli al riguardo) e farò l'assunzione che

  • Serilog sia già configurato ed è il logger predefinito del tuo fornitore di iniezione delle dipendenze

  • nella sua configurazione è incluso l'arricchimento dei messaggi con le proprietà del contesto (Enrich.FromLogContext)

Il passaggio successivo è scegliere in quale sistema di raccolta centralizzata dei log inviare i messaggi da Serilog. Forse la soluzione più diffusa oggi tra il software open source è il stack ELK (Elasticsearch, Logstash e Kibana), e useremo proprio quello. A questo scopo, utilizzeremo l'offerta di Logz.IO — dopo la registrazione al piano gratuito, abbiamo a disposizione tutta la potenza del motore di ricerca Lucene.

Dobbiamo quindi aggiungere al nostro progetto il pacchetto Serilog.Sinks.Logzio

Install-Package Serilog.Sinks.Logzio

E aggiungere l'enricher corrispondente alla configurazione del nostro logger, fornendogli il token di accesso

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

Avviando l'applicazione, saremo in grado di vedere i nostri messaggi non solo nella console, ma anche in Kibana.

Il logging in un ambiente microservizi .Net nella pratica

Interfacce

Il logging in un ambiente microservizi .Net nella pratica

In un'applicazione di tipo servizio possiamo identificare due principali interfacce di interazione con l'esterno, che denomineremo interfaccia verticale e interfaccia orizzontale. L'interfaccia verticale è l'API web, tramite la quale arrivano le chiamate dall'applicazione client. L'interfaccia orizzontale è il broker dei messaggi, utilizzato per lo scambio di dati con altri servizi interni.

Esaminiamo le fasi di implementazione della correlazione su ciascuna di queste interfacce.

Correlazione nelle richieste HTTP

Per ottenere quante più informazioni possibile, dobbiamo generare un identificatore di correlazione il più vicino possibile all'inizio dell'attività, ovvero sul gateway o direttamente sul client (mobile o web). Poiché oggi trattiamo un'applicazione backend, indicheremo semplicemente su di essa il requisito di intestazione obbligatorio "X-Correlation-ID" in tutte le richieste all'API web.

Aggiungiamo il pacchetto CorrelationID, la cui funzione è quella di estrarre il valore dall'intestazione di cui abbiamo bisogno

Install-Package CorrelationID

Aggiungiamo questo nel pipeline di elaborazione della richiesta

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

Ora, con il suo aiuto, realizziamo un semplice action filter:

classe pubblica sigillata 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 aggiungiamolo nel controller

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

}

Di conseguenza, il controller restituirà 400 Bad Request per tutte le richieste senza un'intestazione con l'identificatore corrispondente.

Dopo aver ricevuto l'identificatore dal cliente dobbiamo aggiungerlo nel contesto di registrazione, creiamo per questo uno strato di avvolgimento:

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

Nel nostro applicativo utilizziamo l'ILogger standard del pacchetto Microsoft.Extensions.Logging.Abstractions, quindi il valore sarà aggiunto tramite una semplice estensione.

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

Aggiungiamo lo strato nella pipeline di elaborazione della richiesta e otteniamo il risultato desiderato.

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

Ora tutte le attività generate dalle richieste alla nostra web API contengono un identificatore di correlazione che consente di collegarle facilmente.

Il logging in un ambiente microservizi .Net nella pratica

Correlazione nei messaggi del broker

Il passo successivo è stabilire il trasferimento e la ricezione dell'identificatore di correlazione tramite un broker di messaggi. Nel nostro esempio utilizzeremo RabbitMQ e come client adotteremo il framework MassTransit. Ancora una volta, tralasceremo la configurazione iniziale di MassTransit e passeremo direttamente alla configurazione del logging.

Per iniziare, possiamo abilitare i log di MassTransit stesso, per farlo aggiungeremo al nostro progetto il pacchetto MassTransit.SerilogIntegration

Install-Package MassTransit.SerilogIntegration

Ora, dopo aver aggiunto il logger alle impostazioni di MassTransit, potremo vedere i log del framework.

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

Facciamo in modo che la nostra applicazione, in risposta a una richiesta POST, invii un evento SomethingDoneMessage con il valore «done». Il contratto di tale messaggio può essere descritto in questo modo:

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

I messaggi di MassTransit sono essenzialmente una busta, all'interno della quale si trovano i messaggi del broker. La busta appare più o meno così:

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

Nel messaggio si possono vedere i campi di servizio necessari per il funzionamento del framework stesso, ma abbiamo l'opportunità di aggiungere a questa busta anche le nostre proprietà aggiuntive. Inoltre, MassTransit ha strumenti integrati per lavorare con alcuni campi opzionali, di cui il più interessante per noi è l'identificatore di correlazione CorrelationId.

Aggiungiamo all'interfaccia del messaggio l'interfaccia CorrelatedBy:

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

Implementiamola e assegniamo il valore alla proprietà CorrelationId durante la creazione del messaggio:

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 guardiamo il messaggio aggiornato, vediamo che l'identificatore di correlazione è diventato non solo parte del nostro messaggio, ma anche parte della busta: questo identificatore sarà ora utilizzato in tutti i log di MassTransit, il che significa che ci sarà molto più semplice affrontare i problemi a livello di broker di messaggi.

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

Rimane solo da configurare il logging di queste proprietà del messaggio, per questo aggiungeremo al progetto il pacchetto Serilog.Enrichers.MassTransitMessage. Il pacchetto aggiunge un filtro nella pipeline di elaborazione dei messaggi di MassTransit, che inserisce il contesto del messaggio in uno stack thread-safe. Serilog legge il contesto dallo stack e aggiunge queste proprietà aggiuntive nei nostri oggetti di log.

Install-Package Serilog.Enrichers.MassTransitMessage

In MassTransit inseriamo il filtro

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

E nella configurazione di Serilog aggiungiamo l'enricher

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

Poiché l'applicazione che riceve il messaggio dalla coda RabbitMQ ha accesso a tutte le proprietà della busta MassTransit, possiamo utilizzare l'identificatore di correlazione ottenuto all'interno dell'applicazione consumer e passarne ulteriormente l'uso lungo la catena di chiamate.

Di conseguenza, i nostri log ora contengono il CorrelationId non solo all'interno di un singolo servizio, ma anche durante l'interazione con altre applicazioni.

Il logging in un ambiente microservizi .Net nella pratica

Pertanto, il sistema di logging ottenuto nelle applicazioni .Net ci permette di correlare i log di microservizi completamente diversi — anche quelli che operano attraverso un broker di messaggi. E con Elasticsearch, possiamo condurre analisi dei log in modo rapido e comodo, creando in Kibana i dashboard necessari (un esempio è mostrato nell'immagine del post).

Naturalmente, in questa forma il logging non coprirà le varie complessità delle interazioni tra i vostri servizi e le diverse sistemi esterni, ma mettere ordine fin dall'inizio dello sviluppo del progetto è una di quelle cose per cui vi ringrazierete in seguito.

Potete esaminare il codice sorgente del sistema risultante nel progetto: github.com/a-postx/YA.ServiceTemplate

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster