
Jurnalizarea este un instrument foarte important pentru dezvoltatori, dar, atunci când se creează sisteme distribuite, devine o piatră de temelie care trebuie așezată direct în fundația aplicației dvs., altfel complexitatea dezvoltării microserviciilor se va face simțită foarte repede.
În .Net Core 3 a fost adăugată o excelentă , așadar, dacă aplicațiile dvs. folosesc apeluri HTTP directe pentru interacțiunea între servicii, puteți beneficia de această funcționalitate integrată. Totuși, dacă arhitectura backend-ului dvs. implică interacțiunea printr-un broker de mesaje (RabbitMQ, Kafka etc.), va trebui, în continuare, să vă preocupați de transmiterea contextului de corelare prin aceste mesaje în mod autonom.
În acest articol, vom lua o aplicație web API simplă și vom organiza jurnalizarea care va
păstra o corelație între jurnalele serviciilor independente, astfel încât să puteți vizualiza cu ușurință toate activitățile care au fost declanșate de o anumită cerere de la client
să aveți un punct de intrare unificat cu o analiză convenabilă, astfel încât instrumentul de jurnalizare să poată fi utilizat chiar și de Suport, care primește întrebări de genul "am avut o eroare în aplicație cu acest ID de cerere"
În primul rând, trebuie să ne stabilim un furnizor de jurnalizare în aplicația noastră. Principala cerință pentru o jurnalizare modernă este structurarea, adică trebuie să lucrăm nu cu mesaje text simple, ci cu obiecte. Datorită acestor jurnale, putem construi cu ușurință reprezentări ale mesajelor noastre din diverse perspective și efectua analize.
Pentru aplicația noastră, vom folosi pachetul Serilog (Serilog), care are un suport excelent pentru jurnalizarea structurală și un sistem bogat de extensii. Voi omite etapele de bază ale configurării sale (puteți găsi o mulțime de articole pe această temă) și voi face presupunerea că
Serilog este deja configurat și este logger-ul implicit al furnizorului dvs. de injecție a dependențelor
în configurația sa este inclusă îmbogățirea mesajelor cu proprietățile contextului (Enrich.FromLogContext)
Pasul următor este să alegem în ce sistem centralizat de colectare a jurnalelor să trimitem mesajele din Serilog. Poate cel mai răspândit în prezent dintre opțiunile de software open-source este stiva ELK (Elasticsearch, Logstash și Kibana), pe care o vom folosi. Pentru aceasta, vom utiliza oferta de la — după înregistrarea pe planul gratuit, avem la dispoziție toată puterea motorului de căutare Lucene.
Ne rămâne să adăugăm în proiectul nostru pachetul
Install-Package Serilog.Sinks.Logzio
Și să adăugăm enrichment-ul corespunzător în configurația logger-ului nostru, furnizându-i token-ul de acces
LoggerConfiguration loggerConfig = new LoggerConfiguration();
loggerConfig.WriteTo.Logzio(secrets.LogzioToken, 10, TimeSpan.FromSeconds(10), null, LogEventLevel.Debug);
După ce am lansat aplicația, vom putea observa mesajele noastre nu doar în consolă, ci și în Kibana.

Interfețe

În aplicațiile de tip serviciu, putem distinge două interfețe principale de interacțiune cu lumea externă, pe care le vom numi interfață verticală și orizontală. Interfața verticală este API-ul web, prin care vin solicitările de la aplicația client. Interfața orizontală este brokerul de mesaje, care este folosit pentru schimbul de date cu alte servicii interne.
Să analizăm etapele implementării corelării pentru fiecare dintre aceste interfețe.
Corelarea în solicitările HTTP
Pentru a obține cât mai multe informații, este necesar să generăm un identificator de corelare cât mai aproape de începutul activității, adică la gateway sau direct pe client (mobil sau web). Având în vedere că ne ocupăm astăzi de o aplicație backend, vom specifica pur și simplu cerința unui header obligatoriu „X-Correlation-ID” în toate solicitările către API-ul web.
Adăugăm pachetul , care are rolul de a prelua valoarea din header-ul necesar.
Install-Package CorrelationID
Să-l adăugăm în pipeline-ul de procesare a cererii
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application
.UseCorrelationId(new CorrelationIdOptions
{
Header = "X-Correlation-ID",
IncludeInResponse = false,
UpdateTraceIdentifier = false,
UseGuidForCorrelationId = false
});
}
}
Acum, cu ajutorul său, să facem un simplu action filter:
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 îl vom adăuga în controler
[Route("[controller]")]
[ApiController]
[ServiceFilter(typeof(ApiRequestFilter))]
public class CarsController : ControllerBase
{
}
Ca rezultat, controlerul va returna 400 Bad Request pentru toate cererile fără un antet cu un identificator corespunzător.
După ce am început să primim identificatorul de la client, trebuie să-l adăugăm în contextul de jurnalizare, așa că vom crea un strat de învelire:
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);
}
}
}
În aplicația noastră, folosim ILogger standard din pachetul Microsoft.Extensions.Logging.Abstractions, așa că vom adăuga valoarea folosind o simplă extensie pentru acesta.
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 stratul în lanțul de procesare a cererii și obținem rezultatul dorit.
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application.UseMiddleware();
}
}
Acum toate activitățile generate de cererile la API-ul nostru web conțin identificatorul corelațional, care poate fi folosit pentru a le lega ușor.

Corelația în mesajele brokerului
Următoarea etapă este să configurăm transmisia și primirea identificatorului de corelație prin intermediul unui broker de mesaje. În exemplul nostru, vom folosi RabbitMQ și, ca și client, vom lua framework-ul MassTransit. De asemenea, vom trece peste configurarea inițială a lucrului cu MassTransit și vom trece direct la configurarea jurnalizării.
Prima dată, putem activa jurnalele MassTransit, pentru aceasta trebuie să adăugăm pachetul
Install-Package MassTransit.SerilogIntegration
Acum, după ce am adăugat logger-ul în setările MassTransit, vom putea vedea jurnalele framework-ului.
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
});
});
Să presupunem că aplicația noastră reacționează la un request POST trimițând un eveniment SomethingDoneMessage cu valoarea „done”. Contractul pentru acest mesaj poate fi descris astfel:
namespace MbMessages
{
public interface ISomethingDoneMessageV1
{
string Value { get; }
}
}
Mesajele din MassTransit sunt, în esență, un pachet în care sunt incluse mesajele broker-ului. Pachetul 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"
}
}
În mesaj sunt vizibile câmpuri tehnice necesare pentru funcționarea framework-ului, dar avem posibilitatea de a adăuga și proprietăți suplimentare proprii în acest pachet. Mai mult, MassTransit are instrumente încorporate pentru gestionarea unor câmpuri opționale, cel mai interesant dintre care este identificatorul de corelație CorrelationId.
Adăugăm la contractul mesajului interfața CorrelatedBy:
namespace MbMessages
{
public interface ISomethingDoneMessageV1 : CorrelatedBy
{
string Value { get; }
}
}
Implementăm aceasta și vom aloca valoarea proprietății CorrelationId la creare mesajului:
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 corelație a devenit nu doar o parte a mesajului nostru, ci și o parte a pachetului — acest identificator va fi folosit acum și în toate logurile MassTransit, ceea ce va facilita considerabil rezolvarea problemelor la nivelul brokerului 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"
}
}
Mai trebuie să configurăm logarea acestor proprietăți de serviciu ale mesajului, pentru aceasta vom adăuga în proiect pachetul . Pachetul adaugă un filtru în canalul de procesare a mesajelor MassTransit, care stochează contextul mesajului într-un stivă thread-safe. Serilog citește contextul din stivă și adaugă aceste proprietăți suplimentare la obiectele noastre de logare.
Install-Package Serilog.Enrichers.MassTransitMessage
În MassTransit introducem filtrul
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
cfg.UseSerilogMessagePropertiesEnricher();
});
});
Și în configurația Serilog adăugăm îmbogățitorul
Log.Logger = new LoggerConfiguration()
.Enrich.FromMassTransitMessage()
.CreateLogger();
Deoarece aplicația care primește mesajul din coada RabbitMQ are acces la toate proprietățile pachetului MassTransit, putem folosi identificatorul de corelație obținut în aplicația consumator și, de asemenea, să-l transmitem mai departe pe parcursul întregii lanțuri de apeluri.
În consecință, logurile noastre conțin CorrelationId nu doar în interiorul unui singur serviciu, ci și în interacțiunea cu alte aplicații.

Astfel, sistemul de logare obținut în aplicațiile .Net ne permite să corelăm logurile din microservicii complet diferite — chiar și cele care funcționează prin brokerul de mesaje. Și cu ajutorul Elasticsearch putem analiza rapid și convenabil logurile, construind în Kibana tablouri de bord necesare (un exemplu este prezentat în imaginea postării).
Desigur, în această formă, jurnalizarea nu va acoperi variantele complexe de interacțiune între serviciile dumneavoastră și diversele sisteme externe, dar stabilirea unei astfel de ordini la începutul dezvoltării proiectului este unul dintre acele aspecte pentru care vă veți mulțumi singur mai târziu.
Puteți analiza codul sursă al sistemului rezultat în proiectul:
Sursa: habr.com
