diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs index 80e37246e9f..be8034a4e47 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/KubernetesMetadata.cs @@ -30,12 +30,27 @@ internal sealed class KubernetesMetadata public static KubernetesMetadata FromEnvironmentVariables(string environmentVariablePrefix) { + var limitsMemory = GetEnvironmentVariableAsUInt64($"{environmentVariablePrefix}LIMITS_MEMORY"); + var limitsCpu = GetEnvironmentVariableAsUInt64($"{environmentVariablePrefix}LIMITS_CPU"); + var requestsMemory = GetEnvironmentVariableAsUInt64($"{environmentVariablePrefix}REQUESTS_MEMORY"); + var requestsCpu = GetEnvironmentVariableAsUInt64($"{environmentVariablePrefix}REQUESTS_CPU"); + + if (limitsMemory == 0) + { + throw new InvalidOperationException($"Environment variable '{environmentVariablePrefix}LIMITS_MEMORY' is required and cannot be zero or missing."); + } + + if (limitsCpu == 0) + { + throw new InvalidOperationException($"Environment variable '{environmentVariablePrefix}LIMITS_CPU' is required and cannot be zero or missing."); + } + return new KubernetesMetadata { - LimitsMemory = GetEnvironmentVariableAsUInt64($"{environmentVariablePrefix}LIMITS_MEMORY"), - LimitsCpu = GetEnvironmentVariableAsUInt64($"{environmentVariablePrefix}LIMITS_CPU"), - RequestsMemory = GetEnvironmentVariableAsUInt64($"{environmentVariablePrefix}REQUESTS_MEMORY"), - RequestsCpu = GetEnvironmentVariableAsUInt64($"{environmentVariablePrefix}REQUESTS_CPU"), + LimitsMemory = limitsMemory, + LimitsCpu = limitsCpu, + RequestsMemory = requestsMemory, + RequestsCpu = requestsCpu, }; } 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 index 3af3f4637d9..a60a0e4d505 100644 --- 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 @@ -8,7 +8,6 @@ true - true diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs index f757b09996e..85bdef8e7dd 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationProvider.cs @@ -35,7 +35,7 @@ internal sealed class LinuxUtilizationProvider : ISnapshotProvider 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; + private double _memoryRequest; #pragma warning restore S1450 // Private fields only used as local variables in methods should become local variables private double _cpuRequest; @@ -69,8 +69,8 @@ public LinuxUtilizationProvider( _previousCgroupCpuTime = _parser.GetCgroupCpuUsageInNanoseconds(); var quota = resourceQuotaProvider.GetResourceQuota(); - _memoryLimit = quota.MaxMemoryInBytes; _cpuLimit = quota.MaxCpuInCores; + _memoryLimit = quota.MaxMemoryInBytes; _cpuRequest = quota.BaselineCpuInCores; _memoryRequest = quota.BaselineMemoryInBytes; @@ -127,7 +127,12 @@ public LinuxUtilizationProvider( _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, - observeValues: () => GetMeasurementWithRetry(MemoryPercentage), + observeValues: () => GetMeasurementWithRetry(() => MemoryPercentageLimit()), + unit: "1"); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, + observeValues: () => GetMeasurementWithRetry(() => MemoryPercentageRequest()), unit: "1"); _ = meter.CreateObservableUpDownCounter( @@ -138,12 +143,11 @@ public LinuxUtilizationProvider( _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ProcessMemoryUtilization, - observeValues: () => GetMeasurementWithRetry(MemoryPercentage), + observeValues: () => GetMeasurementWithRetry(() => MemoryPercentageLimit()), unit: "1"); - ulong memoryLimitRounded = (ulong)Math.Round(_memoryLimit); - Resources = new SystemResources(_cpuRequest, _cpuLimit, _memoryRequest, memoryLimitRounded); - _logger.SystemResourcesInfo(_cpuLimit, _cpuRequest, memoryLimitRounded, _memoryRequest); + Resources = new SystemResources(_cpuRequest, _cpuLimit, quota.BaselineMemoryInBytes, quota.MaxMemoryInBytes); + _logger.SystemResourcesInfo(_cpuLimit, _cpuRequest, quota.MaxMemoryInBytes, quota.BaselineMemoryInBytes); } public double CpuUtilizationV2() @@ -151,7 +155,7 @@ public double CpuUtilizationV2() DateTimeOffset now = _timeProvider.GetUtcNow(); lock (_cpuLocker) { - if (now < _refreshAfterCpu) + if (now <= _refreshAfterCpu) { return _lastCpuCoresUsed; } @@ -160,7 +164,7 @@ public double CpuUtilizationV2() (long cpuUsageTime, long cpuPeriodCounter) = _parser.GetCgroupCpuUsageInNanosecondsAndCpuPeriodsV2(); lock (_cpuLocker) { - if (now < _refreshAfterCpu) + if (now <= _refreshAfterCpu) { return _lastCpuCoresUsed; } @@ -193,7 +197,7 @@ public double CpuUtilization() lock (_cpuLocker) { - if (now < _refreshAfterCpu) + if (now <= _refreshAfterCpu) { return _cpuPercentage; } @@ -204,7 +208,7 @@ public double CpuUtilization() lock (_cpuLocker) { - if (now < _refreshAfterCpu) + if (now <= _refreshAfterCpu) { return _cpuPercentage; } @@ -276,12 +280,21 @@ public Snapshot GetSnapshot() memoryUsageInBytes: memoryUsed); } - private double MemoryPercentage() + private double MemoryPercentageLimit() { ulong memoryUsage = MemoryUsage(); double memoryPercentage = Math.Min(One, memoryUsage / _memoryLimit); - _logger.MemoryPercentageData(memoryUsage, _memoryLimit, memoryPercentage); + _logger.MemoryPercentageLimit(memoryUsage, _memoryLimit, memoryPercentage); + return memoryPercentage; + } + + private double MemoryPercentageRequest() + { + ulong memoryUsage = MemoryUsage(); + double memoryPercentage = Math.Min(One, memoryUsage / _memoryRequest); + + _logger.MemoryPercentageRequest(memoryUsage, _memoryRequest, memoryPercentage); return memoryPercentage; } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs index 0721a0ff998..010cfb41fc4 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Log.cs @@ -21,7 +21,7 @@ public static partial void CpuUsageData( [LoggerMessage(2, LogLevel.Debug, "Computed memory usage with MemoryUsedInBytes = {memoryUsed}, MemoryLimit = {memoryLimit}, MemoryPercentage = {memoryPercentage}.")] - public static partial void MemoryPercentageData( + public static partial void MemoryPercentageLimit( this ILogger logger, ulong memoryUsed, double memoryLimit, @@ -56,4 +56,12 @@ public static partial void HandleDiskStatsException( public static partial void MemoryUsageData( this ILogger logger, ulong memoryUsed); + + [LoggerMessage(7, LogLevel.Debug, + "Computed memory usage with MemoryUsedInBytes = {memoryUsed}, MemoryRequest = {memoryRequest}, MemoryPercentage = {memoryPercentage}.")] + public static partial void MemoryPercentageRequest( + this ILogger logger, + ulong memoryUsed, + double memoryRequest, + double memoryPercentage); } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj index 7cdb4b7de49..173ae5f87eb 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj @@ -50,6 +50,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuota.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuota.cs index 2e09eb9fc86..d5131923637 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuota.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceQuota.cs @@ -12,6 +12,9 @@ namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; /// Maximum values define the upper limits of resource usage, while baseline values specify /// the minimum assured resource allocations, usually based on Kubernetes requests or Linux shares and weights distribution. /// +/// +/// Max values will be emitted by limit metrics, and baseline values will be emmited by request metrics. +/// [Experimental(diagnosticId: DiagnosticIds.Experiments.ResourceMonitoring, UrlFormat = DiagnosticIds.UrlFormat)] public sealed class ResourceQuota { diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Log.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Log.cs index c91cd989362..25940bb7d28 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Log.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Log.cs @@ -24,11 +24,12 @@ public static partial void CpuUsageData( double cpuPercentage); [LoggerMessage(4, LogLevel.Debug, - "Computed memory usage for container: CurrentMemoryUsage = {currentMemoryUsage}, TotalMemory = {totalMemory}")] + "Computed memory usage for container: CurrentMemoryUsage = {currentMemoryUsage}, LimitMemory = {limitMemory}, RequestMemory = {requestMemory}.")] public static partial void ContainerMemoryUsageData( this ILogger logger, ulong currentMemoryUsage, - double totalMemory); + double limitMemory, + double requestMemory); [LoggerMessage(5, LogLevel.Debug, "Computed CPU usage with CpuUsageKernelTicks = {cpuUsageKernelTicks}, CpuUsageUserTicks = {cpuUsageUserTicks}, OldCpuUsageTicks = {oldCpuUsageTicks}, TimeTickDelta = {timeTickDelta}, CpuUnits = {cpuUnits}, CpuPercentage = {cpuPercentage}.")] public static partial void CpuContainerUsageData( diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs index 0bf37035ed6..da80b428a30 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsContainerSnapshotProvider.cs @@ -36,7 +36,7 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider 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 _memoryRequest; private double _cpuRequest; #pragma warning restore S1450 // Private fields only used as local variables in methods should become local variables @@ -45,7 +45,8 @@ internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider private DateTimeOffset _refreshAfterCpu; private DateTimeOffset _refreshAfterMemory; private DateTimeOffset _refreshAfterProcessMemory; - private double _cpuPercentage = double.NaN; + private long _cachedUsageTickDelta; + private long _cachedTimeTickDelta; private double _processMemoryPercentage; private ulong _memoryUsage; @@ -95,9 +96,8 @@ internal WindowsContainerSnapshotProvider( _cpuRequest = quota.BaselineCpuInCores; _memoryRequest = quota.BaselineMemoryInBytes; - ulong memoryLimitRounded = (ulong)Math.Round(_memoryLimit); - Resources = new SystemResources(_cpuRequest, _cpuLimit, _memoryRequest, memoryLimitRounded); - _logger.SystemResourcesInfo(_cpuLimit, _cpuRequest, memoryLimitRounded, _memoryRequest); + Resources = new SystemResources(_cpuRequest, _cpuLimit, quota.BaselineMemoryInBytes, quota.MaxMemoryInBytes); + _logger.SystemResourcesInfo(_cpuLimit, _cpuRequest, quota.MaxMemoryInBytes, quota.BaselineMemoryInBytes); var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); _oldCpuUsageTicks = basicAccountingInfo.TotalKernelTime + basicAccountingInfo.TotalUserTime; @@ -123,12 +123,20 @@ internal WindowsContainerSnapshotProvider( _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerCpuLimitUtilization, - observeValue: CpuPercentage); + observeValue: () => CpuPercentage(_cpuLimit)); _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, observeValue: () => Math.Min(_metricValueMultiplier, MemoryUsage() / _memoryLimit * _metricValueMultiplier)); + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerCpuRequestUtilization, + observeValue: () => CpuPercentage(_cpuRequest)); + + _ = meter.CreateObservableGauge( + name: ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, + observeValue: () => Math.Min(_metricValueMultiplier, MemoryUsage() / _memoryRequest * _metricValueMultiplier)); + _ = meter.CreateObservableUpDownCounter( name: ResourceUtilizationInstruments.ContainerMemoryUsage, observeValue: () => (long)MemoryUsage(), @@ -138,7 +146,7 @@ internal WindowsContainerSnapshotProvider( // Process based metrics: _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ProcessCpuUtilization, - observeValue: CpuPercentage); + observeValue: () => CpuPercentage(_cpuLimit)); _ = meter.CreateObservableGauge( name: ResourceUtilizationInstruments.ProcessMemoryUtilization, @@ -207,7 +215,7 @@ private ulong MemoryUsage() { _memoryUsage = memoryUsage; _refreshAfterMemory = now.Add(_memoryRefreshInterval); - _logger.ContainerMemoryUsageData(_memoryUsage, _memoryLimit); + _logger.ContainerMemoryUsageData(_memoryUsage, _memoryLimit, _memoryRequest); } return _memoryUsage; @@ -225,35 +233,27 @@ private IEnumerable> GetCpuTime() [new KeyValuePair("cpu.mode", "system")]); } - private double CpuPercentage() + private double CpuPercentage(double denominator) { var now = _timeProvider.GetUtcNow(); - - lock (_cpuLocker) - { - if (now < _refreshAfterCpu) - { - return _cpuPercentage; - } - } - - using var jobHandle = _createJobHandleObject(); - var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); - var currentCpuTicks = basicAccountingInfo.TotalKernelTime + basicAccountingInfo.TotalUserTime; - lock (_cpuLocker) { if (now >= _refreshAfterCpu) { + using var jobHandle = _createJobHandleObject(); + var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); + var currentCpuTicks = basicAccountingInfo.TotalKernelTime + basicAccountingInfo.TotalUserTime; + var usageTickDelta = currentCpuTicks - _oldCpuUsageTicks; - var timeTickDelta = (now.Ticks - _oldCpuTimeTicks) * _cpuLimit; + var timeTickDelta = now.Ticks - _oldCpuTimeTicks; + if (usageTickDelta > 0 && timeTickDelta > 0) { - // Don't change calculation order, otherwise precision is lost: - _cpuPercentage = Math.Min(_metricValueMultiplier, usageTickDelta / timeTickDelta * _metricValueMultiplier); + _cachedUsageTickDelta = usageTickDelta; + _cachedTimeTickDelta = timeTickDelta; _logger.CpuContainerUsageData( - basicAccountingInfo.TotalKernelTime, basicAccountingInfo.TotalUserTime, _oldCpuUsageTicks, timeTickDelta, _cpuLimit, _cpuPercentage); + basicAccountingInfo.TotalKernelTime, basicAccountingInfo.TotalUserTime, _oldCpuUsageTicks, timeTickDelta, denominator, double.NaN); _oldCpuUsageTicks = currentCpuTicks; _oldCpuTimeTicks = now.Ticks; @@ -261,7 +261,15 @@ private double CpuPercentage() } } - return _cpuPercentage; + if (_cachedUsageTickDelta > 0 && _cachedTimeTickDelta > 0) + { + var timeTickDeltaWithDenominator = _cachedTimeTickDelta * denominator; + + // Don't change calculation order, otherwise precision is lost: + return Math.Min(_metricValueMultiplier, _cachedUsageTickDelta / timeTickDeltaWithDenominator * _metricValueMultiplier); + } + + return double.NaN; } } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs index 82b94dad9e6..122f1813818 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsSnapshotProvider.cs @@ -45,6 +45,7 @@ public WindowsSnapshotProvider(ILogger? logger, IMeterF { } +#pragma warning disable S107 // Methods should not have too many parameters internal WindowsSnapshotProvider( ILogger? logger, IMeterFactory meterFactory, @@ -54,6 +55,7 @@ internal WindowsSnapshotProvider( Func getCpuTicksFunc, Func getMemoryUsageFunc, Func getTotalMemoryInBytesFunc) +#pragma warning restore S107 // Methods should not have too many parameters { _logger = logger ?? NullLogger.Instance; diff --git a/src/Shared/Instruments/ResourceUtilizationInstruments.cs b/src/Shared/Instruments/ResourceUtilizationInstruments.cs index b1593edd396..f3eec4543d1 100644 --- a/src/Shared/Instruments/ResourceUtilizationInstruments.cs +++ b/src/Shared/Instruments/ResourceUtilizationInstruments.cs @@ -50,6 +50,14 @@ internal static class ResourceUtilizationInstruments /// public const string ContainerMemoryLimitUtilization = "container.memory.limit.utilization"; + /// + /// The name of an instrument to retrieve memory request consumption of all processes running inside a container or control group in range [0, 1]. + /// + /// + /// The type of an instrument is . + /// + public const string ContainerMemoryRequestUtilization = "container.memory.request.utilization"; + /// /// The name of an instrument to retrieve memory usage measured in bytes of all processes running inside a container or control group. /// diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesMetadataTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesMetadataTests.cs index 0d925daaf21..cb8619d7123 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesMetadataTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesMetadataTests.cs @@ -72,7 +72,7 @@ public void Build_WithCustomPrefix_ReadsCorrectEnvironmentVariables() } [Fact] - public void Build_WithMissingEnvironmentVariables_SetsZeroValues() + public void Build_WithMissingEnvironmentVariables_ThrowsInvalidOperationException() { // Arrange var tempSetup = new TestKubernetesEnvironmentSetup(); @@ -80,14 +80,12 @@ public void Build_WithMissingEnvironmentVariables_SetsZeroValues() try { - // Act - var result = KubernetesMetadata.FromEnvironmentVariables("NONEXISTENT_PREFIX_"); + // Act & Assert + var exception = Assert.Throws(() => + KubernetesMetadata.FromEnvironmentVariables("NONEXISTENT_PREFIX_")); - // Assert - Assert.Equal(0UL, result.LimitsMemory); - Assert.Equal(0UL, result.LimitsCpu); - Assert.Equal(0UL, result.RequestsMemory); - Assert.Equal(0UL, result.RequestsCpu); + Assert.Contains("LIMITS_MEMORY", exception.Message); + Assert.Contains("is required and cannot be zero or missing", exception.Message); } finally { @@ -100,7 +98,7 @@ public void Build_WithMissingEnvironmentVariables_SetsZeroValues() [InlineData(" ")] [InlineData("\t")] [InlineData("\n")] - public void Build_WithEmptyOrWhitespaceEnvironmentVariables_SetsZeroValues(string envValue) + public void Build_WithEmptyOrWhitespaceEnvironmentVariables_ThrowsInvalidOperationException(string envValue) { // Arrange var tempSetup = new TestKubernetesEnvironmentSetup(); @@ -111,14 +109,12 @@ public void Build_WithEmptyOrWhitespaceEnvironmentVariables_SetsZeroValues(strin try { - // Act - var result = KubernetesMetadata.FromEnvironmentVariables(string.Empty); + // Act & Assert + var exception = Assert.Throws(() => + KubernetesMetadata.FromEnvironmentVariables(string.Empty)); - // Assert - Assert.Equal(0UL, result.LimitsMemory); - Assert.Equal(0UL, result.LimitsCpu); - Assert.Equal(0UL, result.RequestsMemory); - Assert.Equal(0UL, result.RequestsCpu); + Assert.Contains("LIMITS_MEMORY", exception.Message); + Assert.Contains("is required and cannot be zero or missing", exception.Message); } finally { diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesResourceQuotasServiceCollectionExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesResourceQuotasServiceCollectionExtensionsTests.cs index f39f9fb5cf2..a7f70d1cbb5 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesResourceQuotasServiceCollectionExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/KubernetesResourceQuotasServiceCollectionExtensionsTests.cs @@ -128,5 +128,19 @@ public void AddKubernetesResourceMonitoring_WithValidEnvironmentData_RegistersIS Assert.Equal(limitsMemory, quota.MaxMemoryInBytes); Assert.Equal(requestsMemory, quota.BaselineMemoryInBytes); } + + [Fact] + public void LinuxUtilizationProvider_ThrowsWhenLimitsNotFound() + { + using var environmentSetup = new TestKubernetesEnvironmentSetup(); + environmentSetup.SetEnvironmentVariable("TEST_REQUESTS_MEMORY", "1000"); + environmentSetup.SetEnvironmentVariable("TEST_REQUESTS_CPU", "1000"); + + var exception = Assert.Throws(() => + KubernetesMetadata.FromEnvironmentVariables("TEST_")); + + Assert.Contains("LIMITS_MEMORY", exception.Message); + Assert.Contains("is required and cannot be zero or missing", exception.Message); + } #pragma warning restore CS0618 } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Linux/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Linux/AcceptanceTest.cs new file mode 100644 index 00000000000..84bab7c4e8b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Linux/AcceptanceTest.cs @@ -0,0 +1,151 @@ +// 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.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Time.Testing; +using Microsoft.Shared.Instruments; +using Microsoft.TestUtilities; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Linux; + +[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific tests")] +public class AcceptanceTest +{ + [Fact] + public async Task LinuxUtilizationProvider_MeasuredWithKubernetesMetadata() + { + const ulong LimitsMemory = 2_000; + const ulong LimitsCpu = 2_000; // 2 cores in millicores + const ulong RequestsMemory = 1_000; + const ulong RequestsCpu = 1_000; // 1 core in millicores + + using var environmentSetup = new TestKubernetesEnvironmentSetup(); + + environmentSetup.SetupKubernetesEnvironment( + "ACCEPTANCE_TEST_", + LimitsMemory, + LimitsCpu, + RequestsMemory, + RequestsCpu); + + var logger = new FakeLogger(); + var parserMock = new Mock(); + + ulong initialMemoryUsage = 400UL; + ulong updatedMemoryUsage = 1200UL; + + var memoryCallCount = 0; + parserMock.Setup(p => p.GetMemoryUsageInBytes()) + .Returns(() => + { + memoryCallCount++; + return memoryCallCount <= 1 ? initialMemoryUsage : updatedMemoryUsage; + }); + + long initialHostCpuTime = 1000L; + long initialCgroupCpuTime = 100L; + long updatedHostCpuTime = 2000L; + long updatedCgroupCpuTime = 300L; + + var hostCpuCallCount = 0; + parserMock.Setup(p => p.GetHostCpuUsageInNanoseconds()) + .Returns(() => + { + hostCpuCallCount++; + return hostCpuCallCount <= 1 ? initialHostCpuTime : updatedHostCpuTime; + }); + + var cgroupCpuCallCount = 0; + parserMock.Setup(p => p.GetCgroupCpuUsageInNanoseconds()) + .Returns(() => + { + cgroupCpuCallCount++; + return cgroupCpuCallCount <= 1 ? initialCgroupCpuTime : updatedCgroupCpuTime; + }); + + parserMock.Setup(p => p.GetHostCpuCount()).Returns(2.0f); + parserMock.Setup(p => p.GetAvailableMemoryInBytes()).Returns(LimitsMemory); + + var fakeClock = new FakeTimeProvider(); + using var meter = new Meter(nameof(LinuxUtilizationProvider_MeasuredWithKubernetesMetadata)); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())).Returns(meter); + + using var containerCpuLimitMetricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.ContainerCpuLimitUtilization, fakeClock); + using var containerMemoryLimitMetricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, fakeClock); + using var containerCpuRequestMetricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.ContainerCpuRequestUtilization, fakeClock); + using var containerMemoryRequestMetricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, fakeClock); + + var options = Microsoft.Extensions.Options.Options.Create(new ResourceMonitoringOptions + { + MemoryConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2), + CpuConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2), + UseZeroToOneRangeForMetrics = false + }); + + var kubernetesMetadata = new KubernetesMetadata + { + LimitsMemory = LimitsMemory, + LimitsCpu = LimitsCpu, + RequestsMemory = RequestsMemory, + RequestsCpu = RequestsCpu + }; + var kubernetesResourceQuotaProvider = new KubernetesResourceQuotaProvider(kubernetesMetadata); + + var utilizationProvider = new LinuxUtilizationProvider( + options, + parserMock.Object, + meterFactoryMock.Object, + kubernetesResourceQuotaProvider, + logger, + fakeClock); + + containerCpuLimitMetricCollector.RecordObservableInstruments(); + containerMemoryLimitMetricCollector.RecordObservableInstruments(); + containerCpuRequestMetricCollector.RecordObservableInstruments(); + containerMemoryRequestMetricCollector.RecordObservableInstruments(); + + Assert.NotNull(containerCpuLimitMetricCollector.LastMeasurement?.Value); + Assert.NotNull(containerMemoryLimitMetricCollector.LastMeasurement?.Value); + Assert.NotNull(containerCpuRequestMetricCollector.LastMeasurement?.Value); + Assert.NotNull(containerMemoryRequestMetricCollector.LastMeasurement?.Value); + + Assert.True(double.IsNaN(containerCpuLimitMetricCollector.LastMeasurement.Value)); + Assert.True(double.IsNaN(containerCpuRequestMetricCollector.LastMeasurement.Value)); + + // Container memory: 400 bytes / 2000 bytes limit = 20% + Assert.Equal(0.2, containerMemoryLimitMetricCollector.LastMeasurement.Value); + + // Container memory: 400 bytes / 1000 bytes request = 40% + Assert.Equal(0.4, containerMemoryRequestMetricCollector.LastMeasurement.Value); + + fakeClock.Advance(options.Value.MemoryConsumptionRefreshInterval); + containerCpuLimitMetricCollector.RecordObservableInstruments(); + containerMemoryLimitMetricCollector.RecordObservableInstruments(); + containerCpuRequestMetricCollector.RecordObservableInstruments(); + containerMemoryRequestMetricCollector.RecordObservableInstruments(); + + // CPU calculation: (300-100)/(2000-1000) = 200/1000 = 0.2 = 20% + // With 2 host CPUs and 2 core limit: 20% * 2/2 = 20% + Assert.Equal(0.2, containerCpuLimitMetricCollector.LastMeasurement.Value); + + // CPU against 1 core request: 20% * 2/1 = 40% + Assert.Equal(0.4, containerCpuRequestMetricCollector.LastMeasurement.Value); + + // Container memory: 1200 bytes / 2000 bytes limit = 60% + Assert.Equal(0.6, containerMemoryLimitMetricCollector.LastMeasurement.Value); + + // Container memory: 1200 bytes / 1000 bytes request = 120% + Assert.Equal(1, containerMemoryRequestMetricCollector.LastMeasurement.Value); + + await Task.CompletedTask; + } +} 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 index b931a014264..e964e6aa3e0 100644 --- 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 @@ -8,8 +8,16 @@ + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Windows/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Windows/AcceptanceTest.cs new file mode 100644 index 00000000000..8d737a77388 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.Tests/Windows/AcceptanceTest.cs @@ -0,0 +1,180 @@ +// 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.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Time.Testing; +using Microsoft.Shared.Instruments; +using Moq; +using Xunit; +using static Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop.JobObjectInfo; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Windows; + +public class AcceptanceTest +{ + [Fact] + public async Task WindowsContainerSnapshotProvider_MeasuredWithKubernetesMetadata() + { + const ulong LimitsMemory = 2_000; + const ulong LimitsCpu = 2_000; // 2 cores in millicores + const ulong RequestsMemory = 1_000; + const ulong RequestsCpu = 1_000; // 1 core in millicores + + using var environmentSetup = new TestKubernetesEnvironmentSetup(); + + environmentSetup.SetupKubernetesEnvironment( + "ACCEPTANCE_TEST_", + LimitsMemory, + LimitsCpu, + RequestsMemory, + RequestsCpu); + + var logger = new FakeLogger(); + var memoryInfoMock = new Mock(); + var systemInfoMock = new Mock(); + var jobHandleMock = new Mock(); + var processInfoMock = new Mock(); + + var memStatus = new MEMORYSTATUSEX { TotalPhys = 3000UL }; + memoryInfoMock.Setup(m => m.GetMemoryStatus()).Returns(() => memStatus); + + var sysInfo = new SYSTEM_INFO { NumberOfProcessors = 2 }; + systemInfoMock.Setup(s => s.GetSystemInfo()).Returns(() => sysInfo); + + // Setup CPU accounting info with initial and updated values + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION initialAccountingInfo = new() + { + TotalKernelTime = 1000, + TotalUserTime = 1000 + }; + + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION updatedAccountingInfo = new() + { + TotalKernelTime = 1500, // 500 ticks increase + TotalUserTime = 1500 // 500 ticks increase + }; + + var accountingCallCount = 0; + jobHandleMock.Setup(j => j.GetBasicAccountingInfo()) + .Returns(() => + { + accountingCallCount++; + return accountingCallCount <= 1 ? initialAccountingInfo : updatedAccountingInfo; + }); + + var cpuLimit = new JOBOBJECT_CPU_RATE_CONTROL_INFORMATION { CpuRate = 10_000 }; + jobHandleMock.Setup(j => j.GetJobCpuLimitInfo()).Returns(() => cpuLimit); + + var limitInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + JobMemoryLimit = new UIntPtr(LimitsMemory) + }; + jobHandleMock.Setup(j => j.GetExtendedLimitInfo()).Returns(() => limitInfo); + + ulong initialAppMemoryUsage = 200UL; + ulong initialContainerMemoryUsage = 400UL; + ulong updatedAppMemoryUsage = 600UL; + ulong updatedContainerMemoryUsage = 1200UL; + + var processMemoryCallCount = 0; + processInfoMock.Setup(p => p.GetCurrentProcessMemoryUsage()) + .Returns(() => + { + processMemoryCallCount++; + return processMemoryCallCount <= 1 ? initialAppMemoryUsage : updatedAppMemoryUsage; + }); + + var containerMemoryCallCount = 0; + processInfoMock.Setup(p => p.GetMemoryUsage()) + .Returns(() => + { + containerMemoryCallCount++; + return containerMemoryCallCount <= 1 ? initialContainerMemoryUsage : updatedContainerMemoryUsage; + }); + + var fakeClock = new FakeTimeProvider(); + using var meter = new Meter(nameof(WindowsContainerSnapshotProvider_MeasuredWithKubernetesMetadata)); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())).Returns(meter); + + using var containerCpuLimitMetricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.ContainerCpuLimitUtilization, fakeClock); + using var containerMemoryLimitMetricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, fakeClock); + using var containerCpuRequestMetricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.ContainerCpuRequestUtilization, fakeClock); + using var containerMemoryRequestMetricCollector = new MetricCollector(meter, ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, fakeClock); + + var options = new ResourceMonitoringOptions + { + MemoryConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2), + CpuConsumptionRefreshInterval = TimeSpan.FromMilliseconds(2), + UseZeroToOneRangeForMetrics = false + }; + + var kubernetesMetadata = new KubernetesMetadata + { + LimitsMemory = LimitsMemory, + LimitsCpu = LimitsCpu, + RequestsMemory = RequestsMemory, + RequestsCpu = RequestsCpu + }; + var kubernetesResourceQuotaProvider = new KubernetesResourceQuotaProvider(kubernetesMetadata); + + var snapshotProvider = new WindowsContainerSnapshotProvider( + processInfoMock.Object, + logger, + meterFactoryMock.Object, + () => jobHandleMock.Object, + fakeClock, + options, + kubernetesResourceQuotaProvider); + + // Initial state + containerCpuLimitMetricCollector.RecordObservableInstruments(); + containerMemoryLimitMetricCollector.RecordObservableInstruments(); + containerCpuRequestMetricCollector.RecordObservableInstruments(); + containerMemoryRequestMetricCollector.RecordObservableInstruments(); + + Assert.NotNull(containerCpuLimitMetricCollector.LastMeasurement?.Value); + Assert.NotNull(containerMemoryLimitMetricCollector.LastMeasurement?.Value); + Assert.NotNull(containerCpuRequestMetricCollector.LastMeasurement?.Value); + Assert.NotNull(containerMemoryRequestMetricCollector.LastMeasurement?.Value); + + // Initial CPU metrics should be NaN as no time has passed for calculation + Assert.True(double.IsNaN(containerCpuLimitMetricCollector.LastMeasurement.Value)); + Assert.True(double.IsNaN(containerCpuRequestMetricCollector.LastMeasurement.Value)); + + // Container memory: 400 bytes / 2000 bytes limit = 20% + Assert.Equal(20, containerMemoryLimitMetricCollector.LastMeasurement.Value); + + // Container memory: 400 bytes / 1000 bytes request = 40% + Assert.Equal(40, containerMemoryRequestMetricCollector.LastMeasurement.Value); + + // Advance time to trigger refresh + fakeClock.Advance(options.MemoryConsumptionRefreshInterval); + + containerCpuLimitMetricCollector.RecordObservableInstruments(); + containerMemoryLimitMetricCollector.RecordObservableInstruments(); + containerCpuRequestMetricCollector.RecordObservableInstruments(); + containerMemoryRequestMetricCollector.RecordObservableInstruments(); + + // CPU: 1000 ticks increase over 2ms = 2.5% utilization against 2 core limit + Assert.Equal(2.5, containerCpuLimitMetricCollector.LastMeasurement.Value); + + // CPU: 2.5% of 2 cores against 1 core request = 5% + Assert.Equal(5, containerCpuRequestMetricCollector.LastMeasurement.Value); + + // Container memory: 1200 bytes / 2000 bytes limit = 60% + Assert.Equal(60, containerMemoryLimitMetricCollector.LastMeasurement.Value); + + // Container memory: 1200 bytes / 1000 bytes request = 120%, but Min() to 100% + Assert.Equal(100, containerMemoryRequestMetricCollector.LastMeasurement.Value); + + await Task.CompletedTask; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs index d5d6ea3a8c4..7aac3b83a43 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTests.cs @@ -84,7 +84,7 @@ public void Provider_Registers_Instruments() listener.Start(); listener.RecordObservableInstruments(); - Assert.Equal(6, samples.Count); + Assert.Equal(7, samples.Count); Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization).value)); @@ -92,6 +92,9 @@ public void Provider_Registers_Instruments() Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuRequestUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuRequestUtilization).value)); + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryRequestUtilization); + Assert.Equal(0.5, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryRequestUtilization).value); + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization); Assert.Equal(0.5, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization).value); @@ -158,7 +161,7 @@ public void Provider_Registers_Instruments_CgroupV2() listener.Start(); listener.RecordObservableInstruments(); - Assert.Equal(5, samples.Count); + Assert.Equal(6, samples.Count); Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization).value)); @@ -169,6 +172,9 @@ public void Provider_Registers_Instruments_CgroupV2() Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization); Assert.Equal(1, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization).value); + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryRequestUtilization); + Assert.Equal(1, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryRequestUtilization).value); + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ProcessCpuUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ProcessCpuUtilization).value)); @@ -276,7 +282,7 @@ public void Provider_Registers_Instruments_CgroupV2_WithoutHostCpu() listener.Start(); listener.RecordObservableInstruments(); - Assert.Equal(6, samples.Count); + Assert.Equal(7, samples.Count); Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization); Assert.True(double.IsNaN(samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerCpuLimitUtilization).value)); @@ -290,6 +296,9 @@ public void Provider_Registers_Instruments_CgroupV2_WithoutHostCpu() Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization); Assert.Equal(1, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryLimitUtilization).value); + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryRequestUtilization); + Assert.Equal(1, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ContainerMemoryRequestUtilization).value); + Assert.Contains(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization); Assert.Equal(1, samples.Single(i => i.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization).value); } @@ -376,8 +385,9 @@ public void Provider_GetMeasurementWithRetry_UnhandledException_DoesNotBlockFutu var parserMock = new Mock(); parserMock.Setup(p => p.GetMemoryUsageInBytes()).Returns(() => { + // 4 represents number of calls of GetMemoryUsageInBytes() in LinuxUtilizationProvider in one interval callCount++; - if (callCount <= 3) + if (callCount <= 4) { throw new InvalidOperationException("Simulated unhandled exception"); } @@ -416,12 +426,13 @@ public void Provider_GetMeasurementWithRetry_UnhandledException_DoesNotBlockFutu Assert.Throws(() => listener.RecordObservableInstruments()); Assert.DoesNotContain(samples, x => x.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization); + parserMock.Verify(p => p.GetMemoryUsageInBytes(), Times.Exactly(4)); fakeTime.Advance(TimeSpan.FromMinutes(1)); listener.RecordObservableInstruments(); var metric = samples.SingleOrDefault(x => x.instrument.Name == ResourceUtilizationInstruments.ProcessMemoryUtilization); Assert.Equal(1234f / 2000f, metric.value, 0.01f); - parserMock.Verify(p => p.GetMemoryUsageInBytes(), Times.Exactly(4)); + parserMock.Verify(p => p.GetMemoryUsageInBytes(), Times.Exactly(5)); } } diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs index c27fb3a7712..9abb88dc185 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTests.cs @@ -256,6 +256,8 @@ public void SnapshotProvider_EmitsCpuTimeMetric() [InlineData(ResourceUtilizationInstruments.ProcessCpuUtilization, false)] [InlineData(ResourceUtilizationInstruments.ContainerCpuLimitUtilization, true)] [InlineData(ResourceUtilizationInstruments.ContainerCpuLimitUtilization, false)] + [InlineData(ResourceUtilizationInstruments.ContainerCpuRequestUtilization, true)] + [InlineData(ResourceUtilizationInstruments.ContainerCpuRequestUtilization, false)] public void SnapshotProvider_EmitsCpuMetrics(string instrumentName, bool useZeroToOneRange) { // Simulating 10% CPU usage (2 CPUs, 2000 ticks initially, 4000 ticks after 1 ms): @@ -327,6 +329,8 @@ public void SnapshotProvider_EmitsCpuMetrics(string instrumentName, bool useZero [InlineData(ResourceUtilizationInstruments.ProcessMemoryUtilization, false)] [InlineData(ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, true)] [InlineData(ResourceUtilizationInstruments.ContainerMemoryLimitUtilization, false)] + [InlineData(ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, true)] + [InlineData(ResourceUtilizationInstruments.ContainerMemoryRequestUtilization, false)] public void SnapshotProvider_EmitsMemoryMetrics(string instrumentName, bool useZeroToOneRange) { _appMemoryUsage = 200UL;