Logowanie w środowisku mikroserwisowym .Net w praktyce

Logowanie w środowisku mikroserwisowym .Net w praktyce

Rejestrowanie jest niezwykle ważnym narzędziem dla programistów, ale podczas tworzenia systemów rozproszonych staje się to fundamentem, który należy wbudować w podstawy aplikacji, w przeciwnym razie złożoność rozwoju mikroserwisów bardzo szybko zacznie dawać o sobie znać.

W .Net Core 3 dodano świetną możliwość przekazywania kontekstu korelacji w nagłówkach HTTP, więc jeśli Twoje aplikacje korzystają z bezpośrednich wywołań HTTP do interakcji między serwisami, możesz skorzystać z tej wbudowanej funkcjonalności. Jednak, jeśli architektura Twojego backendu zakłada interakcję przez broker wiadomości (RabbitMQ, Kafka itp.), wciąż musisz zająć się przekazywaniem kontekstu korelacji przez te wiadomości samodzielnie.

W tym artykule weźmiemy proste aplikacje web API i zorganizujemy rejestrowanie, które

  • zachowa pełną korelację między logami niezależnych serwisów, aby można było łatwo zobaczyć wszystkie działania, które zostały wywołane przez konkretne żądanie z klienta

  • mieć jednolity punkt wejścia z wygodną analizą, aby narzędzie do rejestrowania mogło być używane nawet przez Dział Wsparcia, który otrzymuje pytania typu „w mojej aplikacji wystąpił błąd z takim a takim identyfikatorem żądania”

Po pierwsze, musimy zdecydować się na dostawcę rejestrowania w naszej aplikacji. Główne wymaganie wobec nowoczesnego rejestrowania to strukturalność, tzn. musimy pracować nie z płaskimi wiadomościami tekstowymi, a z obiektami. Dzięki takim logom możemy łatwo budować przedstawienia naszych wiadomości w różnych perspektywach i prowadzić analizy.

Dla naszej aplikacji skorzystamy z pakietu Serilog, który ma doskonałe wsparcie dla strukturalnego rejestrowania i bogaty system rozszerzeń. Pominę podstawowe etapy jego konfiguracji (możesz znaleźć wiele artykułów na ten temat) i założę, że

  • Serilog jest już skonfigurowany i jest domyślnym rejestratorem w Twoim dostawcy wstrzykiwania zależności

  • w jego konfiguracji włączono wzbogacanie wiadomości właściwościami kontekstu (Enrich.FromLogContext)

Kolejnym krokiem jest wybranie, do jakiego systemu centralnego zbierania logów chcemy wysyłać wiadomości z Serilog. Najpowszechniejszym obecnie rozwiązaniem w otwartym oprogramowaniu jest stos ELK (Elasticsearch, Logstash i Kibana), którego użyjemy. W tym celu skorzystamy z oferty od Logz.IO — po zarejestrowaniu się na darmowym planie mamy w rękach pełną moc silnika wyszukiwania Lucene.

Pozostaje nam dodać do naszego projektu pakiet Serilog.Sinks.Logzio

Install-Package Serilog.Sinks.Logzio

I dodać odpowiedni wzbogacacz do konfiguracji naszego loggera, podając mu token dostępu

LoggerConfiguration loggerConfig = new LoggerConfiguration();
loggerConfig.WriteTo.Logzio(secrets.LogzioToken, 10, TimeSpan.FromSeconds(10), null, LogEventLevel.Debug);

Uruchamiając aplikację, będziemy mogli obserwować nasze wiadomości nie tylko w konsoli, ale także w Kibanie.

Logowanie w środowisku mikroserwisowym .Net w praktyce

Interfejsy

Logowanie w środowisku mikroserwisowym .Net w praktyce

W aplikacji typu serwisowego można wyróżnić dwa główne interfejsy jej interakcji z otoczeniem: oznaczymy je jako interfejs pionowy i poziomy. Interfejs pionowy to API webowe, przez które przychodzą wywołania z aplikacji klienckiej. Poziomy to broker wiadomości, który jest używany do wymiany danych z innymi wewnętrznymi serwisami.

Przyjrzyjmy się krokom wdrożenia korelacji w każdym z tych interfejsów.

Korelacja w zapytaniach HTTP

Aby uzyskać jak najwięcej informacji, musimy generować identyfikator korelacji jak najwcześniej w aktywności, tj. na bramce lub bezpośrednio na kliencie (mobilnym lub webowym). Ponieważ dzisiaj mamy do czynienia z aplikacją backendową, po prostu zaznaczymy na niej wymóg obecności nagłówka "X-Correlation-ID" we wszystkich zapytaniach do API webowego.

Dodajemy pakiet CorrelationID, którego funkcją jest pobieranie wartości z wymaganego nagłówka

Install-Package CorrelationID

Dodajmy go do potoku obsługi zapytania

public class Startup
{
    public void Configure(IApplicationBuilder application)
    {
        application
	    .UseCorrelationId(new CorrelationIdOptions
        {
            Header = "X-Correlation-ID",
            IncludeInResponse = false,
            UpdateTraceIdentifier = false,
            UseGuidForCorrelationId = false
        });
    }
}

Teraz z jego pomocą stworzymy prosty filtr 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 dodajemy go do kontrolera

[Route("[controller]")]
[ApiController]
[ServiceFilter(typeof(ApiRequestFilter))]
public class CarsController : ControllerBase
{

}

W wyniku tego kontroler zacznie zwracać 400 Bad request na wszystkie zapytania bez nagłówka z odpowiednim identyfikatorem.

Po tym, jak zaczęliśmy otrzymywać identyfikator od klienta, musimy dodać go do kontekstu rejestrowania, stworzymy do tego obramowującą warstwę:

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(httpContext);
            }
        }
        else
        {
            await _next(httpContext);
        }
    }
}

W naszej aplikacji używamy standardowego ILogger z pakietu Microsoft.Extensions.Logging.Abstractions, więc wartość będziemy dodawać za pomocą prostego rozszerzenia do niego.

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 potoku przetwarzania zapytania i uzyskujemy potrzebny rezultat.

public class Startup
{
    public void Configure(IApplicationBuilder application)
    {
        application.UseMiddleware();
    }
}

Teraz wszystkie aktywności, które powstają w wyniku zapytań do naszego API internetowego, zawierają identyfikator korelacyjny, dzięki któremu można je łatwo powiązać.

Logowanie w środowisku mikroserwisowym .Net w praktyce

Korelacja w wiadomościach brokera

Kolejnym krokiem będzie skonfigurowanie przekazywania i odbierania identyfikatora korelacji przez brokera wiadomości. W naszym przykładzie użyjemy RabbitMQ, a jako klienta wybierzemy framework MassTransit. Ponownie pominiemy początkową konfigurację pracy z MassTransit i przejdziemy od razu do ustawienia logowania.

Na początek możemy włączyć logi samego MassTransit, w tym celu dodamy do naszej aplikacji pakiet MassTransit.SerilogIntegration

Install-Package MassTransit.SerilogIntegration

Teraz po dodaniu loggera do ustawień MassTransit będziemy mogli widzieć logi frameworka.

services
    .AddSingleton(provider =>
        {
            return Bus.Factory.CreateUsingRabbitMq(cfg =>
            {
                cfg.UseSerilog();
            });
        });

Niech nasza aplikacja w odpowiedzi na żądanie POST wyśle zdarzenie SomethingDoneMessage z wartością „done”. Kontrakt takiej wiadomości można opisać w następujący sposób:

namespace MbMessages
{
    public interface ISomethingDoneMessageV1
    {
        string Value { get; }
    }
}

Wiadomości MassTransit są w zasadzie kopertą, do której wkładane są wiadomości brokera. 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"
  }
}

W wiadomości widoczne są pola serwisowe, które są potrzebne do działania samego frameworka, ale mamy możliwość dodawania do tej koperty swoich dodatkowych właściwości. Co więcej, MassTransit ma wbudowane środki do pracy z niektórymi opcjonalnymi polami, z których najbardziej nas interesuje identyfikator korelacji CorrelationId.

Dodajemy do kontraktu wiadomości interfejs CorrelatedBy:

namespace MbMessages
{
    public interface ISomethingDoneMessageV1 : CorrelatedBy
    {
        string Value { get; }
    }
}

Realizujemy go i będziemy przypisywać wartość 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 spojrzymy na zaktualizowaną wiadomość, zobaczymy, że identyfikator korelacji stał się nie tylko częścią naszej wiadomości, ale również częścią koperty — ten identyfikator będzie teraz również używany we wszystkich logach MassTransit, co oznacza, że będziemy znacznie łatwiej radzić sobie z problemami 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"
  }
}

Pozostaje nam skonfigurować logowanie tych właściwości pomocniczych wiadomości, w tym celu dodamy pakiet do projektu Serilog.Enrichers.MassTransitMessage. Pakiet dodaje filtr do przepływu przetwarzania wiadomości MassTransit, który umieszcza kontekst wiadomości w wątkowo-bezpiecznym stosie. Serilog odczytuje kontekst ze stosu i dodaje te dodatkowe właściwości do naszych obiektów logów.

Install-Package Serilog.Enrichers.MassTransitMessage

W MassTransit wstawiamy filtr

services
    .AddSingleton(provider =>
        {
            return Bus.Factory.CreateUsingRabbitMq(cfg =>
            {
                cfg.UseSerilog();
                cfg.UseSerilogMessagePropertiesEnricher();
            });
        });

A w konfiguracji Seriloga dodajemy wzbogacacz

Log.Logger = new LoggerConfiguration()
    .Enrich.FromMassTransitMessage()
    .CreateLogger();

Ponieważ aplikacja, która odbiera wiadomość z kolejki RabbitMQ, ma dostęp do wszystkich właściwości koperty MassTransit, możemy wykorzystać uzyskany identyfikator korelacji wewnątrz aplikacji konsumenta, a także przekazywać go dalej w całym łańcuchu wywołań.

W rezultacie nasze logi zawierają CorrelationId nie tylko w obrębie jednego serwisu, ale także podczas interakcji z innymi aplikacjami.

Logowanie w środowisku mikroserwisowym .Net w praktyce

Zatem uzyskany system logowania w aplikacjach .Net pozwala nam bez większych problemów korelować logi z zupełnie różnych mikroserwisów — nawet tych, które działają poprzez brokera wiadomości. A dzięki Elasticsearch możemy szybko i wygodnie analizować logi, tworząc w Kibanie potrzebne nam pulpity (przykład zaprezentowany na obrazku do posta).

Oczywiście, w tej formie logowanie nie pokryje złożonych scenariuszy interakcji między Twoimi usługami a różnymi zewnętrznymi systemami, ale wprowadzenie takiego porządku na samym początku rozwoju projektu to jedna z tych rzeczy, za które wielokrotnie podziękujesz sobie później.

Możesz się przyjrzeć kodowi źródłowemu uzyskanej systemu w projekcie: github.com/a-postx/YA.ServiceTemplate

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster