Implémentation d'un système de file d'attente avec .NET Core pour la journalisation des requêtes

Environnement : Projet .NET Core 3.1

Remarque : Cette solution étant un projet de test personnel, certaines données de la file d'attente risquent d'être perdues lors du redémarrage. Pour les applications exigeant une persistance des données, cette approche n'est pas adaptée et nécessiterait une implémentation avec stockage persistant.

De plus, lors du déploiement sur Linux Docker, le recyclage automatique n'est généralement pas activé, mais pour IIS, une configuration simple est requise pour éviter le recyclage. Voici la procédure :

Dans IIS, trouvez le pool d'applications utilisé par ce site, cliquez sur Paramètres avancés...

Recyclage - Intervalle de temps fixe : modifiez à 0

Recyclage - Limite de mémoire virtuelle/dédiée : modifiez à 0

Modèle de processus - Délai d'inactivité : modifiez à 0

1 : Vue d'ensemble des résultats

1.1 : Journalisation des données dans la base de données

1.2 : Journalisation des données dans un fichier

2 : Filtre de journalisation

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Diagnostics;
using QzjcService.Models.Dto.LogModels;
using QzjcService.Controllers;
using SqlSugar.IOC;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using QzjcService.Helper;
using Microsoft.AspNetCore.Mvc;
using QzjcService.Models;

namespace QzjcService.Filters
{
    public class JournalisationActionFilter : ActionFilterAttribute
    {
        private static Stopwatch _chronometre = new Stopwatch();
        public static double? duree = 0;
        private JournalisationActionModel journalisation = new JournalisationActionModel();
        private readonly ILogger<JournalisationActionFilter> _journal;

        public JournalisationActionFilter(ILogger<JournalisationActionFilter> journal)
        {
            _journal = journal;
        }

        public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            _chronometre.Start();
            string nomControleur = context.ActionDescriptor.RouteValues["controller"];
            string nomAction = context.ActionDescriptor.RouteValues["action"];
            string methode = context.HttpContext.Request.Method;
            string queryString = context.HttpContext.Request.QueryString.Value;
            string parametres = JsonConvert.SerializeObject(context.ActionArguments);
            string url = context.HttpContext.Request.Host + context.HttpContext.Request.Path;
            var logStr = string.Format("\r\n[URL] :{0} \r\n[Méthode] :{1} \r\n[Paramètres URL] :{2} \r\n[Paramètres corps] :{3}", new object[] { url, methode, queryString, parametres });
            journalisation.NomControleur = nomControleur;
            journalisation.NomAction = nomAction;

            // Méthode : 0 pour GET, 1 pour POST
            journalisation.Methode = (methode.Equals("get", StringComparison.InvariantCultureIgnoreCase) || methode.Equals("httpget", StringComparison.InvariantCultureIgnoreCase)) ? 0 : 1;
            journalisation.DateCreation = DateTime.Now;
            journalisation.ParametresRequete = logStr;
            var _context = context.HttpContext;
            if (_context != null)
            {
                var tokenStr = context.HttpContext.Request.Headers[ConstData.Authorization].ToString();
                journalisation.utilisateurId = string.IsNullOrEmpty(tokenStr) ? 0 : TokenHelper.GetUserModel(context.HttpContext)?.UserId;
            }
            await base.OnActionExecutionAsync(context, next);
        }
        
        public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
        {
            _chronometre.Stop();
            duree = 0;
            if (context.Result is ObjectResult result)
            {
                if (result != null)
                    journalisation.ParametresReponse = JsonConvert.SerializeObject(result.Value);
            }
            duree = _chronometre.Elapsed.TotalMilliseconds;
            journalisation.DureeMs = duree.Value;
            _chronometre.Reset();
            journalisation.ParametresRequete += "\r\n[Paramètres réponse] :" + journalisation.ParametresReponse + "\r\n============================================";
            _journal.LogCritical(journalisation.ParametresRequete);
            //  await DbScoped.Sugar.Insertable<JournalisationActionModel>(journalisation).ExecuteCommandAsync();
            await FileAttenteLogHelper.AjouterDansFileAttenteAsync(journalisation);
            await base.OnResultExecutionAsync(context, next);
        }

    }
}

3 : Service d'arrière-plan avec minuterie intégrée

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using QzjcService.Helper;
using QzjcService.Models.Dto.LogModels;
using SqlSugar.IOC;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace QzjcService.Services
{
    public class ServiceJournalisation : BackgroundService
    {
        private readonly ILogger<ServiceJournalisation> _journal;
        public ServiceJournalisation(ILogger<ServiceJournalisation> journal)
        {
            _journal = journal;
        }
        
        protected override async Task ExecuteAsync(CancellationToken tokenArret)
        {
            await Task.Factory.StartNew(async () =>
            {
                while (!tokenArret.IsCancellationRequested)
                {
                    try
                    {
                        var modele = await FileAttenteLogHelper.RetirerDeLaFileAttenteAsync();
                        if (modele != null)
                        {
                            Console.WriteLine($"===={DateTime.Now}=ServiceJournalisation=RetraitFileAttente  Réussi====");
                            await DbScoped.Sugar.Insertable<JournalisationActionModel>(modele).ExecuteCommandAsync();
                        }
                        await Task.Delay(3000);
                    }
                    catch (Exception ex)
                    {
                        _journal.LogError($"====={DateTime.Now}===ServiceJournalisation Erreur :=={ex.Message}==========");
                        continue;
                    }
                }
            });
        }
    }
}

4 : Modèle de données pour la journalisation

using SqlSugar;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;

namespace QzjcService.Models.Dto.LogModels
{
    [Table("qzjc_journalisation_actions")]
    [SugarTable("qzjc_journalisation_actions")]
    public class JournalisationActionModel
    {
        /// <summary>
        /// Identifiant unique
        /// </summary>
        [Column("id")]
        [SugarColumn(ColumnName = "id")]
        public int id { get; set; }

        /// <summary>
        /// Nom du contrôleur
        /// </summary>
        [Column("nom_controleur")]
        [SugarColumn(ColumnName = "nom_controleur")]
        public string NomControleur { get; set; }

        /// <summary>
        /// Nom de l'action
        /// </summary>
        [Column("nom_action")]
        [SugarColumn(ColumnName = "nom_action")]
        public string NomAction { get; set; }

        /// <summary>
        /// Paramètres de la requête
        /// </summary>
        [Column("parametres_requete")]
        [SugarColumn(ColumnName = "parametres_requete")]
        public string ParametresRequete { get; set; }

        /// <summary>
        /// Durée d'exécution en millisecondes
        /// </summary>
        [Column("duree_ms")]
        [SugarColumn(ColumnName = "duree_ms")]
        public double DureeMs { get; set; }

        /// <summary>
        /// Date et heure de création
        /// </summary>
        [Column("date_creation")]
        [SugarColumn(ColumnName = "date_creation")]
        public DateTime DateCreation { get; set; }

        /// <summary>
        /// ID de l'utilisateur
        /// </summary>
        [Column("id_utilisateur")]
        [SugarColumn(ColumnName = "id_utilisateur")]
        public int? utilisateurId { get; set; }

        [Column("methode")]
        [SugarColumn(ColumnName = "methode")]
        public int? Methode { get; set; }

        [Column("parametres_reponse")]
        [SugarColumn(ColumnName = "parametres_reponse")]
        public string ParametresReponse { get; set; }
    }
}

5 : Implémentation de la file d'attente avec Queue

using QzjcService.Models.Dto.LogModels;
using System.Collections;
using System.Threading.Channels;
using System.Threading.Tasks;

namespace QzjcService.Helper
{
    public static class FileAttenteLogHelper
    {
        public static Queue fileAttente;
        private static readonly object Verrou = new object();
        static FileAttenteLogHelper()
        {
            fileAttente = new Queue();
        }
        
        private static async Task<Queue> ObtenirFileAttenteAsync()
        {
            if (fileAttente == null)
            {
                lock (Verrou)
                {
                    if (fileAttente == null)
                        fileAttente = new Queue();
                }
            }
            await Task.CompletedTask;
            return fileAttente;
        }

        public static async Task<bool> AjouterDansFileAttenteAsync(JournalisationActionModel modele)
        {
            try
            {
                fileAttente = await ObtenirFileAttenteAsync();
                fileAttente.Enqueue(modele);
                return true;
            }
            catch (System.Exception)
            {
                return false;
            }
        }
        
        public static async Task<JournalisationActionModel> RetirerDeLaFileAttenteAsync()
        {
            try
            {
                fileAttente = await ObtenirFileAttenteAsync();
                objet obj = fileAttente.Dequeue();
                if (obj != null)
                {
                    JournalisationActionModel modele = obj as JournalisationActionModel;
                    return modele ?? null;
                }
                return null;
            }
            catch (System.Exception)
            {
                return null;
            }
        }
    }
}

6 : Configuration de Serilog

using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Serilog;
using Serilog.Events;

namespace QzjcService
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string dateActuelle = DateTime.Now.ToString("yyyy-MM-dd");
            string fichierLog = $"Journaux/{dateActuelle}_logs.txt";
            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Warning()
                  .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
                  .MinimumLevel.Override("System", LogEventLevel.Warning)
                  .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
                  .Enrich.FromLogContext()
                  .WriteTo.Async(c => c.File(fichierLog, rollOnFileSizeLimit: true, fileSizeLimitBytes: 1024 * 1024 * 10, retainedFileCountLimit: 30))
                  .CreateLogger();
            try
            {
                Log.Information("=========Démarrage de l'hôte web==========");
                CreerHote(args).Build().Run();
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, "L'hôte s'est arrêté de manière inattendue!");
            }
            finally
            {
                Log.CloseAndFlush();
            }
        }

        public static IHostBuilder CreerHote(string[] args) =>
            Host.CreateDefaultBuilder(args)
             .ConfigureLogging((contexteHebergement, constructeur) =>
             {
                 constructeur.ClearProviders();
             })
                .ConfigureWebHostDefaults(construteurWeb =>
                {
                    construteurWeb.ConfigureKestrel(c => { c.Limits.MaxRequestBodySize = 1024 * 1024 * 300; });
                    construteurWeb.UseUrls("http://*:4444");
                    construteurWeb.UseStartup<Startup>();
                })
               .UseSerilog();
    }
}<br></br><PackageReference Include="Serilog.Extensions.Hosting" Version="3.1.0" /><br></br><PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" /><br></br><PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" /><br></br><PackageReference Include="Serilog.Sinks.Elasticsearch" Version="8.2.0" /><br></br><PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /><br></br><br></br>

Étiquettes: .NET Core journalisation file d'attente background service Serilog

Publié le 2 juin à 04h04