
La journalisation est un outil très important pour un développeur, mais lors de la construction de systèmes distribués, elle devient une pierre qui doit être posée directement dans les fondations de votre application, sinon la complexité du développement de microservices se fera très vite sentir.
.Net Core 3 a ajouté un excellent , donc si vos applications utilisent des appels HTTP directs pour la communication interservices, vous pouvez profiter de cette fonctionnalité en boîte. Cependant, si l'architecture de votre backend implique une interaction via un courtier de messages (RabbitMQ, Kafka, etc.), vous devez toujours vous occuper vous-même du sujet de la transmission du contexte de corrélation à travers ces messages.
Dans cet article, nous prendrons une simple application API Web et organiserons la journalisation, qui permettra
conserver une corrélation de bout en bout entre les journaux des services indépendants afin que vous puissiez facilement voir toutes les activités provoquées par une demande spécifique du client
avoir un point d'entrée unique avec une analyse pratique afin que même le support puisse utiliser l'outil de journalisation, auquel des questions telles que "J'ai eu une erreur avec tel ou tel ID de demande" sont apparues dans l'application
Tout d’abord, nous devons décider du fournisseur de journalisation dans notre application. La principale exigence d’une exploitation forestière moderne est la structure, c’est-à-dire nous ne devrions pas travailler avec des messages texte plats, mais avec des objets. Grâce à ces journaux, nous pouvons facilement créer des vues de nos messages dans différentes sections et effectuer des analyses.
Pour notre application, nous utiliserons le package Serilog (Serilog), qui offre un excellent support pour la journalisation structurelle et un système complémentaire riche. J'omettrai les étapes de base pour sa configuration (vous pouvez trouver un grand nombre d'articles sur ce sujet) et supposerai que
Le serilog est déjà configuré et est l'enregistreur par défaut de votre fournisseur d'injection de dépendances
L'enrichissement des messages avec des propriétés de contexte est activé dans sa configuration (Enrich.FromLogContext)
L'étape suivante consiste à choisir à quel système de journalisation centralisé envoyer les messages de Serilog. L'option open source la plus courante aujourd'hui est peut-être la pile ELK (Elasticsearch, Logstash et Kibana), et prenons-la. Pour ce faire, nous utilisons la proposition de - après vous être inscrit à un forfait gratuit, toute la puissance du moteur de recherche Lucene est entre nos mains.
Il ne nous reste plus qu'à ajouter un package à notre projet
Install-Package Serilog.Sinks.Logzio
Et ajoutez l'Enricher approprié à la configuration de notre enregistreur en lui fournissant un jeton d'accès
LoggerConfiguration loggerConfig = new LoggerConfiguration();
loggerConfig.WriteTo.Logzio(secrets.LogzioToken, 10, TimeSpan.FromSeconds(10), null, LogEventLevel.Debug);
En exécutant l'application, nous pourrons observer nos messages non seulement dans la console, mais aussi dans Kibane.

Interfaces

Dans une application de type service, on peut distinguer deux interfaces principales de son interaction avec le monde extérieur, nous les désignerons comme verticales et horizontales. Une interface verticale est une API Web via laquelle arrivent les appels d’une application cliente. Horizontal est un courtier de messages utilisé pour échanger des données avec d'autres services internes.
Considérons les étapes de mise en œuvre de la corrélation sur chacune de ces interfaces.
Corrélation dans les requêtes HTTP
Pour obtenir le plus d'informations possible, nous devons générer un identifiant de corrélation le plus proche possible du début de l'activité, c'est-à-dire sur la passerelle ou directement sur le client (mobile ou web). Puisque nous avons affaire aujourd'hui à une application back-end, nous y indiquerons simplement l'exigence de l'en-tête obligatoire « X-Correlation-ID » dans toutes les requêtes vers l'API Web.
Ajouter un paquet , dont la fonction est de prendre la valeur de l'en-tête dont nous avons besoin
Install-Package CorrelationID
Ajoutez-le au pipeline de traitement des demandes
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application
.UseCorrelationId(new CorrelationIdOptions
{
Header = "X-Correlation-ID",
IncludeInResponse = false,
UpdateTraceIdentifier = false,
UseGuidForCorrelationId = false
});
}
}
Créons maintenant un filtre d'action simple avec :
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();
}
}
Et ajoutez-le au contrôleur
[Route("[controller]")]
[ApiController]
[ServiceFilter(typeof(ApiRequestFilter))]
public class CarsController : ControllerBase
{
}
En conséquence, le contrôleur affichera 400 Bad request pour toutes les requêtes sans en-tête avec l'identifiant correspondant.
Après avoir commencé à recevoir un identifiant du client, nous devons l'ajouter au contexte de journalisation, nous allons créer une couche de cadrage pour cela :
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);
}
}
}
Dans notre application, nous utilisons le standard ILogger du package Microsoft.Extensions.Logging.Abstractions, nous y ajouterons donc une valeur en utilisant 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));
}
Nous ajoutons une couche au pipeline de traitement des demandes et obtenons le résultat souhaité.
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application.UseMiddleware<CorrelationIdContextLogger>();
}
}
Désormais, toutes les activités générées par les requêtes adressées à notre API Web contiennent un identifiant de corrélation grâce auquel elles peuvent être facilement liées.

Corrélation dans les messages des courtiers
L'étape suivante consiste à configurer la transmission et la réception de l'identifiant de corrélation via le courtier de messages. Dans notre exemple, nous utiliserons RabbitMQ, et en tant que client nous prendrons le framework MassTransit (MassTranzit). Encore une fois, ignorons la configuration initiale pour travailler avec MassTransit et passons directement à la configuration de la journalisation.
Pour commencer, nous pouvons activer les journaux du MassTransit lui-même, pour cela nous ajouterons un package à notre application
Install-Package MassTransit.SerilogIntegration
Maintenant, après avoir ajouté l'enregistreur aux paramètres MassTransit, nous pourrons voir les journaux du framework.
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
});
});
Laissez notre application envoyer l'événement SomethingDoneMessage avec la valeur "done" en réponse à la 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 MassTransit sont essentiellement une enveloppe contenant des messages de courtier. L'enveloppe ressemble à 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"
}
}
Le message montre les champs de service nécessaires au fonctionnement du framework lui-même, mais nous avons la possibilité d'ajouter nos propres propriétés supplémentaires à cette enveloppe. De plus, MassTransit dispose d'outils intégrés pour travailler avec certains champs facultatifs, le plus intéressant qui nous intéresse est le CorrelationId.
Ajoutez l'interface CorratedBy au contrat de message :
namespace MbMessages
{
public interface ISomethingDoneMessageV1 : CorrelatedBy<Guid>
{
string Value { get; }
}
}
Implémentons-le et attribuons une valeur à la propriété CorrelationId lors de la création d'un 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 regardons le message mis à jour, nous verrons que l'identifiant de corrélation est devenu non seulement une partie de notre message, mais aussi une partie de l'enveloppe - cet identifiant sera désormais également utilisé dans tous les journaux MassTransit, ce qui signifie que ce sera beaucoup plus facile pour nous de traiter 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": "Hello"
}
}
Il ne nous reste plus qu'à configurer la journalisation de ces propriétés de service du message, pour cela nous ajouterons un package au projet . Le package ajoute un filtre au pipeline de traitement des messages MassTransit qui pousse le contexte du message sur une pile thread-safe. Le sérilogue lit le contexte de la pile et ajoute ces propriétés supplémentaires à nos objets de journal.
Install-Package Serilog.Enrichers.MassTransitMessage
Insérer un filtre dans MassTransit
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
cfg.UseSerilogMessagePropertiesEnricher();
});
});
Et dans la configuration Serilog, ajoutez un Enricher
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 de l'enveloppe MassTransit, nous pouvons utiliser l'identifiant de corrélation reçu à l'intérieur de l'application consommatrice, et également le transmettre plus loin dans la chaîne d'appel.
En conséquence, nos journaux ont commencé à contenir CorrelationId non seulement au sein du même service, mais également lors de l'interaction avec d'autres applications.

Ainsi, le système de journalisation qui en résulte dans les applications .Net nous permet de corréler sans aucun problème les journaux de microservices complètement différents - même ceux qui fonctionnent via un courtier de messages. Et avec l'aide d'Elasticsearch, nous pouvons analyser rapidement et facilement les journaux en créant les tableaux de bord dont nous avons besoin dans Kibana (un exemple est montré dans l'image de l'article).
Bien entendu, la connexion sous ce formulaire ne couvrira pas les options complexes d'interaction de vos services et de divers systèmes externes, mais établir un tel ordre au tout début du développement du projet est l'une de ces choses pour lesquelles vous vous remercierez plus d'une fois.
Vous pouvez comprendre le code source du système résultant dans le projet :
Source: habr.com
