From db8afebc5cb5d3b001673d279aec9043b6c6aa57 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 28 Jan 2025 16:46:59 -0600 Subject: [PATCH] refine global stats --- .../Repositories/IBotSharpRepository.cs | 7 ++- .../Statistics/Enums/StatsInterval.cs | 9 +++ .../Statistics/Models/BotSharpStats.cs | 30 +++++++--- .../Statistics/Models/BotSharpStatsInput.cs | 5 +- .../Conversations/Services/TokenStatistics.cs | 1 + .../FileRepository/FileRepository.Stats.cs | 55 +++++++++++++---- .../Services/BotSharpStatsService.cs | 6 +- .../Hooks/GlobalStatsConversationHook.cs | 5 +- .../Collections/GlobalStatisticsDocument.cs | 5 ++ .../Repository/MongoRepository.Stats.cs | 59 +++++++++++++++---- 10 files changed, 147 insertions(+), 35 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatsInterval.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index 976df6cd2..d7ac7d040 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -3,6 +3,7 @@ using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Roles.Models; using BotSharp.Abstraction.Shared; +using BotSharp.Abstraction.Statistics.Enums; using BotSharp.Abstraction.Statistics.Models; using BotSharp.Abstraction.Tasks.Models; using BotSharp.Abstraction.Translation.Models; @@ -120,8 +121,10 @@ public interface IBotSharpRepository : IHaveServiceProvider #endregion #region Statistics - BotSharpStats? GetGlobalStats(string metric, string dimension, DateTime recordTime) => throw new NotImplementedException(); - bool SaveGlobalStats(BotSharpStats body) => throw new NotImplementedException(); + BotSharpStats? GetGlobalStats(string metric, string dimension, DateTime recordTime, StatsInterval interval) + => throw new NotImplementedException(); + bool SaveGlobalStats(BotSharpStats body) + => throw new NotImplementedException(); #endregion diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatsInterval.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatsInterval.cs new file mode 100644 index 000000000..7f3233bf3 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatsInterval.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Abstraction.Statistics.Enums; + +public enum StatsInterval +{ + Hour = 1, + Day = 2, + Week = 3, + Month = 4 +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStats.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStats.cs index 6d6194fce..23e6f2ada 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStats.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStats.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Statistics.Enums; + namespace BotSharp.Abstraction.Statistics.Models; public class BotSharpStats @@ -11,24 +13,36 @@ public class BotSharpStats [JsonPropertyName("data")] public IDictionary Data { get; set; } = new Dictionary(); - private DateTime innerRecordTime; - [JsonPropertyName("record_time")] - public DateTime RecordTime + public DateTime RecordTime { get; set; } = DateTime.UtcNow; + + [JsonIgnore] + public StatsInterval IntervalType { get; set; } + + [JsonPropertyName("interval")] + public string Interval { get { - return innerRecordTime; - } + return IntervalType.ToString(); + } set { - var date = new DateTime(value.Year, value.Month, value.Day, value.Hour, 0, 0); - innerRecordTime = DateTime.SpecifyKind(date, DateTimeKind.Utc); + if (Enum.TryParse(value, out StatsInterval type)) + { + IntervalType = type; + } } } + [JsonPropertyName("start_time")] + public DateTime StartTime { get; set; } + + [JsonPropertyName("end_time")] + public DateTime EndTime { get; set; } + public override string ToString() { - return $"{Metric}-{Dimension}: {Data?.Count ?? 0} ({RecordTime})"; + return $"{Metric}-{Dimension} ({Interval}): {Data?.Count ?? 0}"; } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStatsInput.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStatsInput.cs index ecb7ba46d..0058872e3 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStatsInput.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStatsInput.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Statistics.Enums; + namespace BotSharp.Abstraction.Statistics.Models; public class BotSharpStatsInput @@ -5,5 +7,6 @@ public class BotSharpStatsInput public string Metric { get; set; } public string Dimension { get; set; } public List Data { get; set; } = []; - public DateTime RecordTime { get; set; } + public DateTime RecordTime { get; set; } = DateTime.UtcNow; + public StatsInterval IntervalType { get; set; } = StatsInterval.Day; } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs index 776275932..ade975921 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs @@ -65,6 +65,7 @@ public void AddToken(TokenStatsModel stats, RoleDialogModel message) Metric = StatsCategory.AgentLlmCost, Dimension = message.CurrentAgentId, RecordTime = DateTime.UtcNow, + IntervalType = StatsInterval.Day, Data = [ new StatsKeyValuePair("prompt_token_count_total", stats.PromptCount), new StatsKeyValuePair("completion_token_count_total", stats.CompletionCount), diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Stats.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Stats.cs index 387801ba5..579071dca 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Stats.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Stats.cs @@ -4,28 +4,34 @@ namespace BotSharp.Core.Repository; public partial class FileRepository { - public BotSharpStats? GetGlobalStats(string metric, string dimension, DateTime recordTime) + public BotSharpStats? GetGlobalStats(string metric, string dimension, DateTime recordTime, StatsInterval interval) { var baseDir = Path.Combine(_dbSettings.FileRepository, STATS_FOLDER); - var dir = Path.Combine(baseDir, metric, recordTime.Year.ToString(), recordTime.Month.ToString("D2")); + var (startTime, endTime) = BuildTimeInterval(recordTime, interval); + var dir = Path.Combine(baseDir, metric, startTime.Year.ToString(), startTime.Month.ToString("D2")); if (!Directory.Exists(dir)) return null; var file = Directory.GetFiles(dir).FirstOrDefault(x => Path.GetFileName(x) == STATS_FILE); if (file == null) return null; - var time = BuildRecordTime(recordTime); var text = File.ReadAllText(file); var list = JsonSerializer.Deserialize>(text, _options); var found = list?.FirstOrDefault(x => x.Metric.IsEqualTo(metric) && x.Dimension.IsEqualTo(dimension) - && x.RecordTime == time); + && x.StartTime == startTime + && x.EndTime == endTime); + return found; } public bool SaveGlobalStats(BotSharpStats body) { var baseDir = Path.Combine(_dbSettings.FileRepository, STATS_FOLDER); - var dir = Path.Combine(baseDir, body.Metric, body.RecordTime.Year.ToString(), body.RecordTime.Month.ToString("D2")); + var (startTime, endTime) = BuildTimeInterval(body.RecordTime, body.IntervalType); + body.StartTime = startTime; + body.EndTime = endTime; + + var dir = Path.Combine(baseDir, body.Metric, startTime.Year.ToString(), startTime.Month.ToString("D2")); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); @@ -39,12 +45,12 @@ public bool SaveGlobalStats(BotSharpStats body) } else { - var time = BuildRecordTime(body.RecordTime); var text = File.ReadAllText(file); var list = JsonSerializer.Deserialize>(text, _options); var found = list?.FirstOrDefault(x => x.Metric.IsEqualTo(body.Metric) && x.Dimension.IsEqualTo(body.Dimension) - && x.RecordTime == time); + && x.StartTime == startTime + && x.EndTime == endTime); if (found != null) { @@ -52,6 +58,9 @@ public bool SaveGlobalStats(BotSharpStats body) found.Dimension = body.Dimension; found.Data = body.Data; found.RecordTime = body.RecordTime; + found.StartTime = body.StartTime; + found.EndTime = body.EndTime; + found.Interval = body.Interval; } else if (list != null) { @@ -69,10 +78,36 @@ public bool SaveGlobalStats(BotSharpStats body) } #region Private methods - private DateTime BuildRecordTime(DateTime date) + private (DateTime, DateTime) BuildTimeInterval(DateTime recordTime, StatsInterval interval) { - var recordDate = new DateTime(date.Year, date.Month, date.Day, date.Hour, 0, 0); - return DateTime.SpecifyKind(recordDate, DateTimeKind.Utc); + DateTime startTime = recordTime; + DateTime endTime = DateTime.UtcNow; + + switch (interval) + { + case StatsInterval.Hour: + startTime = new DateTime(recordTime.Year, recordTime.Month, recordTime.Day, recordTime.Hour, 0, 0); + endTime = startTime.AddHours(1); + break; + case StatsInterval.Week: + var dayOfWeek = startTime.DayOfWeek; + var firstDayOfWeek = startTime.AddDays(-(int)dayOfWeek); + startTime = new DateTime(firstDayOfWeek.Year, firstDayOfWeek.Month, firstDayOfWeek.Day, 0, 0, 0); + endTime = startTime.AddDays(7); + break; + case StatsInterval.Month: + startTime = new DateTime(recordTime.Year, recordTime.Month, 1); + endTime = startTime.AddMonths(1); + break; + default: + startTime = new DateTime(recordTime.Year, recordTime.Month, recordTime.Day, 0, 0, 0); + endTime = startTime.AddDays(1); + break; + } + + startTime = DateTime.SpecifyKind(startTime, DateTimeKind.Utc); + endTime = DateTime.SpecifyKind(endTime, DateTimeKind.Utc); + return (startTime, endTime); } #endregion } diff --git a/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatsService.cs b/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatsService.cs index f44217856..fca0697fb 100644 --- a/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatsService.cs +++ b/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatsService.cs @@ -30,7 +30,8 @@ public bool UpdateStats(string resourceKey, BotSharpStatsInput input) || string.IsNullOrEmpty(resourceKey) || input == null || string.IsNullOrEmpty(input.Metric) - || string.IsNullOrEmpty(input.Dimension)) + || string.IsNullOrEmpty(input.Dimension) + || input.Data.IsNullOrEmpty()) { return false; } @@ -39,7 +40,7 @@ public bool UpdateStats(string resourceKey, BotSharpStatsInput input) var res = locker.Lock(resourceKey, () => { var db = _services.GetRequiredService(); - var body = db.GetGlobalStats(input.Metric, input.Dimension, input.RecordTime); + var body = db.GetGlobalStats(input.Metric, input.Dimension, input.RecordTime, input.IntervalType); if (body == null) { var stats = new BotSharpStats @@ -47,6 +48,7 @@ public bool UpdateStats(string resourceKey, BotSharpStatsInput input) Metric = input.Metric, Dimension = input.Dimension, RecordTime = input.RecordTime, + IntervalType = input.IntervalType, Data = input.Data.ToDictionary(x => x.Key, x => x.Value) }; db.SaveGlobalStats(stats); diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs index fc6db8913..41fd5852a 100644 --- a/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs +++ b/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs @@ -33,10 +33,11 @@ private void UpdateAgentCall(RoleDialogModel message) { Metric = StatsCategory.AgentCall, Dimension = message.CurrentAgentId, + RecordTime = DateTime.UtcNow, + IntervalType = StatsInterval.Day, Data = [ new StatsKeyValuePair("agent_call_count", 1) - ], - RecordTime = DateTime.UtcNow + ] }; globalStats.UpdateStats("global-agent-call", body); } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/GlobalStatisticsDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/GlobalStatisticsDocument.cs index 48a0baa24..35d04f8e6 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/GlobalStatisticsDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/GlobalStatisticsDocument.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Statistics.Enums; + namespace BotSharp.Plugin.MongoStorage.Collections; public class GlobalStatisticsDocument : MongoBase @@ -6,4 +8,7 @@ public class GlobalStatisticsDocument : MongoBase public string Dimension { get; set; } public IDictionary Data { get; set; } = new Dictionary(); public DateTime RecordTime { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public string Interval { get; set; } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Stats.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Stats.cs index b90398754..0c0614691 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Stats.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Stats.cs @@ -1,19 +1,21 @@ +using BotSharp.Abstraction.Statistics.Enums; using BotSharp.Abstraction.Statistics.Models; namespace BotSharp.Plugin.MongoStorage.Repository; public partial class MongoRepository { - public BotSharpStats? GetGlobalStats(string metric, string dimension, DateTime recordTime) + public BotSharpStats? GetGlobalStats(string metric, string dimension, DateTime recordTime, StatsInterval interval) { - var time = BuildRecordTime(recordTime); + var (startTime, endTime) = BuildTimeInterval(recordTime, interval); var builder = Builders.Filter; var filters = new List>() { builder.Eq(x => x.Metric, metric), builder.Eq(x => x.Dimension, dimension), - builder.Eq(x => x.RecordTime, time) + builder.Eq(x => x.StartTime, startTime), + builder.Eq(x => x.EndTime, endTime) }; var filterDef = builder.And(filters); @@ -25,19 +27,27 @@ public partial class MongoRepository Metric = found.Metric, Dimension = found.Dimension, Data = found.Data, - RecordTime = found.RecordTime + RecordTime = found.RecordTime, + StartTime = startTime, + EndTime = endTime, + Interval = interval.ToString() }; } public bool SaveGlobalStats(BotSharpStats body) { - var time = BuildRecordTime(body.RecordTime); + var (startTime, endTime) = BuildTimeInterval(body.RecordTime, body.IntervalType); + body.RecordTime = DateTime.SpecifyKind(body.RecordTime, DateTimeKind.Utc); + body.StartTime = startTime; + body.EndTime = endTime; + var builder = Builders.Filter; var filters = new List>() { builder.Eq(x => x.Metric, body.Metric), builder.Eq(x => x.Dimension, body.Dimension), - builder.Eq(x => x.RecordTime, time) + builder.Eq(x => x.StartTime, startTime), + builder.Eq(x => x.EndTime, endTime) }; var filterDef = builder.And(filters); @@ -46,17 +56,46 @@ public bool SaveGlobalStats(BotSharpStats body) .Set(x => x.Metric, body.Metric) .Set(x => x.Dimension, body.Dimension) .Set(x => x.Data, body.Data) - .Set(x => x.RecordTime, time); + .Set(x => x.StartTime, body.StartTime) + .Set(x => x.EndTime, body.EndTime) + .Set(x => x.Interval, body.Interval) + .Set(x => x.RecordTime, body.RecordTime); _dc.GlobalStatistics.UpdateOne(filterDef, updateDef, _options); return true; } #region Private methods - private DateTime BuildRecordTime(DateTime date) + private (DateTime, DateTime) BuildTimeInterval(DateTime recordTime, StatsInterval interval) { - var recordDate = new DateTime(date.Year, date.Month, date.Day, date.Hour, 0, 0); - return DateTime.SpecifyKind(recordDate, DateTimeKind.Utc); + DateTime startTime = recordTime; + DateTime endTime = DateTime.UtcNow; + + switch (interval) + { + case StatsInterval.Hour: + startTime = new DateTime(recordTime.Year, recordTime.Month, recordTime.Day, recordTime.Hour, 0, 0); + endTime = startTime.AddHours(1); + break; + case StatsInterval.Week: + var dayOfWeek = startTime.DayOfWeek; + var firstDayOfWeek = startTime.AddDays(-(int)dayOfWeek); + startTime = new DateTime(firstDayOfWeek.Year, firstDayOfWeek.Month, firstDayOfWeek.Day, 0, 0, 0); + endTime = startTime.AddDays(7); + break; + case StatsInterval.Month: + startTime = new DateTime(recordTime.Year, recordTime.Month, 1); + endTime = startTime.AddMonths(1); + break; + default: + startTime = new DateTime(recordTime.Year, recordTime.Month, recordTime.Day, 0, 0, 0); + endTime = startTime.AddDays(1); + break; + } + + startTime = DateTime.SpecifyKind(startTime, DateTimeKind.Utc); + endTime = DateTime.SpecifyKind(endTime, DateTimeKind.Utc); + return (startTime, endTime); } #endregion }