Logging in a Microservices Environment with .Net in Practice

Logging in a Microservices Environment with .Net in Practice

Il logging è uno strumento molto importante per gli sviluppatori, ma nella creazione di sistemi distribuiti diventa una pietra angolare che deve essere integrata fin dalle fondamenta della tua applicazione; altrimenti, la complessità nello sviluppo dei microservizi si farà sentire molto rapidamente.

In .Net Core 3 è stata aggiunta un'ottima 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 approfittare 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 comunque preoccuparti della trasmissione del contesto di correlazione attraverso questi messaggi in modo autonomo.

In questo articolo prenderemo un semplice applicativo web API e organizzeremo il logging in modo da

  • mantenere una correlazione coerente tra i log dei servizi indipendenti, in modo da poter facilmente vedere tutte le attività scatenate da una specifica richiesta del client.

  • avere un'unica entry point con un'analisi comoda, in modo che lo strumento di logging possa essere utilizzato anche dal supporto, al quale arrivano domande come «ho avuto un errore nel mio applicativo con questo ID di richiesta»

Innanzitutto, dobbiamo decidere quale fornitore di logging utilizzare nella nostra applicazione. Il requisito principale per un logging moderno è la struttura, ovvero dobbiamo lavorare non con messaggi di testo piatti, ma con oggetti. Grazie a questo tipo di log, possiamo facilmente creare rappresentazioni dei nostri messaggi in diverse prospettive e condurre analisi.

Per la nostra applicazione utilizzeremo il pacchetto Serilog, che supporta bene il logging strutturato e ha un ricco sistema di estensioni. Tralascerò le fasi di configurazione di base (puoi trovare molte articoli su questo argomento) e presumerò che

  • Serilog sia già configurato e sia il logger di default per il tuo fornitore di iniezione delle dipendenze

  • nella sua configurazione è attivato il potenziamento dei messaggi con le proprietà del contesto (Enrich.FromLogContext)

Il passo successivo è scegliere in quale sistema di raccolta centralizzata dei log inviare i messaggi da Serilog. Probabilmente, la soluzione open-source più diffusa al momento è il stack ELK (Elasticsearch, Logstash e Kibana), e sarà questa quella che utilizzeremo. A questo scopo, utilizzeremo l'offerta di Logz.IO — dopo la registrazione al piano gratuito, avremo 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, fornendo il token di accesso

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

Una volta avviata l'applicazione, potremo vedere i nostri messaggi non solo nel terminale, ma anche in Kibana.

Logging in a Microservices Environment with .Net in Practice

Interfacce

Logging in a Microservices Environment with .Net in Practice

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

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

Correlazione nelle richieste HTTP

Per ottenere quante più informazioni possibile, è necessario generare un identificatore di correlazione il più vicino possibile all'inizio dell'attività, cioè sul gateway o direttamente sul client (mobile o web). Poiché oggi trattiamo con un'applicazione backend, ci limiteremo a specificare l'obbligo del header "X-Correlation-ID" in tutte le richieste all'API web.

Aggiungiamo il pacchetto CorrelationID, la cui funzione consiste nel prelevare il valore dall'header necessario per noi.

Install-Package CorrelationID

Aggiungiamolo alla 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, creiamo un semplice action filter:

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 aggiungiamolo al controller

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

}

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

Dopo aver iniziato a ricevere l'identificatore dal client, dobbiamo aggiungerlo al contesto di registrazione; creiamo un livello intermedio per questo:

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 dal pacchetto Microsoft.Extensions.Logging.Abstractions, pertanto aggiungeremo il valore utilizzando 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 uno strato alla 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 API web contengono l'identificatore di correlazione tramite il quale possono essere facilmente collegate.

Logging in a Microservices Environment with .Net in Practice

Correlazione nei messaggi del broker

Il passo successivo è configurare la trasmissione e la ricezione dell'identificativo di correlazione tramite un broker di messaggi. Nel nostro esempio utilizzeremo RabbitMQ, e come client prenderemo il framework MassTransit. Anche in questo caso, saltiamo la configurazione iniziale di MassTransit e passiamo direttamente alla configurazione del logging.

Per iniziare, possiamo abilitare i log di MassTransit stesso; a tal fine, aggiungiamo al nostro progetto il pacchetto MassTransit.SerilogIntegration

Install-Package MassTransit.SerilogIntegration

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

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

Facciamo in modo che la nostra applicazione invii un evento SomethingDoneMessage con valore "done" come risposta a una richiesta POST. Il contratto di tale messaggio può essere descritto così:

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

I messaggi di MassTransit sono essenzialmente una busta contenente i messaggi del broker. La busta appare circa 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 sono visibili campi di servizio necessari per il funzionamento del framework stesso, ma abbiamo la possibilità di aggiungere a questo pacchetto anche le nostre proprietà aggiuntive. Inoltre, MassTransit dispone di strumenti integrati per gestire alcuni campi opzionali, 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 osserviamo il messaggio aggiornato, vedremo che l'identificatore di correlazione è diventato parte non solo del nostro messaggio, ma anche della busta — questo identificatore sarà ora utilizzato in tutti i log di MassTransit, il che renderà 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": "Hello"
  }
}

Ci resta da configurare la registrazione di queste proprietà del messaggio, per questo aggiungeremo al progetto il pacchetto Serilog.Enrichers.MassTransitMessage. Il pacchetto aggiunge un filtro alla pipeline di elaborazione dei messaggi di MassTransit, che accumula 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

servizi
    .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à del convertitore MassTransit, possiamo utilizzare l'identificatore di correlazione ricevuto all'interno dell'applicazione consumatore e passarlo ulteriormente lungo tutta la catena di chiamata.

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

Logging in a Microservices Environment with .Net in Practice

Quindi, il sistema di logging ottenuto nelle applicazioni .Net ci consente di correlare senza problemi i log di microservizi completamente diversi — anche quelli che operano tramite un broker di messaggi. E con Elasticsearch possiamo analizare rapidamente e comodamente i log, costruendo nei Kibana i dashboard necessari (un esempio è mostrato nell'immagine del post).

Naturalmente, in questa forma la registrazione non coprirà le complesse interazioni tra i vostri servizi e i vari sistemi esterni, ma stabilire un ordine simile all'inizio dello sviluppo del progetto è una di quelle cose per cui vi ringrazierete da soli 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