diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs
new file mode 100644
index 00000000000..6d8ec54fdda
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Globalization;
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes;
+
+internal class KubernetesMetadata
+{
+ ///
+ /// Gets or sets the resource memory limit the container is allowed to use in bytes.
+ ///
+ public ulong LimitsMemory { get; set; }
+
+ ///
+ /// Gets or sets the resource CPU limit the container is allowed to use in milicores.
+ ///
+ public ulong LimitsCpu { get; set; }
+
+ ///
+ /// Gets or sets the resource memory request the container is allowed to use in bytes.
+ ///
+ public ulong RequestsMemory { get; set; }
+
+ ///
+ /// Gets or sets the resource CPU request the container is allowed to use in milicores.
+ ///
+ public ulong RequestsCpu { get; set; }
+
+ private string _environmentVariablePrefix;
+
+ public KubernetesMetadata(string environmentVariablePrefix)
+ {
+ _environmentVariablePrefix = environmentVariablePrefix;
+ }
+
+ ///
+ /// Fills the object with data loaded from environment variables.
+ ///
+ public void Build()
+ {
+ LimitsMemory = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}LIMITS_MEMORY");
+ LimitsCpu = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}LIMITS_CPU");
+ RequestsMemory = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}REQUESTS_MEMORY");
+ RequestsCpu = GetEnvironmentVariableAsUInt64($"{_environmentVariablePrefix}REQUESTS_CPU");
+ }
+
+ private static ulong GetEnvironmentVariableAsUInt64(string variableName)
+ {
+ var value = Environment.GetEnvironmentVariable(variableName);
+
+ if (string.IsNullOrEmpty(value))
+ {
+ throw new InvalidOperationException($"Environment variable '{variableName}' is not set or is empty.");
+ }
+
+ if (!ulong.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out ulong result))
+ {
+ throw new InvalidOperationException($"Environment variable '{variableName}' contains invalid value '{value}'. Expected a non-negative integer.");
+ }
+
+ return result;
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaProvider.cs
new file mode 100644
index 00000000000..41b8bdb42f4
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaProvider.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes;
+
+internal class KubernetesResourceQuotaProvider : ResourceQuotaProvider
+{
+ private const double MillicoresPerCore = 1000.0;
+ private KubernetesMetadata _kubernetesMetadata;
+
+ public KubernetesResourceQuotaProvider(KubernetesMetadata kubernetesMetadata)
+ {
+ _kubernetesMetadata = kubernetesMetadata;
+ }
+
+ public override ResourceQuota GetResourceQuota()
+ {
+ return new ResourceQuota
+ {
+ GuaranteedCpuInCores = ConvertMillicoreToCpuUnit(_kubernetesMetadata.RequestsCpu),
+ MaxCpuInCores = ConvertMillicoreToCpuUnit(_kubernetesMetadata.LimitsCpu),
+ GuaranteedMemoryInBytes = _kubernetesMetadata.RequestsMemory,
+ MaxMemoryInBytes = _kubernetesMetadata.LimitsMemory,
+ };
+ }
+
+ private static double ConvertMillicoreToCpuUnit(ulong millicores)
+ {
+ return millicores / MillicoresPerCore;
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaServiceCollectionExtensions.cs
new file mode 100644
index 00000000000..27f0b8ef6ed
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesResourceQuotaServiceCollectionExtensions.cs
@@ -0,0 +1,49 @@
+// 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.Extensions;
+using Microsoft.Extensions.Diagnostics.ResourceMonitoring;
+using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+/// Lets you configure and register Kubernetes resource monitoring components.
+///
+public static class KubernetesResourceQuotaServiceCollectionExtensions
+{
+ ///
+ /// Configures and adds an Kubernetes resource monitoring components to a service collection alltoghter with necessary basic resource monitoring components.
+ ///
+ /// The dependency injection container to add the Kubernetes resource monitoring to.
+ /// Optional value of prefix used to read environment variables in the container.
+ /// The value of .
+ ///
+ ///
+ /// If you have configured your Kubernetes container with Downward API to add environment variable MYCLUSTER_LIMITS_CPU with CPU limits,
+ /// then you should pass MYCLUSTER_ to parameter. Environment variables will be read during DI Container resolution.
+ ///
+ ///
+ /// Important: Do not call
+ /// if you are using this method, as it already includes all necessary resource monitoring components and registers a Kubernetes-specific
+ /// implementation. Calling both methods may result in conflicting service registrations.
+ ///
+ ///
+ public static IServiceCollection AddKubernetesResourceMonitoring(
+ this IServiceCollection services,
+ string? environmentVariablePrefix = default)
+ {
+ services.TryAddSingleton(serviceProvider =>
+ {
+ var metadata = new KubernetesMetadata(environmentVariablePrefix ?? string.Empty);
+ metadata.Build();
+
+ return metadata;
+ });
+ services.TryAddSingleton();
+
+ _ = services.AddResourceMonitoring();
+
+ return services;
+ }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.csproj
new file mode 100644
index 00000000000..9fa77891fce
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.csproj
@@ -0,0 +1,42 @@
+
+
+ Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes
+ Measures processor and memory usage.
+ ResourceMonitoring
+ $(NoWarn);CS0436
+
+
+
+ true
+
+
+
+ normal
+ 99
+ 90
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.json b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.json
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/README.md b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/README.md
new file mode 100644
index 00000000000..1430f916d08
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/README.md
@@ -0,0 +1,24 @@
+# Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes
+
+Registers `ResourceQuota` implementation specific to Kubernetes.
+
+## Install the package
+
+From the command-line:
+
+```console
+dotnet add package Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes
+```
+
+Or directly in the C# project file:
+
+```xml
+
+
+
+```
+
+
+## Feedback & Contributing
+
+We welcome feedback and contributions in [our GitHub repo](https://github.com/dotnet/extensions).
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceQuotaProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceQuotaProvider.cs
new file mode 100644
index 00000000000..296977b7675
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceQuotaProvider.cs
@@ -0,0 +1,39 @@
+// 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.Options;
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux;
+
+internal class LinuxResourceQuotaProvider : ResourceQuotaProvider
+{
+ private readonly ILinuxUtilizationParser _parser;
+ private bool _useLinuxCalculationV2;
+
+ public LinuxResourceQuotaProvider(ILinuxUtilizationParser parser, IOptions options)
+ {
+ _parser = parser;
+ _useLinuxCalculationV2 = options.Value.UseLinuxCalculationV2;
+ }
+
+ public override ResourceQuota GetResourceQuota()
+ {
+ var resourceQuota = new ResourceQuota();
+ if (_useLinuxCalculationV2)
+ {
+ resourceQuota.MaxCpuInCores = _parser.GetCgroupLimitV2();
+ resourceQuota.GuaranteedCpuInCores = _parser.GetCgroupRequestCpuV2();
+ }
+ else
+ {
+ resourceQuota.MaxCpuInCores = _parser.GetCgroupLimitedCpus();
+ resourceQuota.GuaranteedCpuInCores = _parser.GetCgroupRequestCpu();
+ }
+
+ resourceQuota.MaxMemoryInBytes = _parser.GetAvailableMemoryInBytes();
+ resourceQuota.GuaranteedMemoryInBytes = resourceQuota.MaxMemoryInBytes; // TODO: use real value
+
+ return resourceQuota;
+ }
+}
+
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs
index 611b96f4f1d..a7a0de86094 100644
--- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs
@@ -22,7 +22,6 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider
private readonly object _memoryLocker = new();
private readonly ILogger _logger;
private readonly ILinuxUtilizationParser _parser;
- private readonly ulong _memoryLimit;
private readonly long _cpuPeriodsInterval;
private readonly TimeSpan _cpuRefreshInterval;
private readonly TimeSpan _memoryRefreshInterval;
@@ -33,6 +32,13 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider
private DateTimeOffset _lastFailure = DateTimeOffset.MinValue;
private int _measurementsUnavailable;
+ private double _memoryLimit;
+ private double _cpuLimit;
+#pragma warning disable S1450 // Private fields only used as local variables in methods should become local variables. This will be used once we bring relevant meters.
+ private ulong _memoryRequest;
+#pragma warning restore S1450 // Private fields only used as local variables in methods should become local variables
+ private double _cpuRequest;
+
private DateTimeOffset _refreshAfterCpu;
private DateTimeOffset _refreshAfterMemory;
private double _cpuPercentage = double.NaN;
@@ -43,8 +49,13 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider
private long _previousCgroupCpuPeriodCounter;
public SystemResources Resources { get; }
- public LinuxUtilizationProvider(IOptions options, ILinuxUtilizationParser parser,
- IMeterFactory meterFactory, ILogger? logger = null, TimeProvider? timeProvider = null)
+ public LinuxUtilizationProvider(
+ IOptions options,
+ ILinuxUtilizationParser parser,
+ IMeterFactory meterFactory,
+ ResourceQuotaProvider resourceQuotaProvider,
+ ILogger? logger = null,
+ TimeProvider? timeProvider = null)
{
_parser = parser;
_logger = logger ?? NullLogger.Instance;
@@ -54,15 +65,18 @@ public LinuxUtilizationProvider(IOptions options, ILi
_memoryRefreshInterval = options.Value.MemoryConsumptionRefreshInterval;
_refreshAfterCpu = now;
_refreshAfterMemory = now;
- _memoryLimit = _parser.GetAvailableMemoryInBytes();
_previousHostCpuTime = _parser.GetHostCpuUsageInNanoseconds();
_previousCgroupCpuTime = _parser.GetCgroupCpuUsageInNanoseconds();
+ var quota = resourceQuotaProvider.GetResourceQuota();
+ _memoryLimit = quota.MaxMemoryInBytes;
+ _cpuLimit = quota.MaxCpuInCores;
+ _cpuRequest = quota.GuaranteedCpuInCores;
+ _memoryRequest = quota.GuaranteedMemoryInBytes;
+
float hostCpus = _parser.GetHostCpuCount();
- float cpuLimit = _parser.GetCgroupLimitedCpus();
- float cpuRequest = _parser.GetCgroupRequestCpu();
- float scaleRelativeToCpuLimit = hostCpus / cpuLimit;
- float scaleRelativeToCpuRequest = hostCpus / cpuRequest;
+ double scaleRelativeToCpuLimit = hostCpus / _cpuLimit;
+ double scaleRelativeToCpuRequest = hostCpus / _cpuRequest;
_scaleRelativeToCpuRequestForTrackerApi = hostCpus; // the division by cpuRequest is performed later on in the ResourceUtilization class
#pragma warning disable CA2000 // Dispose objects before losing scope
@@ -74,21 +88,18 @@ public LinuxUtilizationProvider(IOptions options, ILi
if (options.Value.UseLinuxCalculationV2)
{
- cpuLimit = _parser.GetCgroupLimitV2();
- cpuRequest = _parser.GetCgroupRequestCpuV2();
-
// Get Cpu periods interval from cgroup
_cpuPeriodsInterval = _parser.GetCgroupPeriodsIntervalInMicroSecondsV2();
(_previousCgroupCpuTime, _previousCgroupCpuPeriodCounter) = _parser.GetCgroupCpuUsageInNanosecondsAndCpuPeriodsV2();
_ = meter.CreateObservableGauge(
name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization,
- observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationLimit(cpuLimit)),
+ observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationLimit(_cpuLimit)),
unit: "1");
_ = meter.CreateObservableGauge(
name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization,
- observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationRequest(cpuRequest)),
+ observeValues: () => GetMeasurementWithRetry(() => CpuUtilizationRequest(_cpuRequest)),
unit: "1");
_ = meter.CreateObservableGauge(
@@ -130,12 +141,9 @@ public LinuxUtilizationProvider(IOptions options, ILi
observeValues: () => GetMeasurementWithRetry(MemoryPercentage),
unit: "1");
- // cpuRequest is a CPU request (aka guaranteed number of CPU units) for pod, for host its 1 core
- // cpuLimit is a CPU limit (aka max CPU units available) for a pod or for a host.
- // _memoryLimit - Resource Memory Limit (in k8s terms)
- // _memoryLimit - To keep the contract, this parameter will get the Host available memory
- Resources = new SystemResources(cpuRequest, cpuLimit, _memoryLimit, _memoryLimit);
- _logger.SystemResourcesInfo(cpuLimit, cpuRequest, _memoryLimit, _memoryLimit);
+ ulong memoryLimitRounded = (ulong)Math.Round(_memoryLimit);
+ Resources = new SystemResources(_cpuRequest, _cpuLimit, _memoryRequest, memoryLimitRounded);
+ _logger.SystemResourcesInfo(_cpuLimit, _cpuRequest, memoryLimitRounded, _memoryRequest);
}
public double CpuUtilizationV2()
@@ -271,7 +279,7 @@ public Snapshot GetSnapshot()
private double MemoryPercentage()
{
ulong memoryUsage = MemoryUsage();
- double memoryPercentage = Math.Min(One, (double)memoryUsage / _memoryLimit);
+ double memoryPercentage = Math.Min(One, memoryUsage / _memoryLimit);
_logger.MemoryPercentageData(memoryUsage, _memoryLimit, memoryPercentage);
return memoryPercentage;
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs
index c368e2bff91..cb54bf219a8 100644
--- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringServiceCollectionExtensions.cs
@@ -6,6 +6,7 @@
using System.Runtime.Versioning;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring;
+
#if !NETFRAMEWORK
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Disk;
@@ -114,6 +115,7 @@ private static void PickWindowsSnapshotProvider(this ResourceMonitorBuilder buil
if (JobObjectInfo.SafeJobHandle.IsProcessInJob())
{
builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
}
else
{
@@ -127,6 +129,7 @@ private static ResourceMonitorBuilder AddLinuxProvider(this ResourceMonitorBuild
_ = Throw.IfNull(builder);
builder.Services.TryAddActivatedSingleton();
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton(TimeProvider.System);
builder.Services.TryAddSingleton();
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuota.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuota.cs
new file mode 100644
index 00000000000..bd53c20034c
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuota.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring;
+
+///
+/// Represents resource quota information for a process or container, including CPU and memory constraints.
+/// Maximum values define the upper limits of resource usage, while guaranteed values specify
+/// the minimum assured resource allocations.
+///
+public sealed class ResourceQuota
+{
+ ///
+ /// Gets or sets the maximum memory that can be used in bytes.
+ ///
+ public ulong MaxMemoryInBytes { get; set; }
+
+ ///
+ /// Gets or sets the maximum CPU that can be used in cores.
+ ///
+ public double MaxCpuInCores { get; set; }
+
+ ///
+ /// Gets or sets the guaranteed (minimum) memory allocation in bytes.
+ ///
+ public ulong GuaranteedMemoryInBytes { get; set; }
+
+ ///
+ /// Gets or sets the guaranteed (minimum) CPU allocation in cores.
+ ///
+ public double GuaranteedCpuInCores { get; set; }
+}
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuotaProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuotaProvider.cs
new file mode 100644
index 00000000000..9c4ea2d92a0
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuotaProvider.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring;
+
+///
+/// Provides resource quota information for resource monitoring purposes.
+///
+///
+/// This abstract class defines a contract for retrieving resource quotas, which include
+/// memory and CPU maximum and guaranteed allocations that may be imposed by container orchestrators,
+/// resource management systems, or other runtime constraints.
+///
+public abstract class ResourceQuotaProvider
+{
+ ///
+ /// Gets the current resource quota containing memory and CPU maximum and guaranteed allocations.
+ /// The returned is used in resource monitoring calculations.
+ ///
+ ///
+ /// A instance containing the current resource constraints
+ /// including maximum memory, maximum CPU, guaranteed memory, and guaranteed CPU.
+ ///
+ public abstract ResourceQuota GetResourceQuota();
+}
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerResourceQuotaProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerResourceQuotaProvider.cs
new file mode 100644
index 00000000000..e6844ab8b69
--- /dev/null
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerResourceQuotaProvider.cs
@@ -0,0 +1,96 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Threading;
+using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop;
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows;
+
+internal class WindowsContainerResourceQuotaProvider : ResourceQuotaProvider
+{
+ private readonly ISystemInfo _systemInfo;
+ private readonly Lazy _memoryStatus;
+ private Func _createJobHandleObject;
+
+ public WindowsContainerResourceQuotaProvider()
+ : this(new MemoryInfo(), new SystemInfo(), static () => new JobHandleWrapper())
+ {
+ }
+
+ public WindowsContainerResourceQuotaProvider(IMemoryInfo memoryInfo, ISystemInfo systemInfo, Func createJobHandleObject)
+ {
+ _systemInfo = systemInfo;
+ _createJobHandleObject = createJobHandleObject;
+
+ _memoryStatus = new Lazy(
+ memoryInfo.GetMemoryStatus,
+ LazyThreadSafetyMode.ExecutionAndPublication);
+ }
+
+ public override ResourceQuota GetResourceQuota()
+ {
+ // bring logic from WindowsContainerSnapshotProvider for limits and requests
+ using IJobHandle jobHandle = _createJobHandleObject();
+
+ var resourceQuota = new ResourceQuota
+ {
+ MaxCpuInCores = GetCpuLimit(jobHandle, _systemInfo),
+ MaxMemoryInBytes = GetMemoryLimit(jobHandle),
+ };
+
+ // CPU guaranteed (aka minimum CPU units) is not supported on Windows, so we set it to the same value as CPU maximum (aka limit CPU units).
+ // Memory guaranteed (aka minimum memory) is not supported on Windows, so we set it to the same value as memory maximum (aka limit memory).
+ resourceQuota.GuaranteedCpuInCores = resourceQuota.MaxCpuInCores;
+ resourceQuota.GuaranteedMemoryInBytes = resourceQuota.MaxMemoryInBytes;
+
+ return resourceQuota;
+ }
+
+ private static double GetCpuLimit(IJobHandle jobHandle, ISystemInfo systemInfo)
+ {
+ // Note: This function convert the CpuRate from CPU cycles to CPU units, also it scales
+ // the CPU units with the number of processors (cores) available in the system.
+ const double CpuCycles = 10_000U;
+
+ var cpuLimit = jobHandle.GetJobCpuLimitInfo();
+ double cpuRatio = 1.0;
+ if ((cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlEnable) != 0 &&
+ (cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlHardCap) != 0)
+ {
+ // The CpuRate is represented as number of cycles during scheduling interval, where
+ // a full cpu cycles number would equal 10_000, so for example if the CpuRate is 2_000,
+ // that means that the application (or container) is assigned 20% of the total CPU available.
+ // So, here we divide the CpuRate by 10_000 to convert it to a ratio (ex: 0.2 for 20% CPU).
+ // For more info: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information?redirectedfrom=MSDN
+ cpuRatio = cpuLimit.CpuRate / CpuCycles;
+ }
+
+ SYSTEM_INFO systemInfoValue = systemInfo.GetSystemInfo();
+
+ // Multiply the cpu ratio by the number of processors to get you the portion
+ // of processors used from the system.
+ return cpuRatio * systemInfoValue.NumberOfProcessors;
+ }
+
+ ///
+ /// Gets memory limit of the system.
+ ///
+ /// Memory limit allocated to the system in bytes.
+ private ulong GetMemoryLimit(IJobHandle jobHandle)
+ {
+ var memoryLimitInBytes = jobHandle.GetExtendedLimitInfo().JobMemoryLimit.ToUInt64();
+
+ if (memoryLimitInBytes <= 0)
+ {
+ MEMORYSTATUSEX memoryStatus = _memoryStatus.Value;
+
+ // Technically, the unconstrained limit is memoryStatus.TotalPageFile.
+ // Leaving this at physical as it is more understandable to consumers.
+ memoryLimitInBytes = memoryStatus.TotalPhys;
+ }
+
+ return memoryLimitInBytes;
+ }
+}
+
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs
index 35c64adeedc..59db4e3b2f9 100644
--- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
-using System.Threading;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -19,8 +18,6 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider
private const double Hundred = 100.0d;
private const double TicksPerSecondDouble = TimeSpan.TicksPerSecond;
- private readonly Lazy _memoryStatus;
-
///
/// This represents a factory method for creating the JobHandle.
///
@@ -32,20 +29,25 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider
private readonly TimeProvider _timeProvider;
private readonly IProcessInfo _processInfo;
private readonly ILogger _logger;
- private readonly double _memoryLimit;
- private readonly double _cpuLimit;
private readonly TimeSpan _cpuRefreshInterval;
private readonly TimeSpan _memoryRefreshInterval;
private readonly double _metricValueMultiplier;
+ private double _memoryLimit;
+ private double _cpuLimit;
+#pragma warning disable S1450 // Private fields only used as local variables in methods should become local variables. Those will be used once we bring relevant meters.
+ private ulong _memoryRequest;
+ private double _cpuRequest;
+#pragma warning restore S1450 // Private fields only used as local variables in methods should become local variables
+
private long _oldCpuUsageTicks;
private long _oldCpuTimeTicks;
private DateTimeOffset _refreshAfterCpu;
private DateTimeOffset _refreshAfterMemory;
private DateTimeOffset _refreshAfterProcessMemory;
private double _cpuPercentage = double.NaN;
- private ulong _memoryUsage;
private double _processMemoryPercentage;
+ private ulong _memoryUsage;
public SystemResources Resources { get; }
@@ -55,9 +57,10 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider
public WindowsContainerSnapshotProvider(
ILogger? logger,
IMeterFactory meterFactory,
- IOptions options)
- : this(new MemoryInfo(), new SystemInfo(), new ProcessInfo(), logger, meterFactory,
- static () => new JobHandleWrapper(), TimeProvider.System, options.Value)
+ IOptions options,
+ ResourceQuotaProvider resourceQuotaProvider)
+ : this(new ProcessInfo(), logger, meterFactory,
+ static () => new JobHandleWrapper(), TimeProvider.System, options.Value, resourceQuotaProvider)
{
}
@@ -66,23 +69,19 @@ public WindowsContainerSnapshotProvider(
///
/// This constructor enables the mocking of dependencies for the purpose of Unit Testing only.
internal WindowsContainerSnapshotProvider(
- IMemoryInfo memoryInfo,
- ISystemInfo systemInfo,
IProcessInfo processInfo,
ILogger? logger,
IMeterFactory meterFactory,
Func createJobHandleObject,
TimeProvider timeProvider,
- ResourceMonitoringOptions options)
+ ResourceMonitoringOptions options,
+ ResourceQuotaProvider resourceQuotaProvider)
{
_logger = logger ?? NullLogger.Instance;
_logger.RunningInsideJobObject();
_metricValueMultiplier = options.UseZeroToOneRangeForMetrics ? One : Hundred;
- _memoryStatus = new Lazy(
- memoryInfo.GetMemoryStatus,
- LazyThreadSafetyMode.ExecutionAndPublication);
_createJobHandleObject = createJobHandleObject;
_processInfo = processInfo;
@@ -90,16 +89,15 @@ internal WindowsContainerSnapshotProvider(
using IJobHandle jobHandle = _createJobHandleObject();
- ulong memoryLimitLong = GetMemoryLimit(jobHandle);
- _memoryLimit = memoryLimitLong;
- _cpuLimit = GetCpuLimit(jobHandle, systemInfo);
+ var quota = resourceQuotaProvider.GetResourceQuota();
+ _cpuLimit = quota.MaxCpuInCores;
+ _memoryLimit = quota.MaxMemoryInBytes;
+ _cpuRequest = quota.GuaranteedCpuInCores;
+ _memoryRequest = quota.GuaranteedMemoryInBytes;
- // CPU request (aka guaranteed CPU units) is not supported on Windows, so we set it to the same value as CPU limit (aka maximum CPU units).
- // Memory request (aka guaranteed memory) is not supported on Windows, so we set it to the same value as memory limit (aka maximum memory).
- double cpuRequest = _cpuLimit;
- ulong memoryRequest = memoryLimitLong;
- Resources = new SystemResources(cpuRequest, _cpuLimit, memoryRequest, memoryLimitLong);
- _logger.SystemResourcesInfo(_cpuLimit, cpuRequest, memoryLimitLong, memoryRequest);
+ ulong memoryLimitRounded = (ulong)Math.Round(_memoryLimit);
+ Resources = new SystemResources(_cpuRequest, _cpuLimit, _memoryRequest, memoryLimitRounded);
+ _logger.SystemResourcesInfo(_cpuLimit, _cpuRequest, memoryLimitRounded, _memoryRequest);
var basicAccountingInfo = jobHandle.GetBasicAccountingInfo();
_oldCpuUsageTicks = basicAccountingInfo.TotalKernelTime + basicAccountingInfo.TotalUserTime;
@@ -117,7 +115,6 @@ internal WindowsContainerSnapshotProvider(
Meter meter = meterFactory.Create(ResourceUtilizationInstruments.MeterName);
#pragma warning restore CA2000 // Dispose objects before losing scope
- // Container based metrics:
_ = meter.CreateObservableCounter(
name: ResourceUtilizationInstruments.ContainerCpuTime,
observeValues: GetCpuTime,
@@ -162,52 +159,6 @@ public Snapshot GetSnapshot()
_processInfo.GetCurrentProcessMemoryUsage());
}
- private static double GetCpuLimit(IJobHandle jobHandle, ISystemInfo systemInfo)
- {
- // Note: This function convert the CpuRate from CPU cycles to CPU units, also it scales
- // the CPU units with the number of processors (cores) available in the system.
- const double CpuCycles = 10_000U;
-
- var cpuLimit = jobHandle.GetJobCpuLimitInfo();
- double cpuRatio = 1.0;
- if ((cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlEnable) != 0 &&
- (cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlHardCap) != 0)
- {
- // The CpuRate is represented as number of cycles during scheduling interval, where
- // a full cpu cycles number would equal 10_000, so for example if the CpuRate is 2_000,
- // that means that the application (or container) is assigned 20% of the total CPU available.
- // So, here we divide the CpuRate by 10_000 to convert it to a ratio (ex: 0.2 for 20% CPU).
- // For more info: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information?redirectedfrom=MSDN
- cpuRatio = cpuLimit.CpuRate / CpuCycles;
- }
-
- SYSTEM_INFO systemInfoValue = systemInfo.GetSystemInfo();
-
- // Multiply the cpu ratio by the number of processors to get you the portion
- // of processors used from the system.
- return cpuRatio * systemInfoValue.NumberOfProcessors;
- }
-
- ///
- /// Gets memory limit of the system.
- ///
- /// Memory limit allocated to the system in bytes.
- private ulong GetMemoryLimit(IJobHandle jobHandle)
- {
- var memoryLimitInBytes = jobHandle.GetExtendedLimitInfo().JobMemoryLimit.ToUInt64();
-
- if (memoryLimitInBytes <= 0)
- {
- MEMORYSTATUSEX memoryStatus = _memoryStatus.Value;
-
- // Technically, the unconstrained limit is memoryStatus.TotalPageFile.
- // Leaving this at physical as it is more understandable to consumers.
- memoryLimitInBytes = memoryStatus.TotalPhys;
- }
-
- return memoryLimitInBytes;
- }
-
private double ProcessMemoryPercentage()
{
DateTimeOffset now = _timeProvider.GetUtcNow();
diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesResourceQuotasServiceCollectionExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesResourceQuotasServiceCollectionExtensionsTests.cs
new file mode 100644
index 00000000000..e073df1c2c2
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesResourceQuotasServiceCollectionExtensionsTests.cs
@@ -0,0 +1,29 @@
+// 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 Xunit;
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests;
+
+public class KubernetesResourceQuotasServiceCollectionExtensionsTests
+{
+ [Fact]
+ public void AddKubernetesResourceMonitoring_WithoutConfiguration_RegistersServicesCorrectly()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ // Act
+ services.AddKubernetesResourceMonitoring();
+
+ // Assert
+ using var serviceProvider = services.BuildServiceProvider();
+
+ ResourceQuotaProvider? resourceQuotaProvider = serviceProvider.GetService();
+ Assert.NotNull(resourceQuotaProvider);
+ Assert.IsType(resourceQuotaProvider);
+ Assert.NotNull(serviceProvider.GetService());
+ Assert.NotNull(serviceProvider.GetService());
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests.csproj
new file mode 100644
index 00000000000..b2cb0677778
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test
+ Unit tests for Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/CpuPercentageCalculatorTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/CpuPercentageCalculatorTests.cs
new file mode 100644
index 00000000000..ed7183cf256
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/CpuPercentageCalculatorTests.cs
@@ -0,0 +1,130 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+using Moq;
+
+namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test;
+
+public class CpuPercentageCalculatorTests
+{
+ private const double TicksPerSecondDouble = TimeSpan.TicksPerSecond; // 10,000,000
+ private const int MetricValueMultiplier = 100;
+
+ public class TestCpuCalculator
+ {
+ private readonly double _cpuLimit;
+ private ulong _oldCpuUsageTicks;
+ private long _oldCpuTimeTicks;
+
+ public TestCpuCalculator(double cpuLimit)
+ {
+ _cpuLimit = cpuLimit;
+ }
+
+ public void SetInitialState(ulong initialCpuTicks, long initialTimeTicks)
+ {
+ _oldCpuUsageTicks = initialCpuTicks;
+ _oldCpuTimeTicks = initialTimeTicks;
+ }
+
+ public double CalculateCpuPercentage(ulong currentCpuTicks, long currentTimeTicks)
+ {
+ var usageTickDelta = currentCpuTicks - _oldCpuUsageTicks;
+ var timeTickDelta = (currentTimeTicks - _oldCpuTimeTicks) * _cpuLimit;
+
+ if (usageTickDelta > 0 && timeTickDelta > 0)
+ {
+ return Math.Min(MetricValueMultiplier, usageTickDelta / timeTickDelta * MetricValueMultiplier);
+ }
+ return 0;
+ }
+ }
+
+ [Theory]
+ [InlineData(0.3, 4, 1.2, "Current implementation - with core scaling")]
+ [InlineData(0.3, 1, 0.3, "Fixed implementation - without core scaling")]
+ public void CpuPercentageCalculation_WithDifferentLimits_ShowsScalingImpact(
+ double kubernetesLimit, int coreCount, double expectedCpuLimit, string testCase)
+ {
+ // Arrange - Simulate GetCpuLimit calculation
+ double cpuLimit = kubernetesLimit * coreCount; // Your current implementation
+ var calculator = new TestCpuCalculator(cpuLimit);
+
+ // Initial state - container has been running and consumed some CPU
+ var initialTime = new DateTime(2025, 1, 1, 12, 0, 0).Ticks;
+ var initialCpuTicks = (ulong)(1.5 * TicksPerSecondDouble); // 1.5 seconds total CPU consumed
+ calculator.SetInitialState(initialCpuTicks, initialTime);
+
+ // After 5 seconds, container consumed 0.2 more CPU seconds
+ var currentTime = initialTime + (5 * TimeSpan.TicksPerSecond); // 5 seconds later
+ var currentCpuTicks = initialCpuTicks + (ulong)(0.2 * TicksPerSecondDouble); // +0.2 CPU seconds
+
+ // Act
+ var cpuPercentage = calculator.CalculateCpuPercentage(currentCpuTicks, currentTime);
+
+ // Assert & Debug
+ var actualCpuUsagePercent = (0.2 / 5.0) * 100; // 4% of wall clock time
+ var expectedPercentOfLimit = (0.2 / kubernetesLimit) / 5.0 * 100; // % of allocated CPU budget
+
+ Console.WriteLine($"\n=== {testCase} ===");
+ Console.WriteLine($"Kubernetes CPU limit: {kubernetesLimit} cores");
+ Console.WriteLine($"System cores: {coreCount}");
+ Console.WriteLine($"Calculated _cpuLimit: {cpuLimit}");
+ Console.WriteLine($"Actual CPU usage: 0.2 cores over 5 seconds = {actualCpuUsagePercent:F1}% of wall clock");
+ Console.WriteLine($"Expected % of CPU budget: {expectedPercentOfLimit:F1}%");
+ Console.WriteLine($"Your algorithm reports: {cpuPercentage:F1}%");
+ Console.WriteLine($"Difference factor: {expectedPercentOfLimit / cpuPercentage:F1}x");
+
+ // Verify the scaling issue
+ if (testCase.Contains("Current implementation"))
+ {
+ Assert.True(cpuPercentage < 5, $"Current implementation should show low percentage due to scaling issue. Got: {cpuPercentage}%");
+ }
+ else
+ {
+ Assert.InRange(cpuPercentage, 10, 15); // Should be around 13.3% with correct scaling
+ }
+ }
+
+ [Fact]
+ public void CpuPercentage_RealisticKubernetesScenario_ShowsProblem()
+ {
+ // Arrange - Your exact scenario
+ // K8s: 0.3 CPU limit, 4-core node
+ double cpuLimit = 0.3 * 4; // 1.2 (current implementation)
+ var calculator = new TestCpuCalculator(cpuLimit);
+
+ // Container running for a while, consumed 2 seconds of CPU total
+ var startTime = DateTime.UtcNow.AddMinutes(-5).Ticks;
+ calculator.SetInitialState((ulong)(2.0 * TicksPerSecondDouble), startTime);
+
+ // 5 seconds later, consumed 0.15 more CPU seconds (moderate load)
+ var currentTime = startTime + (5 * TimeSpan.TicksPerSecond);
+ var currentCpuTicks = (ulong)((2.0 + 0.15) * TicksPerSecondDouble);
+
+ // Act
+ var result = calculator.CalculateCpuPercentage(currentCpuTicks, currentTime);
+
+ // Assert
+ Console.WriteLine($"\nRealistic Scenario:");
+ Console.WriteLine($"Used 0.15 CPU cores over 5 seconds");
+ Console.WriteLine($"That's {(0.15 / 5.0) * 100:F1}% of wall clock time");
+ Console.WriteLine($"With 0.3 CPU limit, that's {(0.15 / 0.3 / 5.0) * 100:F1}% of CPU budget per second");
+ Console.WriteLine($"Your algorithm reports: {result:F1}%");
+
+ // This demonstrates the problem - should be around 10%, but will be much lower
+ Assert.True(result < 5, "Should show deflated percentage due to core scaling");
+
+ // Shows what it should be without scaling
+ var correctLimit = 0.3;
+ var correctTimeTickDelta = (currentTime - startTime) * correctLimit;
+ var correctPercentage = ((currentCpuTicks - (ulong)(2.0 * TicksPerSecondDouble)) / correctTimeTickDelta) * 100;
+ Console.WriteLine($"With correct scaling (0.3 limit): {correctPercentage:F1}%");
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringBuilderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringBuilderTests.cs
index d2e3d5d8292..9b4fe5b943b 100644
--- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringBuilderTests.cs
+++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/ResourceMonitoringBuilderTests.cs
@@ -1,9 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System;
+using System.Diagnostics.Metrics;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Publishers;
+using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows;
using Microsoft.TestUtilities;
using Xunit;
@@ -53,4 +56,56 @@ public void AddPublisher_CalledMultipleTimes_AddsMultiplePublishersToServiceColl
Assert.IsAssignableFrom(publishersArray.First());
Assert.IsAssignableFrom(publishersArray.Last());
}
+
+ [ConditionalFact]
+ public void AddResourceMonitoring_WithoutConfigureMonitor_CannotResolveSnapshotProviderRequiringOptions()
+ {
+ // Arrange - Try to register WindowsSnapshotProvider which requires IOptions
+ var services = new ServiceCollection()
+ .AddLogging()
+ .AddSingleton(sp => new FakeMeterFactory())
+ .AddResourceMonitoring()
+ .AddSingleton(); // ← This will fail to resolve
+
+ using var provider = services.BuildServiceProvider();
+
+ // Act & Assert - Should throw because IOptions is not registered
+ var exception = Assert.Throws(() =>
+ provider.GetRequiredService());
+
+ Assert.Contains("IOptions", exception.Message);
+ }
+
+ [ConditionalFact]
+ public void AddResourceMonitoring_WithManualOptionsConfiguration_AllowsSnapshotProviderResolution()
+ {
+ // Arrange - Manually register options to fix the issue
+ var services = new ServiceCollection()
+ .AddLogging()
+ .AddSingleton(sp => new FakeMeterFactory())
+ .AddResourceMonitoring()
+ .Configure(options => { }) // ← Manual fix
+ .AddSingleton();
+
+ using var provider = services.BuildServiceProvider();
+
+ // Act & Assert - Should now work
+ var snapshotProvider = provider.GetRequiredService();
+
+ Assert.NotNull(snapshotProvider);
+ Assert.NotNull(snapshotProvider.Resources);
+ }
+
+ internal sealed class FakeMeterFactory : IMeterFactory
+ {
+ public Meter Create(MeterOptions options)
+ {
+ return new Meter(options.Name, options.Version);
+ }
+
+ public void Dispose()
+ {
+ // Nothing to dispose
+ }
+ }
}