Logowanie do środowiska mikrousług .Net w praktyce

Logowanie do środowiska mikrousług .Net w praktyce

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ą Możliwość przekazywania kontekstu korelacji w nagłówkach HTTP, 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 Logz.IO — 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 Serilog.Sinks.Logzio

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.

Logowanie do środowiska mikrousług .Net w praktyce

Interfejsy

Logowanie do środowiska mikrousług .Net w praktyce

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 KorelacjaID, 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ć.

Logowanie do środowiska mikrousług .Net w praktyce

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 MassTransit.Integracja Serilog

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 Serilog.Enrichers.MassTransitMessage. 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.

Logowanie do środowiska mikrousług .Net w praktyce

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: github.com/a-postx/YA.ServiceTemplate

Źródło: www.habr.com

Kup niezawodny hosting dla stron z ochroną DDoS, serwery VPS VDS 🔥 Kup niezawodny hosting stron internetowych z ochroną DDoS, serwery VPS VDS | ProHoster