Merge branch 'Release/RIOC_AddErrorMan_01'
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace MP.Data.DbModels.Utils
|
||||
{
|
||||
[Table("stats_errors")]
|
||||
public class StatsErrorModel
|
||||
{
|
||||
[Key] public int Id { get; set; }
|
||||
public string Destination { get; set; } = "";
|
||||
public string Type { get; set; } = "";
|
||||
public DateTime Hour { get; set; }
|
||||
public string ErrorMessage { get; set; } = "";
|
||||
public long Count { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace MP.Data.DbModels.Utils
|
||||
{
|
||||
[Table("stats_status_codes")]
|
||||
public class StatsStatusCodeModel
|
||||
{
|
||||
[Key] public int Id { get; set; }
|
||||
public string Destination { get; set; } = "";
|
||||
public string Type { get; set; } = "";
|
||||
public DateTime Hour { get; set; }
|
||||
public int StatusCode { get; set; }
|
||||
public long Count { get; set; }
|
||||
}
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using MP.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MP.Data.Migrations.MoonPro_Utils
|
||||
{
|
||||
[DbContext(typeof(MoonPro_UtilsContext))]
|
||||
[Migration("20260511071223_AddErrorsAndStatusCodes")]
|
||||
partial class AddErrorsAndStatusCodes
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.UseCollation("SQL_Latin1_General_CP1_CI_AS")
|
||||
.HasAnnotation("ProductVersion", "6.0.36")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1);
|
||||
|
||||
modelBuilder.Entity("MP.Data.DbModels.Mtc.MtcSetupModel", b =>
|
||||
{
|
||||
b.Property<string>("IdxMacchina")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("MtcDataItemsRaw")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("IdxMacchina");
|
||||
|
||||
b.ToTable("mtc_setup");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MP.Data.DbModels.Utils.StatsAggregatedModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
|
||||
|
||||
b.Property<double>("AvgDuration")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("Destination")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<DateTime>("Hour")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("MachineId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<double>("MaxDuration")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double>("MinDuration")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<long>("NoReply")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("RequestCount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Destination", "MachineId", "Hour")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("idx_statsaggr_env_mach_hour")
|
||||
.HasFilter("[Destination] IS NOT NULL AND [MachineId] IS NOT NULL");
|
||||
|
||||
b.ToTable("stats_aggr");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MP.Data.DbModels.Utils.StatsDetailModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
|
||||
|
||||
b.Property<double>("AvgDuration")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("Destination")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<DateTime>("Hour")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<double>("MaxDuration")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<double>("MinDuration")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<long>("NoReply")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("RequestCount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Destination", "Type", "Hour")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("idx_statsdet_hour_env_type")
|
||||
.HasFilter("[Destination] IS NOT NULL AND [Type] IS NOT NULL");
|
||||
|
||||
b.ToTable("stats_detail");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MP.Data.DbModels.Utils.StatsErrorModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
|
||||
|
||||
b.Property<long>("Count")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Destination")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<DateTime>("Hour")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Destination", "Type", "Hour", "ErrorMessage")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("idx_statserr_dest_type_hour_err")
|
||||
.HasFilter("[Destination] IS NOT NULL AND [Type] IS NOT NULL AND [ErrorMessage] IS NOT NULL");
|
||||
|
||||
b.ToTable("stats_errors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MP.Data.DbModels.Utils.StatsStatusCodeModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
|
||||
|
||||
b.Property<long>("Count")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Destination")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<DateTime>("Hour")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("StatusCode")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Destination", "Type", "Hour", "StatusCode")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("idx_statscode_dest_type_hour_code")
|
||||
.HasFilter("[Destination] IS NOT NULL AND [Type] IS NOT NULL");
|
||||
|
||||
b.ToTable("stats_status_codes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MP.Data.Migrations.MoonPro_Utils
|
||||
{
|
||||
public partial class AddErrorsAndStatusCodes : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "stats_errors",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Destination = table.Column<string>(type: "nvarchar(450)", nullable: true),
|
||||
Type = table.Column<string>(type: "nvarchar(450)", nullable: true),
|
||||
Hour = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ErrorMessage = table.Column<string>(type: "nvarchar(450)", nullable: true),
|
||||
Count = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_stats_errors", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "stats_status_codes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Destination = table.Column<string>(type: "nvarchar(450)", nullable: true),
|
||||
Type = table.Column<string>(type: "nvarchar(450)", nullable: true),
|
||||
Hour = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
StatusCode = table.Column<int>(type: "int", nullable: false),
|
||||
Count = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_stats_status_codes", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "idx_statserr_dest_type_hour_err",
|
||||
table: "stats_errors",
|
||||
columns: new[] { "Destination", "Type", "Hour", "ErrorMessage" },
|
||||
unique: true,
|
||||
filter: "[Destination] IS NOT NULL AND [Type] IS NOT NULL AND [ErrorMessage] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "idx_statscode_dest_type_hour_code",
|
||||
table: "stats_status_codes",
|
||||
columns: new[] { "Destination", "Type", "Hour", "StatusCode" },
|
||||
unique: true,
|
||||
filter: "[Destination] IS NOT NULL AND [Type] IS NOT NULL");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "stats_errors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "stats_status_codes");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,72 @@ namespace MP.Data.Migrations.MoonPro_Utils
|
||||
|
||||
b.ToTable("stats_detail");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MP.Data.DbModels.Utils.StatsErrorModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
|
||||
|
||||
b.Property<long>("Count")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Destination")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<DateTime>("Hour")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Destination", "Type", "Hour", "ErrorMessage")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("idx_statserr_dest_type_hour_err")
|
||||
.HasFilter("[Destination] IS NOT NULL AND [Type] IS NOT NULL AND [ErrorMessage] IS NOT NULL");
|
||||
|
||||
b.ToTable("stats_errors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MP.Data.DbModels.Utils.StatsStatusCodeModel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
|
||||
|
||||
b.Property<long>("Count")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Destination")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<DateTime>("Hour")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("StatusCode")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Destination", "Type", "Hour", "StatusCode")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("idx_statscode_dest_type_hour_code")
|
||||
.HasFilter("[Destination] IS NOT NULL AND [Type] IS NOT NULL");
|
||||
|
||||
b.ToTable("stats_status_codes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ namespace MP.Data
|
||||
public virtual DbSet<StatsDetailModel> DbSetStatsDet { get; set; }
|
||||
public virtual DbSet<StatsAggregatedModel> DbSetStatsAggr { get; set; }
|
||||
public virtual DbSet<MtcSetupModel> DbSetMtcSetup { get; set; }
|
||||
public virtual DbSet<StatsErrorModel> DbSetStatsError { get; set; }
|
||||
public virtual DbSet<StatsStatusCodeModel> DbSetStatusCode { get; set; }
|
||||
|
||||
#endregion Public Properties
|
||||
|
||||
@@ -91,6 +93,16 @@ namespace MP.Data
|
||||
.HasDatabaseName("idx_statsaggr_env_mach_hour")
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<StatsErrorModel>()
|
||||
.HasIndex(x => new { x.Destination, x.Type, x.Hour, x.ErrorMessage })
|
||||
.HasDatabaseName("idx_statserr_dest_type_hour_err")
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<StatsStatusCodeModel>()
|
||||
.HasIndex(x => new { x.Destination, x.Type, x.Hour, x.StatusCode })
|
||||
.HasDatabaseName("idx_statscode_dest_type_hour_code")
|
||||
.IsUnique();
|
||||
|
||||
OnModelCreatingPartial(modelBuilder);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using EgwCoreLib.Utils;
|
||||
using MP.Data.DbModels.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MP.Data.Repository.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Gestione statistica StatusCode HTML
|
||||
/// </summary>
|
||||
public interface IStatsCodeRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Recupera l'elenco delle statistiche StatusCode per un periodo specifico.
|
||||
/// </summary>
|
||||
/// <param name="dtStart">La data di inizio del periodo.</param>
|
||||
/// <param name="dtEnd">La data di fine del periodo.</param>
|
||||
/// <returns>L'elenco delle statistiche aggregate ordinate cronologicamente.</returns>
|
||||
Task<List<StatsStatusCodeModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd);
|
||||
|
||||
/// <summary>
|
||||
/// Recupera l'intervallo temporale disponibile nel database per le statistiche StatusCode.
|
||||
/// </summary>
|
||||
/// <returns>L'intervallo di date (minima e massima ora presente).</returns>
|
||||
Task<DtUtils.Periodo> GetRangeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Inserisce o aggiorna in blocco le statistiche StatusCode nel database.
|
||||
/// </summary>
|
||||
/// <param name="listRecords">L'elenco dei record da inserire.</param>
|
||||
/// <param name="removeOld">Se true, elimina preventivamente i record nel periodo richiesto.</param>
|
||||
/// <returns>Il numero di record inseriti.</returns>
|
||||
Task<int> UpsertManyAsync(List<StatsStatusCodeModel> listRecords, bool removeOld);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using EgwCoreLib.Utils;
|
||||
using MP.Data.DbModels.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MP.Data.Repository.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Gestione statistiche errori
|
||||
/// </summary>
|
||||
public interface IStatsErrRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Recupera l'elenco delle statistiche errori per un periodo specifico.
|
||||
/// </summary>
|
||||
/// <param name="dtStart">La data di inizio del periodo.</param>
|
||||
/// <param name="dtEnd">La data di fine del periodo.</param>
|
||||
/// <returns>L'elenco delle statistiche errori ordinate cronologicamente.</returns>
|
||||
Task<List<StatsErrorModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd);
|
||||
|
||||
/// <summary>
|
||||
/// Recupera l'intervallo temporale disponibile nel database per le statistiche errore.
|
||||
/// </summary>
|
||||
/// <returns>L'intervallo di date (minima e massima ora presente).</returns>
|
||||
Task<DtUtils.Periodo> GetRangeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Inserisce o aggiorna in blocco le statistiche errore nel database.
|
||||
/// </summary>
|
||||
/// <param name="listRecords">L'elenco dei record da inserire.</param>
|
||||
/// <param name="removeOld">Se true, elimina preventivamente i record nel periodo richiesto.</param>
|
||||
/// <returns>Il numero di record inseriti.</returns>
|
||||
Task<int> UpsertManyAsync(List<StatsErrorModel> listRecords, bool removeOld);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using EgwCoreLib.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MP.Data.DbModels.Utils;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MP.Data.Repository.Utils
|
||||
{
|
||||
public class StatsCodeRepository : BaseRepository, IStatsCodeRepository
|
||||
{
|
||||
#region Public Constructors
|
||||
|
||||
public StatsCodeRepository(IDbContextFactory<MoonPro_UtilsContext> ctxFactory) : base(ctxFactory)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Public Constructors
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<StatsStatusCodeModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd)
|
||||
{
|
||||
await using var dbCtx = await CreateContextAsync();
|
||||
return await dbCtx
|
||||
.DbSetStatusCode
|
||||
.Where(x => x.Hour >= dtStart && x.Hour <= dtEnd)
|
||||
.AsNoTracking()
|
||||
.OrderBy(x => x.Hour)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DtUtils.Periodo> GetRangeAsync()
|
||||
{
|
||||
await using var dbCtx = await CreateContextAsync();
|
||||
DtUtils.Periodo answ = new DtUtils.Periodo(DtUtils.PeriodSet.Today);
|
||||
var query = dbCtx.DbSetStatusCode.AsQueryable();
|
||||
var minHour = await query.MinAsync(x => x.Hour);
|
||||
var maxHour = await query.MaxAsync(x => x.Hour);
|
||||
answ.Inizio = minHour;
|
||||
answ.Fine = maxHour;
|
||||
// ritorno!
|
||||
return answ;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> UpsertManyAsync(List<StatsStatusCodeModel> listRecords, bool removeOld)
|
||||
{
|
||||
if (listRecords == null || !listRecords.Any()) return 0;
|
||||
|
||||
int ans = 0;
|
||||
await using var dbCtx = await CreateContextAsync();
|
||||
await using var tx = await dbCtx.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// 1. Calcolo del range temporale della lista in arrivo per limitare la query di ricerca
|
||||
var minHour = listRecords.Min(x => x.Hour);
|
||||
var maxHour = listRecords.Max(x => x.Hour);
|
||||
|
||||
// 2. Se removeOld è true, manteniamo la logica originale (Eliminazione distruttiva)
|
||||
if (removeOld)
|
||||
{
|
||||
// uso direttamente ExecuteDelete quando in EFCore8...
|
||||
#if false
|
||||
await dbCtx
|
||||
.DbSetStatusCode
|
||||
.Where(x => x.Hour >= startDate && x.Hour <= endDate)
|
||||
.ExecuteDeleteAsync();
|
||||
#endif
|
||||
|
||||
var itemsToRemove = await dbCtx.DbSetStatusCode
|
||||
.Where(x => x.Hour >= minHour && x.Hour <= maxHour)
|
||||
.ToListAsync();
|
||||
if (itemsToRemove.Any())
|
||||
{
|
||||
dbCtx.DbSetStatusCode.RemoveRange(itemsToRemove);
|
||||
await dbCtx.SaveChangesAsync(); // Commit parziale per la cancellazione
|
||||
}
|
||||
}
|
||||
|
||||
// 3. LOGICA DI UPSERT (Merge)
|
||||
// Recuperiamo tutti i record esistenti nel database che cadono nello stesso range temporale
|
||||
// Questo ci permette di confrontare ciò che arriva con ciò che è già presente.
|
||||
var existingRecords = await dbCtx.DbSetStatusCode
|
||||
.Where(x => x.Hour >= minHour && x.Hour <= maxHour)
|
||||
.ToListAsync();
|
||||
|
||||
// Creiamo un dizionario per ricerca rapida O(1) basato sulla chiave univoca (Dest + Hour)
|
||||
// Usiamo una Tupla come chiave del dizionario
|
||||
var lookup = existingRecords.ToDictionary(
|
||||
x => (x.Destination, x.Type, x.Hour, x.StatusCode),
|
||||
x => x
|
||||
);
|
||||
|
||||
foreach (var incoming in listRecords)
|
||||
{
|
||||
var key = (incoming.Destination, incoming.Type, incoming.Hour, incoming.StatusCode);
|
||||
if (lookup.TryGetValue(key, out var existing))
|
||||
{
|
||||
// --- CASO: UPDATE ---
|
||||
existing.Count = incoming.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
// --- CASO: INSERT ---
|
||||
await dbCtx.DbSetStatusCode.AddAsync(incoming);
|
||||
}
|
||||
}
|
||||
// 4. Salvataggio finale
|
||||
ans = await dbCtx.SaveChangesAsync();
|
||||
|
||||
// Commit della transazione
|
||||
await tx.CommitAsync();
|
||||
|
||||
// Pulizia memoria per evitare che il ChangeTracker diventi troppo pesante nei loop lunghi
|
||||
dbCtx.ChangeTracker.Clear();
|
||||
|
||||
return ans;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await tx.RollbackAsync();
|
||||
Log.Error(ex, "Error during UpsertManyAsync");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Public Methods
|
||||
|
||||
#region Protected Fields
|
||||
|
||||
protected static NLog.Logger Log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
#endregion Protected Fields
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using EgwCoreLib.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MP.Data.DbModels.Utils;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MP.Data.Repository.Utils
|
||||
{
|
||||
public class StatsErrRepository : BaseRepository, IStatsErrRepository
|
||||
{
|
||||
#region Public Constructors
|
||||
|
||||
public StatsErrRepository(IDbContextFactory<MoonPro_UtilsContext> ctxFactory) : base(ctxFactory)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Public Constructors
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<StatsErrorModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd)
|
||||
{
|
||||
await using var dbCtx = await CreateContextAsync();
|
||||
return await dbCtx
|
||||
.DbSetStatsError
|
||||
.Where(x => x.Hour >= dtStart && x.Hour <= dtEnd)
|
||||
.AsNoTracking()
|
||||
.OrderBy(x => x.Hour)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DtUtils.Periodo> GetRangeAsync()
|
||||
{
|
||||
await using var dbCtx = await CreateContextAsync();
|
||||
DtUtils.Periodo answ = new DtUtils.Periodo(DtUtils.PeriodSet.Today);
|
||||
var query = dbCtx.DbSetStatsError.AsQueryable();
|
||||
var minHour = await query.MinAsync(x => x.Hour);
|
||||
var maxHour = await query.MaxAsync(x => x.Hour);
|
||||
answ.Inizio = minHour;
|
||||
answ.Fine = maxHour;
|
||||
// ritorno!
|
||||
return answ;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> UpsertManyAsync(List<StatsErrorModel> listRecords, bool removeOld)
|
||||
{
|
||||
if (listRecords == null || !listRecords.Any()) return 0;
|
||||
|
||||
int ans = 0;
|
||||
await using var dbCtx = await CreateContextAsync();
|
||||
await using var tx = await dbCtx.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// 1. Calcolo del range temporale della lista in arrivo per limitare la query di ricerca
|
||||
var minHour = listRecords.Min(x => x.Hour);
|
||||
var maxHour = listRecords.Max(x => x.Hour);
|
||||
|
||||
// 2. Se removeOld è true, manteniamo la logica originale (Eliminazione distruttiva)
|
||||
if (removeOld)
|
||||
{
|
||||
// uso direttamente ExecuteDelete quando in EFCore8...
|
||||
#if false
|
||||
await dbCtx
|
||||
.DbSetStatsError
|
||||
.Where(x => x.Hour >= startDate && x.Hour <= endDate)
|
||||
.ExecuteDeleteAsync();
|
||||
#endif
|
||||
|
||||
var itemsToRemove = await dbCtx.DbSetStatsError
|
||||
.Where(x => x.Hour >= minHour && x.Hour <= maxHour)
|
||||
.ToListAsync();
|
||||
if (itemsToRemove.Any())
|
||||
{
|
||||
dbCtx.DbSetStatsError.RemoveRange(itemsToRemove);
|
||||
await dbCtx.SaveChangesAsync(); // Commit parziale per la cancellazione
|
||||
}
|
||||
}
|
||||
|
||||
// 3. LOGICA DI UPSERT (Merge)
|
||||
// Recuperiamo tutti i record esistenti nel database che cadono nello stesso range temporale
|
||||
// Questo ci permette di confrontare ciò che arriva con ciò che è già presente.
|
||||
var existingRecords = await dbCtx.DbSetStatsError
|
||||
.Where(x => x.Hour >= minHour && x.Hour <= maxHour)
|
||||
.ToListAsync();
|
||||
|
||||
// Creiamo un dizionario per ricerca rapida O(1) basato sulla chiave univoca (Dest + Hour)
|
||||
// Usiamo una Tupla come chiave del dizionario
|
||||
var lookup = existingRecords.ToDictionary(
|
||||
x => (x.Destination, x.Type, x.Hour, x.ErrorMessage),
|
||||
x => x
|
||||
);
|
||||
|
||||
foreach (var incoming in listRecords)
|
||||
{
|
||||
var key = (incoming.Destination, incoming.Type, incoming.Hour, incoming.ErrorMessage);
|
||||
if (lookup.TryGetValue(key, out var existing))
|
||||
{
|
||||
// --- CASO: UPDATE ---
|
||||
existing.Count = incoming.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
// --- CASO: INSERT ---
|
||||
await dbCtx.DbSetStatsError.AddAsync(incoming);
|
||||
}
|
||||
}
|
||||
// 4. Salvataggio finale
|
||||
ans = await dbCtx.SaveChangesAsync();
|
||||
|
||||
// Commit della transazione
|
||||
await tx.CommitAsync();
|
||||
|
||||
// Pulizia memoria per evitare che il ChangeTracker diventi troppo pesante nei loop lunghi
|
||||
dbCtx.ChangeTracker.Clear();
|
||||
|
||||
return ans;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await tx.RollbackAsync();
|
||||
Log.Error(ex, "Error during UpsertManyAsync");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Public Methods
|
||||
|
||||
#region Protected Fields
|
||||
|
||||
protected static NLog.Logger Log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
#endregion Protected Fields
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,6 @@ namespace MP.Data.Services.Utils
|
||||
/// <param name="dtEnd">Data fine periodo</param>
|
||||
Task<List<StatsAggregatedModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd);
|
||||
|
||||
/// <summary>
|
||||
/// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte settimanale
|
||||
/// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsWeekAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte giornaliero
|
||||
/// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica
|
||||
@@ -34,19 +27,19 @@ namespace MP.Data.Services.Utils
|
||||
/// <returns></returns>
|
||||
Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsDayAsync(int numDay);
|
||||
|
||||
/// <summary>
|
||||
/// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte settimanale
|
||||
/// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsWeekAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Recupera il range di periodi valido per le chiamate aggregate.
|
||||
/// Utilizza la cache automaticamente.
|
||||
/// </summary>
|
||||
Task<DtUtils.Periodo> GetRangeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Inserisce o aggiorna in batch le statistiche aggregate nel database.
|
||||
/// Opzionalmente elimina i record precedenti nel periodo specificato.
|
||||
/// </summary>
|
||||
/// <param name="listRecords">Elenco dei record da inserire/aggiornare</param>
|
||||
/// <param name="removeOld">Se true elimina preventivamente i record nel periodo richiesto</param>
|
||||
Task<int> UpsertManyAsync(List<StatsAggregatedModel> listRecords, bool removeOld);
|
||||
/// <summary>
|
||||
/// Helper conversione dati aggregati in statistiche da inviare a ChartJS
|
||||
/// </summary>
|
||||
@@ -56,6 +49,14 @@ namespace MP.Data.Services.Utils
|
||||
/// <returns></returns>
|
||||
List<ChartSeriesDto> GetTimeSeriesData(List<StatsAggregatedModel> rawData, bool groupMach, bool getCount);
|
||||
|
||||
/// <summary>
|
||||
/// Inserisce o aggiorna in batch le statistiche aggregate nel database.
|
||||
/// Opzionalmente elimina i record precedenti nel periodo specificato.
|
||||
/// </summary>
|
||||
/// <param name="listRecords">Elenco dei record da inserire/aggiornare</param>
|
||||
/// <param name="removeOld">Se true elimina preventivamente i record nel periodo richiesto</param>
|
||||
Task<int> UpsertManyAsync(List<StatsAggregatedModel> listRecords, bool removeOld);
|
||||
|
||||
#endregion Public Methods
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using EgwCoreLib.Utils;
|
||||
using MP.Core.DTO;
|
||||
using MP.Data.DbModels.Utils;
|
||||
using MP.Data.DTO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MP.Data.Services.Utils
|
||||
{
|
||||
public interface IStatsCodeService
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Recupera l'elenco delle statistiche StatusCode per un periodo specificato.
|
||||
/// Utilizza la cache automaticamente.
|
||||
/// </summary>
|
||||
/// <param name="dtStart">Data inizio periodo</param>
|
||||
/// <param name="dtEnd">Data fine periodo</param>
|
||||
Task<List<StatsStatusCodeModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd);
|
||||
|
||||
/// <summary>
|
||||
/// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte giornaliero
|
||||
/// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsDayAsync(int numDay);
|
||||
|
||||
/// <summary>
|
||||
/// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte settimanale
|
||||
/// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsWeekAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Recupera il range di periodi valido per le chiamate StatusCode.
|
||||
/// Utilizza la cache automaticamente.
|
||||
/// </summary>
|
||||
Task<DtUtils.Periodo> GetRangeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Helper conversione dati aggregati in statistiche da inviare a ChartJS
|
||||
/// </summary>
|
||||
/// <param name="rawData"></param>
|
||||
/// <returns></returns>
|
||||
List<ChartSeriesDto> GetTimeSeriesData(List<StatsStatusCodeModel> rawData);
|
||||
|
||||
/// <summary>
|
||||
/// Inserisce o aggiorna in batch le statistiche StatusCode nel database.
|
||||
/// Opzionalmente elimina i record precedenti nel periodo specificato.
|
||||
/// </summary>
|
||||
/// <param name="listRecords">Elenco dei record da inserire/aggiornare</param>
|
||||
/// <param name="removeOld">Se true elimina preventivamente i record nel periodo richiesto</param>
|
||||
Task<int> UpsertManyAsync(List<StatsStatusCodeModel> listRecords, bool removeOld);
|
||||
|
||||
#endregion Public Methods
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using EgwCoreLib.Utils;
|
||||
using MP.Core.DTO;
|
||||
using MP.Data.DbModels.Utils;
|
||||
using MP.Data.DTO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MP.Data.Services.Utils
|
||||
{
|
||||
public interface IStatsErrService
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Recupera l'elenco delle statistiche Error per un periodo specificato.
|
||||
/// Utilizza la cache automaticamente.
|
||||
/// </summary>
|
||||
/// <param name="dtStart">Data inizio periodo</param>
|
||||
/// <param name="dtEnd">Data fine periodo</param>
|
||||
Task<List<StatsErrorModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd);
|
||||
|
||||
/// <summary>
|
||||
/// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte giornaliero
|
||||
/// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsDayAsync(int numDay);
|
||||
|
||||
/// <summary>
|
||||
/// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte settimanale
|
||||
/// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsWeekAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Recupera il range di periodi valido per le chiamate Error.
|
||||
/// Utilizza la cache automaticamente.
|
||||
/// </summary>
|
||||
Task<DtUtils.Periodo> GetRangeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Helper conversione dati aggregati in statistiche da inviare a ChartJS
|
||||
/// </summary>
|
||||
/// <param name="rawData"></param>
|
||||
/// <returns></returns>
|
||||
List<ChartSeriesDto> GetTimeSeriesData(List<StatsErrorModel> rawData);
|
||||
|
||||
/// <summary>
|
||||
/// Inserisce o aggiorna in batch le statistiche Error nel database.
|
||||
/// Opzionalmente elimina i record precedenti nel periodo specificato.
|
||||
/// </summary>
|
||||
/// <param name="listRecords">Elenco dei record da inserire/aggiornare</param>
|
||||
/// <param name="removeOld">Se true elimina preventivamente i record nel periodo richiesto</param>
|
||||
Task<int> UpsertManyAsync(List<StatsErrorModel> listRecords, bool removeOld);
|
||||
|
||||
#endregion Public Methods
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
using EgwCoreLib.Utils;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MP.Core.DTO;
|
||||
using MP.Data.DbModels.Utils;
|
||||
using MP.Data.DTO;
|
||||
using MP.Data.Repository.Utils;
|
||||
using StackExchange.Redis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MP.Data.Services.Utils
|
||||
{
|
||||
internal class StatsCodeService : BaseServ, IStatsCodeService
|
||||
{
|
||||
#region Public Constructors
|
||||
|
||||
public StatsCodeService(
|
||||
IConfiguration config,
|
||||
IConnectionMultiplexer redis,
|
||||
IStatsCodeRepository repo) : base(config, redis)
|
||||
{
|
||||
_className = "StatsStatusCode";
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
#endregion Public Constructors
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<StatsStatusCodeModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd)
|
||||
{
|
||||
return await TraceAsync($"{_className}.GetFilt", async (activity) =>
|
||||
{
|
||||
return await GetOrSetCacheAsync(
|
||||
$"{_redisBaseKey}:{_className}:DT:{dtStart:yyyyMMdd}:{dtEnd:yyyyMMdd}",
|
||||
async () => await _repo.GetFiltAsync(dtStart, dtEnd),
|
||||
UltraLongCache
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsDayAsync(int numDay)
|
||||
{
|
||||
return await TraceAsync($"{_className}.GetParetoStatsDayAsync", async (activity) =>
|
||||
{
|
||||
return await GetOrSetCacheAsync(
|
||||
$"{_redisBaseKey}:{_className}:ParetoDay",
|
||||
async () => await GetParetoDestAsync(numDay),
|
||||
LongCache
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsWeekAsync()
|
||||
{
|
||||
return await TraceAsync($"{_className}.GetParetoStatsWeekAsync", async (activity) =>
|
||||
{
|
||||
return await GetOrSetCacheAsync(
|
||||
$"{_redisBaseKey}:{_className}:ParetoWeek",
|
||||
async () => await GetParetoDataAsync(),
|
||||
LongCache
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DtUtils.Periodo> GetRangeAsync()
|
||||
{
|
||||
return await TraceAsync($"{_className}.GetRange", async (activity) =>
|
||||
{
|
||||
return await GetOrSetCacheAsync(
|
||||
$"{_redisBaseKey}:{_className}:Range",
|
||||
async () => await _repo.GetRangeAsync(),
|
||||
UltraFastCache
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<ChartSeriesDto> GetTimeSeriesData(List<StatsStatusCodeModel> rawData)
|
||||
{
|
||||
DateTime adesso = DateTime.Now;
|
||||
|
||||
List<ChartSeriesDto> series = new();
|
||||
series = rawData
|
||||
.GroupBy(s => new { s.Destination })
|
||||
.Select(group => new ChartSeriesDto
|
||||
{
|
||||
SeriesName = group.Key.Destination,
|
||||
DataPoints = group
|
||||
.OrderBy(p => p.Hour)
|
||||
.Select(p => new chartJsData.chartJsTSerie
|
||||
{
|
||||
x = p.Hour,
|
||||
y = p.Count / (p.Hour.Date.Equals(adesso.Date) ? adesso.TimeOfDay.TotalHours : 24)
|
||||
})
|
||||
.ToList()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> UpsertManyAsync(List<StatsStatusCodeModel> listRecords, bool removeOld)
|
||||
{
|
||||
return await TraceAsync($"{_className}.UpsertMany", async (activity) =>
|
||||
{
|
||||
string operation = "UpsertMany";
|
||||
var success = await _repo.UpsertManyAsync(listRecords, removeOld);
|
||||
|
||||
activity?.SetTag("db.operation", operation);
|
||||
|
||||
if (success > 0)
|
||||
{
|
||||
await ClearCacheAsync($"{_redisBaseKey}:{_className}:*");
|
||||
}
|
||||
|
||||
return success;
|
||||
});
|
||||
}
|
||||
|
||||
#endregion Public Methods
|
||||
|
||||
#region Protected Methods
|
||||
|
||||
/// <summary>
|
||||
/// metodo locale per recupero e trasformazione dati da includere con processo generare di tracking & cache
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected async Task<Dictionary<string, List<StatDataDTO>>> GetParetoDataAsync()
|
||||
{
|
||||
Dictionary<string, List<StatDataDTO>> result = new();
|
||||
DateTime oggi = DateTime.Today;
|
||||
int numDays = 7;
|
||||
var rawData = await GetFiltAsync(oggi.AddDays(-numDays), oggi);
|
||||
// calcolo le varie statistiche...
|
||||
var pDestRequest = rawData.GroupBy(x => x.Destination)
|
||||
.Select(g => new StatDataDTO
|
||||
{
|
||||
Label = g.Key,
|
||||
Value = g.Sum(x => x.Count) / numDays
|
||||
})
|
||||
.OrderByDescending(x => x.Value)
|
||||
.ToList();
|
||||
result.Add("Errors (#)", pDestRequest);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected async Task<Dictionary<string, List<StatDataDTO>>> GetParetoDestAsync(int numDay)
|
||||
{
|
||||
Dictionary<string, List<StatDataDTO>> result = new();
|
||||
DateTime adesso = DateTime.Now;
|
||||
DateTime start = DateTime.Today.AddDays(-numDay);
|
||||
var rawData = await GetFiltAsync(start, adesso);
|
||||
var numHour = adesso.Subtract(start).TotalHours;
|
||||
// calcolo le varie statistiche...
|
||||
var pDestRequest = rawData.GroupBy(x => x.Destination)
|
||||
.Select(g => new StatDataDTO
|
||||
{
|
||||
Label = g.Key,
|
||||
Value = g.Sum(x => x.Count) / numHour
|
||||
})
|
||||
.OrderByDescending(x => x.Value)
|
||||
.ToList();
|
||||
result.Add("Dest.Request (#/h)", pDestRequest);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion Protected Methods
|
||||
|
||||
#region Private Fields
|
||||
|
||||
private readonly string _className;
|
||||
private readonly IStatsCodeRepository _repo;
|
||||
|
||||
#endregion Private Fields
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
using EgwCoreLib.Utils;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MP.Core.DTO;
|
||||
using MP.Data.DbModels.Utils;
|
||||
using MP.Data.DTO;
|
||||
using MP.Data.Repository.Utils;
|
||||
using StackExchange.Redis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MP.Data.Services.Utils
|
||||
{
|
||||
public class StatsErrService : BaseServ, IStatsErrService
|
||||
{
|
||||
#region Public Constructors
|
||||
|
||||
public StatsErrService(
|
||||
IConfiguration config,
|
||||
IConnectionMultiplexer redis,
|
||||
IStatsErrRepository repo) : base(config, redis)
|
||||
{
|
||||
_className = "StatsErr";
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
#endregion Public Constructors
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<StatsErrorModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd)
|
||||
{
|
||||
return await TraceAsync($"{_className}.GetFilt", async (activity) =>
|
||||
{
|
||||
return await GetOrSetCacheAsync(
|
||||
$"{_redisBaseKey}:{_className}:DT:{dtStart:yyyyMMdd}:{dtEnd:yyyyMMdd}",
|
||||
async () => await _repo.GetFiltAsync(dtStart, dtEnd),
|
||||
UltraLongCache
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsDayAsync(int numDay)
|
||||
{
|
||||
return await TraceAsync($"{_className}.GetParetoStatsDayAsync", async (activity) =>
|
||||
{
|
||||
return await GetOrSetCacheAsync(
|
||||
$"{_redisBaseKey}:{_className}:ParetoDay",
|
||||
async () => await GetParetoDestAsync(numDay),
|
||||
LongCache
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsWeekAsync()
|
||||
{
|
||||
return await TraceAsync($"{_className}.GetParetoStatsWeekAsync", async (activity) =>
|
||||
{
|
||||
return await GetOrSetCacheAsync(
|
||||
$"{_redisBaseKey}:{_className}:ParetoWeek",
|
||||
async () => await GetParetoDataAsync(),
|
||||
LongCache
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DtUtils.Periodo> GetRangeAsync()
|
||||
{
|
||||
return await TraceAsync($"{_className}.GetRange", async (activity) =>
|
||||
{
|
||||
return await GetOrSetCacheAsync(
|
||||
$"{_redisBaseKey}:{_className}:Range",
|
||||
async () => await _repo.GetRangeAsync(),
|
||||
UltraFastCache
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<ChartSeriesDto> GetTimeSeriesData(List<StatsErrorModel> rawData)
|
||||
{
|
||||
DateTime adesso = DateTime.Now;
|
||||
|
||||
List<ChartSeriesDto> series = new();
|
||||
series = rawData
|
||||
.GroupBy(s => new { s.Destination })
|
||||
.Select(group => new ChartSeriesDto
|
||||
{
|
||||
SeriesName = group.Key.Destination,
|
||||
DataPoints = group
|
||||
.OrderBy(p => p.Hour)
|
||||
.Select(p => new chartJsData.chartJsTSerie
|
||||
{
|
||||
x = p.Hour,
|
||||
y = p.Count / (p.Hour.Date.Equals(adesso.Date) ? adesso.TimeOfDay.TotalHours : 24)
|
||||
})
|
||||
.ToList()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> UpsertManyAsync(List<StatsErrorModel> listRecords, bool removeOld)
|
||||
{
|
||||
return await TraceAsync($"{_className}.UpsertMany", async (activity) =>
|
||||
{
|
||||
string operation = "UpsertMany";
|
||||
var success = await _repo.UpsertManyAsync(listRecords, removeOld);
|
||||
|
||||
activity?.SetTag("db.operation", operation);
|
||||
|
||||
if (success > 0)
|
||||
{
|
||||
await ClearCacheAsync($"{_redisBaseKey}:{_className}:*");
|
||||
}
|
||||
|
||||
return success;
|
||||
});
|
||||
}
|
||||
|
||||
#endregion Public Methods
|
||||
|
||||
#region Protected Methods
|
||||
|
||||
/// <summary>
|
||||
/// metodo locale per recupero e trasformazione dati da includere con processo generare di tracking & cache
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected async Task<Dictionary<string, List<StatDataDTO>>> GetParetoDataAsync()
|
||||
{
|
||||
Dictionary<string, List<StatDataDTO>> result = new();
|
||||
DateTime oggi = DateTime.Today;
|
||||
int numDays = 7;
|
||||
var rawData = await GetFiltAsync(oggi.AddDays(-numDays), oggi);
|
||||
// calcolo le varie statistiche...
|
||||
var pDestRequest = rawData.GroupBy(x => x.Destination)
|
||||
.Select(g => new StatDataDTO
|
||||
{
|
||||
Label = g.Key,
|
||||
Value = g.Sum(x => x.Count) / numDays
|
||||
})
|
||||
.OrderByDescending(x => x.Value)
|
||||
.ToList();
|
||||
result.Add("Errors (#)", pDestRequest);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected async Task<Dictionary<string, List<StatDataDTO>>> GetParetoDestAsync(int numDay)
|
||||
{
|
||||
Dictionary<string, List<StatDataDTO>> result = new();
|
||||
DateTime adesso = DateTime.Now;
|
||||
DateTime start = DateTime.Today.AddDays(-numDay);
|
||||
var rawData = await GetFiltAsync(start, adesso);
|
||||
var numHour = adesso.Subtract(start).TotalHours;
|
||||
// calcolo le varie statistiche...
|
||||
var pDestRequest = rawData.GroupBy(x => x.Destination)
|
||||
.Select(g => new StatDataDTO
|
||||
{
|
||||
Label = g.Key,
|
||||
Value = g.Sum(x => x.Count) / numHour
|
||||
})
|
||||
.OrderByDescending(x => x.Value)
|
||||
.ToList();
|
||||
result.Add("Dest.Request (#/h)", pDestRequest);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion Protected Methods
|
||||
|
||||
#region Private Fields
|
||||
|
||||
private readonly string _className;
|
||||
private readonly IStatsErrRepository _repo;
|
||||
|
||||
#endregion Private Fields
|
||||
}
|
||||
}
|
||||
@@ -4054,7 +4054,7 @@ namespace MP.IOC.Data
|
||||
{
|
||||
Log.Info($"upsertCurrObjItems | idxMacchina: {idxMacchina} | {innovations.Count} innovations");
|
||||
// leggo i valori attuali...
|
||||
List<ObjItemDTO> actValues = MachineParamList(idxMacchina);
|
||||
List<ObjItemDTO> actValues = await MachineParamListAsync(idxMacchina);
|
||||
// per ogni valOut passatomi faccio insert o update rispetto elenco valori correnti in REDIS
|
||||
foreach (var item in actValues)
|
||||
{
|
||||
@@ -4078,7 +4078,7 @@ namespace MP.IOC.Data
|
||||
string serVal = JsonConvert.SerializeObject(innovations);
|
||||
|
||||
var currKey = Utils.RedKeyCurrObjItems(idxMacchina, MpIoNS);
|
||||
RedisValue rawData = redisDb.StringSet(currKey, serVal);
|
||||
RedisValue rawData = await redisDb.StringSetAsync(currKey, serVal);
|
||||
}
|
||||
return answ;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>MP.RIOC</RootNamespace>
|
||||
<Version>8.16.2605.910</Version>
|
||||
<Version>8.16.2605.1111</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -4,11 +4,13 @@ local countInc = tonumber(ARGV[1]) or 0
|
||||
local totalMsInc = tonumber(ARGV[2]) or 0
|
||||
local newMax = tonumber(ARGV[3])
|
||||
local newMin = tonumber(ARGV[4])
|
||||
local sentinel = tonumber(ARGV[5])
|
||||
local noReply = tonumber(ARGV[5])
|
||||
local sentinel = tonumber(ARGV[6])
|
||||
|
||||
-- Incrementi base
|
||||
-- Incrementi
|
||||
redis.call('HINCRBY', key, 'count', countInc)
|
||||
redis.call('HINCRBYFLOAT', key, 'totalMs', totalMsInc)
|
||||
redis.call('HINCRBY', key, 'noReply', noReply)
|
||||
|
||||
-- MAX
|
||||
local currentMaxStr = redis.call('HGET', key, 'maxMs')
|
||||
|
||||
@@ -4,11 +4,13 @@ local countInc = tonumber(ARGV[1]) or 0
|
||||
local totalMsInc = tonumber(ARGV[2]) or 0
|
||||
local newMax = tonumber(ARGV[3])
|
||||
local newMin = tonumber(ARGV[4])
|
||||
local sentinel = tonumber(ARGV[5])
|
||||
local noReply = tonumber(ARGV[5])
|
||||
local sentinel = tonumber(ARGV[6])
|
||||
|
||||
-- Incrementi
|
||||
redis.call('HINCRBY', key, 'count', countInc)
|
||||
redis.call('HINCRBYFLOAT', key, 'totalMs', totalMsInc)
|
||||
redis.call('HINCRBY', key, 'noReply', noReply)
|
||||
|
||||
-- MAX
|
||||
local currentMaxStr = redis.call('HGET', key, 'maxMs')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<body>
|
||||
<i>Modulo MP-RIOC </i>
|
||||
<h4>Versione: 8.16.2605.910</h4>
|
||||
<h4>Versione: 8.16.2605.1111</h4>
|
||||
<br /> Note di rilascio:
|
||||
<ul>
|
||||
<li>
|
||||
|
||||
@@ -1 +1 @@
|
||||
8.16.2605.910
|
||||
8.16.2605.1111
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<item>
|
||||
<version>8.16.2605.910</version>
|
||||
<version>8.16.2605.1111</version>
|
||||
<url>https://nexus.steamware.net/repository/SWS/MP-RIOC/stable/LAST/MP.RIOC.zip</url>
|
||||
<changelog>https://nexus.steamware.net/repository/SWS/MP-RIOC/stable/LAST/ChangeLog.html</changelog>
|
||||
<mandatory>false</mandatory>
|
||||
|
||||
@@ -48,6 +48,7 @@ namespace MP.RIOC.Services
|
||||
private class AggregatedStats
|
||||
{
|
||||
public long Count;
|
||||
public long NoReply;
|
||||
public double TotalMs;
|
||||
public double MaxMs;
|
||||
public double MinMs = double.MaxValue;
|
||||
@@ -111,6 +112,10 @@ namespace MP.RIOC.Services
|
||||
var hoursIndex = HoursIndexKey(dest, method);
|
||||
var hourScore = ToEpochSeconds(hourStart);
|
||||
|
||||
// Calcolo NoReply: Somma di tutti i codici >= 400 o errori espliciti
|
||||
long noReplyCount = stat.ErrorMessages.Sum(x => x.Value);
|
||||
|
||||
// 1. INVIO BUCKET PRINCIPALE (con NoReply)
|
||||
// Usiamo lo script Lua per l'aggiornamento atomico dell'ora
|
||||
tasks.Add(batch.ScriptEvaluateAsync(_updateScript,
|
||||
new RedisKey[] { hourKey },
|
||||
@@ -119,9 +124,34 @@ namespace MP.RIOC.Services
|
||||
totalMs.ToString(CultureInfo.InvariantCulture),
|
||||
maxMs.ToString(CultureInfo.InvariantCulture),
|
||||
minMs.ToString(CultureInfo.InvariantCulture),
|
||||
noReplyCount.ToString(CultureInfo.InvariantCulture),
|
||||
SentinelValue
|
||||
}));
|
||||
|
||||
// 2. INVIO DISTRIBUZIONE STATUS CODES
|
||||
if (stat.StatusCodes.Any())
|
||||
{
|
||||
var statusKey = hourKey + ":status";
|
||||
foreach (var status in stat.StatusCodes)
|
||||
{
|
||||
tasks.Add(batch.HashIncrementAsync(statusKey, status.Key.ToString(), status.Value));
|
||||
}
|
||||
// Aggiungiamo anche questa chiave all'indice per la pulizia automatica
|
||||
tasks.Add(batch.SortedSetAddAsync(hoursIndex, statusKey, hourScore));
|
||||
}
|
||||
|
||||
// 3. INVIO DETTAGLIO ERRORI
|
||||
if (stat.ErrorMessages.Any())
|
||||
{
|
||||
var errorKey = hourKey + ":errors";
|
||||
foreach (var error in stat.ErrorMessages)
|
||||
{
|
||||
// Usiamo HashIncrement per aggregare messaggi uguali
|
||||
tasks.Add(batch.HashIncrementAsync(errorKey, error.Key, error.Value));
|
||||
}
|
||||
tasks.Add(batch.SortedSetAddAsync(hoursIndex, errorKey, hourScore));
|
||||
}
|
||||
|
||||
tasks.Add(batch.SortedSetAddAsync(hoursIndex, hourKey, hourScore));
|
||||
|
||||
// --- LOGICA DAILY (Aggregazione locale per evitare sovrascritture nel loop) ---
|
||||
@@ -136,6 +166,7 @@ namespace MP.RIOC.Services
|
||||
}
|
||||
|
||||
agg.Count += count;
|
||||
agg.NoReply += noReplyCount;
|
||||
agg.TotalMs += totalMs;
|
||||
agg.MaxMs = Math.Max(agg.MaxMs, maxMs);
|
||||
if (minMs != double.MaxValue)
|
||||
@@ -162,6 +193,7 @@ namespace MP.RIOC.Services
|
||||
agg.TotalMs.ToString(CultureInfo.InvariantCulture),
|
||||
agg.MaxMs.ToString(CultureInfo.InvariantCulture),
|
||||
finalMin.ToString(CultureInfo.InvariantCulture),
|
||||
agg.NoReply.ToString(CultureInfo.InvariantCulture),
|
||||
SentinelValue
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace MP.RIOC.Services
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var interval = _config.GetValue<int>("RouteMan:MetricFlushIntervalSeconds", 180);
|
||||
var interval = _config.GetValue<int>("RouteMan:MetricFlushIntervalSeconds", 240);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -79,7 +79,7 @@ namespace MP.RIOC.Services
|
||||
DateTime now = DateTime.Now;
|
||||
|
||||
// Confini temporali per proteggere i dati in corso
|
||||
DateTime currentDayStart = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0);
|
||||
DateTime currentDayStart = DateTime.Today;
|
||||
|
||||
var endpoints = _mux.GetEndPoints();
|
||||
|
||||
@@ -110,17 +110,14 @@ namespace MP.RIOC.Services
|
||||
foreach (var statKey in memberKeys)
|
||||
{
|
||||
var sKey = (RedisKey)$"{statKey}";
|
||||
#if false
|
||||
if (!TryParseKeyMetadata(sKey, out string dest, out string method, out string machId, out DateTime timestamp, out bool isHourType))
|
||||
continue;
|
||||
|
||||
// Verifica se la chiave fosse scaduta rispetto all'orario corrente
|
||||
bool isExpired = isHourType
|
||||
//? timestamp < currentHourStart
|
||||
? false
|
||||
: timestamp < currentDayStart;
|
||||
continue;
|
||||
#endif
|
||||
if (!TryParseKeyMetadata(sKey, out var meta) || meta.IsHourType) continue;
|
||||
|
||||
// Se fosse scaduta e abbiamo il permesso, segnamola per la cancellazione
|
||||
if (isExpired && deleteConfirmed)
|
||||
if (meta.Timestamp < currentDayStart && deleteConfirmed)
|
||||
{
|
||||
// 1. Segna la chiave Hash (Dati) per l'eliminazione
|
||||
keysToDelete.Add(sKey);
|
||||
@@ -152,16 +149,23 @@ namespace MP.RIOC.Services
|
||||
|
||||
if (count <= 0) continue;
|
||||
|
||||
// recupero se presente noReply
|
||||
long noReply = 0;
|
||||
if (dict.TryGetValue("noReply", out var sNoReply))
|
||||
{
|
||||
noReply = long.Parse(sNoReply);
|
||||
}
|
||||
|
||||
aggrRecordsToInsert.Add(new StatsAggregatedModel
|
||||
{
|
||||
Destination = dest,
|
||||
MachineId = machId,
|
||||
Hour = timestamp,
|
||||
Destination = meta.Dest,
|
||||
MachineId = meta.MachId,
|
||||
Hour = meta.Timestamp,
|
||||
RequestCount = count,
|
||||
AvgDuration = totalMs / count,
|
||||
MinDuration = minMs,
|
||||
MaxDuration = maxMs,
|
||||
NoReply = 0
|
||||
NoReply = noReply
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -171,7 +175,7 @@ namespace MP.RIOC.Services
|
||||
}
|
||||
|
||||
// --- FASE UPSERT DB ---
|
||||
if (aggrRecordsToInsert.Count > 0)
|
||||
if (aggrRecordsToInsert.Any())
|
||||
{
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
var aggrService = scope.ServiceProvider.GetRequiredService<IStatsAggrService>();
|
||||
@@ -180,7 +184,7 @@ namespace MP.RIOC.Services
|
||||
}
|
||||
|
||||
// --- FASE PULIZIA REDIS ---
|
||||
if (deleteConfirmed && keysToDelete.Count > 0)
|
||||
if (deleteConfirmed && keysToDelete.Any())
|
||||
{
|
||||
var batch = _db.CreateBatch();
|
||||
int deletedCount = 0;
|
||||
@@ -239,17 +243,20 @@ namespace MP.RIOC.Services
|
||||
foreach (var statKey in memberKeys)
|
||||
{
|
||||
var sKey = (RedisKey)$"{statKey}";
|
||||
if (!TryParseKeyMetadata(sKey, out var meta) || !meta.IsHourType) continue;
|
||||
#if false
|
||||
if (!TryParseKeyMetadata(sKey, out string dest, out string method, out string machId, out DateTime timestamp, out bool isHourType))
|
||||
continue;
|
||||
continue;
|
||||
|
||||
// Verifica se la chiave fosse scaduta rispetto all'orario corrente
|
||||
bool isExpired = isHourType
|
||||
? timestamp < currentHourStart
|
||||
: false;
|
||||
: false;
|
||||
//: timestamp < currentDayStart;
|
||||
#endif
|
||||
|
||||
// Se fosse scaduta e abbiamo il permesso, segnamola per la cancellazione
|
||||
if (isExpired && deleteConfirmed)
|
||||
if (meta.Timestamp < currentHourStart && deleteConfirmed)
|
||||
{
|
||||
// 1. Segna la chiave Hash (Dati) per l'eliminazione
|
||||
keysToDelete.Add(sKey);
|
||||
@@ -280,17 +287,23 @@ namespace MP.RIOC.Services
|
||||
if (maxMs >= SentinelValue) maxMs = SentinelValue;
|
||||
|
||||
if (count <= 0) continue;
|
||||
// recupero se presente noReply
|
||||
long noReply = 0;
|
||||
if (dict.TryGetValue("noReply", out var sNoReply))
|
||||
{
|
||||
noReply = long.Parse(sNoReply);
|
||||
}
|
||||
|
||||
detailRecordsToInsert.Add(new StatsDetailModel
|
||||
{
|
||||
Destination = dest,
|
||||
Type = method,
|
||||
Hour = timestamp,
|
||||
Destination = meta.Dest,
|
||||
Type = meta.Method,
|
||||
Hour = meta.Timestamp,
|
||||
RequestCount = count,
|
||||
AvgDuration = totalMs / count,
|
||||
MinDuration = minMs,
|
||||
MaxDuration = maxMs,
|
||||
NoReply = 0
|
||||
NoReply = noReply
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -322,7 +335,35 @@ namespace MP.RIOC.Services
|
||||
Log.Info($"[CLEANUP HOUR] Deleted {deletedCount} expired metric keys from Redis");
|
||||
}
|
||||
}
|
||||
private bool TryParseKeyMetadata(RedisKey key, out KeyMeta meta)
|
||||
{
|
||||
meta = new KeyMeta();
|
||||
try
|
||||
{
|
||||
string k = key.ToString();
|
||||
string relativeKey = k.Replace($"{_redisBaseKey}:", "");
|
||||
var p = relativeKey.Split(':');
|
||||
if (p.Length < 4) return false;
|
||||
|
||||
meta.IsHourType = p[1].Equals("hour", StringComparison.InvariantCultureIgnoreCase);
|
||||
meta.Dest = p[2];
|
||||
if (meta.IsHourType)
|
||||
{
|
||||
meta.Method = p[3];
|
||||
if (p.Length >= 5) DateTime.TryParseExact(p[4], "yyyyMMddHH", CultureInfo.InvariantCulture, DateTimeStyles.None, out meta.Timestamp);
|
||||
}
|
||||
else
|
||||
{
|
||||
meta.Method = "DAILY";
|
||||
meta.MachId = p[3];
|
||||
if (p.Length >= 5) DateTime.TryParseExact(p[4], "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out meta.Timestamp);
|
||||
}
|
||||
return meta.Timestamp != DateTime.MinValue;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
#if false
|
||||
private bool TryParseKeyMetadata(RedisKey key, out string dest, out string method, out string machId, out DateTime timestamp, out bool isHourType)
|
||||
{
|
||||
dest = "NA";
|
||||
@@ -374,6 +415,16 @@ namespace MP.RIOC.Services
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
private record KeyMeta
|
||||
{
|
||||
public string Dest = "NA";
|
||||
public string Method = "NA";
|
||||
public string MachId = "ALL";
|
||||
public DateTime Timestamp = DateTime.MinValue;
|
||||
public bool IsHourType = true;
|
||||
}
|
||||
|
||||
#endregion Private Methods
|
||||
|
||||
@@ -70,11 +70,9 @@ namespace MP.RIOC.Services
|
||||
var (oldW, newW) = _weightProvider.GetWeightsFor(metodo);
|
||||
var pickNew = DecideByWeights(oldW, newW);
|
||||
var targetLabel = pickNew ? "IOC" : "IO";
|
||||
string sKey = $"{targetLabel}|{metodo}|{id}";
|
||||
|
||||
var destBase = pickNew
|
||||
? _config["ServerConf:NewApiUrl"]
|
||||
: _config["ServerConf:OldApiUrl"];
|
||||
|
||||
var destBase = pickNew ? _config["ServerConf:NewApiUrl"] : _config["ServerConf:OldApiUrl"];
|
||||
if (string.IsNullOrEmpty(destBase))
|
||||
{
|
||||
context.Response.StatusCode = 502;
|
||||
@@ -82,42 +80,57 @@ namespace MP.RIOC.Services
|
||||
return;
|
||||
}
|
||||
|
||||
// Verifica destinazione
|
||||
// per evitare il "doppio slash" (es. .../api/IOB//metodo)
|
||||
if (!destBase.EndsWith("/")) destBase += "/";
|
||||
|
||||
// avvio registrazione statistice
|
||||
_stats.Record(sKey);
|
||||
|
||||
// 4. PREPARAZIONE FORWARDING
|
||||
var originalPath = context.Request.Path;
|
||||
var originalPathBase = context.Request.PathBase;
|
||||
|
||||
try
|
||||
{
|
||||
string sKey = $"{targetLabel}|{metodo}|{id}";
|
||||
_stats.Record(sKey);
|
||||
|
||||
// Assicuriamoci che la destinazione finisca con / e il path inizi senza /
|
||||
// per evitare il "doppio slash" (es. .../api/IOB//metodo)
|
||||
if (!destBase.EndsWith("/")) destBase += "/";
|
||||
|
||||
context.Request.Path = new PathString("/" + relativePath);
|
||||
context.Request.PathBase = PathString.Empty;
|
||||
|
||||
// Esecuzione
|
||||
// ESECUZIONE FORWARDING
|
||||
var error = await _forwarder.SendAsync(context, destBase, _httpClientInvoker, _forwarderConfig, HttpTransformer.Default, context.RequestAborted);
|
||||
// commento transformer custom
|
||||
//var error = await _forwarder.SendAsync(context, destBase, _httpClientInvoker, _forwarderConfig, _transformer, context.RequestAborted);
|
||||
|
||||
|
||||
sw.Stop();
|
||||
_stats.RecordDuration(sKey, sw.Elapsed);
|
||||
|
||||
// REGISTRAZIONE STATUS CODE (Sempre, se disponibile)
|
||||
_stats.RecordStatusCode(sKey, context.Response.StatusCode);
|
||||
|
||||
if (error != ForwarderError.None)
|
||||
{
|
||||
var feat = context.GetForwarderErrorFeature();
|
||||
Log.Error(feat?.Exception, "Forwarder error to {DestBase} for {Method}", destBase, metodo);
|
||||
var errorMsg = feat?.Exception?.Message ?? error.ToString();
|
||||
|
||||
// REGISTRAZIONE ERRORE DETTAGLIATO
|
||||
_stats.RecordError(sKey, errorMsg);
|
||||
|
||||
Log.Error(feat?.Exception, "Forwarder error to {DestBase} for {Method}: {Msg}", destBase, metodo, errorMsg);
|
||||
|
||||
if (!context.Response.HasStarted)
|
||||
{
|
||||
context.Response.StatusCode = 502;
|
||||
await context.Response.WriteAsync($"Forward error: {feat?.Exception?.Message ?? error.ToString()}");
|
||||
await context.Response.WriteAsync($"Forward error: {errorMsg}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
_stats.RecordError(sKey, ex.Message);
|
||||
Log.Fatal(ex, "Critical error in RouteManager");
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Request.Path = originalPath;
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace MP.RIOC.Services
|
||||
public TimeSpan MaxDuration = TimeSpan.Zero;
|
||||
public TimeSpan MinDuration = TimeSpan.MaxValue;
|
||||
public ConcurrentDictionary<int, long> StatusCodes = new();
|
||||
public ConcurrentDictionary<string, long> ErrorMessages = new();
|
||||
public TimeSpan AvgDuration => TotalDuration / (Count > 0 ? Count : 1);
|
||||
}
|
||||
|
||||
@@ -51,6 +52,21 @@ namespace MP.RIOC.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registrazione errore
|
||||
/// </summary>
|
||||
/// <param name="dest_method"></param>
|
||||
/// <param name="errorMessage"></param>
|
||||
public void RecordError(string dest_method, string errorMessage)
|
||||
{
|
||||
if (_map.TryGetValue(dest_method, out var stat))
|
||||
{
|
||||
// Puliamo il messaggio per evitare chiavi infinite (es. togliamo timestamp o ID dinamici)
|
||||
var cleanMsg = errorMessage.Length > 100 ? errorMessage[..100] + "..." : errorMessage;
|
||||
stat.ErrorMessages.AddOrUpdate(cleanMsg, 1, (_, v) => v + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void RecordStatusCode(string method, int statusCode)
|
||||
{
|
||||
if (_map.TryGetValue(method, out var stat))
|
||||
|
||||
Reference in New Issue
Block a user