Journalisation dans un environnement de microservices .Net en pratique

Journalisation dans un environnement de microservices .Net en pratique

La journalisation est un outil très important pour les développeurs, mais lors de la création de systèmes distribués, elle devient une pierre qu'il faut poser dès les fondations de votre application, sinon la complexité du développement de microservices se fera rapidement sentir.

Avec .Net Core 3, il y a une excellente possibilité de transmettre le contexte de corrélation dans les en-têtes HTTP, donc si vos applications utilisent des appels HTTP directs pour l'interaction entre services, vous pouvez profiter de cette fonctionnalité intégrée. Cependant, si l'architecture de votre backend implique une interaction via un courtier de messages (RabbitMQ, Kafka, etc.), vous devez toujours vous occuper de la transmission du contexte de corrélation à travers ces messages vous-même.

Dans cet article, nous allons prendre une simple application d'API web et organiser la journalisation, qui sera

  • capable de maintenir une corrélation continue entre les journaux des services indépendants de sorte que l'on puisse facilement voir toutes les activités déclenchées par une requête spécifique depuis le client

  • avoir un point d'entrée unique avec une analyse pratique, afin que l'outil de journalisation puisse être utilisé même par le support, qui reçoit des questions telles que « j'ai eu une erreur dans mon application avec cet identifiant de requête »

Tout d'abord, nous devons nous décider sur le fournisseur de journalisation dans notre application. La principale exigence pour une journalisation moderne est la structure, c'est-à-dire que nous devons travailler non pas avec de simples messages texte, mais avec des objets. Grâce à ces journaux, nous pouvons facilement construire des représentations de nos messages sous différents angles et effectuer des analyses.

Pour notre application, nous utiliserons le package Serilog, qui a un excellent support pour la journalisation structurée et un riche système d'extensions. Je vais passer les étapes de configuration de base (vous pouvez trouver de nombreux articles à ce sujet) et faire l'hypothèse que

  • Serilog est déjà configuré et est le logger par défaut de votre fournisseur d'injection de dépendances

  • dans sa configuration, l'enrichissement des messages avec les propriétés du contexte est activé (Enrich.FromLogContext)

La prochaine étape consiste à choisir vers quel système de collecte centralisée de logs envoyer les messages de Serilog. Parmi les options les plus courantes de logiciels open source, nous allons choisir la pile ELK (Elasticsearch, Logstash et Kibana). Pour cela, utilisons l'offre de Logz.IO — après l'inscription à l'offre gratuite, nous avons à notre disposition toute la puissance du moteur de recherche Lucene.

Il ne nous reste plus qu'à ajouter à notre projet le paquet Serilog.Sinks.Logzio

Install-Package Serilog.Sinks.Logzio

Et à ajouter l'enrichisseur approprié dans la configuration de notre logger, en lui fournissant le token d'accès

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

En lançant l'application, nous pourrons observer nos messages non seulement dans la console, mais aussi dans Kibana.

Journalisation dans un environnement de microservices .Net en pratique

Interfaces

Journalisation dans un environnement de microservices .Net en pratique

Dans une application de type service, on peut distinguer deux principales interfaces d'interaction avec le monde extérieur, que nous désignerons comme verticale et horizontale. L'interface verticale est l'API web, par laquelle arrivent les appels de l'application cliente. L'horizontale est le broker de messages, utilisé pour échanger des données avec d'autres services internes.

Examinons les étapes de mise en œuvre de la corrélation à chacune de ces interfaces.

Corrélation dans les requêtes HTTP

Pour obtenir le maximum d'informations, nous devons générer un identifiant de corrélation le plus près possible du début de l'activité, c'est-à-dire au niveau du gateway ou directement sur le client (mobile ou web). Comme nous traitons aujourd'hui une application backend, nous allons simplement spécifier qu'un en-tête « X-Correlation-ID » est obligatoire pour toutes les requêtes vers l'API web.

Ajoutons le paquet CorrelationID, dont la fonction consiste à récupérer la valeur de l'en-tête dont nous avons besoin

Install-Package CorrelationID

Ajoutons-le dans le pipeline de traitement de la requête

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

Nous allons maintenant créer un simple filtre d'action avec cela :

classe publique scellée 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();
    }
}

Et nous l'ajouterons au contrôleur

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

}

En conséquence, le contrôleur renverra un 400 Bad Request pour toutes les requêtes sans en-tête avec l'identifiant correspondant.

Après avoir commencé à recevoir l'identifiant du client, nous devons l'ajouter au contexte de journalisation ; créons à cet effet un intermédiaire :

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

Dans notre application, nous utilisons le ILogger standard du package Microsoft.Extensions.Logging.Abstractions, donc nous ajouterons la valeur à l'aide d'une simple extension.

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

Ajoutons l'intermédiaire dans le pipeline de traitement des requêtes et obtenons le résultat souhaité.

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

Maintenant, toutes les activités générées par les requêtes à notre API Web contiennent l'identifiant de corrélation par lequel elles peuvent être facilement liées.

Journalisation dans un environnement de microservices .Net en pratique

Corrélation dans les messages du broker

La prochaine étape consiste à établir l'envoi et la réception de l'identifiant de corrélation via un courtier de messages. Dans notre exemple, nous allons utiliser RabbitMQ et, comme client, nous prendrons le framework MassTransit. Encore une fois, nous allons ignorer la configuration initiale de MassTransit et passer directement à la configuration des journaux.

Pour commencer, nous pouvons activer les journaux de MassTransit lui-même, ajoutons donc à notre application le paquet MassTransit.SerilogIntegration

Install-Package MassTransit.SerilogIntegration

Maintenant, après avoir ajouté le journaliseur à la configuration de MassTransit, nous pourrons voir les journaux du framework.

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

Faisons en sorte que notre application envoie un événement SomethingDoneMessage avec la valeur « done » en réponse à une requête POST. Le contrat d'un tel message peut être décrit comme suit :

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

Les messages de MassTransit sont essentiellement une enveloppe dans laquelle sont inclus les messages du courtier. L'enveloppe ressemble approximativement à ceci :

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

On peut voir dans le message des champs de service nécessaires au fonctionnement même du framework, mais nous avons la possibilité d'ajouter à cette enveloppe nos propres propriétés supplémentaires. De plus, MassTransit dispose de fonctionnalités intégrées pour travailler avec certains champs optionnels, parmi lesquels l'identifiant de corrélation CorrelationId nous intéresse particulièrement.

Ajoutons à l'interface du message l'interface CorrelatedBy :

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

Implémentons-le et assignons la valeur à la propriété CorrelationId lors de la création du message :

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

Si nous examinons le message mis à jour, nous verrons que l'identifiant de corrélation est devenu non seulement une partie de notre message, mais aussi du conteneur — cet identifiant sera désormais également utilisé dans tous les journaux de MassTransit, ce qui signifie qu'il nous sera beaucoup plus facile de résoudre les problèmes au niveau du courtier de messages.

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

Il ne nous reste plus qu'à configurer la journalisation de ces propriétés de message, pour cela, nous allons ajouter un paquet au projet. Serilog.Enrichers.MassTransitMessage. Ce paquet ajoute un filtre au pipeline de traitement des messages MassTransit, qui empile le contexte du message dans une pile thread-safe. Serilog lit le contexte de la pile et ajoute ces propriétés supplémentaires à nos objets de journal.

Install-Package Serilog.Enrichers.MassTransitMessage

Dans MassTransit, nous insérons le filtre

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

Et dans la configuration de Serilog, nous ajoutons l'enrichisseur

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

Étant donné que l'application qui reçoit le message de la file d'attente RabbitMQ a accès à toutes les propriétés du conteneur MassTransit, nous pouvons utiliser l'identifiant de corrélation obtenu à l'intérieur de l'application consommatrice et également le transmettre tout au long de la chaîne d'appels.

En conséquence, nos journaux contiennent le CorrelationId non seulement au sein d'un seul service, mais aussi lors des interactions avec d'autres applications.

Journalisation dans un environnement de microservices .Net en pratique

Ainsi, le système de journalisation obtenu dans les applications .Net nous permet de corréler les journaux de microservices totalement différents — même ceux qui fonctionnent via un courtier de messages. Et avec Elasticsearch, nous pouvons rapidement et facilement analyser les journaux en construisant dans Kibana les tableaux de bord nécessaires (un exemple est fourni dans l'image jointe au post).

Bien sûr, sous cette forme, la journalisation ne couvrira pas des cas d'interaction complexes entre vos services et divers systèmes externes, mais établir un tel ordre dès le début du développement du projet est l'une de ces choses pour lesquelles vous vous remercierez plus d'une fois.

Vous pouvez examiner le code source du système résultant dans le projet : github.com/a-postx/YA.ServiceTemplate

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster