147 lines
6.2 KiB
C#
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
|
|
}
|
|
} |