diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs index 0199126dc..9f0da274a 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs @@ -34,7 +34,9 @@ namespace Squidex.Domain.Apps.Entities.Assets public async Task GetTotalSizeAsync(Guid appId) { - var entries = await usageStore.QueryAsync(appId.ToString(), SummaryDate, SummaryDate); + var key = GetKey(appId); + + var entries = await usageStore.QueryAsync(key, SummaryDate, SummaryDate); return (long)entries.Select(x => x.Counters.Get(CounterTotalSize)).FirstOrDefault(); } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs index dfefeb9e1..5f8bfceae 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs @@ -60,11 +60,16 @@ namespace Squidex.Domain.Apps.Entities.Assets [CounterTotalCount] = count }; - var key = appId.ToString(); + var key = GetKey(appId); return Task.WhenAll( usageStore.TrackUsagesAsync(new UsageUpdate(date, key, Category, counters)), usageStore.TrackUsagesAsync(new UsageUpdate(SummaryDate, key, Category, counters))); } + + private static string GetKey(Guid appId) + { + return $"{appId}_Assets"; + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs index 3f10076f0..19eea457e 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs @@ -251,7 +251,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards if ((properties.Editor == TagsFieldEditor.Checkboxes || properties.Editor == TagsFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Count == 0)) { - yield return new ValidationError("Chekboxes or dropdown list need allowed values.", + yield return new ValidationError("Checkboxes or dropdown list need allowed values.", nameof(properties.AllowedValues)); } diff --git a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs index 3dcc82b19..4de7138ad 100644 --- a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs @@ -96,15 +96,15 @@ namespace Squidex.Infrastructure.UsageTracking } } - public Task TrackAsync(string key, string category, double weight, double elapsedMs) + public Task TrackAsync(Guid appId, string category, double weight, double elapsedMs) { - Guard.NotNull(key, nameof(key)); + var key = GetKey(appId); ThrowIfDisposed(); if (weight > 0) { - category = CleanCategory(category); + category = GetCategory(category); usages.AddOrUpdate((key, category), _ => new Usage(elapsedMs, weight), (k, x) => x.Add(elapsedMs, weight)); } @@ -112,14 +112,14 @@ namespace Squidex.Infrastructure.UsageTracking return TaskHelper.Done; } - public async Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate) + public async Task>> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate) { - Guard.NotNull(key, nameof(key)); - ThrowIfDisposed(); + var key = GetKey(appId); + var usagesFlat = await usageRepository.QueryAsync(key, fromDate, toDate); - var usagesByCategory = usagesFlat.GroupBy(x => CleanCategory(x.Category)).ToDictionary(x => x.Key, x => x.ToList()); + var usagesByCategory = usagesFlat.GroupBy(x => GetCategory(x.Category)).ToDictionary(x => x.Key, x => x.ToList()); var result = new Dictionary>(); @@ -167,12 +167,12 @@ namespace Squidex.Infrastructure.UsageTracking return result; } - public async Task GetMonthlyCallsAsync(string key, DateTime date) + public async Task GetMonthlyCallsAsync(Guid appId, DateTime date) { - Guard.NotNull(key, nameof(key)); - ThrowIfDisposed(); + var key = GetKey(appId); + var dateFrom = new DateTime(date.Year, date.Month, 1); var dateTo = dateFrom.AddMonths(1).AddDays(-1); @@ -181,9 +181,14 @@ namespace Squidex.Infrastructure.UsageTracking return originalUsages.Sum(x => (long)x.Counters.Get(CounterTotalCalls)); } - private static string CleanCategory(string category) + private static string GetCategory(string category) { return !string.IsNullOrWhiteSpace(category) ? category.Trim() : "*"; } + + private static string GetKey(Guid appId) + { + return $"{appId}_API"; + } } } diff --git a/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs index 732a48596..4485a6bb2 100644 --- a/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs @@ -25,27 +25,25 @@ namespace Squidex.Infrastructure.UsageTracking this.inner = inner; } - public Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate) + public Task>> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate) { - return inner.QueryAsync(key, fromDate, toDate); + return inner.QueryAsync(appId, fromDate, toDate); } - public Task TrackAsync(string key, string category, double weight, double elapsedMs) + public Task TrackAsync(Guid appId, string category, double weight, double elapsedMs) { - return inner.TrackAsync(key, category, weight, elapsedMs); + return inner.TrackAsync(appId, category, weight, elapsedMs); } - public Task GetMonthlyCallsAsync(string key, DateTime date) + public Task GetMonthlyCallsAsync(Guid appId, DateTime date) { - Guard.NotNull(key, nameof(key)); - - var cacheKey = string.Concat(key, date); + var cacheKey = string.Concat(appId, date); return Cache.GetOrCreateAsync(cacheKey, entry => { entry.AbsoluteExpirationRelativeToNow = CacheDuration; - return inner.GetMonthlyCallsAsync(key, date); + return inner.GetMonthlyCallsAsync(appId, date); }); } } diff --git a/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs index f0945d1e1..3fe05d8bf 100644 --- a/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs @@ -13,10 +13,10 @@ namespace Squidex.Infrastructure.UsageTracking { public interface IUsageTracker { - Task TrackAsync(string key, string category, double weight, double elapsedMs); + Task TrackAsync(Guid appId, string category, double weight, double elapsedMs); - Task GetMonthlyCallsAsync(string key, DateTime date); + Task GetMonthlyCallsAsync(Guid appId, DateTime date); - Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate); + Task>> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate); } } diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index 67d0295df..fdbcc6a4d 100644 --- a/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -58,7 +58,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics [ApiCosts(0)] public async Task GetMonthlyCalls(string app) { - var count = await usageTracker.GetMonthlyCallsAsync(AppId.ToString(), DateTime.Today); + var count = await usageTracker.GetMonthlyCallsAsync(AppId, DateTime.Today); var plan = appPlanProvider.GetPlanForApp(App); @@ -90,7 +90,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics return BadRequest(); } - var entities = await usageTracker.QueryAsync(AppId.ToString(), fromDate.Date, toDate.Date); + var entities = await usageTracker.QueryAsync(AppId, fromDate.Date, toDate.Date); var response = entities.ToDictionary(x => x.Key, x => x.Value.Select(CallsUsageDto.FromUsage).ToList()); diff --git a/src/Squidex/Pipeline/ApiCostsFilter.cs b/src/Squidex/Pipeline/ApiCostsFilter.cs index 9a41b9c9e..0cba6287f 100644 --- a/src/Squidex/Pipeline/ApiCostsFilter.cs +++ b/src/Squidex/Pipeline/ApiCostsFilter.cs @@ -53,7 +53,7 @@ namespace Squidex.Pipeline { var plan = appPlanProvider.GetPlanForApp(appFeature.App); - var usage = await usageTracker.GetMonthlyCallsAsync(appFeature.App.Id.ToString(), DateTime.Today); + var usage = await usageTracker.GetMonthlyCallsAsync(appFeature.App.Id, DateTime.Today); if (plan.MaxApiCalls >= 0 && usage > plan.MaxApiCalls * 1.1) { @@ -72,7 +72,7 @@ namespace Squidex.Pipeline { var elapsedMs = watch.Stop(); - await usageTracker.TrackAsync(appFeature.App.Id.ToString(), context.HttpContext.User.OpenIdClientId(), FilterDefinition.Weight, elapsedMs); + await usageTracker.TrackAsync(appFeature.App.Id, context.HttpContext.User.OpenIdClientId(), FilterDefinition.Weight, elapsedMs); } } else diff --git a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs b/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs index e3ae304e2..df62c2b69 100644 --- a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs @@ -19,6 +19,7 @@ namespace Squidex.Infrastructure.UsageTracking { private readonly IUsageRepository usageStore = A.Fake(); private readonly ISemanticLog log = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); private readonly BackgroundUsageTracker sut; public BackgroundUsageTrackerTests() @@ -31,7 +32,7 @@ namespace Squidex.Infrastructure.UsageTracking { sut.Dispose(); - return Assert.ThrowsAsync(() => sut.TrackAsync("MyKey1", "category1", 1, 1000)); + return Assert.ThrowsAsync(() => sut.TrackAsync(appId, "category1", 1, 1000)); } [Fact] @@ -39,7 +40,7 @@ namespace Squidex.Infrastructure.UsageTracking { sut.Dispose(); - return Assert.ThrowsAsync(() => sut.QueryAsync("MyKey1", DateTime.Today, DateTime.Today.AddDays(1))); + return Assert.ThrowsAsync(() => sut.QueryAsync(appId, DateTime.Today, DateTime.Today.AddDays(1))); } [Fact] @@ -47,7 +48,7 @@ namespace Squidex.Infrastructure.UsageTracking { sut.Dispose(); - return Assert.ThrowsAsync(() => sut.GetMonthlyCallsAsync("MyKey1", DateTime.Today)); + return Assert.ThrowsAsync(() => sut.GetMonthlyCallsAsync(appId, DateTime.Today)); } [Fact] @@ -63,10 +64,10 @@ namespace Squidex.Infrastructure.UsageTracking new StoredUsage("category1", date.AddDays(7), Counters(17, 22)) }; - A.CallTo(() => usageStore.QueryAsync("MyKey1", new DateTime(2016, 1, 1), new DateTime(2016, 1, 31))) + A.CallTo(() => usageStore.QueryAsync($"{appId}_API", new DateTime(2016, 1, 1), new DateTime(2016, 1, 31))) .Returns(originalData); - var result = await sut.GetMonthlyCallsAsync("MyKey1", date); + var result = await sut.GetMonthlyCallsAsync(appId, date); Assert.Equal(55, result); } @@ -86,10 +87,10 @@ namespace Squidex.Infrastructure.UsageTracking new StoredUsage(null, f.AddDays(2), Counters(11, 14)) }; - A.CallTo(() => usageStore.QueryAsync("MyKey1", f, t)) + A.CallTo(() => usageStore.QueryAsync($"{appId}_API", f, t)) .Returns(originalData); - var result = await sut.QueryAsync("MyKey1", f, t); + var result = await sut.QueryAsync(appId, f, t); var expected = new Dictionary> { @@ -120,10 +121,10 @@ namespace Squidex.Infrastructure.UsageTracking var f = DateTime.Today; var t = DateTime.Today.AddDays(4); - A.CallTo(() => usageStore.QueryAsync("MyKey1", f, t)) + A.CallTo(() => usageStore.QueryAsync($"{appId}_API", f, t)) .Returns(new List()); - var result = await sut.QueryAsync("MyKey1", f, t); + var result = await sut.QueryAsync(appId, f, t); var expected = new Dictionary> { @@ -143,8 +144,8 @@ namespace Squidex.Infrastructure.UsageTracking [Fact] public async Task Should_not_track_if_weight_less_than_zero() { - await sut.TrackAsync("MyKey1", "MyCategory", -1, 1000); - await sut.TrackAsync("MyKey1", "MyCategory", 0, 1000); + await sut.TrackAsync(appId, "MyCategory", -1, 1000); + await sut.TrackAsync(appId, "MyCategory", 0, 1000); sut.Next(); sut.Dispose(); @@ -156,18 +157,22 @@ namespace Squidex.Infrastructure.UsageTracking [Fact] public async Task Should_aggregate_and_store_on_dispose() { + var appId1 = Guid.NewGuid(); + var appId2 = Guid.NewGuid(); + var appId3 = Guid.NewGuid(); + var today = DateTime.Today; - await sut.TrackAsync("MyKey1", "MyCategory1", 1, 1000); + await sut.TrackAsync(appId1, "MyCategory1", 1, 1000); - await sut.TrackAsync("MyKey2", "MyCategory1", 1.0, 2000); - await sut.TrackAsync("MyKey2", "MyCategory1", 0.5, 3000); + await sut.TrackAsync(appId2, "MyCategory1", 1.0, 2000); + await sut.TrackAsync(appId2, "MyCategory1", 0.5, 3000); - await sut.TrackAsync("MyKey3", "MyCategory1", 0.3, 4000); - await sut.TrackAsync("MyKey3", "MyCategory1", 0.1, 5000); + await sut.TrackAsync(appId3, "MyCategory1", 0.3, 4000); + await sut.TrackAsync(appId3, "MyCategory1", 0.1, 5000); - await sut.TrackAsync("MyKey3", null, 0.5, 2000); - await sut.TrackAsync("MyKey3", null, 0.5, 6000); + await sut.TrackAsync(appId3, null, 0.5, 2000); + await sut.TrackAsync(appId3, null, 0.5, 6000); UsageUpdate[] updates = null; @@ -179,10 +184,10 @@ namespace Squidex.Infrastructure.UsageTracking updates.Should().BeEquivalentTo(new[] { - new UsageUpdate(today, "MyKey1", "MyCategory1", Counters(1.0, 1000)), - new UsageUpdate(today, "MyKey2", "MyCategory1", Counters(1.5, 5000)), - new UsageUpdate(today, "MyKey3", "MyCategory1", Counters(0.4, 9000)), - new UsageUpdate(today, "MyKey3", "*", Counters(1, 8000)) + new UsageUpdate(today, $"{appId1}_API", "MyCategory1", Counters(1.0, 1000)), + new UsageUpdate(today, $"{appId2}_API", "MyCategory1", Counters(1.5, 5000)), + new UsageUpdate(today, $"{appId3}_API", "MyCategory1", Counters(0.4, 9000)), + new UsageUpdate(today, $"{appId3}_API", "*", Counters(1, 8000)) }, o => o.ComparingByMembers()); A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) diff --git a/tests/Squidex.Infrastructure.Tests/UsageTracking/ThreadingUsageTrackerTests.cs b/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs similarity index 64% rename from tests/Squidex.Infrastructure.Tests/UsageTracking/ThreadingUsageTrackerTests.cs rename to tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs index 4a3bc30c9..51fec6632 100644 --- a/tests/Squidex.Infrastructure.Tests/UsageTracking/ThreadingUsageTrackerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs @@ -14,13 +14,14 @@ using Xunit; namespace Squidex.Infrastructure.UsageTracking { - public class ThreadingUsageTrackerTests + public class CachingUsageTrackerTests { private readonly MemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + private readonly Guid appId = Guid.NewGuid(); private readonly IUsageTracker inner = A.Fake(); private readonly IUsageTracker sut; - public ThreadingUsageTrackerTests() + public CachingUsageTrackerTests() { sut = new CachingUsageTracker(inner, cache); } @@ -28,34 +29,34 @@ namespace Squidex.Infrastructure.UsageTracking [Fact] public async Task Should_forward_track_call() { - await sut.TrackAsync("MyKey", "MyCategory", 123, 456); + await sut.TrackAsync(appId, "MyCategory", 123, 456); - A.CallTo(() => inner.TrackAsync("MyKey", "MyCategory", 123, 456)) + A.CallTo(() => inner.TrackAsync(appId, "MyCategory", 123, 456)) .MustHaveHappened(); } [Fact] public async Task Should_forward_query_call() { - await sut.QueryAsync("MyKey", DateTime.MaxValue, DateTime.MinValue); + await sut.QueryAsync(appId, DateTime.MaxValue, DateTime.MinValue); - A.CallTo(() => inner.QueryAsync("MyKey", DateTime.MaxValue, DateTime.MinValue)) + A.CallTo(() => inner.QueryAsync(appId, DateTime.MaxValue, DateTime.MinValue)) .MustHaveHappened(); } [Fact] public async Task Should_cache_monthly_usage() { - A.CallTo(() => inner.GetMonthlyCallsAsync("MyKey", DateTime.Today)) + A.CallTo(() => inner.GetMonthlyCallsAsync(appId, DateTime.Today)) .Returns(100); - var result1 = await sut.GetMonthlyCallsAsync("MyKey", DateTime.Today); - var result2 = await sut.GetMonthlyCallsAsync("MyKey", DateTime.Today); + var result1 = await sut.GetMonthlyCallsAsync(appId, DateTime.Today); + var result2 = await sut.GetMonthlyCallsAsync(appId, DateTime.Today); Assert.Equal(100, result1); Assert.Equal(100, result2); - A.CallTo(() => inner.GetMonthlyCallsAsync("MyKey", DateTime.Today)) + A.CallTo(() => inner.GetMonthlyCallsAsync(appId, DateTime.Today)) .MustHaveHappened(Repeated.Exactly.Once); } } diff --git a/tests/Squidex.Tests/Pipeline/ApiCostsFilterTests.cs b/tests/Squidex.Tests/Pipeline/ApiCostsFilterTests.cs index 161dc6864..2a50055b8 100644 --- a/tests/Squidex.Tests/Pipeline/ApiCostsFilterTests.cs +++ b/tests/Squidex.Tests/Pipeline/ApiCostsFilterTests.cs @@ -57,7 +57,7 @@ namespace Squidex.Pipeline A.CallTo(() => appPlan.MaxApiCalls) .ReturnsLazily(x => apiCallsMax); - A.CallTo(() => usageTracker.GetMonthlyCallsAsync(A.Ignored, DateTime.Today)) + A.CallTo(() => usageTracker.GetMonthlyCallsAsync(A.Ignored, DateTime.Today)) .ReturnsLazily(x => Task.FromResult(apiCallsCurrent)); next = () => @@ -85,7 +85,7 @@ namespace Squidex.Pipeline Assert.Equal(429, (actionContext.Result as StatusCodeResult).StatusCode); Assert.False(isNextCalled); - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) .MustNotHaveHappened(); } @@ -103,7 +103,7 @@ namespace Squidex.Pipeline Assert.True(isNextCalled); - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, 13, A.Ignored)) + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, 13, A.Ignored)) .MustHaveHappened(); } @@ -121,7 +121,7 @@ namespace Squidex.Pipeline Assert.True(isNextCalled); - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, 13, A.Ignored)) + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, 13, A.Ignored)) .MustHaveHappened(); } @@ -139,7 +139,7 @@ namespace Squidex.Pipeline Assert.True(isNextCalled); - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) .MustNotHaveHappened(); } @@ -155,7 +155,7 @@ namespace Squidex.Pipeline Assert.True(isNextCalled); - A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) + A.CallTo(() => usageTracker.TrackAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) .MustNotHaveHappened(); }