diff --git a/API/Controllers/StatsController.cs b/API/Controllers/StatsController.cs index 09fcf9738..0ce9bebed 100644 --- a/API/Controllers/StatsController.cs +++ b/API/Controllers/StatsController.cs @@ -25,7 +25,7 @@ namespace API.Controllers { try { - await _statsService.PathData(clientInfoDto); + await _statsService.RecordClientInfo(clientInfoDto); return Ok(); } diff --git a/API/Extensions/ServiceCollectionExtensions.cs b/API/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index 903ed5b88..000000000 --- a/API/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using API.Interfaces.Services; -using API.Services.Clients; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace API.Extensions -{ - public static class ServiceCollectionExtensions - { - public static IServiceCollection AddStartupTask(this IServiceCollection services) - where T : class, IStartupTask - => services.AddTransient(); - - public static IServiceCollection AddStatsClient(this IServiceCollection services, IConfiguration configuration) - { - services.AddHttpClient(client => - { - client.DefaultRequestHeaders.Add("api-key", "MsnvA2DfQqxSK5jh"); - }); - - return services; - } - } -} diff --git a/API/Interfaces/Services/IStatsService.cs b/API/Interfaces/Services/IStatsService.cs index 19d8d9f4b..9e3536e23 100644 --- a/API/Interfaces/Services/IStatsService.cs +++ b/API/Interfaces/Services/IStatsService.cs @@ -5,7 +5,7 @@ namespace API.Interfaces.Services { public interface IStatsService { - Task PathData(ClientInfoDto clientInfoDto); - Task CollectAndSendStatsData(); + Task RecordClientInfo(ClientInfoDto clientInfoDto); + Task Send(); } } diff --git a/API/Services/Clients/StatsApiClient.cs b/API/Services/Clients/StatsApiClient.cs deleted file mode 100644 index 52d1c9fcf..000000000 --- a/API/Services/Clients/StatsApiClient.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Net.Http; -using System.Net.Http.Json; -using System.Threading.Tasks; -using API.DTOs.Stats; -using Microsoft.Extensions.Logging; - -namespace API.Services.Clients -{ - public class StatsApiClient - { - private readonly HttpClient _client; - private readonly ILogger _logger; -#pragma warning disable S1075 - private const string ApiUrl = "http://stats.kavitareader.com"; -#pragma warning restore S1075 - - public StatsApiClient(HttpClient client, ILogger logger) - { - _client = client; - _logger = logger; - _client.Timeout = TimeSpan.FromSeconds(30); - } - - public async Task SendDataToStatsServer(UsageStatisticsDto data) - { - var responseContent = string.Empty; - - try - { - using var response = await _client.PostAsJsonAsync(ApiUrl + "/api/InstallationStats", data); - - responseContent = await response.Content.ReadAsStringAsync(); - - response.EnsureSuccessStatusCode(); - } - catch (HttpRequestException e) - { - var info = new - { - dataSent = data, - response = responseContent - }; - - _logger.LogError(e, "KavitaStats did not respond successfully. {Content}", info); - throw; - } - catch (Exception e) - { - _logger.LogError(e, "An error happened during the request to KavitaStats"); - throw; - } - } - } -} diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 360492303..c850e9084 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -20,6 +20,7 @@ namespace API.Services public static readonly string LogDirectory = Path.Join(Directory.GetCurrentDirectory(), "logs"); public static readonly string CacheDirectory = Path.Join(Directory.GetCurrentDirectory(), "cache"); public static readonly string CoverImageDirectory = Path.Join(Directory.GetCurrentDirectory(), "covers"); + public static readonly string StatsDirectory = Path.Join(Directory.GetCurrentDirectory(), "stats"); public DirectoryService(ILogger logger) { diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index e60161b61..5804ad9ce 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -89,7 +89,7 @@ namespace API.Services } _logger.LogDebug("Scheduling stat collection daily"); - RecurringJob.AddOrUpdate(SendDataTask, () => _statsService.CollectAndSendStatsData(), Cron.Daily, TimeZoneInfo.Local); + RecurringJob.AddOrUpdate(SendDataTask, () => _statsService.Send(), Cron.Daily, TimeZoneInfo.Local); } public void CancelStatsTasks() @@ -102,7 +102,7 @@ namespace API.Services public void RunStatCollection() { _logger.LogInformation("Enqueuing stat collection"); - BackgroundJob.Enqueue(() => _statsService.CollectAndSendStatsData()); + BackgroundJob.Enqueue(() => _statsService.Send()); } #endregion diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs index 70eaecf7d..62393393e 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/API/Services/Tasks/StatsService.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Net.Http; using System.Runtime.InteropServices; using System.Text.Json; using System.Threading; @@ -9,10 +10,11 @@ using API.Data; using API.DTOs.Stats; using API.Interfaces; using API.Interfaces.Services; -using API.Services.Clients; +using Flurl.Http; using Hangfire; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -20,75 +22,36 @@ namespace API.Services.Tasks { public class StatsService : IStatsService { - private const string TempFilePath = "stats/"; - private const string TempFileName = "app_stats.json"; + private const string StatFileName = "app_stats.json"; - private readonly StatsApiClient _client; private readonly DataContext _dbContext; private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; - public StatsService(StatsApiClient client, DataContext dbContext, ILogger logger, +#pragma warning disable S1075 + private const string ApiUrl = "http://stats.kavitareader.com"; +#pragma warning restore S1075 + private static readonly string StatsFilePath = Path.Combine(DirectoryService.StatsDirectory, StatFileName); + + private static bool FileExists => File.Exists(StatsFilePath); + + public StatsService(DataContext dbContext, ILogger logger, IUnitOfWork unitOfWork) { - _client = client; _dbContext = dbContext; _logger = logger; _unitOfWork = unitOfWork; } - private static string FinalPath => Path.Combine(Directory.GetCurrentDirectory(), TempFilePath, TempFileName); - private static bool FileExists => File.Exists(FinalPath); - - public async Task PathData(ClientInfoDto clientInfoDto) - { - _logger.LogDebug("Pathing client data to the file"); - - var statisticsDto = await GetData(); - - statisticsDto.AddClientInfo(clientInfoDto); - - await SaveFile(statisticsDto); - } - - private async Task CollectRelevantData() - { - _logger.LogDebug("Collecting data from the server and database"); - - _logger.LogDebug("Collecting usage info"); - var usageInfo = await GetUsageInfo(); - - _logger.LogDebug("Collecting server info"); - var serverInfo = GetServerInfo(); - - await PathData(serverInfo, usageInfo); - } - - private async Task FinalizeStats() - { - try - { - var data = await GetExistingData(); - await _client.SendDataToStatsServer(data); - if (FileExists) File.Delete(FinalPath); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error Finalizing Stats collection flow"); - throw; - } - } - /// /// Due to all instances firing this at the same time, we can DDOS our server. This task when fired will schedule the task to be run /// randomly over a 6 hour spread /// - public async Task CollectAndSendStatsData() + public async Task Send() { var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).AllowStatCollection; if (!allowStatCollection) { - _logger.LogDebug("User has opted out of stat collection, not registering tasks"); return; } @@ -111,15 +74,92 @@ namespace API.Services.Tasks // ReSharper disable once MemberCanBePrivate.Global public async Task SendData() { - _logger.LogDebug("Sending data to the Stats server"); await CollectRelevantData(); await FinalizeStats(); } + public async Task RecordClientInfo(ClientInfoDto clientInfoDto) + { + var statisticsDto = await GetData(); + statisticsDto.AddClientInfo(clientInfoDto); + + await SaveFile(statisticsDto); + } + + private async Task CollectRelevantData() + { + var usageInfo = await GetUsageInfo(); + var serverInfo = GetServerInfo(); + + await PathData(serverInfo, usageInfo); + } + + private async Task FinalizeStats() + { + try + { + var data = await GetExistingData(); + var successful = await SendDataToStatsServer(data); + + if (successful) + { + ResetStats(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an exception while sending data to KavitaStats"); + } + } + + private async Task SendDataToStatsServer(UsageStatisticsDto data) + { + var responseContent = string.Empty; + + try + { + var response = await (ApiUrl + "/api/InstallationStats") + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-api-key", "MsnvA2DfQqxSK5jh") + .WithHeader("api-key", "MsnvA2DfQqxSK5jh") + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithTimeout(TimeSpan.FromSeconds(30)) + .PostJsonAsync(data); + + if (response.StatusCode != StatusCodes.Status200OK) + { + _logger.LogError("KavitaStats did not respond successfully. {Content}", response); + return false; + } + + return true; + } + catch (HttpRequestException e) + { + var info = new + { + dataSent = data, + response = responseContent + }; + + _logger.LogError(e, "KavitaStats did not respond successfully. {Content}", info); + } + catch (Exception e) + { + _logger.LogError(e, "An error happened during the request to KavitaStats"); + } + + return false; + } + + private static void ResetStats() + { + if (FileExists) File.Delete(StatsFilePath); + } + private async Task PathData(ServerInfoDto serverInfoDto, UsageInfoDto usageInfoDto) { - _logger.LogDebug("Pathing server and usage info to the file"); - var data = await GetData(); data.ServerInfo = serverInfoDto; @@ -130,7 +170,7 @@ namespace API.Services.Tasks await SaveFile(data); } - private async ValueTask GetData() + private static async ValueTask GetData() { if (!FileExists) return new UsageStatisticsDto {InstallId = HashUtil.AnonymousToken()}; @@ -176,39 +216,17 @@ namespace API.Services.Tasks return serverInfo; } - private async Task GetExistingData() + private static async Task GetExistingData() { - _logger.LogInformation("Fetching existing data from file"); - var existingDataJson = await GetFileDataAsString(); - - _logger.LogInformation("Deserializing data from file to object"); - var existingData = JsonSerializer.Deserialize(existingDataJson); - - return existingData; + var json = await File.ReadAllTextAsync(StatsFilePath); + return JsonSerializer.Deserialize(json); } - private async Task GetFileDataAsString() + private static async Task SaveFile(UsageStatisticsDto statisticsDto) { - _logger.LogInformation("Reading file from disk"); - return await File.ReadAllTextAsync(FinalPath); - } + DirectoryService.ExistOrCreate(DirectoryService.StatsDirectory); - private async Task SaveFile(UsageStatisticsDto statisticsDto) - { - _logger.LogDebug("Saving file"); - - var finalDirectory = FinalPath.Replace(TempFileName, string.Empty); - if (!Directory.Exists(finalDirectory)) - { - _logger.LogDebug("Creating tmp directory"); - Directory.CreateDirectory(finalDirectory); - } - - _logger.LogDebug("Serializing data to write"); - var dataJson = JsonSerializer.Serialize(statisticsDto); - - _logger.LogDebug("Writing file to the disk"); - await File.WriteAllTextAsync(FinalPath, dataJson); + await File.WriteAllTextAsync(StatsFilePath, JsonSerializer.Serialize(statisticsDto)); } } } diff --git a/API/Startup.cs b/API/Startup.cs index e04d61279..d0577d06d 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -106,7 +106,6 @@ namespace API services.AddResponseCaching(); - services.AddStatsClient(_config); services.AddHangfire(configuration => configuration .UseSimpleAssemblyNameTypeSerializer()