Files
AX-Copilot-Codex/src/AxCopilot/Services/UsageStatisticsService.cs

213 lines
7.0 KiB
C#

using System.IO;
using System.Text.Json;
using AxCopilot.Models;
namespace AxCopilot.Services;
/// <summary>
/// 일별 사용 통계를 기록하고 조회합니다.
/// 저장 위치: %APPDATA%\AxCopilot\stats\YYYY-MM-DD.json
/// 30일 초과 파일은 자동 삭제(롤링).
/// </summary>
internal static class UsageStatisticsService
{
private static readonly string _statsDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "stats");
private static DailyUsageStats _today = new();
private static bool _initialized;
private static readonly object _lock = new();
private const int RetentionDays = 30;
// ─── 공개 API ────────────────────────────────────────────────────────────
public static void RecordLauncherOpen()
{
EnsureInitialized();
lock (_lock) { _today.LauncherOpens++; }
_ = SaveTodayAsync();
}
public static void RecordCommandUsage(string commandKey)
{
if (string.IsNullOrWhiteSpace(commandKey)) return;
EnsureInitialized();
lock (_lock)
{
_today.CommandUsage.TryGetValue(commandKey, out var cur);
_today.CommandUsage[commandKey] = cur + 1;
}
_ = SaveTodayAsync();
}
public static void AddActiveSeconds(int seconds)
{
if (seconds <= 0) return;
EnsureInitialized();
lock (_lock) { _today.ActiveSeconds += seconds; }
_ = SaveTodayAsync();
}
/// <summary>당일 기록된 PC 활성 시간(초)을 반환합니다. 재시작 전 누적분 포함.</summary>
public static int GetTodayActiveSeconds()
{
EnsureInitialized();
lock (_lock) { return _today.ActiveSeconds; }
}
/// <summary>AX Agent 대화를 탭별로 기록합니다.</summary>
public static void RecordChat(string tab)
{
if (string.IsNullOrWhiteSpace(tab)) tab = "Chat";
EnsureInitialized();
lock (_lock)
{
_today.ChatCounts.TryGetValue(tab, out var cur);
_today.ChatCounts[tab] = cur + 1;
}
_ = SaveTodayAsync();
}
/// <summary>토큰 사용량을 기록합니다.</summary>
public static void RecordTokens(int promptTokens, int completionTokens)
{
if (promptTokens <= 0 && completionTokens <= 0) return;
EnsureInitialized();
lock (_lock)
{
_today.PromptTokens += promptTokens;
_today.CompletionTokens += completionTokens;
_today.TotalTokens += promptTokens + completionTokens;
}
_ = SaveTodayAsync();
}
/// <summary>최근 <paramref name="days"/>일치 통계를 날짜 오름차순으로 반환합니다.</summary>
public static List<DailyUsageStats> GetStats(int days = 30)
{
EnsureInitialized();
var result = new List<DailyUsageStats>();
var today = DateTime.Today;
for (int i = days - 1; i >= 0; i--)
{
var date = today.AddDays(-i);
var dateStr = date.ToString("yyyy-MM-dd");
if (dateStr == _today.Date)
{
DailyUsageStats snapshot;
lock (_lock) { snapshot = CloneToday(); }
result.Add(snapshot);
continue;
}
var path = GetFilePath(dateStr);
if (File.Exists(path))
{
try
{
var json = File.ReadAllText(path);
var stat = JsonSerializer.Deserialize<DailyUsageStats>(json);
if (stat != null) result.Add(stat);
else result.Add(new DailyUsageStats { Date = dateStr });
}
catch { result.Add(new DailyUsageStats { Date = dateStr }); }
}
else
{
result.Add(new DailyUsageStats { Date = dateStr });
}
}
return result;
}
// ─── 내부 ────────────────────────────────────────────────────────────────
private static void EnsureInitialized()
{
if (_initialized) return;
lock (_lock)
{
if (_initialized) return;
try
{
Directory.CreateDirectory(_statsDir);
var todayStr = DateTime.Today.ToString("yyyy-MM-dd");
var path = GetFilePath(todayStr);
if (File.Exists(path))
{
var json = File.ReadAllText(path);
var stat = JsonSerializer.Deserialize<DailyUsageStats>(json);
_today = stat ?? new DailyUsageStats { Date = todayStr };
}
else
{
_today = new DailyUsageStats { Date = todayStr };
}
PurgeOldFiles();
}
catch (Exception ex)
{
LogService.Warn($"통계 서비스 초기화 실패: {ex.Message}");
_today = new DailyUsageStats { Date = DateTime.Today.ToString("yyyy-MM-dd") };
}
_initialized = true;
}
}
private static void PurgeOldFiles()
{
try
{
var cutoff = DateTime.Today.AddDays(-RetentionDays);
foreach (var file in Directory.GetFiles(_statsDir, "????-??-??.json"))
{
var name = Path.GetFileNameWithoutExtension(file);
if (DateTime.TryParse(name, out var fileDate) && fileDate < cutoff)
File.Delete(file);
}
}
catch (Exception ex)
{
LogService.Warn($"통계 파일 정리 실패: {ex.Message}");
}
}
private static async Task SaveTodayAsync()
{
try
{
DailyUsageStats snapshot;
lock (_lock) { snapshot = CloneToday(); }
var path = GetFilePath(snapshot.Date);
var json = JsonSerializer.Serialize(snapshot, new JsonSerializerOptions { WriteIndented = false });
await File.WriteAllTextAsync(path, json);
}
catch (Exception ex)
{
LogService.Warn($"통계 저장 실패: {ex.Message}");
}
}
private static DailyUsageStats CloneToday() => new()
{
Date = _today.Date,
LauncherOpens = _today.LauncherOpens,
ActiveSeconds = _today.ActiveSeconds,
CommandUsage = new Dictionary<string, int>(_today.CommandUsage),
ChatCounts = new Dictionary<string, int>(_today.ChatCounts),
TotalTokens = _today.TotalTokens,
PromptTokens = _today.PromptTokens,
CompletionTokens = _today.CompletionTokens,
};
private static string GetFilePath(string dateStr)
=> Path.Combine(_statsDir, $"{dateStr}.json");
}