JoeShook.ZiggyCreatures.FusionCache.Metrics.EventCounters 1.0.11

dotnet add package JoeShook.ZiggyCreatures.FusionCache.Metrics.EventCounters --version 1.0.11                
NuGet\Install-Package JoeShook.ZiggyCreatures.FusionCache.Metrics.EventCounters -Version 1.0.11                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="JoeShook.ZiggyCreatures.FusionCache.Metrics.EventCounters" Version="1.0.11" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add JoeShook.ZiggyCreatures.FusionCache.Metrics.EventCounters --version 1.0.11                
#r "nuget: JoeShook.ZiggyCreatures.FusionCache.Metrics.EventCounters, 1.0.11"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install JoeShook.ZiggyCreatures.FusionCache.Metrics.EventCounters as a Cake Addin
#addin nuget:?package=JoeShook.ZiggyCreatures.FusionCache.Metrics.EventCounters&version=1.0.11

// Install JoeShook.ZiggyCreatures.FusionCache.Metrics.EventCounters as a Cake Tool
#tool nuget:?package=JoeShook.ZiggyCreatures.FusionCache.Metrics.EventCounters&version=1.0.11                

FusionCache.EventCounters

FusionCache logo

License: MIT Twitter badge

FusionCache.EventCounters is a plugin to capture caching metrics using FusionCache

Metrics are missing from open-source resiliency projects in the .NET ecosystem where in equivalent Java libraries, metrics tend to be common. FusionCache is a feature rich caching library addressing resiliency needs of today’s enterprise implementations. EventCounters is a lightweight .NET Core API library that works in .NET Core. Joining these two excellent libraries together you can easily be caching and writing metrics to your favorite timeseries database or use the dotnet-counters tool to monitor from the console.

Usage

Setup option 1

Notes: MemoryCache is created outside of AddFusionCache extenstion method so it can be passed to FusionCacheEvenSource. This is required if you want the cache count reportable


    var memoryCache = new MemoryCache(new MemoryCacheOptions());
    services.AddSingleton<IMemoryCache>(memoryCache);
    services.AddSingleton<IFusionCachePlugin>(new FusionCacheEventSource("domain", memoryCache));
    services.AddFusionCache(options =>
        options.DefaultEntryOptions = new FusionCacheEntryOptions
            {
                Duration = TimeSpan.FromSeconds(60)
            }
            .SetFailSafe(true, TimeSpan.FromHours(1), TimeSpan.FromSeconds(5))
            .SetFactoryTimeouts(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1))
        );

Setup option 2 or in addtion to 1 if you have multiple caches


    var memoryCache = new MemoryCache(new MemoryCacheOptions());
    
    services.AddSingleton<IEmailService>(serviceProvider =>
    {
        var logger = serviceProvider.GetService<ILogger<ZiggyCreatures.Caching.Fusion.FusionCache>>();

        var fusionCacheOptions = new FusionCacheOptions
        {
            DefaultEntryOptions = new FusionCacheEntryOptions
                {
                    Duration = TimeSpan.FromSeconds(1),
                    JitterMaxDuration = TimeSpan.FromMilliseconds(200)
                }
                .SetFailSafe(true, TimeSpan.FromHours(1), TimeSpan.FromSeconds(1))
                .SetFactoryTimeouts(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1))
        };

        var metrics = new FusionCacheEventSource("email", memoryCache);
        var fusionCache = new ZiggyCreatures.Caching.Fusion.FusionCache(fusionCacheOptions, memoryCache, logger);
        metrics.Start(fusionCache);

        return new EmailService(serviceProvider.GetRequiredService<DataManager>(), fusionCache);
    });

Events:: Incrementing Polling Counters for Hits and Misses

The following counters are all IncrementingPollingCounters which tracks based on a time interval. EventListeners will get a value based on the difference between the current invocation and the last invocation. Read EventCounter API overview to get a better understanding of the various EventCounter implementations.

CacheHit()

The cache tag is "HIT". Every cache hit will increment a counter.

CacheMiss()

The cache tag is "MISS". Every cache miss will increment a counter.

CacheStaleHit()

The cache tag is "STALE_HIT". When FailSafe is enabled and a request times out due to a "soft timeout" and a stale cache item exists then increment a counter. Note this will not trigger the CacheMiss() counter.

CacheBackgroundRefresh()

The cache tag is "STALE_REFRESH". When FailSafe is enabled and a request times out due to a "soft timeout" the request will continue for the length of a "hard timeout". If the request finds data it will call this CacheBackgroundRefresh() and increment a counter. Note it would be normal for this counter and CacheStaleHit() to track with each other.

CacheRemoved()

The cache tag is "REMOVE". When the EvictionReason is Replaced increment a counter.

Incrementing Polling Counter for Evictions

Eviction counters are wired into the ICacheEntries with the PostEvictionDelegate.

CacheExpired

The cache tag is "EXPIRE". When the EvictionReason is Expired increment a counter.

CacheCapacityExpired()

The cache tag is "CAPACITY". When the EvictionReason is Capacity increment a counter.

Reporting

In addition to implementing a EventListener as mentioned previously one can also monitor the events from the command line.

dotnet-counters can listen to the metrics above. Example command line for a example.exe application

dotnet-counters monitor -n example --counters domainCache

Example output would look like the following [domainCache] Cache Background Refresh (Count / 1 sec) 0 Cache Capacity Eviction (Count / 1 sec) 0 Cache Expired Eviction (Count / 1 sec) 5 Cache Hits (Count / 1 sec) 221 Cache Misses (Count / 1 sec) 0 Cache Removed (Count / 1 sec) 0 Cache Size 1,157 Cache Stale Hit (Count / 1 sec) 5

To make reporting seemless implent a EventListener and run it as a HostedService.

Listenting for events

See this EventListener in action in the example project EventCountersPluginExampleDotNetCore


    public class MetricsListenerService : EventListener, IHostedService
    {
        private List<string> RegisteredEventSources = new List<string>();
        private Task _dataSource;
        private InfluxDBClient _influxDBClient;
        private MetricsConfig _metricsConfig;
        private readonly string _measurementName;
        private readonly ISemanticConventions _conventions;
        private readonly IInfluxCloudConfig _influxCloudConfig;

        public MetricsListenerService(
            InfluxDBClient influxDBClient, 
            MetricsConfig metricsConfig, 
            ISemanticConventions conventions = null,
            IInfluxCloudConfig influxCloudConfig = null)
        {
            _influxDBClient = influxDBClient ?? throw new ArgumentNullException(nameof(influxDBClient));
            _metricsConfig = metricsConfig ?? throw  new ArgumentNullException(nameof(metricsConfig));
            _conventions = conventions ?? new SemanticConventions();
            _influxCloudConfig = influxCloudConfig;

            _measurementName = $"{metricsConfig.Prefix}{metricsConfig.ApplicationName}_{metricsConfig.MeasurementName}";
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            _dataSource = Task.Run(async () =>
            {
                while (true)
                {
                    GetNewSources();
                }
            }, cancellationToken);

            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }

        private void GetNewSources()
        {
            foreach (var eventSource in EventSource.GetSources())
            {
                OnEventSourceCreated(eventSource);
            }
        }

        protected override void OnEventSourceCreated(EventSource eventSource)
        {
            if (!RegisteredEventSources.Contains(eventSource.Name))
            {
                RegisteredEventSources.Add(eventSource.Name);
                EnableEvents(eventSource, EventLevel.LogAlways, EventKeywords.All, new Dictionary<string, string>
                {
                    ["EventCounterIntervalSec"] = "5"
                });
            }
        }

        protected override void OnEventWritten(EventWrittenEventArgs eventData)
        {
            if (!eventData.EventSource.Name.Equals("email") &&
                !eventData.EventSource.Name.Equals("domain"))
            {
                return;
            }

            List<PointData> pointData = null;
            var time = DateTime.UtcNow;

            for (int i = 0; i < eventData.Payload?.Count; ++i)
            {
                if (eventData.Payload[i] is IDictionary<string, object> eventPayload)
                {
                    pointData ??= new List<PointData>();
                    var (cacheName, counterName, counterValue) = GetMeasurement(eventPayload);

                    var point = PointData
                        .Measurement(_measurementName)
                        .Field(_conventions.ValueFieldName, counterValue)
                        .Tag(_conventions.ApplicationTagName, _metricsConfig.ApplicationName)
                        .Tag(_conventions.ApplicationVersionTagName, _metricsConfig.ApplicationVersion)
                        .Tag(_conventions.CacheNameTagName, cacheName)
                        .Tag(_conventions.CacheEventTagName, counterName)
                        .Timestamp(time, WritePrecision.S);

                    pointData.Add(point);
                }
            }

            if (pointData != null && pointData.Any())
            {
                WriteData(pointData);
            }
        }

        public virtual void WriteData(List<PointData> pointData)
        {
            using (var writeApi = _influxDBClient.GetWriteApi())
            {
                if (_influxCloudConfig != null)
                {
                    writeApi.WritePoints(_influxCloudConfig.Bucket, _influxCloudConfig.Organization, pointData);
                }
                else
                {
                    writeApi.WritePoints(pointData);
                }
            }
        }

        private (string cacheName, string counterName, long counterValue) GetMeasurement(
            IDictionary<string, object> eventPayload)
        {
            var cacheName = "";
            var counterName = "";
            long counterValue = 0;

            if (eventPayload.TryGetValue("Metadata", out object metaDataValue))
            {
                var metaDataString = Convert.ToString(metaDataValue);
                var metaData = metaDataString
                    .Split(',')
                    .Select(item => item.Split(':'))
                    .ToDictionary(s => s[0], s => s[1]);

                cacheName = metaData[_conventions.CacheNameTagName];
            }

            if (eventPayload.TryGetValue("Name", out object displayValue))
            {
                counterName = displayValue.ToString();
            }

            if (eventPayload.TryGetValue("Increment", out object incrementingPollingCounterValue))
            {
                counterValue = Convert.ToInt64(incrementingPollingCounterValue);
            }
            else if (eventPayload.TryGetValue("Mean", out object pollingCounterValue))
            {
                counterValue = Convert.ToInt64(pollingCounterValue);
            }


            return (cacheName, counterName, counterValue);
        }
    }

Reporting on metrics

The listener example writes metrics to the console Influx database or Influx Cloud. It is very easy to setup Influx Cloud and start experimenting. Below is an pulled from the example project EventCountersPluginExampleDotNetCore


    services.AddSingleton(this.Configuration.GetSection("CacheMetrics").Get<MetricsConfig>());

    switch (this.Configuration.GetValue<string>("UseInflux").ToLowerInvariant())
    {
        case "cloud":
            services.AddSingleton(sp =>
                InfluxDBClientFactory.Create(
                    Configuration["InfluxCloudConfig.Url"],
                    Configuration["InfluxCloudConfig.Token"].ToCharArray()));
            services.AddHostedService<MetricsListenerService>();

            services.AddSingleton<IInfluxCloudConfig>(new InfluxCloudConfig
            {
                Bucket = Configuration["InfluxCloudConfig.Bucket"],
                Organization = Configuration["InfluxCloudConfig.Organization"]
            });
            
            break;

        case "db":
            services.AddSingleton(sp =>
                InfluxDBClientFactory.CreateV1(
                    $"http://{Configuration["InfluxDbConfig.Host"]}:{Configuration["InfluxDbConfig.Port"]}",
                    Configuration["InfluxDbConfig.Username"],
                    Configuration["InfluxDbConfig.Password"].ToCharArray(),
                    Configuration["InfluxDbConfig.Database"],
                    Configuration["InfluxDbConfig.RetentionPolicy"]));
            services.AddHostedService<MetricsListenerService>();

            break;

        default:
            services.AddSingleton(sp =>
                InfluxDBClientFactory.Create(
                    $"http://localhost",
                    "nullToken".ToCharArray()));
            services.AddHostedService<ConsoleMetricsListenter>();

            break;
    }
       
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.11 366 10/17/2023
1.0.7 168 7/27/2023
1.0.6 413 3/28/2023
1.0.5 320 11/25/2022
1.0.4 462 5/2/2022
1.0.3 424 2/20/2022
1.0.3-preview001 487 12/6/2021
1.0.2 1,024 12/1/2021
1.0.0 341 7/26/2021
1.0.0-preview003 223 7/23/2021
1.0.0-preview002 231 7/22/2021
1.0.0-preview001 195 7/20/2021
1.0.0-preview000 180 7/20/2021

Update to FusionCache v0.10