
La registrazione è uno strumento molto importante per uno sviluppatore, ma quando si creano sistemi distribuiti, diventa una pietra che deve essere posta direttamente nelle fondamenta della propria applicazione, altrimenti la complessità dello sviluppo di microservizi diventerà presto evidente.
.Net Core 3 ha aggiunto un grande , quindi se le tue applicazioni utilizzano chiamate HTTP dirette per la comunicazione tra servizi, puoi utilizzare questa funzionalità pronta all'uso. Tuttavia, se la tua architettura backend implica la comunicazione tramite un broker di messaggi (RabbitMQ, Kafka, ecc.), dovrai comunque preoccuparti di passare autonomamente il contesto di correlazione tramite questi messaggi.
In questo articolo, prenderemo una semplice applicazione API web e organizzeremo la registrazione che
mantenere una correlazione end-to-end tra i registri dei servizi indipendenti in modo da poter visualizzare facilmente tutte le attività causate da una richiesta specifica da parte di un client
avere un unico punto di ingresso con un'analisi comoda, in modo che anche il supporto, che riceve domande come "Ho ricevuto un errore nell'applicazione con tale e tale ID richiesta", possa utilizzare lo strumento di registrazione
Innanzitutto, dobbiamo scegliere il provider di logging per la nostra applicazione. Il requisito principale per un logging moderno è la struttura, ovvero non dovremmo lavorare con messaggi di testo semplici, ma con oggetti. Grazie a questi log, possiamo facilmente creare rappresentazioni dei nostri messaggi in diverse sezioni ed eseguire analisi.
Per la nostra applicazione, utilizzeremo il pacchetto Serilog, che offre un eccellente supporto per la registrazione strutturata e un ricco sistema di componenti aggiuntivi. Salterò i passaggi di base per la sua configurazione (sono disponibili numerosi articoli sull'argomento) e partirò dal presupposto che:
Serilog è già configurato ed è il logger predefinito per il provider di iniezione delle dipendenze.
la sua configurazione include l'arricchimento dei messaggi con proprietà di contesto (Enrich.FromLogContext)
Il passo successivo è scegliere a quale sistema centralizzato di raccolta log inviare i messaggi da Serilog. Forse l'opzione open source più comune oggi è lo stack ELK (Elasticsearch, Logstash e Kibana), quindi prendiamolo. Per questo, utilizzeremo l'offerta di — dopo aver effettuato la registrazione per un piano gratuito, tutta la potenza del motore di ricerca Lucene è nelle nostre mani.
Tutto ciò che ci resta da fare è aggiungere un pacchetto al nostro progetto
Install-Package Serilog.Sinks.Logzio
E aggiungiamo l'arricchitore corrispondente alla nostra configurazione del logger, fornendogli il token di accesso
LoggerConfiguration loggerConfig = new LoggerConfiguration();
loggerConfig.WriteTo.Logzio(secrets.LogzioToken, 10, TimeSpan.FromSeconds(10), null, LogEventLevel.Debug);
Eseguendo l'applicazione, potremo vedere i nostri messaggi non solo nella console, ma anche in Kibana.

Interfacce

In un'applicazione di tipo servizio, possiamo distinguere due interfacce principali per l'interazione con il mondo esterno, che chiameremo verticali e orizzontali. L'interfaccia verticale è un'API web attraverso la quale arrivano le chiamate dall'applicazione client. Quella orizzontale è un broker di messaggi, utilizzato per scambiare dati con altri servizi interni.
Consideriamo le fasi di implementazione della correlazione su ciascuna di queste interfacce.
Correlazione nelle richieste HTTP
Per ottenere quante più informazioni possibili, dobbiamo generare l'identificatore di correlazione il più vicino possibile all'inizio dell'attività, ovvero sul gateway o direttamente sul client (mobile o web). Poiché oggi abbiamo a che fare con un'applicazione backend, ci limiteremo a designare il requisito di un'intestazione obbligatoria "X-Correlation-ID" in tutte le richieste all'API web.
Aggiungere un pacchetto , la cui funzione è quella di prendere il valore dall'intestazione di cui abbiamo bisogno
Install-Package CorrelationID
Aggiungiamolo alla pipeline di elaborazione delle richieste
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application
.UseCorrelationId(new CorrelationIdOptions
{
Header = "X-Correlation-ID",
IncludeInResponse = false,
UpdateTraceIdentifier = false,
UseGuidForCorrelationId = false
});
}
}
Ora usiamolo per creare un semplice filtro di azione:
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();
}
}
E aggiungiamolo al controller
[Route("[controller]")]
[ApiController]
[ServiceFilter(typeof(ApiRequestFilter))]
public class CarsController : ControllerBase
{
}
Di conseguenza, il controller emetterà il codice 400 Bad request per tutte le richieste prive di intestazione con l'identificatore corrispondente.
Dopo aver iniziato a ricevere l'identificatore dal client, dobbiamo aggiungerlo al contesto di registrazione. Creiamo un livello di framing 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<CorrelationIdContextLogger> logger, ICorrelationContextAccessor correlationContextAccessor)
{
if (Guid.TryParse(correlationContextAccessor.CorrelationContext.CorrelationId, out Guid correlationId))
{
using (logger.BeginScopeWith(("CorrelationId", correlationId)))
{
await _next(context);
}
}
else
{
await _next(context);
}
}
}
Nella nostra applicazione utilizziamo l'ILogger standard del pacchetto Microsoft.Extensions.Logging.Abstractions, quindi 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 un livello alla pipeline di elaborazione delle richieste e otteniamo il risultato desiderato.
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application.UseMiddleware<CorrelationIdContextLogger>();
}
}
Ora tutte le attività generate dalle richieste alla nostra API web contengono un identificatore di correlazione tramite il quale possono essere facilmente collegate.

Correlazione nei messaggi del broker
Il passo successivo consiste nell'impostare la trasmissione e la ricezione dell'identificatore di correlazione tramite il broker di messaggi. Nel nostro esempio, utilizzeremo RabbitMQ e, come client, il framework MassTransit. Anche in questo caso, salteremo la configurazione iniziale per lavorare con MassTransit e passeremo direttamente alla configurazione del logging.
Per cominciare, possiamo abilitare i log di MassTransit stesso, per questo aggiungeremo un pacchetto alla nostra applicazione
Install-Package MassTransit.SerilogIntegration
Ora, dopo aver aggiunto il logger alle impostazioni di MassTransit, saremo in grado di visualizzare i log del framework.
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
});
});
Supponiamo che la nostra applicazione invii un evento SomethingDoneMessage con il valore "done" in risposta a una richiesta POST. Il contratto di tale messaggio può essere descritto come segue:
namespace MbMessages
{
public interface ISomethingDoneMessageV1
{
string Value { get; }
}
}
I messaggi MassTransit sono essenzialmente una busta in cui sono racchiusi i messaggi del broker. La busta ha un aspetto simile a questo:
{
"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"
}
}
Il messaggio mostra i campi di servizio necessari per il funzionamento del framework stesso, ma abbiamo la possibilità di aggiungere proprietà aggiuntive a questo envelope. Inoltre, MassTransit dispone di strumenti integrati per lavorare con alcuni campi opzionali, tra cui siamo particolarmente interessati all'identificatore di correlazione CorrelationId.
Aggiungiamo l'interfaccia CorrelatedBy al contratto del messaggio:
namespace MbMessages
{
public interface ISomethingDoneMessageV1 : CorrelatedBy<Guid>
{
string Value { get; }
}
}
Implementiamolo e assegniamo un valore alla proprietà CorrelationId quando creiamo un 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 non solo parte del nostro messaggio, ma anche parte della busta: questo identificatore verrà ora utilizzato anche in tutti i log di MassTransit, il che significa che sarà molto più semplice per noi gestire 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"
}
}
Dobbiamo ancora configurare la registrazione di queste proprietà del servizio del messaggio, per questo aggiungeremo un pacchetto al progetto Il pacchetto aggiunge un filtro alla pipeline di elaborazione dei messaggi di MassTransit che inserisce il contesto del messaggio in uno stack thread-safe. Serialog legge il contesto dallo stack e aggiunge queste proprietà aggiuntive ai nostri oggetti di log.
Install-Package Serilog.Enrichers.MassTransitMessage
In MassTransit inseriamo un filtro
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
cfg.UseSerilogMessagePropertiesEnricher();
});
});
E nella configurazione Serilog aggiungiamo l'Enricher
Log.Logger = new LoggerConfiguration()
.Enrich.FromMassTransitMessage()
.CreateLogger();
Poiché l'applicazione che riceve un messaggio da una coda RabbitMQ ha accesso a tutte le proprietà dell'envelope MassTransit, possiamo utilizzare l'identificatore di correlazione risultante all'interno dell'applicazione consumatrice e anche trasmetterlo più avanti nella catena delle chiamate.
Di conseguenza, i nostri log hanno iniziato a contenere CorrelationId non solo all'interno di un servizio, ma anche durante l'interazione con altre applicazioni.

Pertanto, il sistema di logging risultante 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 l'aiuto di Elasticsearch, possiamo analizzare i log in modo rapido e pratico, creando le dashboard di cui abbiamo bisogno in Kibana (un esempio è riportato nell'immagine del post).
Naturalmente, l'accesso tramite questo modulo non coprirà le interazioni complesse tra i tuoi servizi e vari sistemi esterni, ma stabilire tale ordine all'inizio dello sviluppo di un progetto è una di quelle cose per cui ti ringrazierai più di una volta.
È possibile comprendere il codice sorgente del sistema risultante nel progetto:
Fonte: habr.com
