
Rejestrowanie jest bardzo ważnym narzędziem dla programisty, ale w przypadku tworzenia systemów rozproszonych staje się kamieniem węgielnym, który należy umieścić w fundamencie aplikacji. W przeciwnym razie złożoność tworzenia mikrousług szybko stanie się oczywista.
.Net Core 3 dodał świetną , więc jeśli Twoje aplikacje korzystają z bezpośrednich wywołań HTTP do komunikacji między usługami, możesz wykorzystać tę gotową funkcjonalność. Jeśli jednak architektura zaplecza obejmuje interakcję za pośrednictwem brokera komunikatów (RabbitMQ, Kafka itp.), to nadal musisz zadbać o samodzielne przekazywanie kontekstu korelacji za pośrednictwem tych komunikatów.
W tym artykule zajmiemy się prostą aplikacją API sieci Web i zorganizujemy rejestrowanie, które będzie
utrzymywać korelację typu end-to-end pomiędzy logami niezależnych usług, aby móc łatwo przeglądać wszystkie działania spowodowane konkretnym żądaniem klienta
mieć jeden punkt wejścia z wygodną analizą, tak aby nawet Wsparcie, które otrzymuje pytania typu „Wystąpił błąd w aplikacji z takim a takim identyfikatorem żądania”, mogło korzystać z narzędzia do rejestrowania
Najpierw musimy wybrać dostawcę usług rejestrowania danych dla naszej aplikacji. Głównym wymogiem nowoczesnego rejestrowania jest struktura, tzn. powinniśmy pracować nie z płaskimi wiadomościami tekstowymi, ale z obiektami. Dzięki takim logom możemy łatwo budować reprezentacje naszych wiadomości w różnych sekcjach i przeprowadzać analizy.
W naszej aplikacji wykorzystamy pakiet Serilog, który charakteryzuje się doskonałym wsparciem dla strukturalnego rejestrowania danych i bogatym systemem dodatków. Pominę podstawowe kroki konfiguracji (można znaleźć wiele artykułów na ten temat) i założę, że
Serilog jest już skonfigurowany i stanowi domyślny rejestrator dla Twojego dostawcy wstrzykiwania zależności.
jego konfiguracja obejmuje wzbogacanie wiadomości o właściwości kontekstowe (Enrich.FromLogContext)
Następnym krokiem jest wybranie scentralizowanego systemu gromadzenia dzienników, do którego będą wysyłane wiadomości z Serilog. Prawdopodobnie najpopularniejszym rozwiązaniem typu open source jest dziś stos ELK (Elasticsearch, Logstash i Kibana), więc przyjrzymy się mu bliżej. W tym celu skorzystamy z oferty — po zarejestrowaniu się w celu uzyskania bezpłatnego planu, pełna moc wyszukiwarki Lucene jest w naszych rękach.
Pozostaje nam tylko dodać pakiet do naszego projektu
Install-Package Serilog.Sinks.Logzio
I dodaj odpowiedni wzbogacacz do naszej konfiguracji rejestratora, przekazując mu token dostępu
LoggerConfiguration loggerConfig = new LoggerConfiguration();
loggerConfig.WriteTo.Logzio(secrets.LogzioToken, 10, TimeSpan.FromSeconds(10), null, LogEventLevel.Debug);
Po uruchomieniu aplikacji będziemy mogli zobaczyć nasze wiadomości nie tylko w konsoli, ale także w Kibanie.

Interfejsy

W aplikacji typu usługowego możemy wyróżnić dwa główne interfejsy jej interakcji ze światem zewnętrznym; będziemy je oznaczać jako pionowe i poziome. Interfejs pionowy to internetowy interfejs API, poprzez który docierają wywołania z aplikacji klienckiej. Horizontal to broker wiadomości służący do wymiany danych z innymi usługami wewnętrznymi.
Rozważmy etapy wdrażania korelacji na każdym z tych interfejsów.
Korelacja w żądaniach HTTP
Aby uzyskać jak najwięcej informacji, musimy wygenerować identyfikator korelacji jak najbliżej początku aktywności, tzn. na bramce lub bezpośrednio na kliencie (urządzeniu mobilnym lub w sieci). Ponieważ dziś mamy do czynienia z aplikacją zaplecza, po prostu wskażemy wymaganie obowiązkowego nagłówka „X-Correlation-ID” we wszystkich żądaniach do internetowego interfejsu API.
Dodawanie pakietu , którego funkcją jest pobieranie wartości z nagłówka, którego potrzebujemy
Install-Package CorrelationID
Dodajmy to do procesu przetwarzania żądań
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application
.UseCorrelationId(new CorrelationIdOptions
{
Header = "X-Correlation-ID",
IncludeInResponse = false,
UpdateTraceIdentifier = false,
UseGuidForCorrelationId = false
});
}
}
Teraz użyjmy go do utworzenia prostego filtra akcji:
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 dodajmy to do kontrolera
[Route("[controller]")]
[ApiController]
[ServiceFilter(typeof(ApiRequestFilter))]
public class CarsController : ControllerBase
{
}
W rezultacie kontroler wyśle komunikat 400 Błędne żądanie dla wszystkich żądań bez nagłówka z odpowiednim identyfikatorem.
Po rozpoczęciu otrzymywania identyfikatora od klienta musimy dodać go do kontekstu rejestrowania. W tym celu utwórzmy warstwę ramkową:
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);
}
}
}
W naszej aplikacji wykorzystujemy standardowy ILogger z pakietu Microsoft.Extensions.Logging.Abstractions, więc dodamy do niego wartość za pomocą prostego rozszerzenia.
public static IDisposable BeginScopeWith(this ILogger logger, params (string key, object value)[] keys)
{
return logger.BeginScope(keys.ToDictionary(x => x.key, x => x.value));
}
Dodajemy warstwę do procesu przetwarzania żądań i otrzymujemy pożądany wynik.
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application.UseMiddleware<CorrelationIdContextLogger>();
}
}
Teraz wszystkie działania generowane przez żądania wysyłane do naszego interfejsu API zawierają identyfikator korelacji, dzięki któremu można je łatwo powiązać.

Korelacja w komunikatach brokerskich
Następnym krokiem, który musimy skonfigurować, jest transmisja i odbiór identyfikatora korelacji poprzez brokera wiadomości. W naszym przykładzie użyjemy RabbitMQ, a jako klienta weźmiemy framework MassTransit. Ponownie pomińmy początkową konfigurację pracy z MassTransit i przejdźmy bezpośrednio do konfiguracji rejestrowania danych.
Na początek możemy włączyć logi samego MassTransit, w tym celu dodamy pakiet do naszej aplikacji
Install-Package MassTransit.SerilogIntegration
Teraz, po dodaniu rejestratora do ustawień MassTransit, będziemy mogli zobaczyć logi struktury.
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
});
});
Załóżmy, że nasza aplikacja wysyła zdarzenie SomethingDoneMessage z wartością „done” jako odpowiedź na żądanie POST. Kontrakt na taką wiadomość można opisać następująco:
namespace MbMessages
{
public interface ISomethingDoneMessageV1
{
string Value { get; }
}
}
Wiadomości MassTransit to w zasadzie koperta zawierająca wiadomości brokerskie. Koperta wygląda mniej więcej tak:
{
"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"
}
}
Komunikat zawiera pola usług, które są niezbędne do działania samego frameworka, ale mamy możliwość dodania do tej koperty własnych dodatkowych właściwości. Ponadto MassTransit ma wbudowane narzędzia do pracy z niektórymi polami opcjonalnymi, a najciekawszym z nich jest CorrelationId.
Dodajmy interfejs CorrelatedBy do kontraktu wiadomości:
namespace MbMessages
{
public interface ISomethingDoneMessageV1 : CorrelatedBy<Guid>
{
string Value { get; }
}
}
Zaimplementujmy to i przypiszmy wartość do właściwości CorrelationId podczas tworzenia wiadomości:
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; }
}
Jeśli przyjrzymy się zaktualizowanej wiadomości, zobaczymy, że identyfikator korelacji stał się nie tylko częścią naszej wiadomości, ale także częścią koperty — ten identyfikator będzie teraz używany również we wszystkich logach MassTransit, co oznacza, że znacznie łatwiej będzie nam rozwiązywać problemy na poziomie brokera wiadomości.
{
"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"
}
}
Musimy jeszcze skonfigurować rejestrowanie tych właściwości usługi wiadomości, w tym celu dodamy pakiet do projektu . Pakiet dodaje filtr do procesu przetwarzania wiadomości MassTransit, który układa kontekst wiadomości na stosie bezpiecznym dla wątków. Serilog odczytuje kontekst ze stosu i dodaje te dodatkowe właściwości do naszych obiektów dziennika.
Install-Package Serilog.Enrichers.MassTransitMessage
W MassTransit wstawiamy filtr
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
cfg.UseSerilogMessagePropertiesEnricher();
});
});
A w konfiguracji Serilog dodajemy Enricher
Log.Logger = new LoggerConfiguration()
.Enrich.FromMassTransitMessage()
.CreateLogger();
Ponieważ aplikacja odbierająca komunikat z kolejki RabbitMQ ma dostęp do wszystkich właściwości koperty MassTransit, możemy wykorzystać otrzymany identyfikator korelacji w obrębie aplikacji odbierającej i przekazać go dalej w łańcuchu wywołań.
W rezultacie nasze logi zaczęły zawierać CorrelationId nie tylko w obrębie jednej usługi, ale również podczas interakcji z innymi aplikacjami.

W rezultacie powstał system rejestrowania zdarzeń w aplikacjach .Net, który pozwala na korelowanie logów z zupełnie różnych mikrousług bez żadnych problemów — nawet tych, które działają za pośrednictwem brokera komunikatów. Dzięki Elasticsearch możemy szybko i wygodnie analizować logi, tworząc potrzebne nam pulpity nawigacyjne w Kibanie (przykład pokazano na rysunku do wpisu).
Oczywiście, logowanie w tym formularzu nie obejmie skomplikowanych interakcji pomiędzy Twoimi usługami a różnymi systemami zewnętrznymi, ale ustalenie takiego porządku na samym początku rozwoju projektu jest jedną z tych rzeczy, za które podziękujesz sobie nie raz.
Kod źródłowy powstałego systemu można poznać w projekcie:
Źródło: www.habr.com
