diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.csproj index 2c193e256b2..540e7b08f3d 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.csproj +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.csproj @@ -8,7 +8,9 @@ true true + true true + true diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheck.Obsolete.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheck.Obsolete.cs new file mode 100644 index 00000000000..577d4d76e0e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheck.Obsolete.cs @@ -0,0 +1,38 @@ +// 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 System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +/// +/// Represents a health check for in-container resources . +/// +internal sealed partial class ResourceUtilizationHealthCheck : IHealthCheck +{ +#pragma warning disable CS0436 // Type conflicts with imported type + [Obsolete(DiagnosticIds.Obsoletions.NonObservableResourceMonitoringApiMessage, + DiagnosticId = DiagnosticIds.Obsoletions.NonObservableResourceMonitoringApiDiagId, + UrlFormat = DiagnosticIds.UrlFormat)] + public void ObsoleteConstructor(IResourceMonitor dataTracker) => _dataTracker = Throw.IfNull(dataTracker); + + /// + /// Runs the health check. + /// + /// A that can be used to cancel the health check. + /// A that completes when the health check has finished, yielding the status of the component being checked. +#pragma warning disable IDE0060 // Remove unused parameter + [Obsolete(DiagnosticIds.Obsoletions.NonObservableResourceMonitoringApiMessage, + DiagnosticId = DiagnosticIds.Obsoletions.NonObservableResourceMonitoringApiDiagId, + UrlFormat = DiagnosticIds.UrlFormat)] + public Task ObsoleteCheckHealthAsync(CancellationToken cancellationToken = default) + { + var utilization = _dataTracker!.GetUtilization(_options.SamplingWindow); + return ResourceUtilizationHealthCheck.EvaluateHealthStatusAsync(utilization.CpuUsedPercentage, utilization.MemoryUsedPercentage, _options); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheck.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheck.cs index 13ac4cac9bc..d2e41f3a649 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheck.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheck.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; +using System.Diagnostics.Metrics; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Diagnostics.ResourceMonitoring; @@ -13,44 +15,30 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks; /// /// Represents a health check for in-container resources . /// -internal sealed class ResourceUtilizationHealthCheck : IHealthCheck +internal sealed partial class ResourceUtilizationHealthCheck : IHealthCheck, IDisposable { + private readonly double _multiplier; + private readonly MeterListener? _meterListener; private readonly ResourceUtilizationHealthCheckOptions _options; - private readonly IResourceMonitor _dataTracker; + private IResourceMonitor? _dataTracker; + private double _cpuUsedPercentage; + private double _memoryUsedPercentage; - /// - /// Initializes a new instance of the class. - /// - /// The options. - /// The datatracker. - public ResourceUtilizationHealthCheck(IOptions options, - IResourceMonitor dataTracker) - { - _options = Throw.IfMemberNull(options, options.Value); - _dataTracker = Throw.IfNull(dataTracker); - } - - /// - /// Runs the health check. - /// - /// A context object associated with the current execution. - /// A that can be used to cancel the health check. - /// A that completes when the health check has finished, yielding the status of the component being checked. - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) +#pragma warning disable EA0014 // The async method doesn't support cancellation + public static Task EvaluateHealthStatusAsync(double cpuUsedPercentage, double memoryUsedPercentage, ResourceUtilizationHealthCheckOptions options) { - var utilization = _dataTracker.GetUtilization(_options.SamplingWindow); IReadOnlyDictionary data = new Dictionary { - { nameof(utilization.CpuUsedPercentage), utilization.CpuUsedPercentage }, - { nameof(utilization.MemoryUsedPercentage), utilization.MemoryUsedPercentage }, + { "CpuUsedPercentage", cpuUsedPercentage }, + { "MemoryUsedPercentage", memoryUsedPercentage }, }; - bool cpuUnhealthy = utilization.CpuUsedPercentage > _options.CpuThresholds.UnhealthyUtilizationPercentage; - bool memoryUnhealthy = utilization.MemoryUsedPercentage > _options.MemoryThresholds.UnhealthyUtilizationPercentage; + bool cpuUnhealthy = cpuUsedPercentage > options.CpuThresholds.UnhealthyUtilizationPercentage; + bool memoryUnhealthy = memoryUsedPercentage > options.MemoryThresholds.UnhealthyUtilizationPercentage; if (cpuUnhealthy || memoryUnhealthy) { - string message = string.Empty; + string message; if (cpuUnhealthy && memoryUnhealthy) { message = "CPU and memory usage is above the limit"; @@ -67,12 +55,12 @@ public Task CheckHealthAsync(HealthCheckContext context, Canc return Task.FromResult(HealthCheckResult.Unhealthy(message, default, data)); } - bool cpuDegraded = utilization.CpuUsedPercentage > _options.CpuThresholds.DegradedUtilizationPercentage; - bool memoryDegraded = utilization.MemoryUsedPercentage > _options.MemoryThresholds.DegradedUtilizationPercentage; + bool cpuDegraded = cpuUsedPercentage > options.CpuThresholds.DegradedUtilizationPercentage; + bool memoryDegraded = memoryUsedPercentage > options.MemoryThresholds.DegradedUtilizationPercentage; if (cpuDegraded || memoryDegraded) { - string message = string.Empty; + string message; if (cpuDegraded && memoryDegraded) { message = "CPU and memory usage is close to the limit"; @@ -91,4 +79,104 @@ public Task CheckHealthAsync(HealthCheckContext context, Canc return Task.FromResult(HealthCheckResult.Healthy(default, data)); } +#pragma warning restore EA0014 // The async method doesn't support cancellation + + /// + /// Initializes a new instance of the class. + /// + /// The options. + /// The datatracker. + public ResourceUtilizationHealthCheck(IOptions options, IResourceMonitor dataTracker) + { + _options = Throw.IfMemberNull(options, options.Value); + if (!_options.UseObservableResourceMonitoringInstruments) + { + ObsoleteConstructor(dataTracker); + return; + } + +#if NETFRAMEWORK + _multiplier = 1; +#else + // Due to a bug on Windows https://github.com/dotnet/extensions/issues/5472, + // the CPU utilization comes in the range [0, 100]. + if (OperatingSystem.IsWindows()) + { + _multiplier = 1; + } + + // On Linux, the CPU utilization comes in the correct range [0, 1], which we will be converting to percentage. + else + { +#pragma warning disable S109 // Magic numbers should not be used + _multiplier = 100; +#pragma warning restore S109 // Magic numbers should not be used + } +#endif + + _meterListener = new() + { + InstrumentPublished = OnInstrumentPublished + }; + + _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded); + _meterListener.Start(); + } + + /// + /// Runs the health check. + /// + /// A context object associated with the current execution. + /// A that can be used to cancel the health check. + /// A that completes when the health check has finished, yielding the status of the component being checked. + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + if (!_options.UseObservableResourceMonitoringInstruments) + { + return ObsoleteCheckHealthAsync(cancellationToken); + } + + _meterListener!.RecordObservableInstruments(); + + return EvaluateHealthStatusAsync(_cpuUsedPercentage, _memoryUsedPercentage, _options); + } + + /// + public void Dispose() + { + Dispose(true); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + _meterListener?.Dispose(); + } + } + + private void OnInstrumentPublished(Instrument instrument, MeterListener listener) + { + if (instrument.Meter.Name is "Microsoft.Extensions.Diagnostics.ResourceMonitoring") + { + listener.EnableMeasurementEvents(instrument); + } + } + + private void OnMeasurementRecorded( + Instrument instrument, double measurement, + ReadOnlySpan> tags, object? state) + { + switch (instrument.Name) + { + case "process.cpu.utilization": + case "container.cpu.limit.utilization": + _cpuUsedPercentage = measurement * _multiplier; + break; + case "dotnet.process.memory.virtual.utilization": + case "container.memory.limit.utilization": + _memoryUsedPercentage = measurement * _multiplier; + break; + } + } } diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckOptions.cs index 115c94da936..55832e5dcfd 100644 --- a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckOptions.cs @@ -2,9 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using Microsoft.Extensions.Diagnostics.ResourceMonitoring; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Options; using Microsoft.Shared.Data.Validation; +using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.Diagnostics.HealthChecks; @@ -20,8 +21,7 @@ public class ResourceUtilizationHealthCheckOptions /// Gets or sets thresholds for CPU utilization. /// /// - /// The thresholds are periodically compared against the utilization samples provided by - /// the registered . + /// The thresholds are periodically compared against the utilization samples provided by the Resource Monitoring library. /// [ValidateObjectMembers] public ResourceUsageThresholds CpuThresholds { get; set; } = new ResourceUsageThresholds(); @@ -30,18 +30,33 @@ public class ResourceUtilizationHealthCheckOptions /// Gets or sets thresholds for memory utilization. /// /// - /// The thresholds are periodically compared against the utilization samples provided by - /// the registered . + /// The thresholds are periodically compared against the utilization samples provided by the Resource Monitoring library. /// [ValidateObjectMembers] public ResourceUsageThresholds MemoryThresholds { get; set; } = new ResourceUsageThresholds(); /// - /// Gets or sets the time window for used for calculating CPU and memory utilization averages. + /// Gets or sets the time window used for calculating CPU and memory utilization averages. /// /// /// The default value is 5 seconds. /// +#pragma warning disable CS0436 // Type conflicts with imported type + [Obsolete(DiagnosticIds.Obsoletions.NonObservableResourceMonitoringApiMessage, + DiagnosticId = DiagnosticIds.Obsoletions.NonObservableResourceMonitoringApiDiagId, + UrlFormat = DiagnosticIds.UrlFormat)] +#pragma warning restore CS0436 // Type conflicts with imported type [TimeSpan(MinimumSamplingWindow, int.MaxValue)] public TimeSpan SamplingWindow { get; set; } = DefaultSamplingWindow; + + /// + /// Gets or sets a value indicating whether the observable instruments will be used for getting CPU and Memory usage + /// as opposed to the default API which is obsolete. + /// + /// + /// if the observable instruments are used. The default is . + /// In the future the default will be . + /// + [Experimental(diagnosticId: DiagnosticIds.Experiments.HealthChecks, UrlFormat = DiagnosticIds.UrlFormat)] + public bool UseObservableResourceMonitoringInstruments { get; set; } } 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 c5c9783fbe9..41140bb4395 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 @@ -47,5 +47,6 @@ + diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/HealthCheckTestData.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/HealthCheckTestData.cs new file mode 100644 index 00000000000..881b5817320 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/HealthCheckTestData.cs @@ -0,0 +1,138 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Test; + +public class HealthCheckTestData : IEnumerable +{ + public static IEnumerable Data => + new List + { + new object[] + { + HealthStatus.Healthy, + 0.1, + 0UL, + 1000UL, + new ResourceUsageThresholds(), + new ResourceUsageThresholds(), + "", + }, + new object[] + { + HealthStatus.Healthy, + 0.2, + 0UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.2 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.2 }, + "" + }, + new object[] + { + HealthStatus.Healthy, + 0.2, + 2UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.2 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.2 }, + "" + }, + new object[] + { + HealthStatus.Degraded, + 0.4, + 3UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + "CPU and memory usage is close to the limit" + }, + new object[] + { + HealthStatus.Unhealthy, + 0.5, + 5UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + "CPU and memory usage is above the limit" + }, + new object[] + { + HealthStatus.Unhealthy, + 0.5, + 5UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.4, UnhealthyUtilizationPercentage = 0.2 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.4, UnhealthyUtilizationPercentage = 0.2 }, + "CPU and memory usage is above the limit" + }, + new object[] + { + HealthStatus.Degraded, + 0.3, + 3UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2 }, + "CPU and memory usage is close to the limit" + }, + new object[] + { + HealthStatus.Unhealthy, + 0.5, + 5UL, + 1000UL, + new ResourceUsageThresholds { UnhealthyUtilizationPercentage = 0.4 }, + new ResourceUsageThresholds { UnhealthyUtilizationPercentage = 0.4 }, + "CPU and memory usage is above the limit" + }, + new object[] + { + HealthStatus.Degraded, + 0.3, + 3UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.9, UnhealthyUtilizationPercentage = 0.9 }, + "CPU usage is close to the limit" + }, + new object[] + { + HealthStatus.Degraded, + 0.1, + 3UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.9, UnhealthyUtilizationPercentage = 0.9 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + "Memory usage is close to the limit" + }, + new object[] + { + HealthStatus.Unhealthy, + 0.5, + 5UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.9, UnhealthyUtilizationPercentage = 0.9 }, + "CPU usage is above the limit" + }, + new object[] + { + HealthStatus.Unhealthy, + 0.1, + 5UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.9, UnhealthyUtilizationPercentage = 0.9 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + "Memory usage is above the limit" + }, + }; + + public IEnumerator GetEnumerator() => Data.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/Linux/LinuxResourceHealthCheckTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/Linux/LinuxResourceHealthCheckTests.cs new file mode 100644 index 00000000000..ddf2a40a784 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/Linux/LinuxResourceHealthCheckTests.cs @@ -0,0 +1,200 @@ +// 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.Diagnostics.Metrics; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Time.Testing; +using Microsoft.TestUtilities; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Test; + +public class LinuxResourceHealthCheckTests +{ + public static IEnumerable Data => + new List + { + new object[] + { + HealthStatus.Healthy, + 0.1, + 0UL, + 1000UL, + new ResourceUsageThresholds(), + new ResourceUsageThresholds(), + "", + }, + new object[] + { + HealthStatus.Healthy, + 0.2, + 0UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.2 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.2 }, + "" + }, + new object[] + { + HealthStatus.Healthy, + 0.2, + 2UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.2 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.2 }, + "" + }, + new object[] + { + HealthStatus.Degraded, + 0.4, + 3UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + "CPU and memory usage is close to the limit" + }, + new object[] + { + HealthStatus.Unhealthy, + 0.5, + 5UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + "CPU and memory usage is above the limit" + }, + new object[] + { + HealthStatus.Unhealthy, + 0.5, + 5UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.4, UnhealthyUtilizationPercentage = 0.2 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.4, UnhealthyUtilizationPercentage = 0.2 }, + "CPU and memory usage is above the limit" + }, + new object[] + { + HealthStatus.Degraded, + 0.3, + 3UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2 }, + "CPU and memory usage is close to the limit" + }, + new object[] + { + HealthStatus.Unhealthy, + 0.5, + 5UL, + 1000UL, + new ResourceUsageThresholds { UnhealthyUtilizationPercentage = 0.4 }, + new ResourceUsageThresholds { UnhealthyUtilizationPercentage = 0.4 }, + "CPU and memory usage is above the limit" + }, + new object[] + { + HealthStatus.Degraded, + 0.3, + 3UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.9, UnhealthyUtilizationPercentage = 0.9 }, + "CPU usage is close to the limit" + }, + new object[] + { + HealthStatus.Degraded, + 0.1, + 3UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.9, UnhealthyUtilizationPercentage = 0.9 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + "Memory usage is close to the limit" + }, + new object[] + { + HealthStatus.Unhealthy, + 0.5, + 5UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.9, UnhealthyUtilizationPercentage = 0.9 }, + "CPU usage is above the limit" + }, + new object[] + { + HealthStatus.Unhealthy, + 0.1, + 5UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.9, UnhealthyUtilizationPercentage = 0.9 }, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + "Memory usage is above the limit" + }, + }; + + [ConditionalTheory] + [MemberData(nameof(Data))] + [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux-specific test.")] + public async Task TestCpuAndMemoryChecks_WithMetrics( + HealthStatus expected, double utilization, ulong memoryUsed, ulong totalMemory, + ResourceUsageThresholds cpuThresholds, ResourceUsageThresholds memoryThresholds, + string expectedDescription) + { + var fakeClock = new FakeTimeProvider(); + var dataTracker = new Mock(); + var meterName = Guid.NewGuid().ToString(); + var logger = new FakeLogger(); + using var meter = new Meter("Microsoft.Extensions.Diagnostics.ResourceMonitoring"); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())) + .Returns(meter); + + var parser = new Mock(); + parser.Setup(x => x.GetHostCpuCount()).Returns(1); + parser.Setup(x => x.GetCgroupLimitedCpus()).Returns(1); + parser.Setup(x => x.GetCgroupRequestCpu()).Returns(1); + parser.SetupSequence(x => x.GetHostCpuUsageInNanoseconds()) + .Returns(0) + .Returns(1000); + parser.SetupSequence(x => x.GetCgroupCpuUsageInNanoseconds()) + .Returns(0) + .Returns((long)(10 * utilization)); + parser.Setup(x => x.GetMemoryUsageInBytes()).Returns(memoryUsed); + parser.Setup(x => x.GetAvailableMemoryInBytes()).Returns(totalMemory); + + var provider = new LinuxUtilizationProvider(Options.Options.Create(new()), parser.Object, meterFactoryMock.Object, logger, fakeClock); + + var checkContext = new HealthCheckContext(); + var checkOptions = new ResourceUtilizationHealthCheckOptions + { + CpuThresholds = cpuThresholds, + MemoryThresholds = memoryThresholds, + UseObservableResourceMonitoringInstruments = true + }; + + var options = Microsoft.Extensions.Options.Options.Create(checkOptions); + using var healthCheck = new ResourceUtilizationHealthCheck(options, dataTracker.Object); + + // Act + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + var healthCheckResult = await healthCheck.CheckHealthAsync(checkContext); + + // Assert + Assert.Equal(expected, healthCheckResult.Status); + Assert.NotEmpty(healthCheckResult.Data); + if (healthCheckResult.Status != HealthStatus.Healthy) + { + Assert.Equal(expectedDescription, healthCheckResult.Description); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests.csproj index db56ee5e43f..dcd6d4e40db 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests.csproj @@ -7,10 +7,15 @@ + - + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs index 5319e2922d8..dcd9fccce53 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTests.cs @@ -3,14 +3,20 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Metrics; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.ResourceMonitoring; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop; +using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Time.Testing; using Microsoft.TestUtilities; using Moq; using Xunit; +using static Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Interop.JobObjectInfo; namespace Microsoft.Extensions.Diagnostics.HealthChecks.Test; @@ -454,6 +460,83 @@ public void TestNullChecks() Assert.Throws(() => ((IHealthChecksBuilder)null!).AddResourceUtilizationHealthCheck((IConfigurationSection)null!)); } + [ConditionalTheory] + [ClassData(typeof(HealthCheckTestData))] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows-specific test.")] + public async Task TestCpuAndMemoryChecks_WithMetrics( + HealthStatus expected, double utilization, ulong memoryUsed, ulong totalMemory, + ResourceUsageThresholds cpuThresholds, ResourceUsageThresholds memoryThresholds, + string expectedDescription) + { + var logger = new FakeLogger(); + var fakeClock = new FakeTimeProvider(); + var dataTracker = new Mock(); + SYSTEM_INFO sysInfo = default; + sysInfo.NumberOfProcessors = 1; + Mock systemInfoMock = new(); + systemInfoMock.Setup(s => s.GetSystemInfo()) + .Returns(() => sysInfo); + Mock memoryInfoMock = new(); + + JOBOBJECT_CPU_RATE_CONTROL_INFORMATION cpuLimit = default; + cpuLimit.CpuRate = 7_000; + Mock jobHandleMock = new(); + jobHandleMock.Setup(j => j.GetJobCpuLimitInfo()).Returns(() => cpuLimit); + + Mock processInfoMock = new(); + var appMemoryUsage = memoryUsed; + processInfoMock.Setup(p => p.GetMemoryUsage()).Returns(() => appMemoryUsage); + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default; + limitInfo.JobMemoryLimit = new UIntPtr(totalMemory); + jobHandleMock.Setup(j => j.GetExtendedLimitInfo()).Returns(() => limitInfo); + + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION initialAccountingInfo = default; + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accountingInfoAfter1Ms = default; + accountingInfoAfter1Ms.TotalUserTime = (long)(utilization * 100); + jobHandleMock.SetupSequence(j => j.GetBasicAccountingInfo()) + .Returns(() => initialAccountingInfo) // this is called from the WindowsContainerSnapshotProvider's constructor + .Returns(() => accountingInfoAfter1Ms); // this is called from the WindowsContainerSnapshotProvider's CpuPercentage method + + using var meter = new Meter("Microsoft.Extensions.Diagnostics.ResourceMonitoring"); + var meterFactoryMock = new Mock(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())) + .Returns(meter); + + var snapshotProvider = new WindowsContainerSnapshotProvider( + memoryInfoMock.Object, + systemInfoMock.Object, + processInfoMock.Object, + logger, + meterFactoryMock.Object, + () => jobHandleMock.Object, + fakeClock, + new ResourceMonitoringOptions { CpuConsumptionRefreshInterval = TimeSpan.FromMilliseconds(1) }); + + var checkContext = new HealthCheckContext(); + var checkOptions = new ResourceUtilizationHealthCheckOptions + { + CpuThresholds = cpuThresholds, + MemoryThresholds = memoryThresholds, + UseObservableResourceMonitoringInstruments = true + }; + + var options = Microsoft.Extensions.Options.Options.Create(checkOptions); + using var healthCheck = new ResourceUtilizationHealthCheck(options, dataTracker.Object); + + // Act + fakeClock.Advance(TimeSpan.FromMilliseconds(1)); + var healthCheckResult = await healthCheck.CheckHealthAsync(checkContext); + + // Assert + Assert.Equal(expected, healthCheckResult.Status); + Assert.NotEmpty(healthCheckResult.Data); + if (healthCheckResult.Status != HealthStatus.Healthy) + { + Assert.Equal(expectedDescription, healthCheckResult.Description); + } + } + private static IConfiguration SetupResourceHealthCheckConfiguration( string cpuDegradedThreshold, string cpuUnhealthyThreshold, diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckTests.cs index 77a145c218a..c5d90fa58a1 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckTests.cs +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Diagnostics.ResourceMonitoring; using Microsoft.Extensions.Options; @@ -13,133 +12,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks.Test; public class ResourceHealthCheckTests { - public static IEnumerable Data => - new List - { - new object[] - { - HealthStatus.Healthy, - 0.1, - 0UL, - 1000UL, - new ResourceUsageThresholds(), - new ResourceUsageThresholds(), - "", - }, - new object[] - { - HealthStatus.Healthy, - 0.2, - 0UL, - 1000UL, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.2 }, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.2 }, - "" - }, - new object[] - { - HealthStatus.Healthy, - 0.2, - 2UL, - 1000UL, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.2 }, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.2 }, - "" - }, - new object[] - { - HealthStatus.Degraded, - 0.4, - 3UL, - 1000UL, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, - "CPU and memory usage is close to the limit" - }, - new object[] - { - HealthStatus.Unhealthy, - 0.5, - 5UL, - 1000UL, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, - "CPU and memory usage is above the limit" - }, - new object[] - { - HealthStatus.Unhealthy, - 0.5, - 5UL, - 1000UL, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.4, UnhealthyUtilizationPercentage = 0.2 }, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.4, UnhealthyUtilizationPercentage = 0.2 }, - "CPU and memory usage is above the limit" - }, - new object[] - { - HealthStatus.Degraded, - 0.3, - 3UL, - 1000UL, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2 }, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2 }, - "CPU and memory usage is close to the limit" - }, - new object[] - { - HealthStatus.Unhealthy, - 0.5, - 5UL, - 1000UL, - new ResourceUsageThresholds { UnhealthyUtilizationPercentage = 0.4 }, - new ResourceUsageThresholds { UnhealthyUtilizationPercentage = 0.4 }, - "CPU and memory usage is above the limit" - }, - new object[] - { - HealthStatus.Degraded, - 0.3, - 3UL, - 1000UL, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.9, UnhealthyUtilizationPercentage = 0.9 }, - "CPU usage is close to the limit" - }, - new object[] - { - HealthStatus.Degraded, - 0.1, - 3UL, - 1000UL, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.9, UnhealthyUtilizationPercentage = 0.9 }, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, - "Memory usage is close to the limit" - }, - new object[] - { - HealthStatus.Unhealthy, - 0.5, - 5UL, - 1000UL, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.9, UnhealthyUtilizationPercentage = 0.9 }, - "CPU usage is above the limit" - }, - new object[] - { - HealthStatus.Unhealthy, - 0.1, - 5UL, - 1000UL, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.9, UnhealthyUtilizationPercentage = 0.9 }, - new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, - "Memory usage is above the limit" - }, - }; - [Theory] - [MemberData(nameof(Data))] + [ClassData(typeof(HealthCheckTestData))] public async Task TestCpuAndMemoryChecks(HealthStatus expected, double utilization, ulong memoryUsed, ulong totalMemory, ResourceUsageThresholds cpuThresholds, ResourceUsageThresholds memoryThresholds, string expectedDescription) { @@ -159,7 +33,7 @@ public async Task TestCpuAndMemoryChecks(HealthStatus expected, double utilizati }; var options = Microsoft.Extensions.Options.Options.Create(checkOptions); - var healthCheck = new ResourceUtilizationHealthCheck(options, dataTracker.Object); + using var healthCheck = new ResourceUtilizationHealthCheck(options, dataTracker.Object); var healthCheckResult = await healthCheck.CheckHealthAsync(checkContext); Assert.Equal(expected, healthCheckResult.Status); Assert.NotEmpty(healthCheckResult.Data); diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Verified/WindowsContainerSnapshotProviderTests.SnapshotProvider_EmitsLogRecord.Net.verified.txt b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Verified/WindowsContainerSnapshotProviderTests.SnapshotProvider_EmitsLogRecord.Net.verified.txt index 990c5b1cbaa..294a724db56 100644 --- a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Verified/WindowsContainerSnapshotProviderTests.SnapshotProvider_EmitsLogRecord.Net.verified.txt +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/Verified/WindowsContainerSnapshotProviderTests.SnapshotProvider_EmitsLogRecord.Net.verified.txt @@ -65,4 +65,4 @@ LevelEnabled: true, Timestamp: DateTimeOffset_1 } -] \ No newline at end of file +]