diff --git a/CacheMeIfYouCan.sln b/CacheMeIfYouCan.sln index e274bc7..8bb01f9 100644 --- a/CacheMeIfYouCan.sln +++ b/CacheMeIfYouCan.sln @@ -1,32 +1,40 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheMeIfYouCan", "src\CacheMeIfYouCan\CacheMeIfYouCan.csproj", "{0E5C541F-0B77-4073-AEFE-0BA03EC3CA0A}" +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31829.152 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheMeIfYouCan", "src\CacheMeIfYouCan\CacheMeIfYouCan.csproj", "{0E5C541F-0B77-4073-AEFE-0BA03EC3CA0A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheMeIfYouCan.Tests", "tests\CacheMeIfYouCan.Tests\CacheMeIfYouCan.Tests.csproj", "{B4C0DCD9-FFE4-44B9-8457-37262FC5318E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheMeIfYouCan.Tests", "tests\CacheMeIfYouCan.Tests\CacheMeIfYouCan.Tests.csproj", "{B4C0DCD9-FFE4-44B9-8457-37262FC5318E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheMeIfYouCan.ILTemplates", "sandbox\CacheMeIfYouCan.ILTemplates\CacheMeIfYouCan.ILTemplates.csproj", "{42055B9A-138C-4114-AD06-6BAA3ABBC659}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheMeIfYouCan.ILTemplates", "sandbox\CacheMeIfYouCan.ILTemplates\CacheMeIfYouCan.ILTemplates.csproj", "{42055B9A-138C-4114-AD06-6BAA3ABBC659}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheMeIfYouCan.Redis", "src\CacheMeIfYouCan.Redis\CacheMeIfYouCan.Redis.csproj", "{EB0E7F39-18FD-46F3-800B-EC98C38055A7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheMeIfYouCan.Redis", "src\CacheMeIfYouCan.Redis\CacheMeIfYouCan.Redis.csproj", "{EB0E7F39-18FD-46F3-800B-EC98C38055A7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheMeIfYouCan.Redis.Tests", "tests\CacheMeIfYouCan.Redis.Tests\CacheMeIfYouCan.Redis.Tests.csproj", "{09EB8E7F-EE8E-432F-8108-10255FE234EB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheMeIfYouCan.Redis.Tests", "tests\CacheMeIfYouCan.Redis.Tests\CacheMeIfYouCan.Redis.Tests.csproj", "{09EB8E7F-EE8E-432F-8108-10255FE234EB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheMeIfYouCan.Polly", "src\CacheMeIfYouCan.Polly\CacheMeIfYouCan.Polly.csproj", "{7A3AFBE2-4D42-4C20-BB75-C2F01D0C36D1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheMeIfYouCan.Polly", "src\CacheMeIfYouCan.Polly\CacheMeIfYouCan.Polly.csproj", "{7A3AFBE2-4D42-4C20-BB75-C2F01D0C36D1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheMeIfYouCan.Polly.Tests", "tests\CacheMeIfYouCan.Polly.Tests\CacheMeIfYouCan.Polly.Tests.csproj", "{AF8803FC-0559-40E6-A902-72C0A77DF380}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheMeIfYouCan.Polly.Tests", "tests\CacheMeIfYouCan.Polly.Tests\CacheMeIfYouCan.Polly.Tests.csproj", "{AF8803FC-0559-40E6-A902-72C0A77DF380}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheMeIfYouCan.Serializers.ProtoBuf", "src\CacheMeIfYouCan.Serializers.ProtoBuf\CacheMeIfYouCan.Serializers.ProtoBuf.csproj", "{4B7C629B-F976-47F9-8721-5DC8DD60B8B4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheMeIfYouCan.Serializers.ProtoBuf", "src\CacheMeIfYouCan.Serializers.ProtoBuf\CacheMeIfYouCan.Serializers.ProtoBuf.csproj", "{4B7C629B-F976-47F9-8721-5DC8DD60B8B4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheMeIfYouCan.Benchmarks", "benchmarks\CacheMeIfYouCan.Benchmarks\CacheMeIfYouCan.Benchmarks.csproj", "{C133ACD8-ABF2-4DBA-9907-49726AF4AF43}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheMeIfYouCan.Benchmarks", "benchmarks\CacheMeIfYouCan.Benchmarks\CacheMeIfYouCan.Benchmarks.csproj", "{C133ACD8-ABF2-4DBA-9907-49726AF4AF43}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheMeIfYouCan.Cron", "src\CacheMeIfYouCan.Cron\CacheMeIfYouCan.Cron.csproj", "{3C906ADB-686F-46B4-859C-E425040F1CFE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheMeIfYouCan.Cron", "src\CacheMeIfYouCan.Cron\CacheMeIfYouCan.Cron.csproj", "{3C906ADB-686F-46B4-859C-E425040F1CFE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheMeIfYouCan.Cron.Tests", "tests\CacheMeIfYouCan.Cron.Tests\CacheMeIfYouCan.Cron.Tests.csproj", "{A0F382D8-3C5A-4955-A9CA-B558EEDBAD72}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheMeIfYouCan.Cron.Tests", "tests\CacheMeIfYouCan.Cron.Tests\CacheMeIfYouCan.Cron.Tests.csproj", "{A0F382D8-3C5A-4955-A9CA-B558EEDBAD72}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheMeIfYouCan.Serializers.Json", "src\CacheMeIfYouCan.Serializers.Json\CacheMeIfYouCan.Serializers.Json.csproj", "{FC6B01A7-3049-44A4-833B-31B0961D50C1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheMeIfYouCan.Serializers.Json", "src\CacheMeIfYouCan.Serializers.Json\CacheMeIfYouCan.Serializers.Json.csproj", "{FC6B01A7-3049-44A4-833B-31B0961D50C1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheMeIfYouCan.Serializers.Tests", "tests\CacheMeIfYouCan.Serializers.Tests\CacheMeIfYouCan.Serializers.Tests.csproj", "{8A13A189-17C8-42A3-86DD-E1DC6BF23E56}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheMeIfYouCan.Serializers.Tests", "tests\CacheMeIfYouCan.Serializers.Tests\CacheMeIfYouCan.Serializers.Tests.csproj", "{8A13A189-17C8-42A3-86DD-E1DC6BF23E56}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheMeIfYouCan.Serializers.MessagePack", "src\CacheMeIfYouCan.Serializers.MessagePack\CacheMeIfYouCan.Serializers.MessagePack.csproj", "{CDA877B2-5E3A-4D48-8650-D1B20AF773D8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheMeIfYouCan.Serializers.MessagePack", "src\CacheMeIfYouCan.Serializers.MessagePack\CacheMeIfYouCan.Serializers.MessagePack.csproj", "{CDA877B2-5E3A-4D48-8650-D1B20AF773D8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{200A2BA5-847A-46CD-80AF-5E09BCF5D57E}" + ProjectSection(SolutionItems) = preProject + test.runsettings = test.runsettings + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -91,6 +99,10 @@ Global {CDA877B2-5E3A-4D48-8650-D1B20AF773D8}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDA877B2-5E3A-4D48-8650-D1B20AF773D8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection - GlobalSection(NestedProjects) = preSolution + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {930E63DC-AC1E-4694-BE8E-3F268FBE9630} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index e097c93..4816f13 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,25 @@ var cachedFunction = CachedFunctionFactory .Build(); ``` +You can either use an Application Insights telemtry processor or your own to gather performance metrics on the peformance of +distributed cache commands, just implement the ITelemetryProcessor provided by Application Insights. +If you implement an ITelemetryConfig you can provide a threshold so only slower cache calls get logged - useful to help when +diagnosing performance issues with your implementation. +To use, extend the .ConfigureFor call with the .WithApplicationInsightsTelemetry + +```csharp +Func> originalFunction = ... + +var cachedFunction = CachedFunctionFactory + .ConfigureFor(originalFunction) + .WithLocalCache(new DictionaryCache()) + .WithDistributedCache(new RedisCache(...)) + .WithTimeToLive(TimeSpan.FromHours(1)) + .WithApplicationInsightsTelemetry(new MyTelemetryProcessor(), new MyTelemetryConfig(), "myRedisHost", "myCache"); + .OnResult(r => logSuccess(r), ex => logException(ex)) + .Build(); +``` + It is possible to create cached functions where the original function has up to 8 parameters (+ an optional cancellation token) and the cache key can be any function of the input parameters. The original function can be synchronous, asynchronous, or return a `ValueTask` (the underlying implementation uses ValueTasks). The resulting diff --git a/src/CacheMeIfYouCan.Redis/CacheMeIfYouCan.Redis.csproj b/src/CacheMeIfYouCan.Redis/CacheMeIfYouCan.Redis.csproj index 07df0ec..06ca516 100644 --- a/src/CacheMeIfYouCan.Redis/CacheMeIfYouCan.Redis.csproj +++ b/src/CacheMeIfYouCan.Redis/CacheMeIfYouCan.Redis.csproj @@ -6,6 +6,7 @@ Extends CacheMeIfYouCan by providing Redis cache integration + diff --git a/src/CacheMeIfYouCan.Redis/CachedObjectConfigurationManagerExtensions.cs b/src/CacheMeIfYouCan.Redis/CachedObjectConfigurationManagerExtensions.cs new file mode 100644 index 0000000..03d2504 --- /dev/null +++ b/src/CacheMeIfYouCan.Redis/CachedObjectConfigurationManagerExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.ApplicationInsights.Extensibility; + +namespace CacheMeIfYouCan.Redis +{ + public static class CachedObjectConfigurationManagerExtensions + { + public static IDistributedCache WithApplicationInsightsTelemetry( + this IDistributedCache distributedCache, + IDistributedCacheConfig distributedCacheConfig, + ITelemetryProcessor telemetryProcessor, + ITelemetryConfig telemetryConfig) + { + return new DistributedCacheApplicationInsightsWrapper(distributedCache, + distributedCacheConfig, telemetryProcessor, telemetryConfig); + } + + public static IDistributedCache WithApplicationInsightsTelemetry( + this IDistributedCache distributedCache, + IDistributedCacheConfig distributedCacheConfig, + ITelemetryProcessor telemetryProcessor, + ITelemetryConfig telemetryConfig) + { + return new DistributedCacheApplicationInsightsWrapper(distributedCache, + distributedCacheConfig, telemetryProcessor, telemetryConfig); + } + } +} diff --git a/src/CacheMeIfYouCan.Redis/DistributedCacheApplicationInsightsWrapper.cs b/src/CacheMeIfYouCan.Redis/DistributedCacheApplicationInsightsWrapper.cs new file mode 100644 index 0000000..4965b24 --- /dev/null +++ b/src/CacheMeIfYouCan.Redis/DistributedCacheApplicationInsightsWrapper.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.ApplicationInsights.Extensibility; + +namespace CacheMeIfYouCan.Redis +{ + public sealed class DistributedCacheApplicationInsightsWrapper + : DistributedCacheEventsWrapperBase + { + private readonly TelemetryProcessor _telemetryProcessor; + + public DistributedCacheApplicationInsightsWrapper(IDistributedCache innerCache, + IDistributedCacheConfig cacheConfig, + ITelemetryProcessor telemetryProcessor, + ITelemetryConfig telemetryConfig) + : base(innerCache) + { + _telemetryProcessor = new TelemetryProcessor(cacheConfig, telemetryProcessor, telemetryConfig); + } + + protected override void OnTryGetCompletedSuccessfully(TKey key, bool resultSuccess, + ValueAndTimeToLive resultValue, TimeSpan duration) + { + _telemetryProcessor.Add(duration, "StringGetWithExpiryAsync", $"Key '{key}'", true); + + base.OnTryGetCompletedSuccessfully(key, resultSuccess, resultValue, duration); + } + + protected override void OnSetManyCompletedSuccessfully(ReadOnlySpan> values, + TimeSpan timeToLive, TimeSpan duration) + { + var keys = $"Keys '{string.Join(",", values.ToArray().Select(d => d.Key))}'"; + + _telemetryProcessor.Add(duration, "StringSetAsync", keys, true); + + base.OnSetManyCompletedSuccessfully(values, timeToLive, duration); + } + + protected override void OnGetManyCompletedSuccessfully(ReadOnlySpan keys, + ReadOnlySpan>> values, TimeSpan duration) + { + var keysText = $"Keys '{string.Join(",", keys.ToArray())}'"; + + _telemetryProcessor.Add(duration, "StringGetWithExpiryAsync", keysText, true); + + base.OnGetManyCompletedSuccessfully(keys, values, duration); + } + + protected override void OnGetManyException(ReadOnlySpan keys, TimeSpan duration, Exception exception, + out bool exceptionHandled) + { + var keysText = $"Keys '{string.Join(",", keys.ToArray())}'"; + + _telemetryProcessor.Add(duration, "StringGetWithExpiryAsync", keysText, false); + + base.OnGetManyException(keys, duration, exception, out exceptionHandled); + } + + protected override void OnSetCompletedSuccessfully(TKey key, TValue value, TimeSpan timeToLive, + TimeSpan duration) + { + _telemetryProcessor.Add(duration, "StringSetAsync", $"Key '{key}'", true); + + base.OnSetCompletedSuccessfully(key, value, timeToLive, duration); + } + + protected override void OnSetException(TKey key, TValue value, TimeSpan timeToLive, TimeSpan duration, + Exception exception, + out bool exceptionHandled) + { + _telemetryProcessor.Add(duration, "StringSetAsync", $"Key '{key}'", false); + + base.OnSetException(key, value, timeToLive, duration, exception, out exceptionHandled); + } + + protected override void OnSetManyException(ReadOnlySpan> values, TimeSpan timeToLive, + TimeSpan duration, Exception exception, + out bool exceptionHandled) + { + var keys = $"Keys '{string.Join(",", values.ToArray().Select(d => d.Key))}'"; + + _telemetryProcessor.Add(duration, "StringSetAsync", keys, false); + + base.OnSetManyException(values, timeToLive, duration, exception, out exceptionHandled); + } + + protected override void OnTryGetException(TKey key, TimeSpan duration, Exception exception, + out bool exceptionHandled) + { + _telemetryProcessor.Add(duration, "StringGetWithExpiryAsync", $"Key '{key}'", false); + + base.OnTryGetException(key, duration, exception, out exceptionHandled); + } + + protected override void OnTryRemoveCompletedSuccessfully(TKey key, bool wasRemoved, TimeSpan duration) + { + _telemetryProcessor.Add(duration, "KeyDeleteAsync", $"Key '{key}'", true); + + base.OnTryRemoveCompletedSuccessfully(key, wasRemoved, duration); + } + + protected override void OnTryRemoveException(TKey key, TimeSpan duration, Exception exception, + out bool exceptionHandled) + { + _telemetryProcessor.Add(duration, "KeyDeleteAsync", $"Key '{key}'", false); + + base.OnTryRemoveException(key, duration, exception, out exceptionHandled); + } + } + + public sealed class DistributedCacheApplicationInsightsWrapper + : DistributedCacheEventsWrapperBase + { + private readonly TelemetryProcessor _telemetryProcessor; + + public DistributedCacheApplicationInsightsWrapper(IDistributedCache innerCache, + IDistributedCacheConfig cacheConfig, + ITelemetryProcessor telemetryProcessor, + ITelemetryConfig telemetryConfig) : base(innerCache) + { + _telemetryProcessor = new TelemetryProcessor(cacheConfig, telemetryProcessor, telemetryConfig); + } + + protected override void OnSetManyException(TOuterKey outerKey, + ReadOnlySpan> values, TimeSpan timeToLive, TimeSpan duration, + Exception exception, out bool exceptionHandled) + { + var keys = + $"Keys {string.Join(",", values.ToArray().Select(innerKeyValue => $"'{outerKey}.{innerKeyValue.Key}'"))}"; + + _telemetryProcessor.Add(duration, "StringSetAsync", keys, false); + + base.OnSetManyException(outerKey, values, timeToLive, duration, exception, out exceptionHandled); + } + + protected override void OnTryRemoveCompletedSuccessfully(TOuterKey outerKey, TInnerKey innerKey, + bool wasRemoved, TimeSpan duration) + { + _telemetryProcessor.Add(duration, "KeyDeleteAsync", $"Key '{outerKey}.{innerKey}'", true); + + base.OnTryRemoveCompletedSuccessfully(outerKey, innerKey, wasRemoved, duration); + } + + protected override void OnTryRemoveException(TOuterKey outerKey, TInnerKey innerKey, TimeSpan duration, + Exception exception, + out bool exceptionHandled) + { + _telemetryProcessor.Add(duration, "KeyDeleteAsync", $"Key '{outerKey}.{innerKey}'", false); + + base.OnTryRemoveException(outerKey, innerKey, duration, exception, out exceptionHandled); + } + + protected override void OnGetManyException(TOuterKey outerKey, ReadOnlySpan innerKeys, + TimeSpan duration, Exception exception, + out bool exceptionHandled) + { + var keys = $"Keys {string.Join(",", innerKeys.ToArray().Select(innerKey => $"'{outerKey}.{innerKey}'"))}"; + + _telemetryProcessor.Add(duration, "StringGetWithExpiryAsync", keys, false); + + base.OnGetManyException(outerKey, innerKeys, duration, exception, out exceptionHandled); + } + + protected override void OnSetManyCompletedSuccessfully(TOuterKey outerKey, + ReadOnlySpan> values, TimeSpan timeToLive, TimeSpan duration) + { + var keys = + $"Keys {string.Join(",", values.ToArray().Select(innerKeyValue => $"'{outerKey}.{innerKeyValue.Key}'"))}"; + + _telemetryProcessor.Add(duration, "StringSetAsync", keys, true); + + base.OnSetManyCompletedSuccessfully(outerKey, values, timeToLive, duration); + } + + protected override void OnGetManyCompletedSuccessfully(TOuterKey outerKey, ReadOnlySpan innerKeys, + ReadOnlySpan>> values, TimeSpan duration) + { + var keys = $"Keys {string.Join(",", innerKeys.ToArray().Select(innerKey => $"'{outerKey}.{innerKey}'"))}"; + + _telemetryProcessor.Add(duration, "StringGetWithExpiryAsync", keys, true); + + base.OnGetManyCompletedSuccessfully(outerKey, innerKeys, values, duration); + } + } +} diff --git a/src/CacheMeIfYouCan.Redis/RedisCache.cs b/src/CacheMeIfYouCan.Redis/RedisCache.cs index ac05211..c714eae 100644 --- a/src/CacheMeIfYouCan.Redis/RedisCache.cs +++ b/src/CacheMeIfYouCan.Redis/RedisCache.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Buffers; using System.Collections.Generic; using System.IO; @@ -51,7 +51,7 @@ public RedisCache( valueSerializer, recyclableMemoryStreamManager, nullValue); - + _serializeValuesToStreams = true; } @@ -79,10 +79,10 @@ public RedisCache( valueSerializer, valueDeserializer, nullValue); - + _serializeValuesToStreams = false; } - + private RedisCache( IRedisConnection connection, Func keySerializer, @@ -106,14 +106,14 @@ private RedisCache( { if (subscriber is null) throw new ArgumentNullException(nameof(subscriber)); - + _keysChangedRemotely = new Subject<(string, KeyEventType)>(); - + if (keyEventTypesToSubscribeTo.HasFlag(KeyEventType.Set)) _recentlySetKeysManager = new RecentlySetOrRemovedKeysManager(); - + _recentlyRemovedKeysManager = new RecentlySetOrRemovedKeysManager(); - + subscriber.SubscribeToKeyChanges(dbIndex, keyEventTypesToSubscribeTo, OnKeyChanged, keyPrefix); } } @@ -121,11 +121,11 @@ private RedisCache( public async Task<(bool Success, ValueAndTimeToLive Value)> TryGet(TKey key) { CheckDisposed(); - + var redisDb = GetDatabase(); var redisKey = _keySerializer(key); - + var fromRedis = await redisDb .StringGetWithExpiryAsync(redisKey) .ConfigureAwait(false); @@ -141,7 +141,7 @@ private RedisCache( public async Task Set(TKey key, TValue value, TimeSpan timeToLive) { CheckDisposed(); - + var redisDb = GetDatabase(); var redisKey = _keySerializer(key); @@ -152,7 +152,7 @@ public async Task Set(TKey key, TValue value, TimeSpan timeToLive) var redisValue = _redisValueConverter.ConvertToRedisValue(value, out stream); _recentlySetKeysManager?.Mark(((string)redisKey).Substring(_keyPrefixLength)); - + var task = redisDb.StringSetAsync(redisKey, redisValue, timeToLive, flags: _setValueFlags); if (!task.IsCompleted) @@ -169,11 +169,11 @@ public async Task GetMany( Memory>> destination) { CheckDisposed(); - + var redisDb = GetDatabase(); var valuesFoundCount = 0; - + var tasks = CreateTasks(out var pooledTasksArray); try @@ -181,7 +181,7 @@ public async Task GetMany( await Task .WhenAll(tasks) .ConfigureAwait(false); - + return valuesFoundCount == 0 ? 0 : CopyResultsToDestinationArray(); @@ -190,11 +190,11 @@ await Task { ArrayPool>.Shared.Return(pooledTasksArray); } - + IReadOnlyCollection> CreateTasks(out Task<(TKey, RedisValueWithExpiry)>[] pooledArray) { pooledArray = ArrayPool>.Shared.Rent(keys.Length); - + var i = 0; foreach (var innerKey in keys.Span) pooledArray[i++] = GetSingle(innerKey); @@ -226,7 +226,7 @@ int CopyResultsToDestinationArray() continue; var value = _redisValueConverter.ConvertFromRedisValue(fromRedis.Value); - + span[index++] = new KeyValuePair>( key, new ValueAndTimeToLive(value, fromRedis.Expiry ?? TimeSpan.FromDays(1))); @@ -242,7 +242,7 @@ int CopyResultsToDestinationArray() public async Task SetMany(ReadOnlyMemory> values, TimeSpan timeToLive) { CheckDisposed(); - + var redisDb = GetDatabase(); var pooledTasksArray = ArrayPool.Shared.Rent(values.Length); @@ -251,7 +251,7 @@ public async Task SetMany(ReadOnlyMemory> values, Tim : null; var tasks = CreateTasks(); - + try { var waitForAllTasksTask = Task.WhenAll(tasks); @@ -262,16 +262,16 @@ public async Task SetMany(ReadOnlyMemory> values, Tim finally { ArrayPool.Shared.Return(pooledTasksArray); - + if (!(toDispose is null)) { for (var i = 0; i < values.Length; i++) toDispose[i]?.Dispose(); - + ArrayPool.Shared.Return(toDispose); } } - + IReadOnlyCollection CreateTasks() { var index = 0; @@ -288,7 +288,7 @@ IReadOnlyCollection CreateTasks() pooledTasksArray[index++] = redisDb.StringSetAsync(redisKey, redisValue, timeToLive, flags: _setValueFlags); } - + return new ArraySegment(pooledTasksArray, 0, index); } } @@ -296,13 +296,13 @@ IReadOnlyCollection CreateTasks() public async Task TryRemove(TKey key) { CheckDisposed(); - + var redisDb = GetDatabase(); - + var redisKey = _keySerializer(key); _recentlyRemovedKeysManager?.Mark(((string)redisKey).Substring(_keyPrefixLength)); - + return await redisDb .KeyDeleteAsync(redisKey) .ConfigureAwait(false); @@ -310,12 +310,12 @@ public async Task TryRemove(TKey key) public IObservable<(string Key, KeyEventType EventType)> KeysChangedRemotely => _keysChangedRemotely?.AsObservable() ?? Observable.Empty<(string, KeyEventType)>(); - + public void Dispose() { if (_disposed) return; - + _keysChangedRemotely?.Dispose(); _recentlySetKeysManager?.Dispose(); _recentlyRemovedKeysManager?.Dispose(); @@ -328,7 +328,7 @@ private void CheckDisposed() if (_disposed) throw new ObjectDisposedException(this.GetType().ToString()); } - + private void OnKeyChanged(string redisKey, KeyEventType eventType) { bool wasLocalChange; @@ -341,14 +341,14 @@ private void OnKeyChanged(string redisKey, KeyEventType eventType) if (wasLocalChange) return; - + _keysChangedRemotely.OnNext((redisKey, eventType)); } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] private IDatabase GetDatabase() => _connection.Get().GetDatabase(_dbIndex); } - + public sealed class RedisCache : IDistributedCache, IDisposable { private readonly IRedisConnection _connection; @@ -396,7 +396,7 @@ public RedisCache( _serializeValuesToStreams = true; } - + public RedisCache( IRedisConnection connection, Func outerKeySerializer, @@ -428,7 +428,7 @@ public RedisCache( _serializeValuesToStreams = false; } - + private RedisCache( IRedisConnection connection, Func outerKeySerializer, @@ -449,23 +449,23 @@ private RedisCache( _setValueFlags = useFireAndForgetWherePossible ? CommandFlags.FireAndForget : CommandFlags.None; _keySeparator = keySeparator; _keyPrefixLength = keyPrefix.ToString().Length; - + if (keyEventTypesToSubscribeTo != KeyEventType.None) { if (subscriber is null) throw new ArgumentNullException(nameof(subscriber)); - + _keysChangedRemotely = new Subject<(string, string, KeyEventType)>(); - + if (keyEventTypesToSubscribeTo.HasFlag(KeyEventType.Set)) _recentlySetKeysManager = new RecentlySetOrRemovedKeysManager(); - + _recentlyRemovedKeysManager = new RecentlySetOrRemovedKeysManager(); - + subscriber.SubscribeToKeyChanges(dbIndex, keyEventTypesToSubscribeTo, OnKeyChanged, keyPrefix); } } - + public async Task GetMany( TOuterKey outerKey, ReadOnlyMemory innerKeys, @@ -478,7 +478,7 @@ public async Task GetMany( var redisKeyPrefix = _outerKeySerializer(outerKey).Append(_keySeparator); var valuesFoundCount = 0; - + var tasks = CreateTasks(out var pooledTasksArray); try @@ -486,7 +486,7 @@ public async Task GetMany( await Task .WhenAll(tasks) .ConfigureAwait(false); - + return valuesFoundCount == 0 ? 0 : CopyResultsToDestinationArray(); @@ -499,7 +499,7 @@ await Task IReadOnlyCollection> CreateTasks(out Task<(TInnerKey, RedisValueWithExpiry)>[] pooledArray) { pooledArray = ArrayPool>.Shared.Rent(innerKeys.Length); - + var i = 0; foreach (var innerKey in innerKeys.Span) pooledArray[i++] = GetSingle(innerKey); @@ -510,7 +510,7 @@ await Task async Task<(TInnerKey, RedisValueWithExpiry)> GetSingle(TInnerKey innerKey) { var redisKey = redisKeyPrefix.Append(_innerKeySerializer(innerKey)); - + var fromRedis = await redisDb .StringGetWithExpiryAsync(redisKey) .ConfigureAwait(false); @@ -520,7 +520,7 @@ await Task return (innerKey, fromRedis); } - + int CopyResultsToDestinationArray() { var span = destination.Span; @@ -533,7 +533,7 @@ int CopyResultsToDestinationArray() continue; var value = _redisValueConverter.ConvertFromRedisValue(fromRedis.Value); - + span[index++] = new KeyValuePair>( key, new ValueAndTimeToLive(value, fromRedis.Expiry ?? TimeSpan.FromDays(1))); @@ -556,12 +556,12 @@ public async Task SetMany( var redisDb = GetDatabase(); var redisKeyPrefix = _outerKeySerializer(outerKey).Append(_keySeparator); - + var pooledTasksArray = ArrayPool.Shared.Rent(values.Length); var toDispose = _serializeValuesToStreams ? ArrayPool.Shared.Rent(values.Length) : null; - + var streamIndex = 0; try { @@ -575,12 +575,12 @@ public async Task SetMany( finally { ArrayPool.Shared.Return(pooledTasksArray); - + if (!(toDispose is null)) { for (var i = 0; i < streamIndex; i++) toDispose[i]?.Dispose(); - + ArrayPool.Shared.Return(toDispose); } } @@ -593,33 +593,33 @@ IReadOnlyCollection CreateTasks() var redisKey = redisKeyPrefix.Append(_innerKeySerializer(kv.Key)); if (redisKey == new RedisKey()) // Skip if the key is null continue; - + var redisValue = _redisValueConverter.ConvertToRedisValue(kv.Value, out var stream); if (!(toDispose is null)) toDispose[streamIndex++] = stream; _recentlySetKeysManager?.Mark(((string)redisKey).Substring(_keyPrefixLength)); - + pooledTasksArray[index++] = redisDb.StringSetAsync(redisKey, redisValue, timeToLive, flags: _setValueFlags); } - + return new ArraySegment(pooledTasksArray, 0, index); } } - + public async Task TryRemove(TOuterKey outerKey, TInnerKey innerKey) { CheckDisposed(); - + var redisDb = GetDatabase(); - + var redisKey = _outerKeySerializer(outerKey) .Append(_keySeparator) .Append(_innerKeySerializer(innerKey)); _recentlyRemovedKeysManager?.Mark(((string)redisKey).Substring(_keyPrefixLength)); - + return await redisDb .KeyDeleteAsync(redisKey) .ConfigureAwait(false); @@ -632,7 +632,7 @@ public void Dispose() { if (_disposed) return; - + _keysChangedRemotely?.Dispose(); _recentlySetKeysManager?.Dispose(); _recentlyRemovedKeysManager?.Dispose(); @@ -645,7 +645,7 @@ private void CheckDisposed() if (_disposed) throw new ObjectDisposedException(this.GetType().ToString()); } - + private void OnKeyChanged(string redisKey, KeyEventType eventType) { bool wasLocalChange; diff --git a/src/CacheMeIfYouCan.Redis/RedisTelemetryProcessor.cs b/src/CacheMeIfYouCan.Redis/RedisTelemetryProcessor.cs new file mode 100644 index 0000000..ec4e594 --- /dev/null +++ b/src/CacheMeIfYouCan.Redis/RedisTelemetryProcessor.cs @@ -0,0 +1,28 @@ +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Extensibility; + +namespace CacheMeIfYouCan.Redis +{ + internal class RedisTelemetryProcessor : ITelemetryProcessor + { + private readonly ITelemetryConfig _telemetryConfig; + private ITelemetryProcessor Next { get; } + + // next will point to the next RedisTelemetryProcessor in the chain. + public RedisTelemetryProcessor(ITelemetryProcessor next, ITelemetryConfig telemetryConfig) + { + _telemetryConfig = telemetryConfig; + Next = next; + } + + public void Process(ITelemetry item) + { + if (item is DependencyTelemetry request + && request.Duration.TotalMilliseconds > _telemetryConfig?.MillisecondThreshold) + { + Next.Process(item); + } + } + } +} diff --git a/src/CacheMeIfYouCan.Redis/TelemetryProcessor.cs b/src/CacheMeIfYouCan.Redis/TelemetryProcessor.cs new file mode 100644 index 0000000..82b20d2 --- /dev/null +++ b/src/CacheMeIfYouCan.Redis/TelemetryProcessor.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Extensibility; + +namespace CacheMeIfYouCan.Redis +{ + internal class TelemetryProcessor + { + private readonly ITelemetryProcessor _telemetryProcessor; + private readonly IDistributedCacheConfig _config; + + public TelemetryProcessor(IDistributedCacheConfig config, + ITelemetryProcessor telemetryProcessor, + ITelemetryConfig telemetryConfig) + { + _telemetryProcessor = new RedisTelemetryProcessor(telemetryProcessor, telemetryConfig); + _config = config; + } + + public void Add(TimeSpan duration, string command, + string keyOrKeys, bool successful) + { + _telemetryProcessor.Process( + new DependencyTelemetry( + _config.CacheType, + _config.Host, + _config.CacheName, + $"Command {command}{Environment.NewLine}{keyOrKeys}", + DateTime.UtcNow - duration, + duration, + "", successful)); + } + } +} \ No newline at end of file diff --git a/src/CacheMeIfYouCan/CacheMeIfYouCan.csproj b/src/CacheMeIfYouCan/CacheMeIfYouCan.csproj index 6a7b507..b66f475 100644 --- a/src/CacheMeIfYouCan/CacheMeIfYouCan.csproj +++ b/src/CacheMeIfYouCan/CacheMeIfYouCan.csproj @@ -6,12 +6,13 @@ High performance, highly configurable caching library + - - + + - + diff --git a/src/CacheMeIfYouCan/DistributedCacheEventsWrapperBase.cs b/src/CacheMeIfYouCan/DistributedCacheEventsWrapperBase.cs index 75e12c2..c0ece52 100644 --- a/src/CacheMeIfYouCan/DistributedCacheEventsWrapperBase.cs +++ b/src/CacheMeIfYouCan/DistributedCacheEventsWrapperBase.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading.Tasks; using CacheMeIfYouCan.Internal; diff --git a/src/CacheMeIfYouCan/IDistributedCache.cs b/src/CacheMeIfYouCan/IDistributedCache.cs index 1c2b646..9c69db5 100644 --- a/src/CacheMeIfYouCan/IDistributedCache.cs +++ b/src/CacheMeIfYouCan/IDistributedCache.cs @@ -67,5 +67,6 @@ public static Task Set( { return cache.SetMany(outerKey, new[] { new KeyValuePair(innerKey , value) }, timeToLive); } + } } \ No newline at end of file diff --git a/src/CacheMeIfYouCan/IDistributedCacheConfig.cs b/src/CacheMeIfYouCan/IDistributedCacheConfig.cs new file mode 100644 index 0000000..4acd04f --- /dev/null +++ b/src/CacheMeIfYouCan/IDistributedCacheConfig.cs @@ -0,0 +1,9 @@ +namespace CacheMeIfYouCan +{ + public interface IDistributedCacheConfig + { + public string CacheType { get; } + public string Host { get; } + public string CacheName { get; } + } +} \ No newline at end of file diff --git a/src/CacheMeIfYouCan/ITelemetryConfig.cs b/src/CacheMeIfYouCan/ITelemetryConfig.cs new file mode 100644 index 0000000..cdd87f7 --- /dev/null +++ b/src/CacheMeIfYouCan/ITelemetryConfig.cs @@ -0,0 +1,10 @@ +namespace CacheMeIfYouCan +{ + /// + /// Settings used to control what telemetry is collected + /// + public interface ITelemetryConfig + { + int MillisecondThreshold { get; } + } +} diff --git a/test.runsettings b/test.runsettings new file mode 100644 index 0000000..151feab --- /dev/null +++ b/test.runsettings @@ -0,0 +1,10 @@ + + + + + + 10.20.25.58 + + + + \ No newline at end of file diff --git a/tests/CacheMeIfYouCan.Redis.Tests/CacheMeIfYouCan.Redis.Tests.csproj b/tests/CacheMeIfYouCan.Redis.Tests/CacheMeIfYouCan.Redis.Tests.csproj index 252db2e..58c87c0 100644 --- a/tests/CacheMeIfYouCan.Redis.Tests/CacheMeIfYouCan.Redis.Tests.csproj +++ b/tests/CacheMeIfYouCan.Redis.Tests/CacheMeIfYouCan.Redis.Tests.csproj @@ -2,6 +2,7 @@ netcoreapp3.1 false + $(MSBuildProjectDirectory)\test.runsettings diff --git a/tests/CacheMeIfYouCan.Redis.Tests/DistributedCacheConfig.cs b/tests/CacheMeIfYouCan.Redis.Tests/DistributedCacheConfig.cs new file mode 100644 index 0000000..356bd19 --- /dev/null +++ b/tests/CacheMeIfYouCan.Redis.Tests/DistributedCacheConfig.cs @@ -0,0 +1,9 @@ +namespace CacheMeIfYouCan.Redis.Tests +{ + public class DistributedCacheConfig : IDistributedCacheConfig + { + public string CacheType { get; set; } + public string Host { get; set; } + public string CacheName { get; set; } + } +} \ No newline at end of file diff --git a/tests/CacheMeIfYouCan.Redis.Tests/MockTelemetry.cs b/tests/CacheMeIfYouCan.Redis.Tests/MockTelemetry.cs new file mode 100644 index 0000000..332dc8d --- /dev/null +++ b/tests/CacheMeIfYouCan.Redis.Tests/MockTelemetry.cs @@ -0,0 +1,13 @@ +using System; + +namespace CacheMeIfYouCan.Redis.Tests +{ + public class MockTelemetry + { + public string Host { get; set; } + public string Cache { get; set; } + public string Command { get; set; } + public DateTimeOffset Start { get; set; } + public TimeSpan Duration { get; set; } + } +} \ No newline at end of file diff --git a/tests/CacheMeIfYouCan.Redis.Tests/RedisCacheTests1.cs b/tests/CacheMeIfYouCan.Redis.Tests/RedisCacheTests1.cs index c777d38..7fa4c7b 100644 --- a/tests/CacheMeIfYouCan.Redis.Tests/RedisCacheTests1.cs +++ b/tests/CacheMeIfYouCan.Redis.Tests/RedisCacheTests1.cs @@ -336,7 +336,60 @@ await cache4 keysChangedRemotely1.OrderBy(t => t.Item1).ThenBy(t => t.Item2).Should().BeEquivalentTo(expectedEvents1); keysChangedRemotely2.OrderBy(t => t.Item1).ThenBy(t => t.Item2).Should().BeEquivalentTo(expectedEvents2); } - + + [Theory] + [InlineData(0, true)] + [InlineData(1000, false)] + public async Task TestTelemetryOnCommand(int threshold, bool anyTrace) + { + using var connection = BuildConnection(); + + var cacheConfig = new DistributedCacheConfig + { + CacheName = "Test1", + CacheType = "Redis", + Host = "Here" + }; + var mockTelemetryConfig = new TelemetryConfig {MillisecondThreshold = threshold}; + var mockTelemetry = new TelemetryProcessor(); + var cache = BuildDistributedCache(connection, useSerializer: false) + .WithApplicationInsightsTelemetry(cacheConfig, mockTelemetry, mockTelemetryConfig); + + const int elementsToCache = 10; + + var keys = Enumerable + .Range(1, elementsToCache) + .ToArray(); + + var values = keys + .Select(i => new KeyValuePair(i, new TestClass(i))) + .ToArray(); + + await cache.SetMany(values, TimeSpan.FromSeconds(5)).ConfigureAwait(false); + + Task.WaitAll(); + + var trace = mockTelemetry.GetTrace(); + + trace.Should().NotBeNull(); + + if (anyTrace) + { + trace.Should().NotBeEmpty(); + trace.Count.Should().Be(1); + var commandText = trace.First().Command; + commandText.Should().Contain("StringSetAsync"); + commandText.Should().Contain("Keys"); + commandText.Split("Keys ").Length.Should().Be(2); + var actualKeys = commandText.Split("Keys ")[1].Replace("'", ""); + actualKeys.Should().Contain(","); + var actualKeyList = actualKeys.Split(","); + actualKeyList.Length.Should().Be(elementsToCache); + actualKeyList.First().Should().Be("1"); + actualKeyList.Last().Should().Be("10"); + } + } + private static RedisConnection BuildConnection() => new RedisConnection(TestConnectionString.Value); private static RedisCache BuildRedisCache( @@ -371,5 +424,38 @@ private static RedisCache BuildRedisCache( subscriber: connection, keyEventTypesToSubscribeTo: keyEventTypesToSubscribeTo); } + + private static IDistributedCache BuildDistributedCache( + RedisConnection connection, + bool useFireAndForget = false, + string keyPrefix = null, + string nullValue = null, + bool useSerializer = false, + KeyEventType keyEventTypesToSubscribeTo = KeyEventType.None) + { + if (useSerializer) + { + return new RedisCache( + connection, + k => k.ToString(), + new ProtoBufSerializer(), + useFireAndForgetWherePossible: useFireAndForget, + keyPrefix: keyPrefix ?? Guid.NewGuid().ToString(), + nullValue: nullValue, + subscriber: connection, + keyEventTypesToSubscribeTo: keyEventTypesToSubscribeTo); + } + + return new RedisCache( + connection, + k => k.ToString(), + v => v.ToString(), + v => TestClass.Parse(v), + useFireAndForgetWherePossible: useFireAndForget, + keyPrefix: keyPrefix ?? Guid.NewGuid().ToString(), + nullValue: nullValue, + subscriber: connection, + keyEventTypesToSubscribeTo: keyEventTypesToSubscribeTo); + } } } \ No newline at end of file diff --git a/tests/CacheMeIfYouCan.Redis.Tests/RedisCacheTests2.cs b/tests/CacheMeIfYouCan.Redis.Tests/RedisCacheTests2.cs index b711af1..1788f65 100644 --- a/tests/CacheMeIfYouCan.Redis.Tests/RedisCacheTests2.cs +++ b/tests/CacheMeIfYouCan.Redis.Tests/RedisCacheTests2.cs @@ -249,7 +249,59 @@ await cache4 keysChangedRemotely1.OrderBy(t => t.Item1).ThenBy(t => t.Item2).Should().BeEquivalentTo(expectedEvents1); keysChangedRemotely2.OrderBy(t => t.Item1).ThenBy(t => t.Item2).Should().BeEquivalentTo(expectedEvents2); } - + + [Theory] + [InlineData(0, true)] + [InlineData(1000, false)] + public async Task TestTelemetryOnCommand(int threshold, bool anyTrace) + { + using var connection = BuildConnection(); + + var cacheConfig = new DistributedCacheConfig + { + CacheName = "Test2", + CacheType = "Redis", + Host = "Here" + }; + var mockTelemetryConfig = new TelemetryConfig { MillisecondThreshold = threshold }; + var mockTelemetry = new TelemetryProcessor(); + var cache = BuildDistributedCache(connection, useSerializer: false) + .WithApplicationInsightsTelemetry(cacheConfig, mockTelemetry, mockTelemetryConfig); + + const int elementsToCache = 10; + + var keys = Enumerable.Range(1, elementsToCache).ToArray(); + var values = keys + .Where(k => k % 2 == 0) + .Select(i => new KeyValuePair(i, new TestClass(i))) + .ToArray(); + + await cache.SetMany(1, values, TimeSpan.FromSeconds(1)); + + Task.WaitAll(); + + var trace = mockTelemetry.GetTrace(); + + trace.Should().NotBeNull(); + + if (anyTrace) + { + trace.Should().NotBeEmpty(); + trace.Count.Should().Be(1); + var commandText = trace.First().Command; + commandText.Should().Contain("StringSetAsync"); + commandText.Should().Contain("Keys"); + commandText.Split("Keys ").Length.Should().Be(2); + var actualKeys = commandText.Split("Keys ")[1].Replace("'", ""); + actualKeys.Should().Contain(","); + var actualKeyList = actualKeys.Split(","); + actualKeyList.Length.Should().Be(elementsToCache / 2); + var firstKey = actualKeyList.First(); + firstKey.Should().NotBeNullOrEmpty(); + firstKey.Should().Be("1.2"); + } + } + private static RedisConnection BuildConnection() => new RedisConnection(TestConnectionString.Value); private static RedisCache BuildRedisCache( @@ -288,5 +340,42 @@ private static RedisCache BuildRedisCache( subscriber: connection, keyEventTypesToSubscribeTo: keyEventTypesToSubscribeTo); } + + private static IDistributedCache BuildDistributedCache( + RedisConnection connection, + bool useFireAndForget = false, + string keyPrefix = null, + string nullValue = null, + bool useSerializer = false, + KeyEventType keyEventTypesToSubscribeTo = KeyEventType.None) + { + if (useSerializer) + { + return new RedisCache( + connection, + k => k.ToString(), + k => k.ToString(), + new ProtoBufSerializer(), + useFireAndForgetWherePossible: useFireAndForget, + keySeparator: "_", + keyPrefix: keyPrefix ?? Guid.NewGuid().ToString(), + nullValue: nullValue, + subscriber: connection, + keyEventTypesToSubscribeTo: keyEventTypesToSubscribeTo); + } + + return new RedisCache( + connection, + k => k.ToString(), + k => k.ToString(), + v => v.ToString(), + v => TestClass.Parse(v), + useFireAndForgetWherePossible: useFireAndForget, + keySeparator: "_", + keyPrefix: keyPrefix ?? Guid.NewGuid().ToString(), + nullValue: nullValue, + subscriber: connection, + keyEventTypesToSubscribeTo: keyEventTypesToSubscribeTo); + } } } \ No newline at end of file diff --git a/tests/CacheMeIfYouCan.Redis.Tests/TelemetryConfig.cs b/tests/CacheMeIfYouCan.Redis.Tests/TelemetryConfig.cs new file mode 100644 index 0000000..5d9b151 --- /dev/null +++ b/tests/CacheMeIfYouCan.Redis.Tests/TelemetryConfig.cs @@ -0,0 +1,7 @@ +namespace CacheMeIfYouCan.Redis.Tests +{ + public class TelemetryConfig : ITelemetryConfig + { + public int MillisecondThreshold { get; set; } + } +} diff --git a/tests/CacheMeIfYouCan.Redis.Tests/TelemetryProcessor.cs b/tests/CacheMeIfYouCan.Redis.Tests/TelemetryProcessor.cs new file mode 100644 index 0000000..2f4afdb --- /dev/null +++ b/tests/CacheMeIfYouCan.Redis.Tests/TelemetryProcessor.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Extensibility; + +namespace CacheMeIfYouCan.Redis.Tests +{ + public class TelemetryProcessor : ITelemetryProcessor + { + private readonly List _telemetry = new List(); + + public void Process(ITelemetry item) + { + var data = item as DependencyTelemetry; + _telemetry.Add(new + MockTelemetry + { + Host = data?.Target, + Cache = data?.Name, + Command = data?.Data, + Start = data?.Timestamp ?? DateTimeOffset.MinValue, + Duration = data?.Duration ?? TimeSpan.Zero + }); + } + + public List GetTrace() + { + return _telemetry; + } + } +} \ No newline at end of file