Browse Source

Log button.

pull/344/head
Sebastian Stehle 7 years ago
parent
commit
6b27dd4237
  1. 34
      src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs
  2. 18
      src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs
  3. 2
      src/Squidex.Infrastructure/Log/ILogStore.cs
  4. 4
      src/Squidex.Infrastructure/Log/LockingLogStore.cs
  5. 24
      src/Squidex.Infrastructure/Log/NoopLogStore.cs
  6. 8
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  7. 37
      src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
  8. 7
      src/Squidex/Config/Domain/LoggingServices.cs
  9. 8
      src/Squidex/Pipeline/ApiCostsFilter.cs
  10. 14
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.html
  11. 8
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  12. 19
      src/Squidex/app/shared/services/usages.service.spec.ts
  13. 7
      src/Squidex/app/shared/services/usages.service.ts
  14. 11
      src/Squidex/package-lock.json
  15. 2
      src/Squidex/package.json
  16. 4
      tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs
  17. 8
      tests/Squidex.Tests/Pipeline/ApiCostsFilterTests.cs

34
src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs

@ -0,0 +1,34 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
namespace Squidex.Domain.Apps.Entities.Apps
{
public sealed class DefaultAppLogStore : IAppLogStore
{
private readonly ILogStore logStore;
public DefaultAppLogStore(ILogStore logStore)
{
Guard.NotNull(logStore, nameof(logStore));
this.logStore = logStore;
}
public Task ReadLogAsync(IAppEntity app, DateTime from, DateTime to, Stream stream)
{
Guard.NotNull(app, nameof(app));
return logStore.ReadLogAsync(app.Id.ToString(), from, to, stream);
}
}
}

18
src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Entities.Apps
{
public interface IAppLogStore
{
Task ReadLogAsync(IAppEntity app, DateTime from, DateTime to, Stream stream);
}
}

2
src/Squidex.Infrastructure/Log/ILogStore.cs

@ -13,6 +13,6 @@ namespace Squidex.Infrastructure.Log
{
public interface ILogStore
{
Task ReadLockAsync(string key, DateTime from, DateTime to, Stream stream);
Task ReadLogAsync(string key, DateTime from, DateTime to, Stream stream);
}
}

4
src/Squidex.Infrastructure/Log/LockingLogStore.cs

@ -30,7 +30,7 @@ namespace Squidex.Infrastructure.Log
lockGrain = grainFactory.GetGrain<ILockGrain>(SingleGrain.Id);
}
public async Task ReadLockAsync(string key, DateTime from, DateTime to, Stream stream)
public async Task ReadLogAsync(string key, DateTime from, DateTime to, Stream stream)
{
string releaseToken = null;
@ -51,7 +51,7 @@ namespace Squidex.Infrastructure.Log
try
{
await inner.ReadLockAsync(key, from, to, stream);
await inner.ReadLogAsync(key, from, to, stream);
}
finally
{

24
src/Squidex.Infrastructure/Log/NoopLogStore.cs

@ -0,0 +1,24 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Log
{
public sealed class NoopLogStore : ILogStore
{
private static readonly byte[] NoopText = Encoding.UTF8.GetBytes("Not Supported");
public Task ReadLogAsync(string key, DateTime from, DateTime to, Stream stream)
{
return stream.WriteAsync(NoopText, 0, NoopText.Length);
}
}
}

8
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -36,7 +36,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
private readonly IAssetQueryService assetQuery;
private readonly IAssetUsageTracker assetStatsRepository;
private readonly IAppPlansProvider appPlanProvider;
private readonly IAppPlansProvider appPlansProvider;
private readonly IOptions<MyContentsControllerOptions> controllerOptions;
private readonly ITagService tagService;
private readonly AssetOptions assetOptions;
@ -45,7 +45,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
ICommandBus commandBus,
IAssetQueryService assetQuery,
IAssetUsageTracker assetStatsRepository,
IAppPlansProvider appPlanProvider,
IAppPlansProvider appPlansProvider,
IOptions<AssetOptions> assetOptions,
IOptions<MyContentsControllerOptions> controllerOptions,
ITagService tagService)
@ -54,7 +54,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
this.assetOptions = assetOptions.Value;
this.assetQuery = assetQuery;
this.assetStatsRepository = assetStatsRepository;
this.appPlanProvider = appPlanProvider;
this.appPlansProvider = appPlansProvider;
this.controllerOptions = controllerOptions;
this.tagService = tagService;
}
@ -280,7 +280,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
throw new ValidationException("Cannot create asset.", error);
}
var plan = appPlanProvider.GetPlanForApp(App);
var plan = appPlansProvider.GetPlanForApp(App);
var currentSize = await assetStatsRepository.GetTotalSizeAsync(AppId);

37
src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs

@ -11,6 +11,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Statistics.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure.Commands;
@ -27,22 +28,48 @@ namespace Squidex.Areas.Api.Controllers.Statistics
public sealed class UsagesController : ApiController
{
private readonly IUsageTracker usageTracker;
private readonly IAppPlansProvider appPlanProvider;
private readonly IAppLogStore appLogStore;
private readonly IAppPlansProvider appPlansProvider;
private readonly IAssetUsageTracker assetStatsRepository;
public UsagesController(
ICommandBus commandBus,
IUsageTracker usageTracker,
IAppPlansProvider appPlanProvider,
IAppLogStore appLogStore,
IAppPlansProvider appPlansProvider,
IAssetUsageTracker assetStatsRepository)
: base(commandBus)
{
this.usageTracker = usageTracker;
this.appPlanProvider = appPlanProvider;
this.appLogStore = appLogStore;
this.appPlansProvider = appPlansProvider;
this.assetStatsRepository = assetStatsRepository;
}
/// <summary>
/// Get api calls as log file.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Usage tracking results returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/usages/log/")]
[ProducesResponseType(typeof(CurrentCallsDto), 200)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public IActionResult GetLog(string app)
{
var today = DateTime.Today;
return new FileCallbackResult("text/csv", $"Usage-{today:yyy-MM-dd}.csv", stream =>
{
return appLogStore.ReadLogAsync(App, today.AddDays(-30), today, stream);
});
}
/// <summary>
/// Get api calls for this month.
/// </summary>
@ -60,7 +87,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
{
var count = await usageTracker.GetMonthlyCallsAsync(AppId.ToString(), DateTime.Today);
var plan = appPlanProvider.GetPlanForApp(App);
var plan = appPlansProvider.GetPlanForApp(App);
var response = new CurrentCallsDto { Count = count, MaxAllowed = plan.MaxApiCalls };
@ -114,7 +141,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
{
var size = await assetStatsRepository.GetTotalSizeAsync(AppId);
var plan = appPlanProvider.GetPlanForApp(App);
var plan = appPlansProvider.GetPlanForApp(App);
var response = new CurrentStorageDto { Size = size, MaxAllowed = plan.MaxAssetSize };

7
src/Squidex/Config/Domain/LoggingServices.cs

@ -8,6 +8,7 @@
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Log;
using Squidex.Pipeline;
@ -65,6 +66,12 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<SemanticLog>()
.As<ISemanticLog>();
services.AddSingletonAs<DefaultAppLogStore>()
.As<IAppLogStore>();
services.AddSingletonAs<NoopLogStore>()
.As<ILogStore>();
}
}
}

8
src/Squidex/Pipeline/ApiCostsFilter.cs

@ -19,12 +19,12 @@ namespace Squidex.Pipeline
{
public sealed class ApiCostsFilter : IAsyncActionFilter, IFilterContainer
{
private readonly IAppPlansProvider appPlanProvider;
private readonly IAppPlansProvider appPlansProvider;
private readonly IUsageTracker usageTracker;
public ApiCostsFilter(IAppPlansProvider appPlanProvider, IUsageTracker usageTracker)
public ApiCostsFilter(IAppPlansProvider appPlansProvider, IUsageTracker usageTracker)
{
this.appPlanProvider = appPlanProvider;
this.appPlansProvider = appPlansProvider;
this.usageTracker = usageTracker;
}
@ -55,7 +55,7 @@ namespace Squidex.Pipeline
using (Profiler.Trace("CheckUsage"))
{
var plan = appPlanProvider.GetPlanForApp(appFeature.App);
var plan = appPlansProvider.GetPlanForApp(appFeature.App);
var usage = await usageTracker.GetMonthlyCallsAsync(appId, DateTime.Today);

14
src/Squidex/app/features/dashboard/pages/dashboard-page.component.html

@ -69,14 +69,24 @@
</a>
<div class="card card-lg">
<div class="card-header">API Calls</div>
<div class="card-header">
API Calls
<div class="float-right">
<a class="force" (click)="downloadLog()">
<small>Download Log</small>
</a>
</div>
</div>
<div class="card-body">
<chart type="bar" [data]="chartCallsCount" [options]="stackedChartOptions"></chart>
</div>
</div>
<div class="card card-lg">
<div class="card-header">API Performance (ms)
<div class="card-header">
API Performance (ms)
<div class="float-right">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="stacked" [(ngModel)]="isPerformanceStacked" />

8
src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts

@ -6,6 +6,7 @@
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { saveAs } from 'file-saver';
import { Subscription } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators';
@ -206,6 +207,13 @@ export class DashboardPageComponent implements OnDestroy, OnInit {
};
}));
}
public downloadLog() {
this.usagesService.getLog(this.appsState.appName)
.subscribe(buffer => {
saveAs(buffer, 'Log.csv');
});
}
}
function label(category: string) {

19
src/Squidex/app/shared/services/usages.service.spec.ts

@ -143,4 +143,23 @@ describe('UsagesService', () => {
expect(usages!).toEqual(new CurrentStorageDto(130, 150));
}));
it('should make get request to get log',
inject([UsagesService, HttpTestingController], (usagesService: UsagesService, httpMock: HttpTestingController) => {
let blob: Blob;
usagesService.getLog('my-app').subscribe(result => {
blob = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/usages/log');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
expect(blob!).toBeDefined();
}));
});

7
src/Squidex/app/shared/services/usages.service.ts

@ -59,6 +59,13 @@ export class UsagesService {
) {
}
public getLog(app: string): Observable<Blob> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/log`);
return this.http.get(url, { responseType: 'blob' }).pipe(
pretifyError('Failed to load monthly api calls. Please reload.'));
}
public getMonthCalls(app: string): Observable<CurrentCallsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/month`);

11
src/Squidex/package-lock.json

@ -927,6 +927,12 @@
"integrity": "sha512-qjkHL3wF0JMHMqgm/kmL8Pf8rIiqvueEiZ0g6NVTcBX1WN46GWDr+V5z+gsHUeL0n8TfAmXnYmF7ajsxmBp4PQ==",
"dev": true
},
"@types/file-saver": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.0.tgz",
"integrity": "sha512-dxdRrUov2HVTbSRFX+7xwUPlbGYVEZK6PrSqClg2QPos3PNe0bCajkDDkDeeC1znjSH03KOEqVbXpnJuWa2wgQ==",
"dev": true
},
"@types/jasmine": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.3.5.tgz",
@ -4886,6 +4892,11 @@
"schema-utils": "^1.0.0"
}
},
"file-saver": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.0.tgz",
"integrity": "sha512-cYM1ic5DAkg25pHKgi5f10ziAM7RJU37gaH1XQlyNDrtUnzhC/dfoV9zf2OmF0RMKi42jG5B0JWBnPQqyj/G6g=="
},
"filename-regex": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",

2
src/Squidex/package.json

@ -28,6 +28,7 @@
"babel-polyfill": "6.26.0",
"bootstrap": "4.2.1",
"core-js": "2.6.1",
"file-saver": "^2.0.0",
"graphiql": "0.12.0",
"graphql": "14.0.2",
"marked": "^0.6.0",
@ -51,6 +52,7 @@
"@angular/compiler-cli": "7.1.4",
"@ngtools/webpack": "7.1.4",
"@types/core-js": "2.5.0",
"@types/file-saver": "^2.0.0",
"@types/jasmine": "3.3.5",
"@types/marked": "^0.6.0",
"@types/mousetrap": "1.6",

4
tests/Squidex.Infrastructure.Tests/Log/LockingLogStoreTests.cs

@ -45,7 +45,7 @@ namespace Squidex.Infrastructure.Log
A.CallTo(() => lockGrain.AcquireLockAsync(key))
.Returns(releaseToken);
await sut.ReadLockAsync(key, dateFrom, dateTo, stream);
await sut.ReadLogAsync(key, dateFrom, dateTo, stream);
A.CallTo(() => lockGrain.AcquireLockAsync(key))
.MustHaveHappened();
@ -53,7 +53,7 @@ namespace Squidex.Infrastructure.Log
A.CallTo(() => lockGrain.ReleaseLockAsync(releaseToken))
.MustHaveHappened();
A.CallTo(() => inner.ReadLockAsync(key, dateFrom, dateTo, stream))
A.CallTo(() => inner.ReadLogAsync(key, dateFrom, dateTo, stream))
.MustHaveHappened();
}
}

8
tests/Squidex.Tests/Pipeline/ApiCostsFilterTests.cs

@ -26,7 +26,7 @@ namespace Squidex.Pipeline
{
private readonly IActionContextAccessor actionContextAccessor = A.Fake<IActionContextAccessor>();
private readonly IAppEntity appEntity = A.Fake<IAppEntity>();
private readonly IAppPlansProvider appPlanProvider = A.Fake<IAppPlansProvider>();
private readonly IAppPlansProvider appPlansProvider = A.Fake<IAppPlansProvider>();
private readonly IUsageTracker usageTracker = A.Fake<IUsageTracker>();
private readonly IAppLimitsPlan appPlan = A.Fake<IAppLimitsPlan>();
private readonly ActionExecutingContext actionContext;
@ -48,10 +48,10 @@ namespace Squidex.Pipeline
A.CallTo(() => actionContextAccessor.ActionContext)
.Returns(actionContext);
A.CallTo(() => appPlanProvider.GetPlan(null))
A.CallTo(() => appPlansProvider.GetPlan(null))
.Returns(appPlan);
A.CallTo(() => appPlanProvider.GetPlanForApp(appEntity))
A.CallTo(() => appPlansProvider.GetPlanForApp(appEntity))
.Returns(appPlan);
A.CallTo(() => appPlan.MaxApiCalls)
@ -67,7 +67,7 @@ namespace Squidex.Pipeline
return Task.FromResult<ActionExecutedContext>(null);
};
sut = new ApiCostsFilter(appPlanProvider, usageTracker);
sut = new ApiCostsFilter(appPlansProvider, usageTracker);
}
[Fact]

Loading…
Cancel
Save