Browse Source

Log bytes

pull/544/head
Sebastian 6 years ago
parent
commit
69a49bcb1d
  1. 37
      backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs
  2. 3
      backend/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs
  3. 30
      backend/src/Squidex.Domain.Apps.Entities/Apps/RequestLog.cs
  4. 54
      backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs
  5. 4
      backend/src/Squidex.Web/Pipeline/UsageStream.cs
  6. 31
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppLogStoreTests.cs
  7. 67
      backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs

37
backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs

@ -14,7 +14,6 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CsvHelper; using CsvHelper;
using CsvHelper.Configuration; using CsvHelper.Configuration;
using NodaTime;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log.Store; using Squidex.Infrastructure.Log.Store;
@ -24,6 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{ {
private const string FieldAuthClientId = "AuthClientId"; private const string FieldAuthClientId = "AuthClientId";
private const string FieldAuthUserId = "AuthUserId"; private const string FieldAuthUserId = "AuthUserId";
private const string FieldBytes = "Bytes";
private const string FieldCosts = "Costs"; private const string FieldCosts = "Costs";
private const string FieldRequestElapsedMs = "RequestElapsedMs"; private const string FieldRequestElapsedMs = "RequestElapsedMs";
private const string FieldRequestMethod = "RequestMethod"; private const string FieldRequestMethod = "RequestMethod";
@ -39,26 +39,27 @@ namespace Squidex.Domain.Apps.Entities.Apps
this.requestLogStore = requestLogStore; this.requestLogStore = requestLogStore;
} }
public Task LogAsync(Guid appId, Instant timestamp, string? requestMethod, string? requestPath, string? userId, string? clientId, long elapsedMs, double costs) public Task LogAsync(Guid appId, RequestLog request)
{ {
var request = new Request var storedRequest = new Request
{ {
Key = appId.ToString(), Key = appId.ToString(),
Properties = new Dictionary<string, string> Properties = new Dictionary<string, string>
{ {
[FieldCosts] = costs.ToString(CultureInfo.InvariantCulture) [FieldCosts] = request.Costs.ToString(CultureInfo.InvariantCulture)
}, },
Timestamp = timestamp Timestamp = request.Timestamp
}; };
Append(request, FieldAuthClientId, clientId); Append(storedRequest, FieldAuthClientId, request.UserClientId);
Append(request, FieldAuthUserId, userId); Append(storedRequest, FieldAuthUserId, request.UserId);
Append(request, FieldCosts, costs.ToString(CultureInfo.InvariantCulture)); Append(storedRequest, FieldBytes, request.Bytes);
Append(request, FieldRequestElapsedMs, elapsedMs.ToString(CultureInfo.InvariantCulture)); Append(storedRequest, FieldCosts, request.Costs);
Append(request, FieldRequestMethod, requestMethod); Append(storedRequest, FieldRequestElapsedMs, request.ElapsedMs);
Append(request, FieldRequestPath, requestPath); Append(storedRequest, FieldRequestMethod, request.RequestMethod);
Append(storedRequest, FieldRequestPath, request.RequestPath);
return requestLogStore.LogAsync(request); return requestLogStore.LogAsync(storedRequest);
} }
public async Task ReadLogAsync(Guid appId, DateTime fromDate, DateTime toDate, Stream stream, CancellationToken ct = default) public async Task ReadLogAsync(Guid appId, DateTime fromDate, DateTime toDate, Stream stream, CancellationToken ct = default)
@ -77,6 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
csv.WriteField(FieldCosts); csv.WriteField(FieldCosts);
csv.WriteField(FieldAuthClientId); csv.WriteField(FieldAuthClientId);
csv.WriteField(FieldAuthUserId); csv.WriteField(FieldAuthUserId);
csv.WriteField(FieldBytes);
await csv.NextRecordAsync(); await csv.NextRecordAsync();
@ -89,6 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
csv.WriteField(GetDouble(request, FieldCosts)); csv.WriteField(GetDouble(request, FieldCosts));
csv.WriteField(GetString(request, FieldAuthClientId)); csv.WriteField(GetString(request, FieldAuthClientId));
csv.WriteField(GetString(request, FieldAuthUserId)); csv.WriteField(GetString(request, FieldAuthUserId));
csv.WriteField(GetString(request, FieldBytes));
await csv.NextRecordAsync(); await csv.NextRecordAsync();
}, appId.ToString(), fromDate, toDate, ct); }, appId.ToString(), fromDate, toDate, ct);
@ -108,6 +111,16 @@ namespace Squidex.Domain.Apps.Entities.Apps
} }
} }
private static void Append(Request request, string key, double value)
{
request.Properties[key] = value.ToString(CultureInfo.InvariantCulture);
}
private static void Append(Request request, string key, long value)
{
request.Properties[key] = value.ToString(CultureInfo.InvariantCulture);
}
private static string GetString(Request request, string key) private static string GetString(Request request, string key)
{ {
return request.Properties.GetValueOrDefault(key, string.Empty)!; return request.Properties.GetValueOrDefault(key, string.Empty)!;

3
backend/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs

@ -9,13 +9,12 @@ using System;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NodaTime;
namespace Squidex.Domain.Apps.Entities.Apps namespace Squidex.Domain.Apps.Entities.Apps
{ {
public interface IAppLogStore public interface IAppLogStore
{ {
Task LogAsync(Guid appId, Instant timestamp, string? requestMethod, string? requestPath, string? userId, string? clientId, long elapsedMs, double costs); Task LogAsync(Guid appId, RequestLog request);
Task ReadLogAsync(Guid appId, DateTime fromDate, DateTime toDate, Stream stream, CancellationToken ct = default); Task ReadLogAsync(Guid appId, DateTime fromDate, DateTime toDate, Stream stream, CancellationToken ct = default);
} }

30
backend/src/Squidex.Domain.Apps.Entities/Apps/RequestLog.cs

@ -0,0 +1,30 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NodaTime;
namespace Squidex.Domain.Apps.Entities.Apps
{
public struct RequestLog
{
public Instant Timestamp;
public string? RequestMethod;
public string? RequestPath;
public string? UserId;
public string? UserClientId;
public long ElapsedMs;
public long Bytes;
public double Costs;
}
}

54
backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs

@ -18,17 +18,17 @@ namespace Squidex.Web.Pipeline
{ {
public sealed class UsageMiddleware : IMiddleware public sealed class UsageMiddleware : IMiddleware
{ {
private readonly IAppLogStore log; private readonly IAppLogStore logStore;
private readonly IApiUsageTracker usageTracker; private readonly IApiUsageTracker usageTracker;
private readonly IClock clock; private readonly IClock clock;
public UsageMiddleware(IAppLogStore log, IApiUsageTracker usageTracker, IClock clock) public UsageMiddleware(IAppLogStore logStore, IApiUsageTracker usageTracker, IClock clock)
{ {
Guard.NotNull(log, nameof(log)); Guard.NotNull(logStore, nameof(logStore));
Guard.NotNull(usageTracker, nameof(usageTracker)); Guard.NotNull(usageTracker, nameof(usageTracker));
Guard.NotNull(clock, nameof(clock)); Guard.NotNull(clock, nameof(clock));
this.log = log; this.logStore = logStore;
this.usageTracker = usageTracker; this.usageTracker = usageTracker;
@ -51,37 +51,37 @@ namespace Squidex.Web.Pipeline
{ {
var appId = context.Features.Get<IAppFeature>()?.AppId; var appId = context.Features.Get<IAppFeature>()?.AppId;
var costs = context.Features.Get<IApiCostsFeature>()?.Costs ?? 0;
if (appId != null) if (appId != null)
{ {
var elapsedMs = watch.Stop(); var bytes = usageBody.BytesWritten;
var now = clock.GetCurrentInstant();
var userId = context.User.OpenIdSubject(); if (context.Request.ContentLength != null)
var userClient = context.User.OpenIdClientId(); {
bytes += context.Request.ContentLength.Value;
}
await log.LogAsync(appId.Id, now, var request = default(RequestLog);
context.Request.Method,
context.Request.Path,
userId,
userClient,
elapsedMs,
costs);
if (costs > 0) request.Bytes = bytes;
{ request.Costs = context.Features.Get<IApiCostsFeature>()?.Costs ?? 0;
var bytes = usageBody.BytesWritten; request.ElapsedMs = watch.Stop();
request.RequestMethod = context.Request.Method;
request.RequestPath = context.Request.Path;
request.Timestamp = clock.GetCurrentInstant();
request.UserClientId = context.User.OpenIdClientId();
request.UserId = context.User.OpenIdSubject();
if (context.Request.ContentLength != null) await logStore.LogAsync(appId.Id, request);
{
bytes += context.Request.ContentLength.Value;
}
var date = now.ToDateTimeUtc().Date; if (request.Costs > 0)
{
var date = request.Timestamp.ToDateTimeUtc().Date;
await usageTracker.TrackAsync(date, appId.Id.ToString(), userClient, costs, elapsedMs, bytes); await usageTracker.TrackAsync(date, appId.Id.ToString(),
request.UserClientId,
request.Costs,
request.ElapsedMs,
request.Bytes);
} }
} }
} }

4
backend/src/Squidex.Web/Pipeline/UsageStream.cs

@ -64,7 +64,7 @@ namespace Squidex.Web.Pipeline
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{ {
await base.WriteAsync(buffer, offset, count, cancellationToken); await inner.WriteAsync(buffer, offset, count, cancellationToken);
bytesWritten += count; bytesWritten += count;
} }
@ -78,7 +78,7 @@ namespace Squidex.Web.Pipeline
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{ {
await base.WriteAsync(buffer, cancellationToken); await inner.WriteAsync(buffer, cancellationToken);
bytesWritten += buffer.Length; bytesWritten += buffer.Length;
} }

31
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppLogStoreTests.cs

@ -33,22 +33,27 @@ namespace Squidex.Domain.Apps.Entities.Apps
A.CallTo(() => requestLogStore.LogAsync(A<Request>._)) A.CallTo(() => requestLogStore.LogAsync(A<Request>._))
.Invokes((Request request) => recordedRequest = request); .Invokes((Request request) => recordedRequest = request);
var clientId = "frontend"; var request = default(RequestLog);
var costs = 2; request.Bytes = 1024;
var elapsedMs = 120; request.Costs = 1.5;
var requestMethod = "GET"; request.ElapsedMs = 120;
var requestPath = "/my-path"; request.RequestMethod = "GET";
var userId = "user1"; request.RequestPath = "/my-path";
request.Timestamp = default;
await sut.LogAsync(Guid.NewGuid(), default, requestMethod, requestPath, userId, clientId, elapsedMs, costs); request.UserClientId = "frontend";
request.UserId = "user1";
await sut.LogAsync(Guid.NewGuid(), request);
Assert.NotNull(recordedRequest); Assert.NotNull(recordedRequest);
Assert.Contains(clientId, recordedRequest!.Properties.Values); Assert.Contains(request.Bytes.ToString(), recordedRequest!.Properties.Values);
Assert.Contains(costs.ToString(), recordedRequest!.Properties.Values); Assert.Contains(request.Costs.ToString(), recordedRequest!.Properties.Values);
Assert.Contains(elapsedMs.ToString(), recordedRequest!.Properties.Values); Assert.Contains(request.ElapsedMs.ToString(), recordedRequest!.Properties.Values);
Assert.Contains(requestMethod, recordedRequest!.Properties.Values); Assert.Contains(request.RequestMethod, recordedRequest!.Properties.Values);
Assert.Contains(requestPath, recordedRequest!.Properties.Values); Assert.Contains(request.RequestPath, recordedRequest!.Properties.Values);
Assert.Contains(request.UserClientId, recordedRequest!.Properties.Values);
Assert.Contains(request.UserId, recordedRequest!.Properties.Values);
} }
[Fact] [Fact]

67
backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs

@ -6,6 +6,8 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.IO;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -15,6 +17,8 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.UsageTracking; using Squidex.Infrastructure.UsageTracking;
using Xunit; using Xunit;
#pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator
namespace Squidex.Web.Pipeline namespace Squidex.Web.Pipeline
{ {
public class UsageMiddlewareTests public class UsageMiddlewareTests
@ -109,14 +113,35 @@ namespace Squidex.Web.Pipeline
} }
[Fact] [Fact]
public async Task Should_track_response_bytes() public async Task Should_track_response_bytes_with_writer()
{
httpContext.Features.Set<IAppFeature>(new AppFeature(appId));
httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13));
await sut.InvokeAsync(httpContext, async x =>
{
await x.Response.BodyWriter.WriteAsync(Encoding.Default.GetBytes("Hello World"));
await next(x);
});
Assert.True(isNextCalled);
var date = instant.ToDateTimeUtc().Date;
A.CallTo(() => usageTracker.TrackAsync(date, A<string>._, A<string>._, 13, A<long>._, 11))
.MustHaveHappened();
}
[Fact]
public async Task Should_track_response_bytes_with_stream()
{ {
httpContext.Features.Set<IAppFeature>(new AppFeature(appId)); httpContext.Features.Set<IAppFeature>(new AppFeature(appId));
httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13)); httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13));
await sut.InvokeAsync(httpContext, async x => await sut.InvokeAsync(httpContext, async x =>
{ {
await x.Response.WriteAsync("Hello World"); await x.Response.Body.WriteAsync(Encoding.Default.GetBytes("Hello World"));
await next(x); await next(x);
}); });
@ -129,6 +154,37 @@ namespace Squidex.Web.Pipeline
.MustHaveHappened(); .MustHaveHappened();
} }
[Fact]
public async Task Should_track_response_bytes_with_file()
{
httpContext.Features.Set<IAppFeature>(new AppFeature(appId));
httpContext.Features.Set<IApiCostsFeature>(new ApiCostsAttribute(13));
var tempFileName = Path.GetTempFileName();
try
{
File.WriteAllText(tempFileName, "Hello World");
await sut.InvokeAsync(httpContext, async x =>
{
await x.Response.SendFileAsync(tempFileName, 0, new FileInfo(tempFileName).Length);
await next(x);
});
}
finally
{
File.Delete(tempFileName);
}
Assert.True(isNextCalled);
var date = instant.ToDateTimeUtc().Date;
A.CallTo(() => usageTracker.TrackAsync(date, A<string>._, A<string>._, 13, A<long>._, 11))
.MustHaveHappened();
}
[Fact] [Fact]
public async Task Should_not_track_if_costs_are_zero() public async Task Should_not_track_if_costs_are_zero()
{ {
@ -156,7 +212,12 @@ namespace Squidex.Web.Pipeline
await sut.InvokeAsync(httpContext, next); await sut.InvokeAsync(httpContext, next);
A.CallTo(() => appLogStore.LogAsync(appId.Id, instant, "GET", "/my-path", null, null, A<long>._, 0)) A.CallTo(() => appLogStore.LogAsync(appId.Id,
A<RequestLog>.That.Matches(x =>
x.Timestamp == instant &&
x.RequestMethod == "GET" &&
x.RequestPath == "/my-path" &&
x.Costs == 0)))
.MustHaveHappened(); .MustHaveHappened();
} }
} }

Loading…
Cancel
Save