Browse Source

Tracking improved

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
6f465c5e2a
  1. 1
      src/Squidex.Events/Apps/AppMasterLanguageSet.cs
  2. 4
      src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsage.cs
  3. 4
      src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsageStore.cs
  4. 17
      src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs
  5. 2
      src/Squidex.Infrastructure/UsageTracking/IUsageStore.cs
  6. 2
      src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs
  7. 1
      src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs
  8. 1
      src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs
  9. 1
      src/Squidex.Read/Contents/Repositories/IContentRepository.cs
  10. 7
      src/Squidex/Config/Web/WebDependencies.cs
  11. 8
      src/Squidex/Config/Web/WebpackUsages.cs
  12. 4
      src/Squidex/Controllers/Api/Apps/AppClientsController.cs
  13. 3
      src/Squidex/Controllers/Api/Apps/AppContributorsController.cs
  14. 3
      src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs
  15. 2
      src/Squidex/Controllers/Api/Apps/AppsController.cs
  16. 5
      src/Squidex/Controllers/Api/Assets/AssetsController.cs
  17. 2
      src/Squidex/Controllers/Api/Docs/DocsController.cs
  18. 4
      src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs
  19. 1
      src/Squidex/Controllers/Api/History/HistoryController.cs
  20. 1
      src/Squidex/Controllers/Api/Languages/LanguagesController.cs
  21. 1
      src/Squidex/Controllers/Api/Ping/PingController.cs
  22. 8
      src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs
  23. 7
      src/Squidex/Controllers/Api/Schemas/SchemasController.cs
  24. 4
      src/Squidex/Controllers/Api/Statistics/UsagesController.cs
  25. 3
      src/Squidex/Controllers/Api/Users/UserManagementController.cs
  26. 2
      src/Squidex/Controllers/ContentApi/ContentSwaggerController.cs
  27. 8
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  28. 34
      src/Squidex/Pipeline/ApiCostsAttribute.cs
  29. 50
      src/Squidex/Pipeline/AppTrackingFilter.cs
  30. 48
      src/Squidex/Pipeline/AppTrackingMiddleware.cs
  31. 15
      src/Squidex/Pipeline/IAppTrackingWeightFeature.cs
  32. 22
      src/Squidex/Pipeline/LogPerformanceMiddleware.cs
  33. 1
      src/Squidex/Startup.cs
  34. 1
      tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs
  35. 33
      tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs

1
src/Squidex.Events/Apps/AppMasterLanguageSet.cs

@ -6,7 +6,6 @@
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Events.Apps

4
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; }
}
}

4
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();
}
}
}

17
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;
}

2
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<IReadOnlyList<StoredUsage>> QueryAsync(string key, DateTime fromDate, DateTime toDate);
}

2
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<long> GetMonthlyCalls(string key, DateTime date);

1
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;

1
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;

1
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

7
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();
}
}
}

8
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<LogPerformanceMiddleware>();
app.UseMiddleware<AppTrackingMiddleware>();
return app;
}
}
}

4
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<IActionResult> 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<IActionResult> 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
/// </returns>
[HttpPut]
[Route("apps/{app}/clients/{clientId}/")]
[ApiCosts(1)]
public async Task<IActionResult> 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
/// </returns>
[HttpDelete]
[Route("apps/{app}/clients/{clientId}/")]
[ApiCosts(1)]
public async Task<IActionResult> DeleteClient(string app, string clientId)
{
await CommandBus.PublishAsync(new RevokeClient { Id = clientId });

3
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<IActionResult> 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<IActionResult> 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<IActionResult> DeleteContributor(string app, string id)
{
await CommandBus.PublishAsync(new RemoveContributor { ContributorId = id });

3
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<IActionResult> 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<IActionResult> 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<IActionResult> DeleteLanguage(string app, string language)
{
await CommandBus.PublishAsync(new RemoveLanguage { Language = ParseLanguage(language) });

2
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<IActionResult> 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<IActionResult> PostApp([FromBody] CreateAppDto request)
{
var command = SimpleMapper.Map(request, new CreateApp());

5
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<IActionResult> 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<string>();
@ -117,6 +118,7 @@ namespace Squidex.Controllers.Api.Assets
[HttpGet]
[Route("apps/{app}/assets/{id}")]
[ProducesResponseType(typeof(AssetsDto), 200)]
[ApiCosts(1)]
public async Task<IActionResult> 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<IActionResult> PutAssetContent(string app, Guid id, List<IFormFile> 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<IActionResult> 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
/// </returns>
[HttpDelete]
[Route("apps/{app}/assets/{id}/")]
[ApiCosts(1)]
public async Task<IActionResult> DeleteAsset(string app, Guid id)
{
await CommandBus.PublishAsync(new DeleteAsset { AssetId = id });

2
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";

4
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> Reset(string name)
{
await eventConsumerRepository.ResetAsync(name);

1
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<IActionResult> GetHistory(string app, string channel)
{
var entity = await appProvider.FindAppByNameAsync(app);

1
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();

1
src/Squidex/Controllers/Api/Ping/PingController.cs

@ -34,6 +34,7 @@ namespace Squidex.Controllers.Api.Ping
/// </remarks>
[HttpGet]
[Route("ping/{app}/")]
[ApiCosts(0)]
public IActionResult GetPing()
{
return NoContent();

8
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> DisableField(string app, string name, long id)
{
var command = new DisableField { FieldId = id };
@ -227,6 +234,7 @@ namespace Squidex.Controllers.Api.Schemas
/// </returns>
[HttpDelete]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/")]
[ApiCosts(1)]
public async Task<IActionResult> DeleteField(string app, string name, long id)
{
var command = new DeleteField { FieldId = id };

7
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<IActionResult> 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<IActionResult> 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<IActionResult> PostSchema(string app, [FromBody] CreateSchemaDto request)
{
var command = SimpleMapper.Map(request, new CreateSchema());
@ -126,6 +129,7 @@ namespace Squidex.Controllers.Api.Schemas
/// </returns>
[HttpPut]
[Route("apps/{app}/schemas/{name}/")]
[ApiCosts(1)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> UnpublishSchema(string app, string name)
{
await CommandBus.PublishAsync(new UnpublishSchema());
@ -186,6 +192,7 @@ namespace Squidex.Controllers.Api.Schemas
/// </returns>
[HttpDelete]
[Route("apps/{app}/schemas/{name}/")]
[ApiCosts(1)]
public async Task<IActionResult> DeleteSchema(string app, string name)
{
await CommandBus.PublishAsync(new DeleteSchema());

4
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> GetStorageSizes(string app, DateTime fromDate, DateTime toDate)
{
if (fromDate > toDate && (toDate - fromDate).TotalDays > 100)

3
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<IActionResult> 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<IActionResult> 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<IActionResult> Unlock(string id)
{
if (IsSelf(id))

2
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<IActionResult> GetSwagger(string app)
{
var appEntity = await appProvider.FindAppByNameAsync(app);

8
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -44,6 +44,7 @@ namespace Squidex.Controllers.ContentApi
[HttpGet]
[Route("content/{app}/{name}")]
[ApiCosts(2)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> PutContent(Guid id)
{
var command = new DeleteContent { ContentId = id };

34
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<IAppTrackingWeightFeature>(new WeightFeature { Weight = weight });
}
}
}

50
src/Squidex/Pipeline/AppTrackingFilter.cs

@ -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<IAppFeature>();
if (appFeature?.App != null)
{
context.HttpContext.Items["AppWatch"] = Stopwatch.StartNew();
}
}
public override void OnActionExecuted(ActionExecutedContext context)
{
var appFeature = context.HttpContext.Features.Get<IAppFeature>();
if (appFeature?.App != null)
{
var stopWatch = (Stopwatch)context.HttpContext.Items["AppWatch"];
stopWatch.Stop();
usageTracker.TrackAsync(appFeature.App.Id.ToString(), stopWatch.ElapsedMilliseconds);
}
}
}
}

48
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<IAppFeature>();
if (appFeature?.App != null)
{
stopWatch.Stop();
var weight = context.Features.Get<IAppTrackingWeightFeature>()?.Weight ?? 1;
await usageTracker.TrackAsync(appFeature.App.Id.ToString(), weight, stopWatch.ElapsedMilliseconds);
}
}
}
}

15
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; }
}
}

22
src/Squidex/Pipeline/LogPerformanceAttribute.cs → 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));

1
src/Squidex/Startup.cs

@ -106,6 +106,7 @@ namespace Squidex
app.UseMyCors();
app.UseMyForwardingRules();
app.UseMyTracking();
MapAndUseIdentity(app);
MapAndUseApi(app);

1
tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs

@ -7,7 +7,6 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Reflection;
using Xunit;

33
tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs

@ -33,7 +33,7 @@ namespace Squidex.Infrastructure.UsageTracking
{
sut.Dispose();
return Assert.ThrowsAsync<ObjectDisposedException>(() => sut.TrackAsync("key1", 1000));
return Assert.ThrowsAsync<ObjectDisposedException>(() => 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<DateTime>(), It.IsAny<string>(), It.IsAny<double>(), It.IsAny<long>()), 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();

Loading…
Cancel
Save