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
}
}