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