Logování v mikroslužbové architektuře .Net na praktické úrovni

Logování v mikroslužbové architektuře .Net na praktické úrovni

Logování je pro vývojáře velmi důležitým nástrojem, ale při vytváření distribuovaných systémů se stává kamenem, který je třeba vložit přímo do základů vaší aplikace, jinak se složitost vývoje mikroslužeb velmi rychle projeví.

Ve .Net Core 3 byla přidána vynikající možnost předávat kontext korelace v HTTP záhlaví, takže pokud vaše aplikace používají přímé HTTP volání pro mezislužební interakci, můžete využít tuto integrovanou funkčnost. Nicméně, pokud architektura vašeho backendu předpokládá interakci prostřednictvím messaging brokera (RabbitMQ, Kafka atd.), musíte se stále postarat o předání korelačního kontextu prostřednictvím těchto zpráv sami.

V tomto článku vezmeme jednoduchou webovou API aplikaci a uspořádáme logování, které bude

  • udržovat průběžnou korelaci mezi logy nezávislých služeb, aby bylo možné snadno zkontrolovat všechny aktivity, které byly vyvolány konkrétní žádostí od klienta

  • mít jediný vstupní bod s pohodlnou analýzou, aby mohl nástroj pro logování používat i tým podpory, který dostává otázky jako „v aplikaci mi vyskočila chyba s tímto ID žádosti“

Nejprve se musíme rozhodnout pro poskytovatele logování v naší aplikaci. Hlavním požadavkem na moderní logování je strukturovanost, tj. musíme pracovat ne s plochými textovými zprávami, ale s objekty. Díky těmto logům můžeme snadno vytvářet zobrazení našich zpráv v různých kontextech a provádět analýzu.

Pro naši aplikaci využijeme balíček Serilog, který má vynikající podporu pro strukturované logování a bohatý systém rozšíření. Opustím základní kroky jeho konfigurace (můžete najít spoustu článků na toto téma) a budu předpokládat, že

  • Serilog je již nakonfigurován a je výchozím loggerem vašeho poskytovatele závislostí

  • jeho konfiguraci je zahrnuto obohacení zpráv vlastnostmi kontextu (Enrich.FromLogContext)

Dalším krokem je vybrat systém centralizovaného sběru logů, do kterého budeme posílat zprávy z Serilog. Nejrozšířenější variantou z otevřeného softwaru je dnes stack ELK (Elasticsearch, Logstash a Kibana), a ten použijeme. K tomu využijeme nabídku od Logz.IO — po registraci na bezplatném tarifu máme k dispozici veškerou sílu vyhledávacího enginu Lucene.

Zbývá nám přidat do našeho projektu balíček Serilog.Sinks.Logzio

Install-Package Serilog.Sinks.Logzio

A přidat odpovídající enrichers do konfigurace našeho loggeru, dodávající mu přístupový token

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

Po spuštění aplikace budeme moci sledovat naše zprávy nejen v konzoli, ale také v Kibana.

Logování v mikroslužbové architektuře .Net na praktické úrovni

Rozhraní

Logování v mikroslužbové architektuře .Net na praktické úrovni

V aplikaci typu service lze identifikovat dvě hlavní rozhraní její interakce se světem, označme je jako vertikální a horizontální. Vertikální rozhraní je webové API, přes které přicházejí volání z klientské aplikace. Horizontální je zprostředkovatel zpráv, který se používá k výměně dat s jinými interními službami.

Podívejme se na etapy zavádění korelace na každém z těchto rozhraní.

Korelace v HTTP požadavcích

Abychom získali co nejvíce informací, musíme generovat identifikátor korelace co nejblíže začátku aktivity, tj. na bráně nebo přímo na klientovi (mobilním či webovém). Protože se dnes zabýváme backendovou aplikací, tak zavedeme požadavek na povinnou hlavičku „X-Correlation-ID“ ve všech požadavcích na webové API.

Přidáváme balíček CorrelationID, jehož funkcí je získat hodnotu z potřebné hlavičky

Install-Package CorrelationID

Přidáme ho do zpracovatelského kanálu požadavku

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

Nyní s jeho pomocí vytvoříme jednoduchý action filtr:

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();
    }
}

A přidáme jej do kontroleru

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

}

V důsledku toho kontroler vrátí 400 Bad request na všechny požadavky bez hlavičky se příslušným identifikátorem.

Poté, co jsme začali přijímat identifikátor od klienta, musíme jej přidat do kontextu logování, uděláme pro to obalovou vrstvu:

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);
        }
    }
}

V naší aplikaci používáme standardní ILogger z balíčku Microsoft.Extensions.Logging.Abstractions, takže hodnotu budeme přidávat pomocí jednoduchého rozšíření.

public static IDisposable BeginScopeWith(this ILogger logger, params (string key, object value)[] keys)
{
    return logger.BeginScope(keys.ToDictionary(x => x.key, x => x.value));
}

Přidáváme vrstvu do zpracovatelského konveje a dosahujeme požadovaného výsledku.

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

Nyní všechny aktivity, které jsou generovány požadavky na naše webové API, obsahují korelační identifikátor, podle kterého je lze snadno spojit.

Logování v mikroslužbové architektuře .Net na praktické úrovni

Korelace ve zprávách brokera

Dalším krokem je nastavení přenosu a příjmu korelačního identifikátoru přes zprostředkovatele zpráv. V našem příkladu použijeme RabbitMQ a jako klienta vezmeme framework MassTransit. Opět se zaměříme přímo na nastavení logování.

Nejprve můžeme zapnout logy samotného MassTransit, k tomu do naší aplikace přidáme balíček MassTransit.SerilogIntegration

Install-Package MassTransit.SerilogIntegration

Nyní po přidání loggeru do nastavení MassTransit uvidíme logy frameworku.

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

Ať naše aplikace v reakci na POST žádost odešle událost SomethingDoneMessage s hodnotou „done“. Smlouvu takové zprávy lze popsat takto:

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

Zprávy MassTransit jsou v podstatě obálkou, do které jsou vloženy zprávy zprostředkovatele. Obálka vypadá přibližně takto:

{
  "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"
  }
}

Ve zprávě jsou viditelná systémová pole, která jsou nezbytná pro fungování samotného frameworku, ale máme možnost přidávat do této obálky také naše vlastní dodatečné vlastnosti. Navíc MassTransit má zabudované nástroje pro práci s některými volitelnými poli, přičemž nás nejvíce zajímá identifikátor korelace CorrelationId.

Přidáme k smlouvě zprávy rozhraní CorrelatedBy:

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

Implementujeme ho a budeme při vytváření zprávy přiřazovat hodnotu vlastnosti CorrelationId:

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; }
}

Pokud se podíváme na aktualizovanou zprávu, uvidíme, že identifikátor korelace se stal součástí naší zprávy, ale také součástí obálky — tento identifikátor bude nyní také používán ve všech logech MassTransit, což znamená, že nám to velmi usnadní řešení problémů na úrovni zprostředkovatele zpráv.

{
  "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": "Ahoj"
  }
}

zbývá nastavit logování těchto provozních vlastností zprávy, k tomu přidáme do projektu balíček Serilog.Enrichers.MassTransitMessage. Balíček přidává filtr do zpracovatelského řetězce zpráv MassTransit, který ukládá kontext zprávy do thread-safe zásobníku. Serilog čte kontext ze zásobníku a přidává tyto další vlastnosti do našich logovacích objektů.

Install-Package Serilog.Enrichers.MassTransitMessage

V MassTransit přidáváme filtr

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

A do konfigurace Serilogu přidáváme enrichér

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

Jelikož aplikace, která přijímá zprávu z fronty RabbitMQ, má přístup ke všem vlastnostem obálky MassTransit, můžeme použít získaný identifikátor korelace uvnitř aplikace spotřebitele a také jej předávat dál v celé řetězci volání.

V důsledku toho naše logy nyní obsahují CorrelationId nejen v rámci jednoho servisu, ale i při interakci s ostatními aplikacemi.

Logování v mikroslužbové architektuře .Net na praktické úrovni

Takže získaný systém logování v .Net aplikacích nám umožňuje bez větších problémů korelovat logy z naprosto různých mikroslužeb — dokonce i těch, které pracují přes zprostředkovatele zpráv. A s pomocí Elasticsearch můžeme rychle a pohodlně provádět analýzu logů tím, že v Kibana vytvoříme potřebné dashboardy (některé příklady zasíláme na obrázku k příspěvku).

Samozřejmě, v této podobě logování nepokrývá složité varianty interakce vašich služeb a různých externích systémů, ale vytvoření takového pořádku na začátku vývoje projektu je jednou z těch věcí, za které si později sami poděkujete.

Můžete se podívat na zdrojový kód vzniklého systému v projektu: github.com/a-postx/YA.ServiceTemplate

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster