Files
mapo-core/MP.IOC/Services/MetricsFlushService.cs
T
2026-04-07 11:26:32 +02:00

147 lines
6.2 KiB
C#

using NLog;
using StackExchange.Redis;
using System.Globalization;
namespace MP.IOC.Services
{
public class MetricsFlushService : BackgroundService
{
#region Public Constructors
//public MetricsFlushService(RouteStatsManager stats, ILogger<MetricsFlushService> logger, IConfiguration config)
public MetricsFlushService(RouteStatsManager stats, IConfiguration config, IConnectionMultiplexer mux)
{
_stats = stats;
_config = config;
_db = mux.GetDatabase();
_redisBaseKey = _config.GetValue<string>("ServerConf:RedisBaseKey") ?? "MP_IOC";
}
#endregion Public Constructors
#region Protected Methods
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var interval = _config.GetValue<int>("RouteMan:FlushIntervalSeconds", 30);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(interval), stoppingToken);
var snapshot = _stats.Snapshot();
if (snapshot.Count == 0) continue;
var utcNow = DateTime.Now;
var hourStart = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, utcNow.Hour, 0, 0);
var dayStart = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, 0, 0, 0);
foreach (var kv in snapshot)
{
var method = kv.Key;
var stat = kv.Value;
var count = Interlocked.Read(ref stat.Count);
var totalMs = stat.TotalDuration.TotalMilliseconds;
Log.Info("Method {method} Count {count} TotalDurationMs {totalMs} Destinations {dests}",
method, count, totalMs, string.Join(",", stat.Destinations.Select(x => $"{x.Key}:{x.Value}")));
if (_db == null) continue;
// Keys
var hourKey = HourBucketKey(method, hourStart);
var hoursIndex = HoursIndexKey(method);
var dayKey = DayBucketKey(method, dayStart);
var daysIndex = DaysIndexKey(method);
// Use batch to reduce roundtrips
var batch = _db.CreateBatch();
// Increment hash fields (count and totalMs)
var taskHourCount = batch.HashIncrementAsync(hourKey, "count", count);
var taskHourTotal = batch.HashIncrementAsync(hourKey, "totalMs", totalMs);
var taskDayCount = batch.HashIncrementAsync(dayKey, "count", count);
var taskDayTotal = batch.HashIncrementAsync(dayKey, "totalMs", totalMs);
// Add to sorted set indices with score = epoch seconds of bucket start
var hourScore = ToEpochSeconds(hourStart);
var dayScore = ToEpochSeconds(dayStart);
var taskZAddHour = batch.SortedSetAddAsync(hoursIndex, hourKey, hourScore);
var taskZAddDay = batch.SortedSetAddAsync(daysIndex, dayKey, dayScore);
//// Optionally set TTL on bucket hashes (e.g., keep daily buckets 400 days)
//var taskExpireHour = batch.KeyExpireAsync(hourKey, TimeSpan.FromDays(11)); // keep ~11 days for hourly
//var taskExpireDay = batch.KeyExpireAsync(dayKey, TimeSpan.FromDays(400)); // keep ~400 days for daily
// Execute batch
batch.Execute();
// Await tasks to ensure completion
await Task.WhenAll(taskHourCount, taskHourTotal, taskDayCount, taskDayTotal, taskZAddHour, taskZAddDay);
//await Task.WhenAll(taskHourCount, taskHourTotal, taskDayCount, taskDayTotal,
// taskZAddHour, taskZAddDay, taskExpireHour, taskExpireDay);
//// Trim indices to keep only last N entries (use ZREMRANGEBYRANK)
//// Note: trimming is separate calls (not in batch) to ensure ordering; can be batched too.
//await _db.SortedSetRemoveRangeByRankAsync(hoursIndex, 0, -MaxHourlyBuckets - 1); // keep last MaxHourlyBuckets
//await _db.SortedSetRemoveRangeByRankAsync(daysIndex, 0, -MaxDailyBuckets - 1);
}
_stats.Clear();
}
catch (TaskCanceledException) { }
catch (Exception ex)
{
Log.Error(ex, "Error flushing metrics");
}
}
}
#endregion Protected Methods
#region Private Fields
private const int MaxDailyBuckets = 365;
private const int MaxHourlyBuckets = 24 * 10;
private static string _redisBaseKey = "";
private static Logger Log = LogManager.GetCurrentClassLogger();
private readonly IConfiguration _config;
private readonly IDatabase _db;
private readonly RouteStatsManager _stats;
#endregion Private Fields
#region Private Methods
private static string DayBucketKey(string method, DateTime dtRif)
{
return $"{_redisBaseKey}:stats:day:{method}:{dtRif.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}";
}
private static string DaysIndexKey(string method)
{
return $"{_redisBaseKey}:stats:days:{method}";
}
private static string HourBucketKey(string method, DateTime dtRif)
{
return $"{_redisBaseKey}:stats:hour:{method}:{dtRif.ToString("yyyyMMddHH", CultureInfo.InvariantCulture)}";
}
private static string HoursIndexKey(string method)
{
return $"{_redisBaseKey}:stats:hours:{method}";
}
private static long ToEpochSeconds(DateTime dt)
{
return new DateTimeOffset(dt).ToUnixTimeSeconds();
}
#endregion Private Methods
}
}