
Logging-ul este un instrument foarte important pentru dezvoltatori, dar atunci când se creează sisteme distribuite, acesta devine o piatră care trebuie pusă chiar în temelia aplicației dvs., altfel complexitatea dezvoltării microserviciilor își va lua rapid amprenta.
.Net Core 3 a adăugat un excelent , deci, dacă aplicațiile dvs. folosesc apeluri HTTP directe pentru comunicarea între servicii, atunci puteți profita de această funcționalitate ieșită din cutie. Cu toate acestea, dacă arhitectura dvs. de backend implică interacțiunea printr-un broker de mesaje (RabbitMQ, Kafka etc.), atunci trebuie să vă faceți griji cu privire la transmiterea contextului de corelare prin aceste mesaje.
În acest articol vom lua o simplă aplicație web-api și vom organiza înregistrarea, ceea ce va face
menține corelația end-to-end între jurnalele serviciilor independente, astfel încât să poți vizualiza cu ușurință toate activitățile care au fost cauzate de o anumită solicitare din partea clientului
au un singur punct de intrare cu analiză convenabilă, astfel încât chiar și asistența să poată utiliza instrumentul de înregistrare, la care pot fi folosite întrebări precum „Am primit o eroare în aplicație cu un astfel de ID de solicitare”
În primul rând, trebuie să decidem asupra unui furnizor de jurnalizare pentru aplicația noastră. Principala cerință pentru exploatarea forestieră modernă este structura, adică nu ar trebui să lucrăm cu mesaje text plate, ci cu obiecte. Datorită unor astfel de jurnale, putem construi cu ușurință vizualizări ale mesajelor noastre din diferite perspective și putem efectua analize.
Pentru aplicația noastră vom folosi pachetul Serilog, care are suport excelent pentru logare structurată și un sistem de suplimente bogat. Voi sări peste pașii de bază de configurare (puteți găsi un număr mare de articole pe acest subiect) și voi presupune că
Serilog este deja configurat și este loggerul implicit pentru furnizorul dvs. de injecție de dependență
configurația sa include îmbogățirea mesajelor cu proprietăți de context (Enrich.FromLogContext)
Următorul pas este să alegeți la ce sistem centralizat de colectare a jurnalelor să trimiteți mesajele de la Serilog. Poate că cea mai comună opțiune open source astăzi este stiva ELK (Elasticsearch, Logstash și Kibana), așa că să o luăm. Pentru a face acest lucru, vom folosi oferta de la — după înregistrarea pentru un plan gratuit, avem toată puterea motorului de căutare Lucene în mâinile noastre.
Tot ce trebuie să facem este să adăugăm pachetul în proiectul nostru
Install-Package Serilog.Sinks.Logzio
Și adăugați îmbogățitorul corespunzător la configurația logger-ului nostru, alimentându-i cu un token de acces
LoggerConfiguration loggerConfig = new LoggerConfiguration();
loggerConfig.WriteTo.Logzio(secrets.LogzioToken, 10, TimeSpan.FromSeconds(10), null, LogEventLevel.Debug);
Lansând aplicația, vom putea să ne observăm mesajele nu numai în consolă, ci și în Kibana.

interfețe

Într-o aplicație de tip serviciu, putem distinge două interfețe principale pentru interacțiunea sa cu lumea exterioară, să le desemnăm ca fiind verticale și orizontale. Interfața verticală este un API web prin care ajung apelurile din aplicația client. Horizontal este un broker de mesaje care este folosit pentru a face schimb de date cu alte servicii interne.
Să luăm în considerare etapele introducerii corelației pe fiecare dintre aceste interfețe.
Corelația în solicitările HTTP
Pentru a obține cât mai multe informații, trebuie să generăm un identificator de corelare cât mai aproape de începutul activității, adică. pe gateway sau direct pe client (mobil sau web). Deoarece astăzi avem de-a face cu o aplicație backend, vom indica pur și simplu pe ea cerința antetului obligatoriu „X-Correlation-ID” în toate solicitările către API-ul web.
Adăugarea unui pachet , a cărui funcție este de a lua valoarea din antetul de care avem nevoie
Install-Package CorrelationID
Să-l adăugăm la canalul de procesare a cererilor
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application
.UseCorrelationId(new CorrelationIdOptions
{
Header = "X-Correlation-ID",
IncludeInResponse = false,
UpdateTraceIdentifier = false,
UseGuidForCorrelationId = false
});
}
}
Acum îl vom folosi pentru a crea un filtru simplu de acțiune:
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();
}
}
Și adăugați-l la controler
[Route("[controller]")]
[ApiController]
[ServiceFilter(typeof(ApiRequestFilter))]
public class CarsController : ControllerBase
{
}
Ca rezultat, controlerul va afișa 400 Solicitare greșită pentru toate cererile fără antet cu identificatorul corespunzător.
După ce am început să primim un identificator de la client, trebuie să-l adăugăm la contextul de înregistrare, vom face un strat de încadrare pentru asta:
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);
}
}
}
În aplicația noastră, folosim ILogger standard din pachetul Microsoft.Extensions.Logging.Abstractions, așa că vom adăuga valoarea folosind o extensie simplă.
public static IDisposable BeginScopeWith(this ILogger logger, params (string key, object value)[] keys)
{
return logger.BeginScope(keys.ToDictionary(x => x.key, x => x.value));
}
Adăugăm un strat la conducta de procesare a cererii și obținem rezultatul dorit.
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application.UseMiddleware<CorrelationIdContextLogger>();
}
}
Acum, toate activitățile care sunt generate de solicitările către API-ul nostru web conțin un identificator de corelare prin care pot fi conectate cu ușurință.

Corelația în mesajele brokerului
Următorul pas este configurarea transmiterii și recepționării identificatorului de corelare prin brokerul de mesaje. În exemplul nostru, vom folosi RabbitMQ și vom lua framework-ul MassTransit ca client. Din nou, să omitem configurarea inițială a lucrului cu MassTransit și să trecem direct la configurarea înregistrării.
Pentru început, putem include jurnalele MassTransit în sine pentru aceasta vom adăuga un pachet la aplicația noastră
Install-Package MassTransit.SerilogIntegration
Acum, după adăugarea logger-ului la setările MassTransit, vom putea vedea jurnalele cadru.
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
});
});
Lăsați aplicația noastră să răspundă la o solicitare POST trimițând un eveniment SomethingDoneMessage cu valoarea „done”. Contractul pentru un astfel de mesaj poate fi descris după cum urmează:
namespace MbMessages
{
public interface ISomethingDoneMessageV1
{
string Value { get; }
}
}
Mesajele MassTransit sunt în esență un plic în care sunt incluse mesajele brokerului. Plicul arată cam așa:
{
"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"
}
}
Mesajul arată câmpuri de servicii care sunt necesare pentru funcționarea cadrului în sine, dar avem capacitatea de a adăuga propriile proprietăți suplimentare la acest plic. Mai mult, MassTransit are instrumente încorporate pentru lucrul cu unele câmpuri opționale, dintre care cel mai interesant este identificatorul de corelație CorrelationId.
Să adăugăm interfața CorrelatedBy la contractul de mesaje:
namespace MbMessages
{
public interface ISomethingDoneMessageV1 : CorrelatedBy<Guid>
{
string Value { get; }
}
}
Să-l implementăm și să atribuim o valoare proprietății CorrelationId atunci când creăm un mesaj:
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; }
}
Dacă ne uităm la mesajul actualizat, vom vedea că identificatorul de corelare a devenit nu numai o parte a mesajului nostru, ci și o parte a plicului - acest identificator va fi acum folosit și în toate jurnalele MassTransit, ceea ce înseamnă că va fi mult mai ușor. pentru ca noi să ne ocupăm de probleme la nivel de broker de mesaje.
{
"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"
}
}
Tot ce trebuie să facem este să configurați înregistrarea acestor proprietăți de serviciu ale mesajului pentru aceasta vom adăuga un pachet în proiect . Pachetul adaugă un filtru la conducta de procesare a mesajelor MassTransit care stivuiește contextul mesajului într-o stivă sigură pentru fire. Serilog citește contextul din stivă și adaugă aceste proprietăți suplimentare obiectelor noastre de jurnal.
Install-Package Serilog.Enrichers.MassTransitMessage
În MassTransit introducem un filtru
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
cfg.UseSerilogMessagePropertiesEnricher();
});
});
Iar în configurația Serilog adăugăm un îmbogățitor
Log.Logger = new LoggerConfiguration()
.Enrich.FromMassTransitMessage()
.CreateLogger();
Deoarece aplicația care primește mesajul din coada RabbitMQ are acces la toate proprietățile plicului MassTransit, putem folosi identificatorul de corelație rezultat în cadrul aplicației consumatoare, precum și să-l transmitem mai departe de-a lungul lanțului de apeluri.
Ca urmare, jurnalele noastre au început să conțină ID de corelare nu numai în cadrul unui serviciu, ci și atunci când interacționați cu alte aplicații.

Deci, sistemul de logare rezultat în aplicațiile .Net ne permite să corelăm fără probleme jurnalele de la microservicii complet diferite - chiar și cele care funcționează printr-un broker de mesaje. Și cu ajutorul Elasticsearch, putem analiza rapid și convenabil jurnalele prin construirea tablourilor de bord de care avem nevoie în Kibana (un exemplu este afișat în imagine pentru postare).
Desigur, autentificarea în acest formular nu va acoperi interacțiunile complexe dintre serviciile dumneavoastră și diverse sisteme externe, dar stabilirea unei astfel de ordini chiar la începutul dezvoltării proiectului este unul dintre acele lucruri pentru care vă veți mulțumi de mai multe ori.
Puteți înțelege codul sursă al sistemului rezultat în proiect:
Sursa: www.habr.com
