Merge branch 'dev' into j3/TODO_erreurs
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Webzine.Business.Contracts\Webzine.Business.Contracts.csproj" />
|
||||
<ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" />
|
||||
<ProjectReference Include="..\Webzine.Repository.Contracts\Webzine.Repository.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
13
Webzine.WebApplication/Configuration/Middlewares.cs
Normal file
13
Webzine.WebApplication/Configuration/Middlewares.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Webzine.WebApplication.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Options de seuil pour la détection des opérations EF Core lentes.
|
||||
/// </summary>
|
||||
public class EfPerformanceOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Obtient ou définit le seuil en millisecondes au-delà duquel une commande SQL est journalisée.
|
||||
/// Valeur par défaut : 200 ms.
|
||||
/// </summary>
|
||||
public int SeuilMs { get; set; } = 200;
|
||||
}
|
||||
@@ -51,6 +51,8 @@ namespace Webzine.WebApplication.Controllers
|
||||
return this.RedirectToAction("Index");
|
||||
}
|
||||
|
||||
this.titreRepository.IncrementNbLectures(titre);
|
||||
|
||||
var vm = new TitreDetail
|
||||
{
|
||||
Details = new TitreContent
|
||||
@@ -110,10 +112,11 @@ namespace Webzine.WebApplication.Controllers
|
||||
if (titre == null)
|
||||
{
|
||||
this.logger.LogWarning("Impossible d'ajouter un like. Titre ID {Id} introuvable.", model.IdTitre);
|
||||
return this.RedirectToAction("Index");
|
||||
}
|
||||
|
||||
titre.NbLikes++;
|
||||
else
|
||||
{
|
||||
this.titreRepository.IncrementNbLikes(titre);
|
||||
}
|
||||
|
||||
return this.RedirectToAction("Details", new { id = model.IdTitre });
|
||||
}
|
||||
|
||||
146
Webzine.WebApplication/Interceptors/EfSlowQueryInterceptor.cs
Normal file
146
Webzine.WebApplication/Interceptors/EfSlowQueryInterceptor.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
namespace Webzine.WebApplication.Interceptors;
|
||||
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics;
|
||||
|
||||
using Configuration;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Intercepteur EF Core qui journalise uniquement les commandes SQL dépassant le seuil configuré.
|
||||
/// Remonte la pile d'appels pour identifier la méthode repository (<c>Webzine.Repository.*</c>) à l'origine de la requête.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Références :</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// EF Core interceptors (doc officielle) :
|
||||
/// <see href="https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors"/>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <see cref="DbCommandInterceptor"/> API :
|
||||
/// <see href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.diagnostics.dbcommandinterceptor"/>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// Exemple de slow-query interceptor (SO) :
|
||||
/// <see href="https://medium.com/@sudipdevdev/how-to-detect-and-log-slow-queries-in-entity-framework-core-e2ab71024849"/>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <see cref="System.Diagnostics.StackTrace"/> pour remonter l'appelant :
|
||||
/// <see href="https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.stacktrace"/>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// Enregistrement via <c>AddInterceptors</c> :
|
||||
/// <see href="https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#registering-interceptors"/>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class EfSlowQueryInterceptor : DbCommandInterceptor
|
||||
{
|
||||
private readonly ILogger<EfSlowQueryInterceptor> logger;
|
||||
private readonly int seuilMs;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EfSlowQueryInterceptor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Le service de journalisation injecté pour suivre les opérations de l'intercepteur.</param>
|
||||
/// <param name="options">Les options de performance EF injectées pour récupérer le seuil de lenteur configuré.</param>
|
||||
public EfSlowQueryInterceptor(ILogger<EfSlowQueryInterceptor> logger, IOptions<EfPerformanceOptions> options)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.seuilMs = options.Value.SeuilMs;
|
||||
|
||||
this.logger.LogDebug("[EfSlowQueryInterceptor] Constructeur appelé — seuil : {SeuilMs} ms.", this.seuilMs);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result)
|
||||
{
|
||||
this.JournaliserSiLent(eventData.Duration);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask<DbDataReader> ReaderExecutedAsync(DbCommand command, CommandExecutedEventData eventData, DbDataReader result, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.JournaliserSiLent(eventData.Duration);
|
||||
return ValueTask.FromResult(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int NonQueryExecuted(DbCommand command, CommandExecutedEventData eventData, int result)
|
||||
{
|
||||
this.JournaliserSiLent(eventData.Duration);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask<int> NonQueryExecutedAsync(DbCommand command, CommandExecutedEventData eventData, int result, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.JournaliserSiLent(eventData.Duration);
|
||||
return ValueTask.FromResult(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? ScalarExecuted(DbCommand command, CommandExecutedEventData eventData, object? result)
|
||||
{
|
||||
this.JournaliserSiLent(eventData.Duration);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask<object?> ScalarExecutedAsync(DbCommand command, CommandExecutedEventData eventData, object? result, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.JournaliserSiLent(eventData.Duration);
|
||||
return ValueTask.FromResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remonte la pile d'appels pour trouver la première méthode dans <c>Webzine.Repository</c>.
|
||||
/// Toutes les requêtes EF Core du projet transitent par ce namespace, ce qui garantit
|
||||
/// un résultat pertinent sans parcourir l'intégralité de la stack.
|
||||
/// </summary>
|
||||
/// <returns>Chaîne <c>Classe.Méthode</c> ou <c>"inconnu"</c> si rien trouvé.</returns>
|
||||
/// <remarks>
|
||||
/// <see cref="StackTrace"/> est instancié uniquement quand le seuil est dépassé,
|
||||
/// ce qui évite tout impact sur le chemin nominal.
|
||||
/// Ref : <see href="https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.stacktrace"/>.
|
||||
/// </remarks>
|
||||
private static string TrouverAppelantRepository()
|
||||
{
|
||||
// skipFrames: 1 pour sauter TrouverAppelantRepository elle-même
|
||||
// fNeedFileInfo: false — on ne veut pas les numéros de ligne (coût supplémentaire inutile)
|
||||
var frames = new StackTrace(skipFrames: 1, fNeedFileInfo: false).GetFrames();
|
||||
|
||||
foreach (var frame in frames)
|
||||
{
|
||||
var methode = frame.GetMethod();
|
||||
if (methode?.DeclaringType?.Namespace?.StartsWith("Webzine.Repository", StringComparison.Ordinal) == true)
|
||||
{
|
||||
return $"{methode.DeclaringType.Name}.{methode.Name}";
|
||||
}
|
||||
}
|
||||
|
||||
return "inconnu";
|
||||
}
|
||||
|
||||
private void JournaliserSiLent(TimeSpan duree)
|
||||
{
|
||||
if (duree.TotalMilliseconds > this.seuilMs)
|
||||
{
|
||||
var appelant = TrouverAppelantRepository();
|
||||
|
||||
this.logger.LogWarning(
|
||||
"[EfSlowQueryInterceptor] Opération EF Core lente détectée — durée réelle : {DureeMs} ms — seuil : {SeuilMs} ms — dépassement : +{Depassement} ms.{NewLine}Appelant : {Appelant}",
|
||||
duree.TotalMilliseconds.ToString("F2"),
|
||||
this.seuilMs,
|
||||
(duree.TotalMilliseconds - this.seuilMs).ToString("F2"),
|
||||
Environment.NewLine,
|
||||
appelant);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ using Webzine.Repository;
|
||||
using Webzine.Repository.Contracts;
|
||||
using Webzine.WebApplication.Configuration;
|
||||
using Webzine.WebApplication.Extensions;
|
||||
using Webzine.WebApplication.Interceptors;
|
||||
|
||||
// Initiation du logger NLog pour la classe courante afin de pouvoir l'utiliser pour logger des messages d'information, d'erreur, etc avant la construction de l'application.
|
||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||
@@ -37,6 +38,12 @@ try
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Host.UseNLog();
|
||||
|
||||
builder.Services.Configure<EfPerformanceOptions>(options =>
|
||||
{
|
||||
options.SeuilMs = builder.Configuration.GetValue<int>("EfPerformance:SeuilMs");
|
||||
});
|
||||
builder.Services.AddSingleton<EfSlowQueryInterceptor>();
|
||||
|
||||
// En fonction de la configuration, utilise soit les repositories basés sur une base de données, soit les repositories basés sur des listes locales.
|
||||
var repositoryType = builder.Configuration.GetValue<RepositoryType>("Repository");
|
||||
var seederType = builder.Configuration.GetValue<SeederType>("Seeder");
|
||||
@@ -45,13 +52,21 @@ try
|
||||
{
|
||||
if (builder.Environment.IsProduction())
|
||||
{
|
||||
builder.Services.AddDbContext<WebzineDbContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("PostGreSQLConnection")));
|
||||
builder.Services.AddDbContext<WebzineDbContext>((serviceProvider, options) =>
|
||||
{
|
||||
options
|
||||
.UseNpgsql(builder.Configuration.GetConnectionString("PostGreSQLConnection"))
|
||||
.AddInterceptors(serviceProvider.GetRequiredService<EfSlowQueryInterceptor>());
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Services.AddDbContext<WebzineDbContext>(options =>
|
||||
options.UseSqlite(builder.Configuration.GetConnectionString("SqliteConnection")));
|
||||
builder.Services.AddDbContext<WebzineDbContext>((serviceProvider, options) =>
|
||||
{
|
||||
options
|
||||
.UseSqlite(builder.Configuration.GetConnectionString("SqliteConnection"))
|
||||
.AddInterceptors(serviceProvider.GetRequiredService<EfSlowQueryInterceptor>());
|
||||
});
|
||||
}
|
||||
|
||||
builder.Services.AddScoped<DbEntityRepository>();
|
||||
|
||||
@@ -13,5 +13,8 @@
|
||||
"SqliteConnection": "Data Source=Data/webzine.sqlite",
|
||||
"PostGreSQLConnection": "Host=localhost;Port=5432;Username=admin;Password=admin123;Database=webzine_db"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"EfPerformance": {
|
||||
"SeuilMs": 10
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}|${aspnet-request-url:whenEmpty=NoRequest}" />
|
||||
|
||||
<!-- Console pour debug immédiat -->
|
||||
<target xsi:type="Console" name="console"
|
||||
<target xsi:type="Console" name="Info"
|
||||
layout="${longdate}|${level:uppercase=true}|${logger}|${message}" />
|
||||
</targets>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user