Update colleazione statistiche x aggregazione destination (server) oppure macchina + update cleanup rebootLog a 60 min

This commit is contained in:
Samuele Locatelli
2026-04-28 18:38:49 +02:00
parent 379b35cfe0
commit 5733be8968
12 changed files with 311 additions and 53 deletions
+80
View File
@@ -0,0 +1,80 @@
namespace MP.Core.DTO
{
public class StatInfoDto
{
#region Public Enums
/// <summary>
/// Modalità di aggregwzione del dato
/// </summary>
public enum AggrLevel
{
/// <summary>
/// Non specificato
/// </summary>
None,
/// <summary>
/// Aggregazione x server (IO/IOC)
/// </summary>
Service,
/// <summary>
/// Aggregazione x metodo chiamato
/// </summary>
Method,
/// <summary>
/// Aggregazione x Macchina chiamante
/// </summary>
Machine
}
/// <summary>
/// Tipologia del dato collezionato
/// </summary>
public enum DataType
{
/// <summary>
/// Non specificato
/// </summary>
None,
/// <summary>
/// Conteggio
/// </summary>
Count,
/// <summary>
/// Durata temporale (Avg)
/// </summary>
AvgDuration
}
#endregion Public Enums
#region Public Properties
/// <summary>
/// Titolo della statistica
/// </summary>
public string Title { get; set; } = "";
/// <summary>
/// Livello di aggregazione delle statistiche collezionate
/// </summary>
public AggrLevel Grouping { get; set; } = AggrLevel.None;
/// <summary>
/// Tipologia di dato della statistica
/// </summary>
public DataType Type { get; set; } = DataType.None;
/// <summary>
/// Dati statistici ordinati in logica Pareto
/// </summary>
public List<StatDataDTO> DataCollection { get; set; } = new();
#endregion Public Properties
}
}
@@ -1,6 +1,7 @@
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;
@@ -46,6 +47,14 @@ namespace MP.Data.Services.Utils
/// <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>
/// <param name="rawData"></param>
/// <param name="groupMach">raggruppamento per macchina/server</param>
/// <param name="getCount">metrica conteggio/avgDuration</param>
/// <returns></returns>
List<ChartSeriesDto> GetTimeSeriesData(List<StatsAggregatedModel> rawData, bool groupMach, bool getCount);
#endregion Public Methods
}
@@ -38,7 +38,9 @@ namespace MP.Data.Services.Utils
/// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica
/// </summary>
/// <returns></returns>
Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsDayAsync();
//Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsDayAsync();
Task<List<StatInfoDto>> GetParetoStatsDayAsync();
/// <summary>
/// Recupera il range di periodi valido per le chiamate di dettaglio.
+67 -12
View File
@@ -2,6 +2,7 @@
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;
@@ -41,18 +42,6 @@ namespace MP.Data.Services.Utils
});
}
/// <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<Dictionary<string, List<StatDataDTO>>> GetParetoStatsDayAsync(int numDay)
{
@@ -66,6 +55,19 @@ namespace MP.Data.Services.Utils
});
}
/// <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()
{
@@ -79,6 +81,59 @@ namespace MP.Data.Services.Utils
});
}
/// <inheritdoc />
public List<ChartSeriesDto> GetTimeSeriesData(List<StatsAggregatedModel> rawData, bool groupMach, bool getCount)
{
List<ChartSeriesDto> series = new();
// a seconda della richiesta cambio il group by...
if (groupMach)
{
series = rawData
.GroupBy(s => new { s.Destination, s.MachineId }) // Raggruppiamo per la chiave composta
.Select(group => new ChartSeriesDto
{
// Creiamo un nome leggibile per la legenda del grafico
SeriesName = $"{group.Key.Destination}|{group.Key.MachineId}",
// Per ogni gruppo, creiamo la lista dei punti temporali
DataPoints = group
.OrderBy(p => p.Hour) // Fondamentale: l'asse X deve essere cronologico
.Select(p => new chartJsData.chartJsTSerie
{
x = p.Hour,
y = getCount ? p.RequestCount : p.AvgDuration * p.RequestCount // La metrica richiesta
})
.ToList()
})
.OrderBy(s => s.SeriesName) // Opzionale: ordina le serie alfabeticamente
.ToList();
}
else
{
series = rawData
.GroupBy(s => new { s.MachineId, s.Destination }) // Raggruppiamo per la chiave composta
.Select(group => new ChartSeriesDto
{
// Creiamo un nome leggibile per la legenda del grafico
SeriesName = $"{group.Key.MachineId}|{group.Key.Destination}",
// Per ogni gruppo, creiamo la lista dei punti temporali
DataPoints = group
.OrderBy(p => p.Hour) // Fondamentale: l'asse X deve essere cronologico
.Select(p => new chartJsData.chartJsTSerie
{
x = p.Hour,
y = getCount ? p.RequestCount : p.AvgDuration // La metrica richiesta
})
.ToList()
})
.OrderBy(s => s.SeriesName) // Opzionale: ordina le serie alfabeticamente
.ToList();
}
return series;
}
/// <inheritdoc />
public async Task<int> UpsertManyAsync(List<StatsAggregatedModel> listRecords, bool removeOld)
{
+76 -2
View File
@@ -74,7 +74,7 @@ namespace MP.Data.Services.Utils
}
/// <inheritdoc />
public async Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsDayAsync()
public async Task<List<StatInfoDto>> GetParetoStatsDayAsync()
{
return await TraceAsync($"{_className}.GetDailyParetoStats", async (activity) =>
{
@@ -152,8 +152,80 @@ namespace MP.Data.Services.Utils
/// 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()
protected async Task<List<StatInfoDto>> GetParetoDataAsync()
{
List<StatInfoDto> result = new();
DateTime adesso = DateTime.Now;
var rawData = await GetFiltAsync(adesso.AddDays(-1), adesso);
// calcolo le varie statistiche...
var pDestRequest = rawData.GroupBy(x => x.Destination)
.Select(g => new StatDataDTO
{
Label = g.Key,
Value = g.Sum(x => x.RequestCount)
})
.OrderByDescending(x => x.Value)
.ToList();
result.Add(new StatInfoDto()
{
Title = "Dest.Request (#)",
DataCollection = pDestRequest,
Grouping = StatInfoDto.AggrLevel.Service,
Type = StatInfoDto.DataType.Count
});
var pDestDuration = rawData.GroupBy(x => x.Destination)
.Select(g => new StatDataDTO
{
Label = g.Key,
Value = g.Sum(x => x.RequestCount * x.AvgDuration)
})
.OrderByDescending(x => x.Value)
.ToList();
result.Add(new StatInfoDto()
{
Title = "Dest.Duration (ms)",
DataCollection = pDestDuration,
Grouping = StatInfoDto.AggrLevel.Service,
Type = StatInfoDto.DataType.AvgDuration
});
var pTypeRequest = rawData.GroupBy(x => x.Type)
.Select(g => new StatDataDTO
{
Label = g.Key,
Value = g.Sum(x => x.RequestCount)
})
.OrderByDescending(x => x.Value)
.ToList();
result.Add(new StatInfoDto()
{
Title = "Type.Request (#)",
DataCollection = pTypeRequest,
Grouping = StatInfoDto.AggrLevel.Method,
Type = StatInfoDto.DataType.Count
});
var pTypeDuration = rawData.GroupBy(x => x.Type)
.Select(g => new StatDataDTO
{
Label = g.Key,
Value = g.Sum(x => x.RequestCount * x.AvgDuration)
})
.OrderByDescending(x => x.Value)
.ToList();
result.Add(new StatInfoDto()
{
Title = "Type.Duration (ms)",
DataCollection = pTypeDuration,
Grouping = StatInfoDto.AggrLevel.Method,
Type = StatInfoDto.DataType.AvgDuration
});
return result;
#if false
Dictionary<string, List<StatDataDTO>> result = new();
DateTime adesso = DateTime.Now;
var rawData = await GetFiltAsync(adesso.AddDays(-1), adesso);
@@ -217,8 +289,10 @@ namespace MP.Data.Services.Utils
//result.Add("DestType.Duration (ms)", pDestTypeDuration);
return result;
#endif
}
#endregion Protected Methods
#region Private Fields
+3 -3
View File
@@ -22,9 +22,9 @@
<ul class="list-group shadow">
@foreach (var item in ParetoDay)
{
<li class="list-group-item @CheckSelect(item.Key) d-flex justify-content-between align-items-start small py-1">
<span class="align-items-center">@item.Key</span>
<button class="btn btn-sm btn-info" @onclick="() => DoSelect(item.Key)">
<li class="list-group-item @CheckSelect(item.Title) d-flex justify-content-between align-items-start small py-1">
<span class="align-items-center">@item.Title</span>
<button class="btn btn-sm btn-info" @onclick="() => DoSelect(item)">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
</li>
+53 -15
View File
@@ -26,8 +26,7 @@ namespace MP.IOC.Components.Pages
private string currHistId = "";
private string currId = "";
private StatInfoDto? currStatSel = null;
private string currPieId = "";
private string currTitle = "";
@@ -38,7 +37,7 @@ namespace MP.IOC.Components.Pages
private int numDays = 5;
private Dictionary<string, List<StatDataDTO>> ParetoDay = new();
private List<StatInfoDto> ParetoDay = new();
private List<chartJsData.chartJsTSerie> tsData = new List<chartJsData.chartJsTSerie>();
@@ -107,31 +106,30 @@ namespace MP.IOC.Components.Pages
private string CheckSelect(string curKey)
{
return !string.IsNullOrEmpty(currId) && currId == curKey ? "active" : "";
return currStatSel != null && currStatSel.Title == curKey ? "active" : "";
}
private void DoReset()
{
currId = "";
currStatSel = null;
currHistId = "";
currPieId = "";
currTsId = "";
currData = new();
}
private void DoSelect(string reqKey)
private void DoSelect(StatInfoDto currStat)
{
if (ParetoDay.ContainsKey(reqKey))
{
currId = reqKey;
// salvo dati!
currStatSel = currStat;
string reqKey = currStat.Title;
currHistId = $"Bar_{reqKey}";
currPieId = $"Pie_{reqKey}";
currTsId = $"TS_{reqKey}";
currTitle = $"Pareto | {reqKey}";
currData = ParetoDay[reqKey];
currData = currStat.DataCollection;
tsDataDetail = new();
}
}
private List<string> GetDistinctColors(int numRecords, string alpha)
{
@@ -218,7 +216,13 @@ namespace MP.IOC.Components.Pages
var machDay = await SAggService.GetParetoStatsDayAsync(0);
foreach (var item in machDay)
{
ParetoDay.TryAdd(item.Key, item.Value.Where(x => x.Label != "ALL").ToList());
ParetoDay.Add(new StatInfoDto()
{
Title = item.Key,
DataCollection = item.Value.Where(x => x.Label != "ALL").ToList(),
Grouping = StatInfoDto.AggrLevel.Machine,
Type = item.Key.Contains("#") ? StatInfoDto.DataType.Count : StatInfoDto.DataType.AvgDuration
});
}
}
@@ -229,20 +233,54 @@ namespace MP.IOC.Components.Pages
private async Task ShowDetail(string selDetail)
{
currDetail = selDetail;
// recupero dettaglio 7gg...
if (currStatSel != null)
{
DateTime adesso = DateTime.Now;
bool isCount = currStatSel.Type == StatInfoDto.DataType.Count;
bool grpMachine = currStatSel.Grouping == StatInfoDto.AggrLevel.Machine;
switch (currStatSel.Grouping)
{
case StatInfoDto.AggrLevel.Service:
case StatInfoDto.AggrLevel.Machine:
// recupero statistiche aggregate 7 gg..
var rawAggrData = await SAggService.GetFiltAsync(adesso.AddDays(-numDays), adesso);
// escludo gli "all"
// filtro x selezione (macchina o servizio)...
if (grpMachine)
{
rawAggrData = rawAggrData.Where(x => x.MachineId == selDetail).ToList();
}
else
{
rawAggrData = rawAggrData.Where(x => !x.MachineId.Equals("all", StringComparison.InvariantCultureIgnoreCase) && x.Destination == selDetail).ToList();
}
// conversione con grouping
tsData = rawAggrData.Select(r => new chartJsData.chartJsTSerie() { x = r.Hour, y = (double)r.AvgDuration })
.OrderBy(o => o.x)
.ToList();
// ...secondo tipo richiesto (macchina/server e duration/count)
tsDataDetail = SAggService.GetTimeSeriesData(rawAggrData, grpMachine, isCount);
break;
case StatInfoDto.AggrLevel.Method:
// recupero dettaglio 7gg...
List<StatsDetailModel> rawData = await SDetService.GetFiltAsync(adesso.AddDays(-numDays), adesso, "", selDetail);
// conversione con grouping
tsData = rawData.Select(r => new chartJsData.chartJsTSerie() { x = r.Hour, y = (double)r.AvgDuration })
.OrderBy(o => o.x)
.ToList();
// ...secondo tipo richiesto (duration/count)
bool showCount = currHistId.Contains("(#)");
tsDataDetail = SDetService.GetTimeSeriesData(rawData, showCount);
tsDataDetail = SDetService.GetTimeSeriesData(rawData, isCount);
break;
case StatInfoDto.AggrLevel.None:
default:
break;
}
lineTitles = tsDataDetail.Select(x => x.SeriesName).ToList();
TSDataMulti = tsDataDetail.Select(x => x.DataPoints).ToList();
}
}
#endregion Private Methods
}
}
+2 -2
View File
@@ -3447,10 +3447,10 @@ namespace MP.IOC.Data
/// <returns></returns>
public async Task<bool> RemRebootLogAddAsync(RemoteRebootLogModel newRec)
{
// verifica preliminare ultima esecuzione (max 1 ogni 15 min...)
// verifica preliminare ultima esecuzione (max 1 ogni 60 min...)
DateTime adesso = DateTime.Now;
bool doClean = false;
if (adesso.Subtract(lastCleanupRRL).TotalMinutes > 15)
if (adesso.Subtract(lastCleanupRRL).TotalMinutes > 60)
{
lastCleanupRRL = adesso;
doClean = true;
+1 -1
View File
@@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>8.16.2604.2809</Version>
<Version>8.16.2604.2818</Version>
</PropertyGroup>
<ItemGroup>
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MP-IOC </i>
<h4>Versione: 8.16.2604.2809</h4>
<h4>Versione: 8.16.2604.2818</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
8.16.2604.2809
8.16.2604.2818
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>8.16.2604.2809</version>
<version>8.16.2604.2818</version>
<url>https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/MP.IOC.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>