From 46a0faa5c9bb5a4495855539be95c1c6ad124dcf Mon Sep 17 00:00:00 2001 From: mokarchi Date: Sun, 28 Sep 2025 19:05:43 +0330 Subject: [PATCH 1/6] Add tag-aware metrics to HybridCache for richer telemetry Enhanced the HybridCache system with support for tag-aware metrics, controlled via the `ReportTagMetrics` option. This enables emitting detailed telemetry for cache operations, including tag dimensions, using `System.Diagnostics.Metrics`. - Added counters for cache hits, misses, writes, and tag invalidations with optional tag dimensions. - Updated `HybridCacheOptions` with detailed documentation on `ReportTagMetrics`, including security and PII guidance. - Introduced methods in `HybridCacheEventSource` for emitting tag-aware metrics conditionally. - Ensured high-cardinality tags are avoided to prevent performance issues in metrics systems. - Added integration and unit tests to validate behavior with and without tags. - Maintained backward compatibility by ensuring metrics are emitted without tag dimensions when `ReportTagMetrics` is disabled. - Improved observability by allowing metrics categorization by tags like "region", "service", and "environment". This update provides developers with a powerful tool for analyzing cache performance by tag categories, enabling better insights and decision-making. --- .../HybridCacheOptions.cs | 33 +- .../Internal/DefaultHybridCache.L2.cs | 4 +- .../DefaultHybridCache.StampedeStateT.cs | 12 +- .../DefaultHybridCache.TagInvalidation.cs | 4 +- .../Internal/DefaultHybridCache.cs | 9 +- .../Internal/HybridCacheEventSource.cs | 316 ++++++++++++++++++ .../HybridCacheEventSourceTests.cs | 75 +++++ ...oft.Extensions.Caching.Hybrid.Tests.csproj | 1 + .../ReportTagMetricsIntegrationTests.cs | 138 ++++++++ .../ReportTagMetricsTests.cs | 184 ++++++++++ 10 files changed, 759 insertions(+), 17 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsIntegrationTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsTests.cs diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs index d55ac1a4ea1..f408f71b32e 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs @@ -53,8 +53,37 @@ public class HybridCacheOptions /// to use "tags" data as dimensions on metric reporting; otherwise, . /// /// - /// If enabled, take care to ensure that tags don't contain data that - /// should not be visible in metrics systems. + /// + /// When enabled, cache operations will emit System.Diagnostics.Metrics with tag values as dimensions, + /// providing richer telemetry for cache performance analysis by tag categories. + /// + /// + /// Important PII and Security Considerations: + /// + /// + /// + /// Ensure that tag values do not contain personally identifiable information (PII), + /// sensitive data, or high-cardinality values that could overwhelm metrics systems. + /// + /// + /// Tag values will be visible in metrics collection systems, dashboards, and telemetry pipelines. + /// Only use tags that are safe for observability purposes. + /// + /// + /// Consider using categorical values like "region", "service", "environment" rather than + /// user-specific identifiers or sensitive business data. + /// + /// + /// High-cardinality tags (e.g., user IDs, session IDs) can cause performance issues + /// in metrics systems and should be avoided. + /// + /// + /// + /// Example of appropriate tags: ["region:us-west", "service:api", "environment:prod"] + /// + /// + /// Example of inappropriate tags: ["user:john.doe@company.com", "session:abc123", "customer-data:sensitive"] + /// /// public bool ReportTagMetrics { get; set; } diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.L2.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.L2.cs index 4293d54bc30..a5929deffc3 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.L2.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.L2.cs @@ -211,9 +211,9 @@ internal void SetL1(string key, CacheItem value, HybridCacheEntryOptions? // commit cacheEntry.Dispose(); - if (HybridCacheEventSource.Log.IsEnabled()) + if (HybridCacheEventSource.Log.IsEnabled() || _options.ReportTagMetrics) { - HybridCacheEventSource.Log.LocalCacheWrite(); + HybridCacheEventSource.Log.LocalCacheWriteWithTags(value.Tags, _options.ReportTagMetrics); } } } diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.StampedeStateT.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.StampedeStateT.cs index 34a68ef30aa..31e5fa83da6 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.StampedeStateT.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.StampedeStateT.cs @@ -5,11 +5,9 @@ using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using static Microsoft.Extensions.Caching.Hybrid.Internal.DefaultHybridCache; namespace Microsoft.Extensions.Caching.Hybrid.Internal; @@ -188,15 +186,15 @@ private async Task BackgroundFetchAsync() } result = await Cache.GetFromL2DirectAsync(Key.Key, SharedToken).ConfigureAwait(false); - if (eventSourceEnabled) + if (eventSourceEnabled || Cache._options.ReportTagMetrics) { if (result.HasValue) { - HybridCacheEventSource.Log.DistributedCacheHit(); + HybridCacheEventSource.Log.DistributedCacheHitWithTags(CacheItem.Tags, Cache._options.ReportTagMetrics); } else { - HybridCacheEventSource.Log.DistributedCacheMiss(); + HybridCacheEventSource.Log.DistributedCacheMissWithTags(CacheItem.Tags, Cache._options.ReportTagMetrics); } } } @@ -367,9 +365,9 @@ private async Task BackgroundFetchAsync() { await Cache.SetL2Async(Key.Key, cacheItem, in buffer, _options, SharedToken).ConfigureAwait(false); - if (eventSourceEnabled) + if (eventSourceEnabled || Cache._options.ReportTagMetrics) { - HybridCacheEventSource.Log.DistributedCacheWrite(); + HybridCacheEventSource.Log.DistributedCacheWriteWithTags(CacheItem.Tags, Cache._options.ReportTagMetrics); } } catch (Exception ex) diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.TagInvalidation.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.TagInvalidation.cs index ef5b7f1a01a..0ca54e2e26e 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.TagInvalidation.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.TagInvalidation.cs @@ -254,9 +254,9 @@ private void InvalidateTagLocalCore(string tag, long timestamp, bool isNow) { _tagInvalidationTimes[tag] = timestampTask; - if (HybridCacheEventSource.Log.IsEnabled()) + if (HybridCacheEventSource.Log.IsEnabled() || _options.ReportTagMetrics) { - HybridCacheEventSource.Log.TagInvalidated(); + HybridCacheEventSource.Log.TagInvalidatedWithTags(tag, _options.ReportTagMetrics); } } } diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs index 93e1e5457cb..0ba31caa5a8 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs @@ -152,6 +152,7 @@ public override ValueTask GetOrCreateAsync(string key, TState stat } bool eventSourceEnabled = HybridCacheEventSource.Log.IsEnabled(); + TagSet tagSet = TagSet.Create(tags); if ((flags & HybridCacheEntryFlags.DisableLocalCacheRead) == 0) { @@ -159,18 +160,18 @@ public override ValueTask GetOrCreateAsync(string key, TState stat && typed.TryGetValue(_logger, out T? value)) { // short-circuit - if (eventSourceEnabled) + if (eventSourceEnabled || _options.ReportTagMetrics) { - HybridCacheEventSource.Log.LocalCacheHit(); + HybridCacheEventSource.Log.LocalCacheHitWithTags(tagSet, _options.ReportTagMetrics); } return new(value); } else { - if (eventSourceEnabled) + if (eventSourceEnabled || _options.ReportTagMetrics) { - HybridCacheEventSource.Log.LocalCacheMiss(); + HybridCacheEventSource.Log.LocalCacheMissWithTags(tagSet, _options.ReportTagMetrics); } } } diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs index 412f713034f..f844629e50e 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Diagnostics.Tracing; using System.Runtime.CompilerServices; using System.Threading; @@ -13,6 +15,16 @@ internal sealed class HybridCacheEventSource : EventSource { public static readonly HybridCacheEventSource Log = new(); + // System.Diagnostics.Metrics instruments for tag-aware metrics + private static readonly Meter _sMeter = new("Microsoft.Extensions.Caching.Hybrid"); + private static readonly Counter _sLocalCacheHits = _sMeter.CreateCounter("hybrid_cache.local.hits", description: "Total number of local cache hits"); + private static readonly Counter _sLocalCacheMisses = _sMeter.CreateCounter("hybrid_cache.local.misses", description: "Total number of local cache misses"); + private static readonly Counter _sDistributedCacheHits = _sMeter.CreateCounter("hybrid_cache.distributed.hits", description: "Total number of distributed cache hits"); + private static readonly Counter _sDistributedCacheMisses = _sMeter.CreateCounter("hybrid_cache.distributed.misses", description: "Total number of distributed cache misses"); + private static readonly Counter _sLocalCacheWrites = _sMeter.CreateCounter("hybrid_cache.local.writes", description: "Total number of local cache writes"); + private static readonly Counter _sDistributedCacheWrites = _sMeter.CreateCounter("hybrid_cache.distributed.writes", description: "Total number of distributed cache writes"); + private static readonly Counter _sTagInvalidations = _sMeter.CreateCounter("hybrid_cache.tag.invalidations", description: "Total number of tag invalidations"); + internal const int EventIdLocalCacheHit = 1; internal const int EventIdLocalCacheMiss = 2; internal const int EventIdDistributedCacheGet = 3; @@ -196,6 +208,222 @@ internal void TagInvalidated() WriteEvent(EventIdTagInvalidated); } + /// + /// Reports a local cache hit with optional tag dimensions for System.Diagnostics.Metrics. + /// + /// The cache entry tags to include as metric dimensions. + /// Whether to emit tag dimensions in System.Diagnostics.Metrics. + [NonEvent] + public void LocalCacheHitWithTags(TagSet tags, bool reportTagMetrics) + { + if (IsEnabled()) + LocalCacheHit(); // Emit EventSource event + + // Also emit metrics when requested + if (reportTagMetrics) + { + if (tags.Count > 0) + EmitLocalCacheHitMetric(tags); + else + _sLocalCacheHits.Add(1); + } + } + + /// + /// Reports a local cache miss with optional tag dimensions for System.Diagnostics.Metrics. + /// + /// The cache entry tags to include as metric dimensions. + /// Whether to emit tag dimensions in System.Diagnostics.Metrics. + [NonEvent] + public void LocalCacheMissWithTags(TagSet tags, bool reportTagMetrics) + { + if (IsEnabled()) + LocalCacheMiss(); // Emit EventSource event + + // Also emit metrics when requested + if (reportTagMetrics) + { + if (tags.Count > 0) + EmitLocalCacheMissMetric(tags); + else + _sLocalCacheMisses.Add(1); + } + } + + /// + /// Reports a distributed cache hit with optional tag dimensions for System.Diagnostics.Metrics. + /// + /// The cache entry tags to include as metric dimensions. + /// Whether to emit tag dimensions in System.Diagnostics.Metrics. + [NonEvent] + public void DistributedCacheHitWithTags(TagSet tags, bool reportTagMetrics) + { + if (IsEnabled()) + DistributedCacheHit(); // Emit EventSource event + + // Also emit metrics when requested + if (reportTagMetrics) + { + if (tags.Count > 0) + EmitDistributedCacheHitMetric(tags); + else + _sDistributedCacheHits.Add(1); + } + } + + /// + /// Reports a distributed cache miss with optional tag dimensions for System.Diagnostics.Metrics. + /// + /// The cache entry tags to include as metric dimensions. + /// Whether to emit tag dimensions in System.Diagnostics.Metrics. + [NonEvent] + public void DistributedCacheMissWithTags(TagSet tags, bool reportTagMetrics) + { + if (IsEnabled()) + DistributedCacheMiss(); // Emit EventSource event + + // Also emit metrics when requested + if (reportTagMetrics) + { + if (tags.Count > 0) + EmitDistributedCacheMissMetric(tags); + else + _sDistributedCacheMisses.Add(1); + } + } + + /// + /// Reports a local cache write with optional tag dimensions for System.Diagnostics.Metrics. + /// + /// The cache entry tags to include as metric dimensions. + /// Whether to emit tag dimensions in System.Diagnostics.Metrics. + [NonEvent] + public void LocalCacheWriteWithTags(TagSet tags, bool reportTagMetrics) + { + if (IsEnabled()) + LocalCacheWrite(); // Emit EventSource event + + // Also emit metrics when requested + if (reportTagMetrics) + { + if (tags.Count > 0) + EmitLocalCacheWriteMetric(tags); + else + _sLocalCacheWrites.Add(1); + } + } + + /// + /// Reports a distributed cache write with optional tag dimensions for System.Diagnostics.Metrics. + /// + /// The cache entry tags to include as metric dimensions. + /// Whether to emit tag dimensions in System.Diagnostics.Metrics. + [NonEvent] + public void DistributedCacheWriteWithTags(TagSet tags, bool reportTagMetrics) + { + if (IsEnabled()) + DistributedCacheWrite(); // Emit EventSource event + + // Also emit metrics when requested + if (reportTagMetrics) + { + if (tags.Count > 0) + EmitDistributedCacheWriteMetric(tags); + else + _sDistributedCacheWrites.Add(1); + } + } + + /// + /// Reports a tag invalidation with optional tag dimensions for System.Diagnostics.Metrics. + /// + /// The specific tag that was invalidated. + /// Whether to emit tag dimensions in System.Diagnostics.Metrics. + [NonEvent] + public void TagInvalidatedWithTags(string tag, bool reportTagMetrics) + { + if (IsEnabled()) + TagInvalidated(); // Emit EventSource event + + // Also emit metrics when requested + if (reportTagMetrics) + { + _sTagInvalidations.Add(1, new KeyValuePair("tag", tag)); + } + } + + /// + /// Emits a local cache hit metric with tag dimensions. + /// + /// The tags to include as metric dimensions. + [NonEvent] + private static void EmitLocalCacheHitMetric(TagSet tags) + { + var tagList = CreateTagList(tags); + _sLocalCacheHits.Add(1, tagList); + } + + [NonEvent] + private static void EmitLocalCacheMissMetric(TagSet tags) + { + var tagList = CreateTagList(tags); + _sLocalCacheMisses.Add(1, tagList); + } + + [NonEvent] + private static void EmitDistributedCacheHitMetric(TagSet tags) + { + var tagList = CreateTagList(tags); + _sDistributedCacheHits.Add(1, tagList); + } + + [NonEvent] + private static void EmitDistributedCacheMissMetric(TagSet tags) + { + var tagList = CreateTagList(tags); + _sDistributedCacheMisses.Add(1, tagList); + } + + [NonEvent] + private static void EmitLocalCacheWriteMetric(TagSet tags) + { + var tagList = CreateTagList(tags); + _sLocalCacheWrites.Add(1, tagList); + } + + [NonEvent] + private static void EmitDistributedCacheWriteMetric(TagSet tags) + { + var tagList = CreateTagList(tags); + _sDistributedCacheWrites.Add(1, tagList); + } + + /// + /// Converts a TagSet to a TagList for use with System.Diagnostics.Metrics instruments. + /// Tags are added with keys "tag_0", "tag_1", etc. to maintain order and avoid conflicts. + /// + /// The TagSet to convert. + /// A TagList containing the tag values as dimensions. + [NonEvent] + private static TagList CreateTagList(TagSet tags) + { + var tagList = new TagList(); + switch (tags.Count) + { + case 0: + break; // no tags to add + case 1: + tagList.Add("tag_0", tags.GetSinglePrechecked()); + break; + default: + var span = tags.GetSpanPrechecked(); + for (int i = 0; i < span.Length; i++) + tagList.Add($"tag_{i}", span[i]); + break; + } + return tagList; + } + #if !(NETSTANDARD2_0 || NET462) [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Lifetime exceeds obvious scope; handed to event source")] [NonEvent] @@ -221,6 +449,94 @@ protected override void OnEventCommand(EventCommandEventArgs command) base.OnEventCommand(command); } + + /// + /// Emits only System.Diagnostics.Metrics for local cache hit when ReportTagMetrics is enabled. + /// + /// The cache entry tags to include as metric dimensions. + [NonEvent] + public static void EmitLocalCacheHitMetrics(TagSet tags) + { + if (tags.Count > 0) + EmitLocalCacheHitMetric(tags); + else + _sLocalCacheHits.Add(1); + } + + /// + /// Emits only System.Diagnostics.Metrics for local cache miss when ReportTagMetrics is enabled. + /// + /// The cache entry tags to include as metric dimensions. + [NonEvent] + public static void EmitLocalCacheMissMetrics(TagSet tags) + { + if (tags.Count > 0) + EmitLocalCacheMissMetric(tags); + else + _sLocalCacheMisses.Add(1); + } + + /// + /// Emits only System.Diagnostics.Metrics for distributed cache hit when ReportTagMetrics is enabled. + /// + /// The cache entry tags to include as metric dimensions. + [NonEvent] + public static void EmitDistributedCacheHitMetrics(TagSet tags) + { + if (tags.Count > 0) + EmitDistributedCacheHitMetric(tags); + else + _sDistributedCacheHits.Add(1); + } + + /// + /// Emits only System.Diagnostics.Metrics for distributed cache miss when ReportTagMetrics is enabled. + /// + /// The cache entry tags to include as metric dimensions. + [NonEvent] + public static void EmitDistributedCacheMissMetrics(TagSet tags) + { + if (tags.Count > 0) + EmitDistributedCacheMissMetric(tags); + else + _sDistributedCacheMisses.Add(1); + } + + /// + /// Emits only System.Diagnostics.Metrics for local cache write when ReportTagMetrics is enabled. + /// + /// The cache entry tags to include as metric dimensions. + [NonEvent] + public static void EmitLocalCacheWriteMetrics(TagSet tags) + { + if (tags.Count > 0) + EmitLocalCacheWriteMetric(tags); + else + _sLocalCacheWrites.Add(1); + } + + /// + /// Emits only System.Diagnostics.Metrics for distributed cache write when ReportTagMetrics is enabled. + /// + /// The cache entry tags to include as metric dimensions. + [NonEvent] + public static void EmitDistributedCacheWriteMetrics(TagSet tags) + { + if (tags.Count > 0) + EmitDistributedCacheWriteMetric(tags); + else + _sDistributedCacheWrites.Add(1); + } + + /// + /// Emits only System.Diagnostics.Metrics for tag invalidation when ReportTagMetrics is enabled. + /// + /// The specific tag that was invalidated. + [NonEvent] + public static void EmitTagInvalidationMetrics(string tag) + { + _sTagInvalidations.Add(1, new KeyValuePair("tag", tag)); + } #endif [NonEvent] diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/HybridCacheEventSourceTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/HybridCacheEventSourceTests.cs index 8e23143475f..d35c73576cf 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/HybridCacheEventSourceTests.cs +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/HybridCacheEventSourceTests.cs @@ -242,4 +242,79 @@ private async Task AssertCountersAsync() Skip.If(count == 0, "No counters received"); } + + [SkippableFact] + public void LocalCacheHitWithTags() + { + AssertEnabled(); + + var tags = TagSet.Create(["region", "product"]); + + listener.Reset().Source.LocalCacheHitWithTags(tags, reportTagMetrics: false); + listener.AssertSingleEvent(HybridCacheEventSource.EventIdLocalCacheHit, "LocalCacheHit", EventLevel.Verbose); + } + + [SkippableFact] + public void LocalCacheMissWithTags() + { + AssertEnabled(); + + var tags = TagSet.Create(["region", "product"]); + + listener.Reset().Source.LocalCacheMissWithTags(tags, reportTagMetrics: false); + listener.AssertSingleEvent(HybridCacheEventSource.EventIdLocalCacheMiss, "LocalCacheMiss", EventLevel.Verbose); + } + + [SkippableFact] + public void DistributedCacheHitWithTags() + { + AssertEnabled(); + + var tags = TagSet.Create(["region", "product"]); + + listener.Reset().Source.DistributedCacheHitWithTags(tags, reportTagMetrics: false); + listener.AssertSingleEvent(HybridCacheEventSource.EventIdDistributedCacheHit, "DistributedCacheHit", EventLevel.Verbose); + } + + [SkippableFact] + public void DistributedCacheMissWithTags() + { + AssertEnabled(); + + var tags = TagSet.Create(["region", "product"]); + + listener.Reset().Source.DistributedCacheMissWithTags(tags, reportTagMetrics: false); + listener.AssertSingleEvent(HybridCacheEventSource.EventIdDistributedCacheMiss, "DistributedCacheMiss", EventLevel.Verbose); + } + + [SkippableFact] + public void LocalCacheWriteWithTags() + { + AssertEnabled(); + + var tags = TagSet.Create(["region", "product"]); + + listener.Reset().Source.LocalCacheWriteWithTags(tags, reportTagMetrics: false); + listener.AssertSingleEvent(HybridCacheEventSource.EventIdLocalCacheWrite, "LocalCacheWrite", EventLevel.Verbose); + } + + [SkippableFact] + public void DistributedCacheWriteWithTags() + { + AssertEnabled(); + + var tags = TagSet.Create(["region", "product"]); + + listener.Reset().Source.DistributedCacheWriteWithTags(tags, reportTagMetrics: false); + listener.AssertSingleEvent(HybridCacheEventSource.EventIdDistributedCacheWrite, "DistributedCacheWrite", EventLevel.Verbose); + } + + [SkippableFact] + public void TagInvalidatedWithTags() + { + AssertEnabled(); + + listener.Reset().Source.TagInvalidatedWithTags("test-tag", reportTagMetrics: false); + listener.AssertSingleEvent(HybridCacheEventSource.EventIdTagInvalidated, "TagInvalidated", EventLevel.Verbose); + } } diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/Microsoft.Extensions.Caching.Hybrid.Tests.csproj b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/Microsoft.Extensions.Caching.Hybrid.Tests.csproj index 3cd6a56dca5..7dfe3ba3bf5 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/Microsoft.Extensions.Caching.Hybrid.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/Microsoft.Extensions.Caching.Hybrid.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsIntegrationTests.cs new file mode 100644 index 00000000000..cc909e2728e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsIntegrationTests.cs @@ -0,0 +1,138 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; + +namespace Microsoft.Extensions.Caching.Hybrid.Tests; + +public class ReportTagMetricsIntegrationTests +{ + private readonly HybridCache _cache; + + public ReportTagMetricsIntegrationTests() + { + var services = new ServiceCollection(); + services.AddHybridCache(options => + { + options.ReportTagMetrics = true; + }); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + _cache = serviceProvider.GetRequiredService(); + } + + [Fact] + public async Task GetOrCreateAsync_WithTags_EmitsTaggedMetrics() + { + // Arrange + using var collector = new MetricCollector(null, "Microsoft.Extensions.Caching.Hybrid", "hybrid_cache.local.misses"); + + // Act - first call should miss + var result1 = await _cache.GetOrCreateAsync("test-key", "initial-state", + (state, token) => new ValueTask($"value-for-{state}"), + tags: ["region:us-west", "service:test"]); + + // Act - second call should hit + var result2 = await _cache.GetOrCreateAsync("test-key", "second-state", + (state, token) => new ValueTask($"value-for-{state}"), + tags: ["region:us-west", "service:test"]); + + // Assert + Assert.Equal("value-for-initial-state", result1); + Assert.Equal("value-for-initial-state", result2); // Should get cached value + + // Verify metrics were emitted + var measurements = collector.GetMeasurementSnapshot(); + Assert.True(measurements.Count > 0, "Expected cache miss metrics to be emitted"); + + var latestMeasurement = measurements.Last(); + Assert.Equal(1, latestMeasurement.Value); + Assert.True(latestMeasurement.Tags.Count >= 2, "Expected tag dimensions in metrics"); + } + + [Fact] + public async Task SetAsync_WithTags_EmitsTaggedWriteMetrics() + { + // Arrange + using var writeCollector = new MetricCollector(null, "Microsoft.Extensions.Caching.Hybrid", "hybrid_cache.local.writes"); + + // Act + await _cache.SetAsync("set-key", "set-value", tags: ["operation:set", "category:test"]); + + // Assert + var measurements = writeCollector.GetMeasurementSnapshot(); + Assert.True(measurements.Count > 0, "Expected cache write metrics to be emitted"); + + var latestMeasurement = measurements.Last(); + Assert.Equal(1, latestMeasurement.Value); + Assert.True(latestMeasurement.Tags.Count >= 2, "Expected tag dimensions in write metrics"); + } + + [Fact] + public async Task RemoveByTagAsync_EmitsTagInvalidationMetrics() + { + // Arrange + using var invalidationCollector = new MetricCollector(null, "Microsoft.Extensions.Caching.Hybrid", "hybrid_cache.tag.invalidations"); + + // Setup - add some data first + await _cache.SetAsync("tagged-key", "tagged-value", tags: ["invalidation-test"]); + + // Act + await _cache.RemoveByTagAsync("invalidation-test"); + + // Assert + var measurements = invalidationCollector.GetMeasurementSnapshot(); + Assert.True(measurements.Count > 0, "Expected tag invalidation metrics to be emitted"); + + var latestMeasurement = measurements.Last(); + Assert.Equal(1, latestMeasurement.Value); + Assert.Contains(latestMeasurement.Tags, kvp => kvp.Key == "tag" && kvp.Value?.ToString() == "invalidation-test"); + } + + [Fact] + public async Task CacheOperations_WithoutTags_EmitsMetricsWithoutDimensions() + { + // Arrange + using var collector = new MetricCollector(null, "Microsoft.Extensions.Caching.Hybrid", "hybrid_cache.local.misses"); + + // Act - cache operation without tags + var result = await _cache.GetOrCreateAsync("no-tags-key", "state", + (state, token) => new ValueTask($"value-{state}")); + + // Assert + Assert.Equal("value-state", result); + + var measurements = collector.GetMeasurementSnapshot(); + Assert.True(measurements.Count > 0, "Expected metrics to be emitted even without tags"); + + var latestMeasurement = measurements.Last(); + Assert.Equal(1, latestMeasurement.Value); + Assert.Empty(latestMeasurement.Tags); // No dimensions when no tags + } + + [Theory] + [InlineData("single-tag")] + [InlineData("tag1", "tag2")] + [InlineData("tag1", "tag2", "tag3", "tag4", "tag5")] + public async Task CacheOperations_WithVariousTagCounts_EmitsCorrectDimensions(params string[] tags) + { + // Arrange + using var collector = new MetricCollector(null, "Microsoft.Extensions.Caching.Hybrid", "hybrid_cache.local.misses"); + + // Act + var result = await _cache.GetOrCreateAsync($"key-{string.Join("-", tags)}", "state", + (state, token) => new ValueTask($"value-{state}"), + tags: tags); + + // Assert + Assert.Equal("value-state", result); + + var measurements = collector.GetMeasurementSnapshot(); + Assert.True(measurements.Count > 0, "Expected metrics to be emitted"); + + var latestMeasurement = measurements.Last(); + Assert.Equal(1, latestMeasurement.Value); + Assert.Equal(tags.Length, latestMeasurement.Tags.Count); // Should have one dimension per tag + } +} diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsTests.cs new file mode 100644 index 00000000000..a6eda5ba95f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsTests.cs @@ -0,0 +1,184 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Caching.Hybrid.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; + +namespace Microsoft.Extensions.Caching.Hybrid.Tests; + +public class ReportTagMetricsTests +{ + [Fact] + public void HybridCacheMetricsInstrumentsAreCreated() + { + // Verify that the System.Diagnostics.Metrics instruments are properly created + using var meterListener = new MeterListener(); + var meterNames = new List(); + + meterListener.InstrumentPublished = (instrument, listener) => + { + if (instrument.Meter.Name == "Microsoft.Extensions.Caching.Hybrid") + { + meterNames.Add(instrument.Name); + listener.EnableMeasurementEvents(instrument); + } + }; + + meterListener.Start(); + + // Creating HybridCacheEventSource should initialize the metrics + using var eventSource = new HybridCacheEventSource(); + + // Verify expected metric names are registered + Assert.Contains("hybrid_cache.local.hits", meterNames); + Assert.Contains("hybrid_cache.local.misses", meterNames); + Assert.Contains("hybrid_cache.distributed.hits", meterNames); + Assert.Contains("hybrid_cache.distributed.misses", meterNames); + Assert.Contains("hybrid_cache.local.writes", meterNames); + Assert.Contains("hybrid_cache.distributed.writes", meterNames); + Assert.Contains("hybrid_cache.tag.invalidations", meterNames); + } + + [Fact] + public async Task ReportTagMetrics_Enabled_EmitsTagDimensions() + { + var services = new ServiceCollection(); + services.AddHybridCache(options => + { + options.ReportTagMetrics = true; + }); + + await using var provider = services.BuildServiceProvider(); + var cache = provider.GetRequiredService(); + + using var collector = new MetricCollector(null, "Microsoft.Extensions.Caching.Hybrid", "hybrid_cache.local.hits"); + + // Perform cache operation with tags + await cache.GetOrCreateAsync("test-key", "test-state", + (state, token) => new ValueTask("test-value"), + tags: ["region:us-west", "service:api"]); + + // Get the value again to trigger a cache hit + await cache.GetOrCreateAsync("test-key", "test-state", + (state, token) => new ValueTask("test-value"), + tags: ["region:us-west", "service:api"]); + + // Check that metrics with tag dimensions were emitted + var measurements = collector.GetMeasurementSnapshot(); + if (measurements.Count > 0) + { + var measurement = measurements.Last(); + Assert.True(measurement.Tags.Count > 0, "Expected tag dimensions to be present when ReportTagMetrics is enabled"); + } + } + + [Fact] + public async Task ReportTagMetrics_Disabled_NoTagDimensions() + { + var services = new ServiceCollection(); + services.AddHybridCache(options => + { + options.ReportTagMetrics = false; // Explicitly disabled + }); + + await using var provider = services.BuildServiceProvider(); + var cache = provider.GetRequiredService(); + + using var collector = new MetricCollector(null, "Microsoft.Extensions.Caching.Hybrid", "hybrid_cache.local.hits"); + + // Perform cache operation with tags + await cache.GetOrCreateAsync("test-key", "test-state", + (state, token) => new ValueTask("test-value"), + tags: ["region:us-west", "service:api"]); + + // Get the value again to trigger a cache hit + await cache.GetOrCreateAsync("test-key", "test-state", + (state, token) => new ValueTask("test-value"), + tags: ["region:us-west", "service:api"]); + + // No metric measurements should be emitted when ReportTagMetrics is disabled + var measurements = collector.GetMeasurementSnapshot(); + + // We expect no measurements or measurements without tag dimensions when ReportTagMetrics is disabled + Assert.True(measurements.Count == 0 || measurements.All(m => m.Tags.Count == 0), + "Expected no tag dimensions when ReportTagMetrics is disabled"); + } + + [Fact] + public void EventSource_LocalCacheHitWithTags_ReportTagMetrics_True() + { + using var collector = new MetricCollector(null, "Microsoft.Extensions.Caching.Hybrid", "hybrid_cache.local.hits"); + + var tags = TagSet.Create(["region", "product"]); + var eventSource = HybridCacheEventSource.Log; + + eventSource.LocalCacheHitWithTags(tags, reportTagMetrics: true); + + var measurements = collector.GetMeasurementSnapshot(); + Assert.True(measurements.Count > 0, "Expected metrics to be emitted when reportTagMetrics is true"); + + var measurement = measurements.Last(); + Assert.Equal(1, measurement.Value); + Assert.True(measurement.Tags.Count >= 2, "Expected tag dimensions to be present"); + } + + [Fact] + public void EventSource_LocalCacheHitWithTags_ReportTagMetrics_False() + { + using var collector = new MetricCollector(null, "Microsoft.Extensions.Caching.Hybrid", "hybrid_cache.local.hits"); + + var tags = TagSet.Create(["region", "product"]); + var eventSource = HybridCacheEventSource.Log; + + eventSource.LocalCacheHitWithTags(tags, reportTagMetrics: false); + + // When reportTagMetrics is false, no System.Diagnostics.Metrics should be emitted + var measurements = collector.GetMeasurementSnapshot(); + Assert.True(measurements.Count == 0, "Expected no metrics to be emitted when reportTagMetrics is false"); + } + + [Fact] + public void EventSource_TagInvalidatedWithTags_ReportTagMetrics_True() + { + using var collector = new MetricCollector(null, "Microsoft.Extensions.Caching.Hybrid", "hybrid_cache.tag.invalidations"); + + var eventSource = HybridCacheEventSource.Log; + + eventSource.TagInvalidatedWithTags("test-tag", reportTagMetrics: true); + + var measurements = collector.GetMeasurementSnapshot(); + Assert.True(measurements.Count > 0, "Expected metrics to be emitted when reportTagMetrics is true"); + + var measurement = measurements.Last(); + Assert.Equal(1, measurement.Value); + Assert.Contains(measurement.Tags, kvp => kvp.Key == "tag" && kvp.Value?.ToString() == "test-tag"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EventSource_EmptyTags_ReportTagMetrics(bool reportTagMetrics) + { + using var collector = new MetricCollector(null, "Microsoft.Extensions.Caching.Hybrid", "hybrid_cache.local.hits"); + + var emptyTags = TagSet.Empty; + var eventSource = HybridCacheEventSource.Log; + + eventSource.LocalCacheHitWithTags(emptyTags, reportTagMetrics); + + var measurements = collector.GetMeasurementSnapshot(); + if (reportTagMetrics) + { + Assert.True(measurements.Count > 0, "Expected metrics to be emitted when reportTagMetrics is true, even with empty tags"); + var measurement = measurements.Last(); + Assert.Equal(1, measurement.Value); + Assert.Equal(0, measurement.Tags.Count); // No tag dimensions for empty tags + } + else + { + Assert.True(measurements.Count == 0, "Expected no metrics when reportTagMetrics is false"); + } + } +} From d16c225388aae045265411bcf2cc5fa89296330b Mon Sep 17 00:00:00 2001 From: mokarchi Date: Sun, 28 Sep 2025 19:32:13 +0330 Subject: [PATCH 2/6] Add IDisposable to ReportTagMetricsIntegrationTests Updated the `ReportTagMetricsIntegrationTests` class to implement the `IDisposable` interface for proper resource cleanup. Introduced a private `_serviceProvider` field to store the `ServiceProvider` instance, enabling its disposal in the new `Dispose` method. Adjusted the constructor to use `_serviceProvider` for creating and retrieving the `HybridCache` instance. --- .../ReportTagMetricsIntegrationTests.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsIntegrationTests.cs index cc909e2728e..5e50c62fb1f 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsIntegrationTests.cs @@ -6,8 +6,9 @@ namespace Microsoft.Extensions.Caching.Hybrid.Tests; -public class ReportTagMetricsIntegrationTests +public class ReportTagMetricsIntegrationTests : IDisposable { + private readonly ServiceProvider _serviceProvider; private readonly HybridCache _cache; public ReportTagMetricsIntegrationTests() @@ -18,8 +19,8 @@ public ReportTagMetricsIntegrationTests() options.ReportTagMetrics = true; }); - ServiceProvider serviceProvider = services.BuildServiceProvider(); - _cache = serviceProvider.GetRequiredService(); + _serviceProvider = services.BuildServiceProvider(); + _cache = _serviceProvider.GetRequiredService(); } [Fact] @@ -135,4 +136,9 @@ public async Task CacheOperations_WithVariousTagCounts_EmitsCorrectDimensions(pa Assert.Equal(1, latestMeasurement.Value); Assert.Equal(tags.Length, latestMeasurement.Tags.Count); // Should have one dimension per tag } + + public void Dispose() + { + _serviceProvider?.Dispose(); + } } From 87583c04cafd57214413973f94a4605f42e97002 Mon Sep 17 00:00:00 2001 From: mokarchi Date: Sun, 28 Sep 2025 20:25:21 +0330 Subject: [PATCH 3/6] Improve metrics handling and update documentation Updated XML documentation in `HybridCacheOptions.cs` for clarity and consistency. Enhanced `LocalCacheHitWithTags` in `HybridCacheEventSource.cs` by adding braces for readability and refining metrics emission logic to handle empty tag sets. Fixed a missing `return` statement in `CreateTagList` to ensure proper functionality. These changes improve code readability, maintainability, and correctness. --- .../HybridCacheOptions.cs | 4 ++-- .../Internal/HybridCacheEventSource.cs | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs index f408f71b32e..aac2d7c4bfc 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs @@ -79,10 +79,10 @@ public class HybridCacheOptions /// /// /// - /// Example of appropriate tags: ["region:us-west", "service:api", "environment:prod"] + /// Example of appropriate tags: ["region:us-west", "service:api", "environment:prod"]. /// /// - /// Example of inappropriate tags: ["user:john.doe@company.com", "session:abc123", "customer-data:sensitive"] + /// Example of inappropriate tags: ["user:john.doe@company.com", "session:abc123", "customer-data:sensitive"]. /// /// public bool ReportTagMetrics { get; set; } diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs index f844629e50e..56eefecf124 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs @@ -217,15 +217,21 @@ internal void TagInvalidated() public void LocalCacheHitWithTags(TagSet tags, bool reportTagMetrics) { if (IsEnabled()) - LocalCacheHit(); // Emit EventSource event + { + LocalCacheHit();// Emit EventSource event + } // Also emit metrics when requested if (reportTagMetrics) { if (tags.Count > 0) + { EmitLocalCacheHitMetric(tags); + } else + { _sLocalCacheHits.Add(1); + } } } @@ -421,6 +427,7 @@ private static TagList CreateTagList(TagSet tags) tagList.Add($"tag_{i}", span[i]); break; } + return tagList; } From d600b2d5c21409d173e2b725c1116d70383c2eb2 Mon Sep 17 00:00:00 2001 From: mokarchi Date: Sun, 28 Sep 2025 20:50:59 +0330 Subject: [PATCH 4/6] Refactor metric emission and improve code consistency Refactored methods to add braces around single-line `if` statements for improved readability and maintainability. Enhanced metric emission logic in methods like `LocalCacheHitWithTags`, `DistributedCacheMissWithTags`, and others to handle `reportTagMetrics` more robustly. Introduced static helper methods for emitting metrics (`EmitLocalCacheHitMetrics`, `EmitDistributedCacheMissMetrics`, etc.) to centralize logic, reduce duplication, and improve maintainability. Ensured consistent structure across all metric-emitting methods. --- .../Internal/HybridCacheEventSource.cs | 68 +++++++++++++++++-- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs index 56eefecf124..16d85365cc6 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs @@ -244,15 +244,21 @@ public void LocalCacheHitWithTags(TagSet tags, bool reportTagMetrics) public void LocalCacheMissWithTags(TagSet tags, bool reportTagMetrics) { if (IsEnabled()) - LocalCacheMiss(); // Emit EventSource event + { + LocalCacheMiss();// Emit EventSource event + } // Also emit metrics when requested if (reportTagMetrics) { if (tags.Count > 0) + { EmitLocalCacheMissMetric(tags); + } else + { _sLocalCacheMisses.Add(1); + } } } @@ -265,15 +271,21 @@ public void LocalCacheMissWithTags(TagSet tags, bool reportTagMetrics) public void DistributedCacheHitWithTags(TagSet tags, bool reportTagMetrics) { if (IsEnabled()) - DistributedCacheHit(); // Emit EventSource event + { + DistributedCacheHit();// Emit EventSource event + } // Also emit metrics when requested if (reportTagMetrics) { if (tags.Count > 0) + { EmitDistributedCacheHitMetric(tags); + } else + { _sDistributedCacheHits.Add(1); + } } } @@ -286,15 +298,21 @@ public void DistributedCacheHitWithTags(TagSet tags, bool reportTagMetrics) public void DistributedCacheMissWithTags(TagSet tags, bool reportTagMetrics) { if (IsEnabled()) - DistributedCacheMiss(); // Emit EventSource event + { + DistributedCacheMiss();// Emit EventSource event + } // Also emit metrics when requested if (reportTagMetrics) { if (tags.Count > 0) + { EmitDistributedCacheMissMetric(tags); + } else + { _sDistributedCacheMisses.Add(1); + } } } @@ -307,15 +325,21 @@ public void DistributedCacheMissWithTags(TagSet tags, bool reportTagMetrics) public void LocalCacheWriteWithTags(TagSet tags, bool reportTagMetrics) { if (IsEnabled()) - LocalCacheWrite(); // Emit EventSource event + { + LocalCacheWrite();// Emit EventSource event + } // Also emit metrics when requested if (reportTagMetrics) { if (tags.Count > 0) + { EmitLocalCacheWriteMetric(tags); + } else + { _sLocalCacheWrites.Add(1); + } } } @@ -328,15 +352,21 @@ public void LocalCacheWriteWithTags(TagSet tags, bool reportTagMetrics) public void DistributedCacheWriteWithTags(TagSet tags, bool reportTagMetrics) { if (IsEnabled()) - DistributedCacheWrite(); // Emit EventSource event + { + DistributedCacheWrite();// Emit EventSource event + } // Also emit metrics when requested if (reportTagMetrics) { if (tags.Count > 0) + { EmitDistributedCacheWriteMetric(tags); + } else + { _sDistributedCacheWrites.Add(1); + } } } @@ -349,7 +379,9 @@ public void DistributedCacheWriteWithTags(TagSet tags, bool reportTagMetrics) public void TagInvalidatedWithTags(string tag, bool reportTagMetrics) { if (IsEnabled()) - TagInvalidated(); // Emit EventSource event + { + TagInvalidated();// Emit EventSource event + } // Also emit metrics when requested if (reportTagMetrics) @@ -465,9 +497,13 @@ protected override void OnEventCommand(EventCommandEventArgs command) public static void EmitLocalCacheHitMetrics(TagSet tags) { if (tags.Count > 0) + { EmitLocalCacheHitMetric(tags); + } else + { _sLocalCacheHits.Add(1); + } } /// @@ -478,9 +514,13 @@ public static void EmitLocalCacheHitMetrics(TagSet tags) public static void EmitLocalCacheMissMetrics(TagSet tags) { if (tags.Count > 0) + { EmitLocalCacheMissMetric(tags); + } else + { _sLocalCacheMisses.Add(1); + } } /// @@ -491,9 +531,13 @@ public static void EmitLocalCacheMissMetrics(TagSet tags) public static void EmitDistributedCacheHitMetrics(TagSet tags) { if (tags.Count > 0) + { EmitDistributedCacheHitMetric(tags); + } else + { _sDistributedCacheHits.Add(1); + } } /// @@ -504,9 +548,13 @@ public static void EmitDistributedCacheHitMetrics(TagSet tags) public static void EmitDistributedCacheMissMetrics(TagSet tags) { if (tags.Count > 0) + { EmitDistributedCacheMissMetric(tags); + } else + { _sDistributedCacheMisses.Add(1); + } } /// @@ -517,9 +565,13 @@ public static void EmitDistributedCacheMissMetrics(TagSet tags) public static void EmitLocalCacheWriteMetrics(TagSet tags) { if (tags.Count > 0) + { EmitLocalCacheWriteMetric(tags); + } else + { _sLocalCacheWrites.Add(1); + } } /// @@ -530,9 +582,13 @@ public static void EmitLocalCacheWriteMetrics(TagSet tags) public static void EmitDistributedCacheWriteMetrics(TagSet tags) { if (tags.Count > 0) + { EmitDistributedCacheWriteMetric(tags); + } else + { _sDistributedCacheWrites.Add(1); + } } /// From a22f3ba9cba119c2b1b4c57b1d6a0966230297f7 Mon Sep 17 00:00:00 2001 From: mokarchi Date: Sun, 28 Sep 2025 21:07:04 +0330 Subject: [PATCH 5/6] Refactor CreateTagList initialization and loop style Updated `CreateTagList` to initialize `tagList` with `default` instead of `new`, potentially improving performance. Added curly braces to the `for` loop in the `default` case for better readability and maintainability. --- .../Internal/HybridCacheEventSource.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs index 16d85365cc6..6ed9ecc5824 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/HybridCacheEventSource.cs @@ -445,7 +445,7 @@ private static void EmitDistributedCacheWriteMetric(TagSet tags) [NonEvent] private static TagList CreateTagList(TagSet tags) { - var tagList = new TagList(); + var tagList = default(TagList); switch (tags.Count) { case 0: @@ -456,7 +456,10 @@ private static TagList CreateTagList(TagSet tags) default: var span = tags.GetSpanPrechecked(); for (int i = 0; i < span.Length; i++) + { tagList.Add($"tag_{i}", span[i]); + } + break; } From c6834ba48cd3e504628cbe322cf944bd70f294fa Mon Sep 17 00:00:00 2001 From: mokarchi Date: Sun, 28 Sep 2025 21:32:43 +0330 Subject: [PATCH 6/6] Make `ReportTagMetricsIntegrationTests` sealed, improve Dispose The `ReportTagMetricsIntegrationTests` class was made `sealed` to prevent inheritance, enhancing clarity and potential performance. A `_disposed` field was added to implement the `Dispose` pattern more robustly, ensuring idempotency and avoiding multiple disposals of `_serviceProvider`. The `Dispose` method was updated to check `_disposed` before executing disposal logic. Minor formatting adjustments were made to comments in `CacheOperations_WithoutTags_EmitsMetricsWithoutDimensions` for consistency. In `EventSource_EmptyTags_ReportTagMetrics`, the assertion for empty tags was updated to use `Assert.Empty` for improved readability. --- .../ReportTagMetricsIntegrationTests.cs | 13 ++++++++++--- .../ReportTagMetricsTests.cs | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsIntegrationTests.cs index 5e50c62fb1f..ab9a47419df 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsIntegrationTests.cs @@ -6,10 +6,11 @@ namespace Microsoft.Extensions.Caching.Hybrid.Tests; -public class ReportTagMetricsIntegrationTests : IDisposable +public sealed class ReportTagMetricsIntegrationTests : IDisposable { private readonly ServiceProvider _serviceProvider; private readonly HybridCache _cache; + private bool _disposed; public ReportTagMetricsIntegrationTests() { @@ -109,7 +110,7 @@ public async Task CacheOperations_WithoutTags_EmitsMetricsWithoutDimensions() var latestMeasurement = measurements.Last(); Assert.Equal(1, latestMeasurement.Value); - Assert.Empty(latestMeasurement.Tags); // No dimensions when no tags + Assert.Empty(latestMeasurement.Tags);// No dimensions when no tags } [Theory] @@ -139,6 +140,12 @@ public async Task CacheOperations_WithVariousTagCounts_EmitsCorrectDimensions(pa public void Dispose() { - _serviceProvider?.Dispose(); + if (_disposed) + { + return; + } + + _serviceProvider.Dispose(); + _disposed = true; } } diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsTests.cs index a6eda5ba95f..f6044074aea 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ReportTagMetricsTests.cs @@ -174,7 +174,7 @@ public void EventSource_EmptyTags_ReportTagMetrics(bool reportTagMetrics) Assert.True(measurements.Count > 0, "Expected metrics to be emitted when reportTagMetrics is true, even with empty tags"); var measurement = measurements.Last(); Assert.Equal(1, measurement.Value); - Assert.Equal(0, measurement.Tags.Count); // No tag dimensions for empty tags + Assert.Empty(measurement.Tags); // No tag dimensions for empty tags } else {