230 lines
7.5 KiB
C#
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
|
|
}
|
|
} |