mirror of https://github.com/Squidex/squidex.git
24 changed files with 493 additions and 109 deletions
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Log |
||||
|
{ |
||||
|
public interface ILogProfilerSessionProvider |
||||
|
{ |
||||
|
ProfilerSession GetSession(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Log |
||||
|
{ |
||||
|
public sealed class NoopDisposable : IDisposable |
||||
|
{ |
||||
|
public static readonly NoopDisposable Instance = new NoopDisposable(); |
||||
|
|
||||
|
private NoopDisposable() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Log |
||||
|
{ |
||||
|
public static class Profile |
||||
|
{ |
||||
|
private static ILogProfilerSessionProvider sessionProvider; |
||||
|
|
||||
|
private sealed class Timer : IDisposable |
||||
|
{ |
||||
|
private readonly Stopwatch watch = Stopwatch.StartNew(); |
||||
|
private readonly ProfilerSession session; |
||||
|
private readonly string key; |
||||
|
|
||||
|
public Timer(ProfilerSession session, string key) |
||||
|
{ |
||||
|
this.session = session; |
||||
|
this.key = key; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
watch.Stop(); |
||||
|
|
||||
|
session.Measured(key, watch.ElapsedMilliseconds); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void Init(ILogProfilerSessionProvider provider) |
||||
|
{ |
||||
|
sessionProvider = provider; |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Method<T>([CallerMemberName] string memberName = null) |
||||
|
{ |
||||
|
return Key($"{typeof(T).Name}/{memberName}"); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Method(string objectName, [CallerMemberName] string memberName = null) |
||||
|
{ |
||||
|
return Key($"{objectName}/{memberName}"); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Key(string key) |
||||
|
{ |
||||
|
Guard.NotNull(key, nameof(key)); |
||||
|
|
||||
|
var session = sessionProvider?.GetSession(); |
||||
|
|
||||
|
if (session == null) |
||||
|
{ |
||||
|
return NoopDisposable.Instance; |
||||
|
} |
||||
|
|
||||
|
return new Timer(session, key); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Concurrent; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Log |
||||
|
{ |
||||
|
public sealed class ProfilerSession |
||||
|
{ |
||||
|
private struct ProfilerItem |
||||
|
{ |
||||
|
public long Total; |
||||
|
public long Count; |
||||
|
} |
||||
|
|
||||
|
private readonly ConcurrentDictionary<string, ProfilerItem> traces = new ConcurrentDictionary<string, ProfilerItem>(); |
||||
|
|
||||
|
public void Measured(string name, long elapsed) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(name, nameof(name)); |
||||
|
|
||||
|
traces.AddOrUpdate(name, x => |
||||
|
{ |
||||
|
return new ProfilerItem { Total = elapsed, Count = 1 }; |
||||
|
}, |
||||
|
(x, result) => |
||||
|
{ |
||||
|
result.Total += elapsed; |
||||
|
result.Count++; |
||||
|
|
||||
|
return result; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public void Write(IObjectWriter writer) |
||||
|
{ |
||||
|
Guard.NotNull(writer, nameof(writer)); |
||||
|
|
||||
|
if (traces.Count > 0) |
||||
|
{ |
||||
|
writer.WriteObject("profiler", p => |
||||
|
{ |
||||
|
foreach (var kvp in traces) |
||||
|
{ |
||||
|
p.WriteObject(kvp.Key, k => k |
||||
|
.WriteProperty("elapsedMsTotal", kvp.Value.Total) |
||||
|
.WriteProperty("elapsedMsAvg", kvp.Value.Total / kvp.Value.Count) |
||||
|
.WriteProperty("count", kvp.Value.Count)); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.UsageTracking |
||||
|
{ |
||||
|
public sealed class CachingUsageTracker : CachingProviderBase, IUsageTracker |
||||
|
{ |
||||
|
private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(10); |
||||
|
private readonly IUsageTracker inner; |
||||
|
|
||||
|
public CachingUsageTracker(IUsageTracker inner, IMemoryCache cache) |
||||
|
: base(cache) |
||||
|
{ |
||||
|
Guard.NotNull(inner, nameof(inner)); |
||||
|
|
||||
|
this.inner = inner; |
||||
|
} |
||||
|
|
||||
|
public Task<IReadOnlyList<StoredUsage>> QueryAsync(string key, DateTime fromDate, DateTime toDate) |
||||
|
{ |
||||
|
return inner.QueryAsync(key, fromDate, toDate); |
||||
|
} |
||||
|
|
||||
|
public Task TrackAsync(string key, double weight, double elapsedMs) |
||||
|
{ |
||||
|
return inner.TrackAsync(key, weight, elapsedMs); |
||||
|
} |
||||
|
|
||||
|
public async Task<long> GetMonthlyCallsAsync(string key, DateTime date) |
||||
|
{ |
||||
|
Guard.NotNull(key, nameof(key)); |
||||
|
|
||||
|
var cacheKey = string.Concat(key, date); |
||||
|
|
||||
|
if (Cache.TryGetValue<long>(cacheKey, out var result)) |
||||
|
{ |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
result = await inner.GetMonthlyCallsAsync(key, date); |
||||
|
|
||||
|
Cache.Set(cacheKey, result, CacheTime); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Microsoft.AspNetCore.Http; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
|
||||
|
namespace Squidex.Pipeline |
||||
|
{ |
||||
|
public sealed class RequestLogProfilerSessionProvider : ILogProfilerSessionProvider |
||||
|
{ |
||||
|
private const string ItemKey = "ProfilerSesison"; |
||||
|
private readonly IHttpContextAccessor httpContextAccessor; |
||||
|
|
||||
|
public RequestLogProfilerSessionProvider(IHttpContextAccessor httpContextAccessor) |
||||
|
{ |
||||
|
this.httpContextAccessor = httpContextAccessor; |
||||
|
|
||||
|
Profile.Init(this); |
||||
|
} |
||||
|
|
||||
|
public ProfilerSession GetSession() |
||||
|
{ |
||||
|
var context = httpContextAccessor?.HttpContext?.Items[ItemKey] as ProfilerSession; |
||||
|
|
||||
|
return context; |
||||
|
} |
||||
|
|
||||
|
public void Start(HttpContext httpContext, ProfilerSession session) |
||||
|
{ |
||||
|
Guard.NotNull(httpContext, nameof(httpContext)); |
||||
|
|
||||
|
httpContext.Items[ItemKey] = session; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,62 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.UsageTracking |
||||
|
{ |
||||
|
public sealed class ThreadingUsageTrackerTests |
||||
|
{ |
||||
|
private readonly MemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); |
||||
|
private readonly IUsageTracker inner = A.Fake<IUsageTracker>(); |
||||
|
private readonly IUsageTracker sut; |
||||
|
|
||||
|
public ThreadingUsageTrackerTests() |
||||
|
{ |
||||
|
sut = new CachingUsageTracker(inner, cache); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_forward_track_call() |
||||
|
{ |
||||
|
await sut.TrackAsync("MyKey", 123, 456); |
||||
|
|
||||
|
A.CallTo(() => inner.TrackAsync("MyKey", 123, 456)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_forward_query_call() |
||||
|
{ |
||||
|
await sut.QueryAsync("MyKey", DateTime.MaxValue, DateTime.MinValue); |
||||
|
|
||||
|
A.CallTo(() => inner.QueryAsync("MyKey", DateTime.MaxValue, DateTime.MinValue)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_cache_monthly_usage() |
||||
|
{ |
||||
|
A.CallTo(() => inner.GetMonthlyCallsAsync("MyKey", DateTime.Today)) |
||||
|
.Returns(100); |
||||
|
|
||||
|
var result1 = await sut.GetMonthlyCallsAsync("MyKey", DateTime.Today); |
||||
|
var result2 = await sut.GetMonthlyCallsAsync("MyKey", DateTime.Today); |
||||
|
|
||||
|
Assert.Equal(100, result1); |
||||
|
Assert.Equal(100, result2); |
||||
|
|
||||
|
A.CallTo(() => inner.GetMonthlyCallsAsync("MyKey", DateTime.Today)) |
||||
|
.MustHaveHappened(Repeated.Exactly.Once); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue