Files
Mapo-IOB-WIN/IOB-UT-NEXT/CallMetricsCollector.cs
2026-04-29 12:29:04 +02:00

230 lines
7.5 KiB
C#

using Newtonsoft.Json;
using StackExchange.Redis;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
namespace IOB_UT_NEXT
{
public static class CallMetricsCollector
{
#region Public Methods
/// <summary>
/// Registra una chiamata tracciata. Thread-safe.
/// </summary>
public static void AddCall(string key, TimeSpan elapsed)
{
_metrics.AddOrUpdate(key,
_ => new CallStats(1, elapsed),
(_, existing) => new CallStats(existing.Count + 1, existing.TotalElapsed.Add(elapsed)));
}
/// <summary>
/// Carica lo stato persistente da Redis. Thread-safe.
/// </summary>
public static async Task LoadFromRedisAsync(IDatabase redisDb, string redisKey = "app:callmetrics")
{
if (redisDb == null) throw new ArgumentNullException(nameof(redisDb));
;
var json = await redisDb.StringGetAsync(redisKey);
if (!json.HasValue) return;
Dictionary<string, CallStats> parsed;
try
{
parsed = JsonConvert.DeserializeObject<Dictionary<string, CallStats>>(json.ToString());
}
catch (Exception ex)
{
// Log: dati Redis corrotti o schema incompatibile. Si ignora per sicurezza.
return;
}
if (parsed == null || parsed.Count == 0) return;
// 5. Applicazione dati sotto lock
lock (_serializationLock)
{
_metrics.Clear();
foreach (var kvp in parsed)
{
_metrics[kvp.Key] = kvp.Value;
}
}
}
/// <summary>
/// Metodo per estrarre il report Pareto e reset del contatore.
/// </summary>
/// <param name="keys">Se null o vuoto, elabora tutte le chiavi. Altrimenti filtra solo quelle indicate.</param>
public static List<ParetoEntry> LogPareto(IEnumerable<string> keys = null)
{
List<ParetoEntry> result;
lock (_extractLock)
{
// 1. Snapshot atomico + reset
var snapshot = _metrics.ToDictionary(
kvp => kvp.Key,
kvp => new CallStats(kvp.Value.Count, kvp.Value.TotalElapsed));
_metrics.Clear();
// 2. Filtro opzionale (compatibile .NET 4.7.0)
HashSet<string> filter = null;
if (keys != null)
{
filter = new HashSet<string>(keys);
}
var filtered = (filter != null && filter.Any())
? snapshot.Where(kvp => filter.Contains(kvp.Key))
: snapshot;
double totalCalls = filtered.Sum(kvp => kvp.Value.Count);
if (totalCalls == 0) return new List<ParetoEntry>();
// 3. Ordinamento decrescente e calcolo Pareto cumulativo
var sorted = filtered
.OrderByDescending(kvp => kvp.Value.Count)
.ThenByDescending(kvp => kvp.Value.TotalElapsed)
.ToList();
double cumulative = 0;
result = sorted.Select(kvp =>
{
double countShare = kvp.Value.Count / totalCalls;
cumulative += countShare;
double avgMs = (100 * kvp.Value.TotalElapsed.TotalMilliseconds / kvp.Value.Count) / 100;
return new ParetoEntry(
key: kvp.Key,
count: kvp.Value.Count,
totalElapsed: kvp.Value.TotalElapsed,
avgElapsed: TimeSpan.FromMilliseconds(avgMs),
cumulativeParetoPct: cumulative);
}).ToList();
}
return result;
}
/// <summary>
/// Salva lo stato corrente in Redis. Thread-safe.
/// </summary>
/// <param name="redisDb">Istanza di IDatabase</param>
/// <param name="redisKey">Chiave Redis dove salvare i dati</param>
/// <param name="clearAfterSave">Se true, resetta i contatori dopo il salvataggio</param>
public static async Task SaveToRedisAsync(IDatabase redisDb, string redisKey = "app:callmetrics", bool clearAfterSave = true)
{
if (redisDb == null) throw new ArgumentNullException(nameof(redisDb));
// 1. Snapshot atomico sotto lock
Dictionary<string, CallStats> snapshot;
lock (_serializationLock)
{
snapshot = _metrics
.OrderByDescending(x => x.Value.Count)
.ToDictionary(
kvp => kvp.Key,
kvp => new CallStats(kvp.Value.Count, kvp.Value.TotalElapsed));
}
if (snapshot.Count == 0) return;
// 2. Serializzazione JSON (fuori dal lock)
var json = JsonConvert.SerializeObject(snapshot);
// 3. Scrittura su Redis (fuori dal lock)
await redisDb.StringSetAsync(redisKey, json);
// 4. Reset opzionale (sotto lock per sicurezza)
if (clearAfterSave)
{
lock (_serializationLock)
{
_metrics.Clear();
}
}
}
#endregion Public Methods
#region Public Classes
/// <summary>
/// Risultato dell'analisi Pareto
/// </summary>
public class ParetoEntry
{
#region Public Constructors
public ParetoEntry(string key, int count, TimeSpan totalElapsed, TimeSpan avgElapsed, double cumulativeParetoPct)
{
Key = key;
Count = count;
TotalElapsed = totalElapsed;
AvgElapsed = avgElapsed;
CumulativeParetoPct = cumulativeParetoPct;
}
#endregion Public Constructors
#region Public Properties
public TimeSpan AvgElapsed { get; }
public int Count { get; }
public double CumulativeParetoPct { get; }
public string Key { get; }
public TimeSpan TotalElapsed { get; }
#endregion Public Properties
}
#endregion Public Classes
#region Private Fields
// Lock dedicato solo alla fase di snapshot e reset
private static readonly object _extractLock = new object();
// Accumulatore thread-safe
private static readonly ConcurrentDictionary<string, CallStats> _metrics = new ConcurrentDictionary<string, CallStats>();
// lock x serializzazione su redisDb
private static readonly object _serializationLock = new object();
#endregion Private Fields
#region Private Classes
/// <summary>
/// Struttura interna per l'accumulo
/// </summary>
private class CallStats
{
#region Public Constructors
public CallStats(int count, TimeSpan totalElapsed)
{
Count = count;
TotalElapsed = totalElapsed;
}
#endregion Public Constructors
#region Public Properties
public int Count { get; }
public TimeSpan TotalElapsed { get; }
#endregion Public Properties
}
#endregion Private Classes
}
}