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 /// /// Registra una chiamata tracciata. Thread-safe. /// 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))); } /// /// Carica lo stato persistente da Redis. Thread-safe. /// 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 parsed; try { parsed = JsonConvert.DeserializeObject>(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; } } } /// /// Metodo per estrarre il report Pareto e reset del contatore. /// /// Se null o vuoto, elabora tutte le chiavi. Altrimenti filtra solo quelle indicate. public static List LogPareto(IEnumerable keys = null) { List 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 filter = null; if (keys != null) { filter = new HashSet(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(); // 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; } /// /// Salva lo stato corrente in Redis. Thread-safe. /// /// Istanza di IDatabase /// Chiave Redis dove salvare i dati /// Se true, resetta i contatori dopo il salvataggio 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 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 /// /// Risultato dell'analisi Pareto /// 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 _metrics = new ConcurrentDictionary(); // lock x serializzazione su redisDb private static readonly object _serializationLock = new object(); #endregion Private Fields #region Private Classes /// /// Struttura interna per l'accumulo /// 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 } }