Browse Source

Track usage by category (in this case clients)

pull/315/head
Sebastian Stehle 8 years ago
parent
commit
288fb02d5a
  1. 2
      src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs
  2. 5
      src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
  3. 3
      src/Squidex/Pipeline/ApiCostsFilter.cs
  4. 14
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.html
  5. 14
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.scss
  6. 94
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  7. 35
      src/Squidex/app/shared/services/usages.service.spec.ts
  8. 21
      src/Squidex/app/shared/services/usages.service.ts

2
src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs

@ -27,7 +27,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics.Models
/// </summary> /// </summary>
public long AverageMs { get; set; } public long AverageMs { get; set; }
public static CallsUsageDto FromUsage(StoredUsage usage) public static CallsUsageDto FromUsage(DateUsage usage)
{ {
var averageMs = usage.TotalCount == 0 ? 0 : usage.TotalElapsedMs / usage.TotalCount; var averageMs = usage.TotalCount == 0 ? 0 : usage.TotalElapsedMs / usage.TotalCount;

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -82,7 +83,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
/// </returns> /// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/usages/calls/{fromDate}/{toDate}/")] [Route("apps/{app}/usages/calls/{fromDate}/{toDate}/")]
[ProducesResponseType(typeof(CallsUsageDto[]), 200)] [ProducesResponseType(typeof(Dictionary<string, CallsUsageDto[]>), 200)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> GetUsages(string app, DateTime fromDate, DateTime toDate) public async Task<IActionResult> GetUsages(string app, DateTime fromDate, DateTime toDate)
{ {
@ -93,7 +94,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
var entities = await usageTracker.QueryAsync(App.Id.ToString(), fromDate.Date, toDate.Date); var entities = await usageTracker.QueryAsync(App.Id.ToString(), fromDate.Date, toDate.Date);
var response = entities.Select(CallsUsageDto.FromUsage); var response = entities.ToDictionary(x => x.Key, x => x.Value.Select(CallsUsageDto.FromUsage).ToList());
return Ok(response); return Ok(response);
} }

3
src/Squidex/Pipeline/ApiCostsFilter.cs

@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.UsageTracking; using Squidex.Infrastructure.UsageTracking;
namespace Squidex.Pipeline namespace Squidex.Pipeline
@ -71,7 +72,7 @@ namespace Squidex.Pipeline
{ {
var elapsedMs = watch.Stop(); var elapsedMs = watch.Stop();
await usageTracker.TrackAsync(appFeature.App.Id.ToString(), FilterDefinition.Weight, elapsedMs); await usageTracker.TrackAsync(appFeature.App.Id.ToString(), context.HttpContext.User.OpenIdClientId(), FilterDefinition.Weight, elapsedMs);
} }
} }
else else

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

@ -69,21 +69,24 @@
</a> </a>
<div class="card card-lg"> <div class="card card-lg">
<div class="card-header">API Calls</div>
<div class="card-body"> <div class="card-body">
<chart type="bar" [data]="chartCallsCount" [options]="chartOptions"></chart> <chart type="bar" [data]="chartCallsCount" [options]="chartOptions"></chart>
</div> </div>
</div> </div>
<div class="card card-lg"> <div class="card card-lg">
<div class="card-header">API Performance (ms)</div>
<div class="card-body"> <div class="card-body">
<chart type="bar" [data]="chartCallsPerformance" [options]="chartOptions"></chart> <chart type="bar" [data]="chartCallsPerformance" [options]="chartOptions"></chart>
</div> </div>
</div> </div>
<div class="card card"> <div class="card card">
<div class="card-header">API Calls</div>
<div class="card-body"> <div class="card-body">
<div class="aggregation" *ngIf="callsCurrent >= 0"> <div class="aggregation" *ngIf="callsCurrent >= 0">
<div class="aggregation-label">API calls this month</div> <div class="aggregation-label">This month</div>
<div class="aggregation-value">{{callsCurrent | sqxKNumber}}</div> <div class="aggregation-value">{{callsCurrent | sqxKNumber}}</div>
<div class="aggregation-label" *ngIf="callsMax > 0">Monthly limit: {{callsMax | sqxKNumber}}</div> <div class="aggregation-label" *ngIf="callsMax > 0">Monthly limit: {{callsMax | sqxKNumber}}</div>
</div> </div>
@ -91,15 +94,17 @@
</div> </div>
<div class="card card-lg"> <div class="card card-lg">
<div class="card-header">Assets Count</div>
<div class="card-body"> <div class="card-body">
<chart type="line" [data]="chartStorageCount" [options]="chartOptions"></chart> <chart type="line" [data]="chartStorageCount" [options]="chartOptions"></chart>
</div> </div>
</div> </div>
<div class="card card"> <div class="card card">
<div class="card-header">Assets Size (MB)</div>
<div class="card-body"> <div class="card-body">
<div class="aggregation" *ngIf="assetsCurrent >= 0"> <div class="aggregation" *ngIf="assetsCurrent >= 0">
<div class="aggregation-label">Asset size today</div> <div class="aggregation-label">Total Size</div>
<div class="aggregation-value">{{assetsCurrent | sqxFileSize}}</div> <div class="aggregation-value">{{assetsCurrent | sqxFileSize}}</div>
<div class="aggregation-label" *ngIf="assetsMax > 0">Total limit: {{assetsMax | sqxFileSize}}</div> <div class="aggregation-label" *ngIf="assetsMax > 0">Total limit: {{assetsMax | sqxFileSize}}</div>
</div> </div>
@ -107,15 +112,14 @@
</div> </div>
<div class="card card-lg"> <div class="card card-lg">
<div class="card-header">Assets Size (MB)</div>
<div class="card-body"> <div class="card-body">
<chart type="line" [data]="chartStorageSize" [options]="chartOptions"></chart> <chart type="line" [data]="chartStorageSize" [options]="chartOptions"></chart>
</div> </div>
</div> </div>
<div class="card card-lg"> <div class="card card-lg">
<div class="card-header"> <div class="card-header">History</div>
History
</div>
<div class="card-body card-history card-body-scroll"> <div class="card-body card-history card-body-scroll">
<sqx-history-list [events]="history"></sqx-history-list> <sqx-history-list [events]="history"></sqx-history-list>
</div> </div>

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

@ -20,7 +20,9 @@
} }
:host /deep/ canvas { :host /deep/ canvas {
height: 13rem !important; height: 12rem !important;
margin-top: -1rem;
margin-bottom: 0;
} }
.subtext { .subtext {
@ -34,6 +36,8 @@
margin-right: 1rem; margin-right: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
width: 16rem; width: 16rem;
min-height: 16rem;
max-height: 16rem;
float: left; float: left;
} }
@ -41,17 +45,11 @@
width: 33rem; width: 33rem;
} }
&-body {
min-height: 15.5rem;
}
&-image { &-image {
text-align: center; text-align: center;
} }
&-history { &-history {
min-height: 12.4rem;
max-height: 12.4rem;
overflow-y: auto; overflow-y: auto;
} }
@ -100,7 +98,7 @@
&-value { &-value {
font-size: 3rem; font-size: 3rem;
margin-top: 2rem; margin-top: 1rem;
margin-bottom: .5rem; margin-bottom: .5rem;
} }
} }

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

@ -20,6 +20,20 @@ import {
UsagesService UsagesService
} from '@app/shared'; } from '@app/shared';
const COLORS = [
' 51, 137, 213',
'211, 50, 50',
'131, 211, 50',
' 50, 211, 131',
' 50, 211, 211',
' 50, 131, 211',
' 50, 50, 211',
' 50, 211, 50',
'131, 50, 211',
'211, 50, 211',
'211, 50, 131'
];
@Component({ @Component({
selector: 'sqx-dashboard-page', selector: 'sqx-dashboard-page',
styleUrls: ['./dashboard-page.component.scss'], styleUrls: ['./dashboard-page.component.scss'],
@ -43,18 +57,16 @@ export class DashboardPageComponent implements OnDestroy, OnInit {
public chartOptions = { public chartOptions = {
responsive: true, responsive: true,
scales: { scales: {
xAxes: [ xAxes: [{
{ display: true,
display: true stacked: true
} }],
], yAxes: [{
yAxes: [ ticks: {
{ beginAtZero: true
ticks: { },
beginAtZero: true stacked: true
} }]
}
]
}, },
maintainAspectRatio: false maintainAspectRatio: false
}; };
@ -111,15 +123,17 @@ export class DashboardPageComponent implements OnDestroy, OnInit {
this.app.pipe( this.app.pipe(
switchMap(app => this.usagesService.getStorageUsages(app.name, DateTime.today().addDays(-20), DateTime.today()))) switchMap(app => this.usagesService.getStorageUsages(app.name, DateTime.today().addDays(-20), DateTime.today())))
.subscribe(dtos => { .subscribe(dtos => {
const labels = createLabels(dtos);
this.chartStorageCount = { this.chartStorageCount = {
labels: createLabels(dtos), labels,
datasets: [ datasets: [
{ {
label: 'Number of Assets', label: 'All',
lineTension: 0, lineTension: 0,
fill: false, fill: false,
backgroundColor: 'rgba(51, 137, 213, 0.6)', backgroundColor: `rgba(${COLORS[0]}, 0.6)`,
borderColor: 'rgba(51, 137, 213, 1)', borderColor: `rgba(${COLORS[0]}, 1)`,
borderWidth: 1, borderWidth: 1,
data: dtos.map(x => x.count) data: dtos.map(x => x.count)
} }
@ -127,14 +141,14 @@ export class DashboardPageComponent implements OnDestroy, OnInit {
}; };
this.chartStorageSize = { this.chartStorageSize = {
labels: createLabels(dtos), labels,
datasets: [ datasets: [
{ {
label: 'Size of Assets (MB)', label: 'All',
lineTension: 0, lineTension: 0,
fill: false, fill: false,
backgroundColor: 'rgba(51, 137, 213, 0.6)', backgroundColor: `rgba(${COLORS[0]}, 0.6)`,
borderColor: 'rgba(51, 137, 213, 1)', borderColor: `rgba(${COLORS[0]}, 1)`,
borderWidth: 1, borderWidth: 1,
data: dtos.map(x => Math.round(10 * (x.size / (1024 * 1024))) / 10) data: dtos.map(x => Math.round(10 * (x.size / (1024 * 1024))) / 10)
} }
@ -146,36 +160,44 @@ export class DashboardPageComponent implements OnDestroy, OnInit {
this.app.pipe( this.app.pipe(
switchMap(app => this.usagesService.getCallsUsages(app.name, DateTime.today().addDays(-20), DateTime.today()))) switchMap(app => this.usagesService.getCallsUsages(app.name, DateTime.today().addDays(-20), DateTime.today())))
.subscribe(dtos => { .subscribe(dtos => {
const labels = createLabelsFromSet(dtos);
this.chartCallsCount = { this.chartCallsCount = {
labels: createLabels(dtos), labels,
datasets: [ datasets: Object.keys(dtos).map((k, i) => (
{ {
label: 'Number of API Calls', label: label(k),
backgroundColor: 'rgba(51, 137, 213, 0.6)', backgroundColor: `rgba(${COLORS[i]}, 0.6)`,
borderColor: 'rgba(51, 137, 213, 1)', borderColor: `rgba(${COLORS[i]}, 1)`,
borderWidth: 1, borderWidth: 1,
data: dtos.map(x => x.count) data: dtos[k].map(x => x.count)
} }))
]
}; };
this.chartCallsPerformance = { this.chartCallsPerformance = {
labels: createLabels(dtos), labels,
datasets: [ datasets: Object.keys(dtos).map((k, i) => (
{ {
label: 'API Performance (Milliseconds)', label: label(k),
backgroundColor: 'rgba(51, 137, 213, 0.6)', backgroundColor: `rgba(${COLORS[i]}, 0.6)`,
borderColor: 'rgba(51, 137, 213, 1)', borderColor: `rgba(${COLORS[i]}, 1)`,
borderWidth: 1, borderWidth: 1,
data: dtos.map(x => x.averageMs) data: dtos[k].map(x => x.averageMs)
} }))
]
}; };
})); }));
} }
} }
function label(category: string) {
return category === '*' ? 'All Clients' : category;
}
function createLabels(dtos: { date: DateTime }[]): string[] { function createLabels(dtos: { date: DateTime }[]): string[] {
return dtos.map(d => d.date.toStringFormat('M-DD')); return dtos.map(d => d.date.toStringFormat('M-DD'));
} }
function createLabelsFromSet(dtos: { [category: string]: { date: DateTime }[] }): string[] {
return createLabels(dtos[Object.keys(dtos)[0]]);
}

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

@ -38,7 +38,7 @@ describe('UsagesService', () => {
it('should make get request to get calls usages', it('should make get request to get calls usages',
inject([UsagesService, HttpTestingController], (usagesService: UsagesService, httpMock: HttpTestingController) => { inject([UsagesService, HttpTestingController], (usagesService: UsagesService, httpMock: HttpTestingController) => {
let usages: CallsUsageDto[]; let usages: { [category: string]: CallsUsageDto[] };
usagesService.getCallsUsages('my-app', DateTime.parseISO_UTC('2017-10-12'), DateTime.parseISO_UTC('2017-10-13')).subscribe(result => { usagesService.getCallsUsages('my-app', DateTime.parseISO_UTC('2017-10-12'), DateTime.parseISO_UTC('2017-10-13')).subscribe(result => {
usages = result; usages = result;
@ -49,24 +49,27 @@ describe('UsagesService', () => {
expect(req.request.method).toEqual('GET'); expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull(); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush([ req.flush({
{ category1: [
date: '2017-10-12', {
count: 10, date: '2017-10-12',
averageMs: 130 count: 10,
}, averageMs: 130
{ },
date: '2017-10-13', {
count: 13, date: '2017-10-13',
averageMs: 170 count: 13,
} averageMs: 170
]); }
]
});
expect(usages!).toEqual( expect(usages!).toEqual({
[ category1: [
new CallsUsageDto(DateTime.parseISO_UTC('2017-10-12'), 10, 130), new CallsUsageDto(DateTime.parseISO_UTC('2017-10-12'), 10, 130),
new CallsUsageDto(DateTime.parseISO_UTC('2017-10-13'), 13, 170) new CallsUsageDto(DateTime.parseISO_UTC('2017-10-13'), 13, 170)
]); ]
});
})); }));
it('should make get request to get month calls', it('should make get request to get month calls',

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

@ -83,21 +83,24 @@ export class UsagesService {
pretifyError('Failed to load todays storage size. Please reload.')); pretifyError('Failed to load todays storage size. Please reload.'));
} }
public getCallsUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable<CallsUsageDto[]> { public getCallsUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable<{ [category: string]: CallsUsageDto[] }> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/${fromDate.toUTCStringFormat('YYYY-MM-DD')}/${toDate.toUTCStringFormat('YYYY-MM-DD')}`); const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/${fromDate.toUTCStringFormat('YYYY-MM-DD')}/${toDate.toUTCStringFormat('YYYY-MM-DD')}`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { map(response => {
const body = response.payload.body; const body = response.payload.body;
const items: any[] = body; const result: { [category: string]: CallsUsageDto[] } = {};
return items.map(item => { for (let category of Object.keys(body)) {
return new CallsUsageDto( result[category] = body[category].map((item: any) => {
DateTime.parseISO_UTC(item.date), return new CallsUsageDto(
item.count, DateTime.parseISO_UTC(item.date),
item.averageMs); item.count,
}); item.averageMs);
});
}
return result;
}), }),
pretifyError('Failed to load calls usage. Please reload.')); pretifyError('Failed to load calls usage. Please reload.'));
} }

Loading…
Cancel
Save