From 6f465c5e2ae6bdbe1be600a6e59e150ae6ca7837 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 13 May 2017 21:24:27 +0200 Subject: [PATCH] Tracking improved --- .../Apps/AppMasterLanguageSet.cs | 1 - .../UsageTracker/MongoUsage.cs | 4 +- .../UsageTracker/MongoUsageStore.cs | 4 +- .../UsageTracking/BackgroundUsageTracker.cs | 17 ++++--- .../UsageTracking/IUsageStore.cs | 2 +- .../UsageTracking/IUsageTracker.cs | 2 +- .../Contents/MongoContentRepository.cs | 1 - .../Contents/Builders/EdmModelBuilder.cs | 1 - .../Repositories/IContentRepository.cs | 1 - src/Squidex/Config/Web/WebDependencies.cs | 7 +-- src/Squidex/Config/Web/WebpackUsages.cs | 8 +++ .../Api/Apps/AppClientsController.cs | 4 ++ .../Api/Apps/AppContributorsController.cs | 3 ++ .../Api/Apps/AppLanguagesController.cs | 3 ++ .../Controllers/Api/Apps/AppsController.cs | 2 + .../Api/Assets/AssetsController.cs | 5 ++ .../Controllers/Api/Docs/DocsController.cs | 2 + .../EventConsumersController.cs | 4 ++ .../Api/History/HistoryController.cs | 1 + .../Api/Languages/LanguagesController.cs | 1 + .../Controllers/Api/Ping/PingController.cs | 1 + .../Api/Schemas/SchemaFieldsController.cs | 8 +++ .../Api/Schemas/SchemasController.cs | 7 +++ .../Api/Statistics/UsagesController.cs | 4 ++ .../Api/Users/UserManagementController.cs | 3 ++ .../ContentApi/ContentSwaggerController.cs | 2 + .../ContentApi/ContentsController.cs | 8 +++ src/Squidex/Pipeline/ApiCostsAttribute.cs | 34 +++++++++++++ src/Squidex/Pipeline/AppTrackingFilter.cs | 50 ------------------- src/Squidex/Pipeline/AppTrackingMiddleware.cs | 48 ++++++++++++++++++ .../Pipeline/IAppTrackingWeightFeature.cs | 15 ++++++ ...tribute.cs => LogPerformanceMiddleware.cs} | 22 ++++---- src/Squidex/Startup.cs | 1 + .../TypeNameRegistryTests.cs | 1 - .../BackgroundUsageTrackerTests.cs | 33 ++++++++---- 35 files changed, 216 insertions(+), 94 deletions(-) create mode 100644 src/Squidex/Pipeline/ApiCostsAttribute.cs delete mode 100644 src/Squidex/Pipeline/AppTrackingFilter.cs create mode 100644 src/Squidex/Pipeline/AppTrackingMiddleware.cs create mode 100644 src/Squidex/Pipeline/IAppTrackingWeightFeature.cs rename src/Squidex/Pipeline/{LogPerformanceAttribute.cs => LogPerformanceMiddleware.cs} (59%) diff --git a/src/Squidex.Events/Apps/AppMasterLanguageSet.cs b/src/Squidex.Events/Apps/AppMasterLanguageSet.cs index 4407d2d51..a0ae5b073 100644 --- a/src/Squidex.Events/Apps/AppMasterLanguageSet.cs +++ b/src/Squidex.Events/Apps/AppMasterLanguageSet.cs @@ -6,7 +6,6 @@ // All rights reserved. // ========================================================================== -using System; using Squidex.Infrastructure; namespace Squidex.Events.Apps diff --git a/src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsage.cs b/src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsage.cs index 4f62d3f9d..070e7556c 100644 --- a/src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsage.cs +++ b/src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsage.cs @@ -30,10 +30,10 @@ namespace Squidex.Infrastructure.MongoDb.UsageTracker [BsonRequired] [BsonElement] - public long TotalCount { get; set; } + public double TotalCount { get; set; } [BsonRequired] [BsonElement] - public long TotalElapsedMs { get; set; } + public double TotalElapsedMs { get; set; } } } diff --git a/src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsageStore.cs b/src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsageStore.cs index 92dd64ce3..9a5f68d5e 100644 --- a/src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsageStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsageStore.cs @@ -34,7 +34,7 @@ namespace Squidex.Infrastructure.MongoDb.UsageTracker return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.Key).Ascending(x => x.Date)); } - public Task TrackUsagesAsync(DateTime date, string key, long count, long elapsedMs) + public Task TrackUsagesAsync(DateTime date, string key, double count, double elapsedMs) { var id = $"{key}_{date:yyyy-MM-dd}"; @@ -52,7 +52,7 @@ namespace Squidex.Infrastructure.MongoDb.UsageTracker { var entities = await Collection.Find(x => x.Key == key && x.Date >= fromDate && x.Date <= toDate).ToListAsync(); - return entities.Select(x => new StoredUsage(x.Date, x.TotalCount, x.TotalElapsedMs)).ToList(); + return entities.Select(x => new StoredUsage(x.Date, (long)x.TotalCount, (long)x.TotalElapsedMs)).ToList(); } } } diff --git a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs index 8c34b5e7f..3a2e54278 100644 --- a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs @@ -27,19 +27,19 @@ namespace Squidex.Infrastructure.UsageTracking public sealed class Usage { - public readonly long Count; - public readonly long ElapsedMs; + public readonly double Count; + public readonly double ElapsedMs; - public Usage(long elapsed, long count = 1) + public Usage(double elapsed, double count) { ElapsedMs = elapsed; Count = count; } - public Usage Add(long elapsed) + public Usage Add(double elapsed, double weight) { - return new Usage(ElapsedMs + elapsed, Count + 1); + return new Usage(ElapsedMs + elapsed, Count + weight); } } @@ -93,13 +93,16 @@ namespace Squidex.Infrastructure.UsageTracking } } - public Task TrackAsync(string key, long elapsedMs) + public Task TrackAsync(string key, double weight, double elapsedMs) { Guard.NotNull(key, nameof(key)); ThrowIfDisposed(); - usages.AddOrUpdate(key, _ => new Usage(elapsedMs), (k, x) => x.Add(elapsedMs)); + if (weight > 0) + { + usages.AddOrUpdate(key, _ => new Usage(elapsedMs, weight), (k, x) => x.Add(elapsedMs, weight)); + } return TaskHelper.Done; } diff --git a/src/Squidex.Infrastructure/UsageTracking/IUsageStore.cs b/src/Squidex.Infrastructure/UsageTracking/IUsageStore.cs index 1fe07c9cc..6e66bf86f 100644 --- a/src/Squidex.Infrastructure/UsageTracking/IUsageStore.cs +++ b/src/Squidex.Infrastructure/UsageTracking/IUsageStore.cs @@ -14,7 +14,7 @@ namespace Squidex.Infrastructure.UsageTracking { public interface IUsageStore { - Task TrackUsagesAsync(DateTime date, string key, long count, long elapsedMs); + Task TrackUsagesAsync(DateTime date, string key, double count, double elapsedMs); Task> QueryAsync(string key, DateTime fromDate, DateTime toDate); } diff --git a/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs index bbb6a291f..4defaadc4 100644 --- a/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs @@ -14,7 +14,7 @@ namespace Squidex.Infrastructure.UsageTracking { public interface IUsageTracker { - Task TrackAsync(string key, long elapsedMs); + Task TrackAsync(string key, double weight, double elapsedMs); Task GetMonthlyCalls(string key, DateTime date); diff --git a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs index 454921f4e..3fb01734c 100644 --- a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs @@ -12,7 +12,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.OData.Core; using MongoDB.Driver; -using Squidex.Core; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Read.Apps; diff --git a/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs b/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs index ad9f4f9c2..31e77a48c 100644 --- a/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs +++ b/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs @@ -7,7 +7,6 @@ // ========================================================================== using System; -using System.Linq; using Microsoft.Extensions.Caching.Memory; using Microsoft.OData.Edm; using Microsoft.OData.Edm.Library; diff --git a/src/Squidex.Read/Contents/Repositories/IContentRepository.cs b/src/Squidex.Read/Contents/Repositories/IContentRepository.cs index a59762a5d..1e8009b43 100644 --- a/src/Squidex.Read/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Read/Contents/Repositories/IContentRepository.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Squidex.Core; using Squidex.Read.Apps; namespace Squidex.Read.Contents.Repositories diff --git a/src/Squidex/Config/Web/WebDependencies.cs b/src/Squidex/Config/Web/WebDependencies.cs index 8ebe98bcd..019def3c5 100644 --- a/src/Squidex/Config/Web/WebDependencies.cs +++ b/src/Squidex/Config/Web/WebDependencies.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Config.Domain; -using Squidex.Pipeline; namespace Squidex.Config.Web { @@ -16,11 +15,7 @@ namespace Squidex.Config.Web { public static void AddMyMvc(this IServiceCollection services) { - services.AddMvc(options => - { - options.Filters.Add(typeof(LogPerformanceAttribute)); - options.Filters.Add(typeof(AppTrackingFilter)); - }).AddMySerializers(); + services.AddMvc().AddMySerializers(); } } } diff --git a/src/Squidex/Config/Web/WebpackUsages.cs b/src/Squidex/Config/Web/WebpackUsages.cs index d8470b47c..01ff6278b 100644 --- a/src/Squidex/Config/Web/WebpackUsages.cs +++ b/src/Squidex/Config/Web/WebpackUsages.cs @@ -19,5 +19,13 @@ namespace Squidex.Config.Web return app; } + + public static IApplicationBuilder UseMyTracking(this IApplicationBuilder app) + { + app.UseMiddleware(); + app.UseMiddleware(); + + return app; + } } } diff --git a/src/Squidex/Controllers/Api/Apps/AppClientsController.cs b/src/Squidex/Controllers/Api/Apps/AppClientsController.cs index d403464b1..b9f04e324 100644 --- a/src/Squidex/Controllers/Api/Apps/AppClientsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppClientsController.cs @@ -54,6 +54,7 @@ namespace Squidex.Controllers.Api.Apps [HttpGet] [Route("apps/{app}/clients/")] [ProducesResponseType(typeof(ClientDto[]), 200)] + [ApiCosts(1)] public async Task GetClients(string app) { var entity = await appProvider.FindAppByNameAsync(app); @@ -86,6 +87,7 @@ namespace Squidex.Controllers.Api.Apps [HttpPost] [Route("apps/{app}/clients/")] [ProducesResponseType(typeof(ClientDto), 201)] + [ApiCosts(1)] public async Task PostClient(string app, [FromBody] CreateAppClientDto request) { var context = await CommandBus.PublishAsync(SimpleMapper.Map(request, new AttachClient())); @@ -108,6 +110,7 @@ namespace Squidex.Controllers.Api.Apps /// [HttpPut] [Route("apps/{app}/clients/{clientId}/")] + [ApiCosts(1)] public async Task PutClient(string app, string clientId, [FromBody] UpdateAppClientDto request) { await CommandBus.PublishAsync(SimpleMapper.Map(request, new RenameClient { Id = clientId })); @@ -126,6 +129,7 @@ namespace Squidex.Controllers.Api.Apps /// [HttpDelete] [Route("apps/{app}/clients/{clientId}/")] + [ApiCosts(1)] public async Task DeleteClient(string app, string clientId) { await CommandBus.PublishAsync(new RevokeClient { Id = clientId }); diff --git a/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs b/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs index b39866ced..7b4b2fa1e 100644 --- a/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs @@ -50,6 +50,7 @@ namespace Squidex.Controllers.Api.Apps [HttpGet] [Route("apps/{app}/contributors/")] [ProducesResponseType(typeof(ContributorDto[]), 200)] + [ApiCosts(1)] public async Task GetContributors(string app) { var entity = await appProvider.FindAppByNameAsync(app); @@ -79,6 +80,7 @@ namespace Squidex.Controllers.Api.Apps [HttpPost] [Route("apps/{app}/contributors/")] [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] public async Task PostContributor(string app, [FromBody] AssignAppContributorDto request) { await CommandBus.PublishAsync(SimpleMapper.Map(request, new AssignContributor())); @@ -99,6 +101,7 @@ namespace Squidex.Controllers.Api.Apps [HttpDelete] [Route("apps/{app}/contributors/{id}/")] [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] public async Task DeleteContributor(string app, string id) { await CommandBus.PublishAsync(new RemoveContributor { ContributorId = id }); diff --git a/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs b/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs index 8ed4c07bc..fe3d92d56 100644 --- a/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs @@ -92,6 +92,7 @@ namespace Squidex.Controllers.Api.Apps [Route("apps/{app}/languages/")] [ProducesResponseType(typeof(AppLanguageDto), 201)] [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] public async Task PostLanguage(string app, [FromBody] AddAppLanguageDto request) { await CommandBus.PublishAsync(SimpleMapper.Map(request, new AddLanguage())); @@ -115,6 +116,7 @@ namespace Squidex.Controllers.Api.Apps [Authorize(Roles = SquidexRoles.AppOwner)] [HttpPut] [Route("apps/{app}/languages/{language}")] + [ApiCosts(1)] public async Task Update(string app, string language, [FromBody] UpdateAppLanguageDto model) { await CommandBus.PublishAsync(SimpleMapper.Map(model, new UpdateLanguage { Language = language })); @@ -135,6 +137,7 @@ namespace Squidex.Controllers.Api.Apps [Authorize(Roles = SquidexRoles.AppOwner)] [HttpDelete] [Route("apps/{app}/languages/{language}")] + [ApiCosts(1)] public async Task DeleteLanguage(string app, string language) { await CommandBus.PublishAsync(new RemoveLanguage { Language = ParseLanguage(language) }); diff --git a/src/Squidex/Controllers/Api/Apps/AppsController.cs b/src/Squidex/Controllers/Api/Apps/AppsController.cs index b37f80f75..0855827ec 100644 --- a/src/Squidex/Controllers/Api/Apps/AppsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppsController.cs @@ -51,6 +51,7 @@ namespace Squidex.Controllers.Api.Apps [HttpGet] [Route("apps/")] [ProducesResponseType(typeof(AppDto[]), 200)] + [ApiCosts(1)] public async Task GetApps() { var subject = HttpContext.User.OpenIdSubject(); @@ -87,6 +88,7 @@ namespace Squidex.Controllers.Api.Apps [ProducesResponseType(typeof(EntityCreatedDto), 201)] [ProducesResponseType(typeof(ErrorDto), 400)] [ProducesResponseType(typeof(ErrorDto), 409)] + [ApiCosts(1)] public async Task PostApp([FromBody] CreateAppDto request) { var command = SimpleMapper.Map(request, new CreateApp()); diff --git a/src/Squidex/Controllers/Api/Assets/AssetsController.cs b/src/Squidex/Controllers/Api/Assets/AssetsController.cs index 82b1c93f7..6353a4134 100644 --- a/src/Squidex/Controllers/Api/Assets/AssetsController.cs +++ b/src/Squidex/Controllers/Api/Assets/AssetsController.cs @@ -66,6 +66,7 @@ namespace Squidex.Controllers.Api.Assets [HttpGet] [Route("apps/{app}/assets/")] [ProducesResponseType(typeof(AssetsDto), 200)] + [ApiCosts(1)] public async Task GetAssets(string app, [FromQuery] string query = null, [FromQuery] string mimeTypes = null, [FromQuery] string ids = null, [FromQuery] int skip = 0, [FromQuery] int take = 10) { var mimeTypeList = new HashSet(); @@ -117,6 +118,7 @@ namespace Squidex.Controllers.Api.Assets [HttpGet] [Route("apps/{app}/assets/{id}")] [ProducesResponseType(typeof(AssetsDto), 200)] + [ApiCosts(1)] public async Task GetAsset(string app, Guid id) { var entity = await assetRepository.FindAssetAsync(id); @@ -175,6 +177,7 @@ namespace Squidex.Controllers.Api.Assets [Route("apps/{app}/assets/{id}/content")] [ProducesResponseType(typeof(AssetReplacedDto), 201)] [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] public async Task PutAssetContent(string app, Guid id, List file) { var assetFile = GetAssetFile(file); @@ -202,6 +205,7 @@ namespace Squidex.Controllers.Api.Assets [HttpPut] [Route("apps/{app}/assets/{id}")] [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] public async Task PutAsset(string app, Guid id, [FromBody] AssetUpdateDto request) { var command = SimpleMapper.Map(request, new RenameAsset { AssetId = id }); @@ -222,6 +226,7 @@ namespace Squidex.Controllers.Api.Assets /// [HttpDelete] [Route("apps/{app}/assets/{id}/")] + [ApiCosts(1)] public async Task DeleteAsset(string app, Guid id) { await CommandBus.PublishAsync(new DeleteAsset { AssetId = id }); diff --git a/src/Squidex/Controllers/Api/Docs/DocsController.cs b/src/Squidex/Controllers/Api/Docs/DocsController.cs index f83151776..738aaec07 100644 --- a/src/Squidex/Controllers/Api/Docs/DocsController.cs +++ b/src/Squidex/Controllers/Api/Docs/DocsController.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; +using Squidex.Pipeline; namespace Squidex.Controllers.Api.Docs { @@ -16,6 +17,7 @@ namespace Squidex.Controllers.Api.Docs { [HttpGet] [Route("docs/")] + [ApiCosts(0)] public IActionResult Docs() { ViewBag.Specification = "~/swagger/v1/swagger.json"; diff --git a/src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs b/src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs index b191a8d46..7dca968e0 100644 --- a/src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs +++ b/src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs @@ -33,6 +33,7 @@ namespace Squidex.Controllers.Api.EventConsumers [HttpGet] [Route("event-consumers/")] + [ApiCosts(0)] public async Task GetEventConsumers() { var entities = await eventConsumerRepository.QueryAsync(); @@ -44,6 +45,7 @@ namespace Squidex.Controllers.Api.EventConsumers [HttpPut] [Route("event-consumers/{name}/start")] + [ApiCosts(0)] public async Task Start(string name) { await eventConsumerRepository.StartAsync(name); @@ -53,6 +55,7 @@ namespace Squidex.Controllers.Api.EventConsumers [HttpPut] [Route("event-consumers/{name}/stop")] + [ApiCosts(0)] public async Task Stop(string name) { await eventConsumerRepository.StopAsync(name); @@ -62,6 +65,7 @@ namespace Squidex.Controllers.Api.EventConsumers [HttpPut] [Route("event-consumers/{name}/reset")] + [ApiCosts(0)] public async Task Reset(string name) { await eventConsumerRepository.ResetAsync(name); diff --git a/src/Squidex/Controllers/Api/History/HistoryController.cs b/src/Squidex/Controllers/Api/History/HistoryController.cs index abefb5aee..e3c527ef3 100644 --- a/src/Squidex/Controllers/Api/History/HistoryController.cs +++ b/src/Squidex/Controllers/Api/History/HistoryController.cs @@ -52,6 +52,7 @@ namespace Squidex.Controllers.Api.History [HttpGet] [Route("apps/{app}/history/")] [ProducesResponseType(typeof(HistoryEventDto), 200)] + [ApiCosts(0.1)] public async Task GetHistory(string app, string channel) { var entity = await appProvider.FindAppByNameAsync(app); diff --git a/src/Squidex/Controllers/Api/Languages/LanguagesController.cs b/src/Squidex/Controllers/Api/Languages/LanguagesController.cs index bed7d44b1..83d68afdb 100644 --- a/src/Squidex/Controllers/Api/Languages/LanguagesController.cs +++ b/src/Squidex/Controllers/Api/Languages/LanguagesController.cs @@ -36,6 +36,7 @@ namespace Squidex.Controllers.Api.Languages [HttpGet] [Route("languages/")] [ProducesResponseType(typeof(string[]), 200)] + [ApiCosts(0)] public IActionResult GetLanguages() { var response = Language.AllLanguages.Select(x => SimpleMapper.Map(x, new LanguageDto())).ToList(); diff --git a/src/Squidex/Controllers/Api/Ping/PingController.cs b/src/Squidex/Controllers/Api/Ping/PingController.cs index f7b97c752..cf1417be3 100644 --- a/src/Squidex/Controllers/Api/Ping/PingController.cs +++ b/src/Squidex/Controllers/Api/Ping/PingController.cs @@ -34,6 +34,7 @@ namespace Squidex.Controllers.Api.Ping /// [HttpGet] [Route("ping/{app}/")] + [ApiCosts(0)] public IActionResult GetPing() { return NoContent(); diff --git a/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs b/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs index 90fb2f9ea..8a4d3426b 100644 --- a/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs +++ b/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs @@ -49,6 +49,7 @@ namespace Squidex.Controllers.Api.Schemas [ProducesResponseType(typeof(EntityCreatedDto), 201)] [ProducesResponseType(typeof(ErrorDto), 409)] [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] public async Task PostField(string app, string name, [FromBody] AddFieldDto request) { var command = new AddField { Name = request.Name, Partitioning = request.Partitioning, Properties = request.Properties.ToProperties() }; @@ -75,6 +76,7 @@ namespace Squidex.Controllers.Api.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/ordering")] [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] public async Task PutFieldOrdering(string app, string name, [FromBody] ReorderFields request) { var command = new ReorderFields { FieldIds = request.FieldIds }; @@ -100,6 +102,7 @@ namespace Squidex.Controllers.Api.Schemas [Route("apps/{app}/schemas/{name}/fields/{id:long}/")] [ProducesResponseType(typeof(ErrorDto), 409)] [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] public async Task PutField(string app, string name, long id, [FromBody] UpdateFieldDto request) { var command = new UpdateField { FieldId = id, Properties = request.Properties.ToProperties() }; @@ -126,6 +129,7 @@ namespace Squidex.Controllers.Api.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/hide/")] [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] public async Task HideField(string app, string name, long id) { var command = new HideField { FieldId = id }; @@ -152,6 +156,7 @@ namespace Squidex.Controllers.Api.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/show/")] [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] public async Task ShowField(string app, string name, long id) { var command = new ShowField { FieldId = id }; @@ -179,6 +184,7 @@ namespace Squidex.Controllers.Api.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/enable/")] [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] public async Task EnableField(string app, string name, long id) { var command = new EnableField { FieldId = id }; @@ -206,6 +212,7 @@ namespace Squidex.Controllers.Api.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/disable/")] [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] public async Task DisableField(string app, string name, long id) { var command = new DisableField { FieldId = id }; @@ -227,6 +234,7 @@ namespace Squidex.Controllers.Api.Schemas /// [HttpDelete] [Route("apps/{app}/schemas/{name}/fields/{id:long}/")] + [ApiCosts(1)] public async Task DeleteField(string app, string name, long id) { var command = new DeleteField { FieldId = id }; diff --git a/src/Squidex/Controllers/Api/Schemas/SchemasController.cs b/src/Squidex/Controllers/Api/Schemas/SchemasController.cs index 187fabfd1..38b462865 100644 --- a/src/Squidex/Controllers/Api/Schemas/SchemasController.cs +++ b/src/Squidex/Controllers/Api/Schemas/SchemasController.cs @@ -52,6 +52,7 @@ namespace Squidex.Controllers.Api.Schemas [HttpGet] [Route("apps/{app}/schemas/")] [ProducesResponseType(typeof(SchemaDto[]), 200)] + [ApiCosts(1)] public async Task GetSchemas(string app) { var schemas = await schemaRepository.QueryAllAsync(AppId); @@ -73,6 +74,7 @@ namespace Squidex.Controllers.Api.Schemas [HttpGet] [Route("apps/{app}/schemas/{name}/")] [ProducesResponseType(typeof(SchemaDetailsDto[]), 200)] + [ApiCosts(1)] public async Task GetSchema(string app, string name) { var entity = await schemaRepository.FindSchemaAsync(AppId, name); @@ -104,6 +106,7 @@ namespace Squidex.Controllers.Api.Schemas [ProducesResponseType(typeof(EntityCreatedDto), 201)] [ProducesResponseType(typeof(ErrorDto), 400)] [ProducesResponseType(typeof(ErrorDto), 409)] + [ApiCosts(1)] public async Task PostSchema(string app, [FromBody] CreateSchemaDto request) { var command = SimpleMapper.Map(request, new CreateSchema()); @@ -126,6 +129,7 @@ namespace Squidex.Controllers.Api.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/")] + [ApiCosts(1)] public async Task PutSchema(string app, string name, [FromBody] UpdateSchemaDto request) { var properties = SimpleMapper.Map(request, new SchemaProperties()); @@ -148,6 +152,7 @@ namespace Squidex.Controllers.Api.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/publish")] [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] public async Task PublishSchema(string app, string name) { await CommandBus.PublishAsync(new PublishSchema()); @@ -168,6 +173,7 @@ namespace Squidex.Controllers.Api.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/unpublish")] [ProducesResponseType(typeof(ErrorDto), 400)] + [ApiCosts(1)] public async Task UnpublishSchema(string app, string name) { await CommandBus.PublishAsync(new UnpublishSchema()); @@ -186,6 +192,7 @@ namespace Squidex.Controllers.Api.Schemas /// [HttpDelete] [Route("apps/{app}/schemas/{name}/")] + [ApiCosts(1)] public async Task DeleteSchema(string app, string name) { await CommandBus.PublishAsync(new DeleteSchema()); diff --git a/src/Squidex/Controllers/Api/Statistics/UsagesController.cs b/src/Squidex/Controllers/Api/Statistics/UsagesController.cs index 44c6f1569..bf988abf0 100644 --- a/src/Squidex/Controllers/Api/Statistics/UsagesController.cs +++ b/src/Squidex/Controllers/Api/Statistics/UsagesController.cs @@ -52,6 +52,7 @@ namespace Squidex.Controllers.Api.Statistics [HttpGet] [Route("apps/{app}/usages/calls/month")] [ProducesResponseType(typeof(CurrentCallsDto), 200)] + [ApiCosts(0)] public async Task GetMonthlyCalls(string app) { var count = await usageTracker.GetMonthlyCalls(App.Id.ToString(), DateTime.Today); @@ -74,6 +75,7 @@ namespace Squidex.Controllers.Api.Statistics [HttpGet] [Route("apps/{app}/usages/calls/{fromDate}/{toDate}")] [ProducesResponseType(typeof(CallsUsageDto[]), 200)] + [ApiCosts(0)] public async Task GetUsages(string app, DateTime fromDate, DateTime toDate) { if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) @@ -105,6 +107,7 @@ namespace Squidex.Controllers.Api.Statistics [HttpGet] [Route("apps/{app}/usages/storage/today")] [ProducesResponseType(typeof(CurrentStorageDto), 200)] + [ApiCosts(0)] public async Task GetCurrentStorageSize(string app) { var size = await assetStatsRepository.GetTotalSizeAsync(App.Id); @@ -127,6 +130,7 @@ namespace Squidex.Controllers.Api.Statistics [HttpGet] [Route("apps/{app}/usages/storage/{fromDate}/{toDate}")] [ProducesResponseType(typeof(StorageUsageDto[]), 200)] + [ApiCosts(0)] public async Task GetStorageSizes(string app, DateTime fromDate, DateTime toDate) { if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) diff --git a/src/Squidex/Controllers/Api/Users/UserManagementController.cs b/src/Squidex/Controllers/Api/Users/UserManagementController.cs index aa03c55a4..f7e1a6399 100644 --- a/src/Squidex/Controllers/Api/Users/UserManagementController.cs +++ b/src/Squidex/Controllers/Api/Users/UserManagementController.cs @@ -36,6 +36,7 @@ namespace Squidex.Controllers.Api.Users [HttpGet] [Route("user-management")] + [ApiCosts(0)] public async Task GetUsers([FromQuery] string query = null, [FromQuery] int skip = 0, [FromQuery] int take = 10) { var taskForUsers = userRepository.QueryByEmailAsync(query, take, skip); @@ -54,6 +55,7 @@ namespace Squidex.Controllers.Api.Users [HttpPut] [Route("user-management/{id}/lock/")] + [ApiCosts(0)] public async Task Lock(string id) { if (IsSelf(id)) @@ -68,6 +70,7 @@ namespace Squidex.Controllers.Api.Users [HttpPut] [Route("user-management/{id}/unlock/")] + [ApiCosts(0)] public async Task Unlock(string id) { if (IsSelf(id)) diff --git a/src/Squidex/Controllers/ContentApi/ContentSwaggerController.cs b/src/Squidex/Controllers/ContentApi/ContentSwaggerController.cs index a1713c725..1f8752cb7 100644 --- a/src/Squidex/Controllers/ContentApi/ContentSwaggerController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentSwaggerController.cs @@ -36,6 +36,7 @@ namespace Squidex.Controllers.ContentApi [HttpGet] [Route("content/{app}/docs/")] + [ApiCosts(0)] public IActionResult Docs(string app) { ViewBag.Specification = $"~/content/{app}/swagger/v1/swagger.json"; @@ -45,6 +46,7 @@ namespace Squidex.Controllers.ContentApi [HttpGet] [Route("content/{app}/swagger/v1/swagger.json")] + [ApiCosts(0)] public async Task GetSwagger(string app) { var appEntity = await appProvider.FindAppByNameAsync(app); diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index 9ac25f47e..1733da6ee 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -44,6 +44,7 @@ namespace Squidex.Controllers.ContentApi [HttpGet] [Route("content/{app}/{name}")] + [ApiCosts(2)] public async Task GetContents(string name, [FromQuery] bool nonPublished = false, [FromQuery] bool hidden = false) { var schemaEntity = await schemas.FindSchemaByNameAsync(AppId, name); @@ -81,6 +82,7 @@ namespace Squidex.Controllers.ContentApi [HttpGet] [Route("content/{app}/{name}/{id}")] + [ApiCosts(1)] public async Task GetContent(string name, Guid id, bool hidden = false) { var schemaEntity = await schemas.FindSchemaByNameAsync(AppId, name); @@ -111,6 +113,7 @@ namespace Squidex.Controllers.ContentApi [HttpPost] [Route("content/{app}/{name}/")] + [ApiCosts(1)] public async Task PostContent([FromBody] ContentData request, [FromQuery] bool publish = false) { var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish }; @@ -127,6 +130,7 @@ namespace Squidex.Controllers.ContentApi [HttpPut] [Route("content/{app}/{name}/{id}")] + [ApiCosts(1)] public async Task PutContent(Guid id, [FromBody] ContentData request) { var command = new UpdateContent { ContentId = id, Data = request.ToCleaned() }; @@ -138,6 +142,7 @@ namespace Squidex.Controllers.ContentApi [HttpPatch] [Route("content/{app}/{name}/{id}")] + [ApiCosts(1)] public async Task PatchContent(Guid id, [FromBody] ContentData request) { var command = new PatchContent { ContentId = id, Data = request.ToCleaned() }; @@ -149,6 +154,7 @@ namespace Squidex.Controllers.ContentApi [HttpPut] [Route("content/{app}/{name}/{id}/publish")] + [ApiCosts(1)] public async Task PublishContent(Guid id) { var command = new PublishContent { ContentId = id }; @@ -160,6 +166,7 @@ namespace Squidex.Controllers.ContentApi [HttpPut] [Route("content/{app}/{name}/{id}/unpublish")] + [ApiCosts(1)] public async Task UnpublishContent(Guid id) { var command = new UnpublishContent { ContentId = id }; @@ -171,6 +178,7 @@ namespace Squidex.Controllers.ContentApi [HttpDelete] [Route("content/{app}/{name}/{id}")] + [ApiCosts(1)] public async Task PutContent(Guid id) { var command = new DeleteContent { ContentId = id }; diff --git a/src/Squidex/Pipeline/ApiCostsAttribute.cs b/src/Squidex/Pipeline/ApiCostsAttribute.cs new file mode 100644 index 000000000..bb482412d --- /dev/null +++ b/src/Squidex/Pipeline/ApiCostsAttribute.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// ApiWeightAttribute.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Squidex.Pipeline +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public sealed class ApiCostsAttribute : ActionFilterAttribute + { + private readonly double weight; + + private sealed class WeightFeature : IAppTrackingWeightFeature + { + public double Weight { get; set; } + } + + public ApiCostsAttribute(double weight) + { + this.weight = weight; + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + context.HttpContext.Features.Set(new WeightFeature { Weight = weight }); + } + } +} diff --git a/src/Squidex/Pipeline/AppTrackingFilter.cs b/src/Squidex/Pipeline/AppTrackingFilter.cs deleted file mode 100644 index 4f906eace..000000000 --- a/src/Squidex/Pipeline/AppTrackingFilter.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// AppTrackingFilter.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Diagnostics; -using Microsoft.AspNetCore.Mvc.Filters; -using Squidex.Infrastructure.UsageTracking; - -// ReSharper disable InvertIf - -namespace Squidex.Pipeline -{ - public sealed class AppTrackingFilter : ActionFilterAttribute - { - private readonly IUsageTracker usageTracker; - - public AppTrackingFilter(IUsageTracker usageTracker) - { - this.usageTracker = usageTracker; - } - - public override void OnActionExecuting(ActionExecutingContext context) - { - var appFeature = context.HttpContext.Features.Get(); - - if (appFeature?.App != null) - { - context.HttpContext.Items["AppWatch"] = Stopwatch.StartNew(); - } - } - - public override void OnActionExecuted(ActionExecutedContext context) - { - var appFeature = context.HttpContext.Features.Get(); - - if (appFeature?.App != null) - { - var stopWatch = (Stopwatch)context.HttpContext.Items["AppWatch"]; - - stopWatch.Stop(); - - usageTracker.TrackAsync(appFeature.App.Id.ToString(), stopWatch.ElapsedMilliseconds); - } - } - } -} diff --git a/src/Squidex/Pipeline/AppTrackingMiddleware.cs b/src/Squidex/Pipeline/AppTrackingMiddleware.cs new file mode 100644 index 000000000..d3d779d85 --- /dev/null +++ b/src/Squidex/Pipeline/AppTrackingMiddleware.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// AppTrackingMiddleware.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Squidex.Infrastructure.UsageTracking; + +// ReSharper disable InvertIf + +namespace Squidex.Pipeline +{ + public sealed class AppTrackingMiddleware + { + private readonly RequestDelegate next; + private readonly IUsageTracker usageTracker; + + public AppTrackingMiddleware(RequestDelegate next, IUsageTracker usageTracker) + { + this.next = next; + + this.usageTracker = usageTracker; + } + + public async Task Invoke(HttpContext context) + { + var stopWatch = Stopwatch.StartNew(); + + await next(context); + + var appFeature = context.Features.Get(); + + if (appFeature?.App != null) + { + stopWatch.Stop(); + + var weight = context.Features.Get()?.Weight ?? 1; + + await usageTracker.TrackAsync(appFeature.App.Id.ToString(), weight, stopWatch.ElapsedMilliseconds); + } + } + } +} diff --git a/src/Squidex/Pipeline/IAppTrackingWeightFeature.cs b/src/Squidex/Pipeline/IAppTrackingWeightFeature.cs new file mode 100644 index 000000000..56274c735 --- /dev/null +++ b/src/Squidex/Pipeline/IAppTrackingWeightFeature.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// IAppTrackingWeightFeature.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Pipeline +{ + public interface IAppTrackingWeightFeature + { + double Weight { get; } + } +} diff --git a/src/Squidex/Pipeline/LogPerformanceAttribute.cs b/src/Squidex/Pipeline/LogPerformanceMiddleware.cs similarity index 59% rename from src/Squidex/Pipeline/LogPerformanceAttribute.cs rename to src/Squidex/Pipeline/LogPerformanceMiddleware.cs index ce7c919af..df6a563c5 100644 --- a/src/Squidex/Pipeline/LogPerformanceAttribute.cs +++ b/src/Squidex/Pipeline/LogPerformanceMiddleware.cs @@ -1,5 +1,5 @@ // ========================================================================== -// LogPerformanceAttribute.cs +// LogPerformanceMiddleware.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -7,29 +7,31 @@ // ========================================================================== using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; using Squidex.Infrastructure.Log; namespace Squidex.Pipeline { - public sealed class LogPerformanceAttribute : ActionFilterAttribute + public sealed class LogPerformanceMiddleware : ActionFilterAttribute { + private readonly RequestDelegate next; private readonly ISemanticLog log; - public LogPerformanceAttribute(ISemanticLog log) + public LogPerformanceMiddleware(RequestDelegate next, ISemanticLog log) { + this.next = next; + this.log = log; } - public override void OnActionExecuting(ActionExecutingContext context) + public async Task Invoke(HttpContext context) { - context.HttpContext.Items["Watch"] = Stopwatch.StartNew(); - } + var stopWatch = Stopwatch.StartNew(); + + await next(context); - public override void OnActionExecuted(ActionExecutedContext context) - { - var stopWatch = (Stopwatch)context.HttpContext.Items["Watch"]; - stopWatch.Stop(); log.LogInformation(w => w.WriteProperty("elapsedRequestMs", stopWatch.ElapsedMilliseconds)); diff --git a/src/Squidex/Startup.cs b/src/Squidex/Startup.cs index 6a4892a94..880838469 100644 --- a/src/Squidex/Startup.cs +++ b/src/Squidex/Startup.cs @@ -106,6 +106,7 @@ namespace Squidex app.UseMyCors(); app.UseMyForwardingRules(); + app.UseMyTracking(); MapAndUseIdentity(app); MapAndUseApi(app); diff --git a/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs b/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs index 2a72a2512..3e851e4e7 100644 --- a/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs +++ b/tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs @@ -7,7 +7,6 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Reflection; using Xunit; diff --git a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs b/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs index b0a233400..17f1f2918 100644 --- a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs @@ -33,7 +33,7 @@ namespace Squidex.Infrastructure.UsageTracking { sut.Dispose(); - return Assert.ThrowsAsync(() => sut.TrackAsync("key1", 1000)); + return Assert.ThrowsAsync(() => sut.TrackAsync("key1", 1, 1000)); } [Fact] @@ -103,23 +103,36 @@ namespace Squidex.Infrastructure.UsageTracking }); } + [Fact] + public async Task Should_not_track_if_weight_less_than_zero() + { + await sut.TrackAsync("key1", -1, 1000); + await sut.TrackAsync("key1", 0, 1000); + + sut.Next(); + + await Task.Delay(100); + + usageStore.Verify(x => x.TrackUsagesAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + } + [Fact] public async Task Should_aggregate_and_store_on_dispose() { var today = DateTime.Today; - usageStore.Setup(x => x.TrackUsagesAsync(today, "key1", 1, 1000)).Returns(TaskHelper.Done).Verifiable(); - usageStore.Setup(x => x.TrackUsagesAsync(today, "key2", 2, 5000)).Returns(TaskHelper.Done).Verifiable(); - usageStore.Setup(x => x.TrackUsagesAsync(today, "key3", 3, 15000)).Returns(TaskHelper.Done).Verifiable(); + usageStore.Setup(x => x.TrackUsagesAsync(today, "key1", 1.0, 1000)).Returns(TaskHelper.Done).Verifiable(); + usageStore.Setup(x => x.TrackUsagesAsync(today, "key2", 1.5, 5000)).Returns(TaskHelper.Done).Verifiable(); + usageStore.Setup(x => x.TrackUsagesAsync(today, "key3", 0.9, 15000)).Returns(TaskHelper.Done).Verifiable(); - await sut.TrackAsync("key1", 1000); + await sut.TrackAsync("key1", 1, 1000); - await sut.TrackAsync("key2", 2000); - await sut.TrackAsync("key2", 3000); + await sut.TrackAsync("key2", 1.0, 2000); + await sut.TrackAsync("key2", 0.5, 3000); - await sut.TrackAsync("key3", 4000); - await sut.TrackAsync("key3", 5000); - await sut.TrackAsync("key3", 6000); + await sut.TrackAsync("key3", 0.3, 4000); + await sut.TrackAsync("key3", 0.1, 5000); + await sut.TrackAsync("key3", 0.5, 6000); sut.Next();