Skip to content

Add built-in OpenTelemetry metrics to MemoryCache#126146

Draft
rjmurillo wants to merge 1 commit intodotnet:mainfrom
rjmurillo:feature/memory-cache-metrics
Draft

Add built-in OpenTelemetry metrics to MemoryCache#126146
rjmurillo wants to merge 1 commit intodotnet:mainfrom
rjmurillo:feature/memory-cache-metrics

Conversation

@rjmurillo
Copy link
Copy Markdown

@rjmurillo rjmurillo commented Mar 26, 2026

Summary

Adds OpenTelemetry metrics to MemoryCache. Follows the System.Net.Http pattern (#87319).

Fixes #124140

API Changes

Microsoft.Extensions.Caching.Abstractions

public class MemoryCacheStatistics
{
    public long TotalEvictions { get; init; } // NEW
}

Microsoft.Extensions.Caching.Memory

public class MemoryCacheOptions
{
    public string Name { get; set; } = "Default"; // NEW
}

public class MemoryCache
{
    // ILoggerFactory now nullable
    public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor, ILoggerFactory? loggerFactory);

    // NEW
    public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor, ILoggerFactory? loggerFactory, IMeterFactory? meterFactory);
}

Instruments

Meter: Microsoft.Extensions.Caching.Memory.MemoryCache

Instrument Type Dimensions
cache.requests ObservableCounter<long> cache.name, cache.request.type (hit/miss)
cache.evictions ObservableCounter<long> cache.name
cache.entries ObservableUpDownCounter<long> cache.name
cache.estimated_size ObservableGauge<long> cache.name

Design

  • Gated on TrackStatistics — no instruments created unless TrackStatistics = true
  • SharedMeter fallback — no IMeterFactory, no problem. Singleton SharedMeter with NOP Dispose. Same pattern as System.Net.Http.MetricsHandler.SharedMeter
  • MeterOptions tags — factory path sets Tags = [new("cache.name", _options.Name)] for meter deduplication in DefaultMeterFactory
  • WeakReference callbacks — observable callbacks capture WeakReference<MemoryCache>. Dead caches return empty measurements. No GC leaks
  • IEnumerable overloadsFunc<IEnumerable<Measurement<long>>> prevents phantom zero measurements from disposed or collected caches
  • Eviction countingRemoveEntry calls ConcurrentDictionary.TryRemove(KVP). Only the thread that removes the entry increments TotalEvictions. No double-counting
  • DI unchanged — new constructor auto-selected. No changes to MemoryCacheServiceCollectionExtensions
  • Conditional DiagnosticSource ref — in-box for net11.0+

Tests

  • 142 tests pass (net11.0)
  • 11 new metrics tests: instrument creation, SharedMeter fallback, null factory/logger, TrackStatistics gate, Name property, eviction accuracy, disposed cache, MeterOptions tags, over-count prevention
  • 2 new eviction statistics tests

Copilot AI review requested due to automatic review settings March 26, 2026 04:35
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Mar 26, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-extensions-caching
See info in area-owners.md if you want to be subscribed.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds built-in OpenTelemetry metrics support to Microsoft.Extensions.Caching.Memory.MemoryCache by exposing additional cache statistics and wiring them into observable instruments, gated by MemoryCacheOptions.TrackStatistics.

Changes:

  • Added TotalEvictions to MemoryCacheStatistics and tracked eviction counts in MemoryCache.
  • Introduced MemoryCacheOptions.Name and new MemoryCache ctor overload supporting IMeterFactory.
  • Added unit tests for metrics publication and eviction statistics, plus conditional DiagnosticSource references for non-inbox TFMs.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs Implements eviction counting and publishes OTEL observable instruments via Meter/IMeterFactory.
src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCacheOptions.cs Adds Name option used as a metrics dimension.
src/libraries/Microsoft.Extensions.Caching.Memory/src/Microsoft.Extensions.Caching.Memory.csproj Adds conditional DiagnosticSource reference for TFMs missing in-box metrics types.
src/libraries/Microsoft.Extensions.Caching.Memory/ref/Microsoft.Extensions.Caching.Memory.cs Updates public API surface for new ctor overload and Name option.
src/libraries/Microsoft.Extensions.Caching.Memory/tests/MemoryCacheMetricsTests.cs Adds tests validating instrument creation and basic measurements.
src/libraries/Microsoft.Extensions.Caching.Memory/tests/MemoryCacheGetCurrentStatisticsTests.cs Adds tests for eviction statistics behavior.
src/libraries/Microsoft.Extensions.Caching.Memory/tests/Microsoft.Extensions.Caching.Memory.Tests.csproj Adds conditional DiagnosticSource project reference for .NETFramework tests.
src/libraries/Microsoft.Extensions.Caching.Abstractions/src/MemoryCacheStatistics.cs Adds TotalEvictions to the statistics snapshot type.
src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.cs Updates ref assembly for TotalEvictions.

@cincuranet cincuranet self-assigned this Mar 26, 2026
@rjmurillo rjmurillo force-pushed the feature/memory-cache-metrics branch from 2d7d40d to f3ef1c4 Compare March 26, 2026 13:08
@rjmurillo rjmurillo marked this pull request as draft March 26, 2026 13:25
Copy link
Copy Markdown
Contributor

@cincuranet cincuranet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the _accumulatedEvictions comments from Copilot, I think the idea with bool return is sound.

Or maybe putting the logic into RemoveEntry - given there's already _cacheSize handling - with a flag whether to count or not (not all RemoveEntry calls (should) update _accumulatedEvictions).

Up to you.

Copilot AI review requested due to automatic review settings March 26, 2026 14:34
@rjmurillo rjmurillo force-pushed the feature/memory-cache-metrics branch from f3ef1c4 to 98aa196 Compare March 26, 2026 14:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Implements dotnet#124140. Adds observable OTEL instruments for
cache requests (hit/miss), evictions, entry count, and estimated size.

Key design decisions:
- MeterOptions with cache.name tag for per-cache meter deduplication
- WeakReference<MemoryCache> in observable callbacks to prevent GC leaks
- RemoveEntry returns bool for accurate eviction counting
- IEnumerable<Measurement<long>> overloads to avoid phantom zero measurements
- No instruments without IMeterFactory (GetCurrentStatistics still works)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 27, 2026 16:43
@rjmurillo rjmurillo force-pushed the feature/memory-cache-metrics branch from c4b258a to eace3f4 Compare March 27, 2026 16:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.


_meter = meterFactory?.Create(new MeterOptions("Microsoft.Extensions.Caching.Memory.MemoryCache")
{
Tags = [new("cache.name", _options.Name)]
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InitializeMetrics creates 4 new observable instruments per MemoryCache instance. When an IMeterFactory is used, DefaultMeterFactory caches and returns the same Meter for the same name+tags; when no factory is provided, SharedMeter.Instance is a singleton. In both cases, creating multiple MemoryCache instances with the same options.Name will repeatedly add duplicate observable instruments to the same Meter with no way to unregister them, leading to unbounded instrument growth and potentially duplicated/incorrect metric streams. Consider ensuring instruments are created once per meter (e.g., static/once-init per meter) and have the observable callbacks aggregate across all caches sharing that meter/name (using a registry of weak references), rather than creating per-cache instruments.

Suggested change
Tags = [new("cache.name", _options.Name)]
Tags =
[
new("cache.name", _options.Name),
new("cache.instance.id", RuntimeHelpers.GetHashCode(this))
]

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observable instruments on a shared Meter are not deduplicated. Each callback holds a WeakReference<MemoryCache>. GC collects a cache, the callback returns empty measurements. Cost is near zero.

Adding cache.instance.id breaks meter-level deduplication in DefaultMeterFactory. Different tags produce different meters. That defeats MeterOptions.Tags.

This follows the System.Net.Http.MetricsHandler.SharedMeter pattern from PR #87319. One to ten cache instances — instrument overhead is negligible.

@cincuranet confirmed the approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Extensions-Caching community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Metrics for M.E.Caching.MemoryCache

3 participants