Skip to content

Commit 2534f08

Browse files
Make HealthChecks.ResourceUtilization use observable instruments (#5798)
1 parent acc985a commit 2534f08

File tree

11 files changed

+610
-166
lines changed

11 files changed

+610
-166
lines changed

src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
<PropertyGroup>
99
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
1010
<InjectSharedDataValidation>true</InjectSharedDataValidation>
11+
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
1112
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
13+
<InjectObsoleteAttributeOnLegacy>true</InjectObsoleteAttributeOnLegacy>
1214
</PropertyGroup>
1315

1416
<PropertyGroup>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.Extensions.Diagnostics.ResourceMonitoring;
8+
using Microsoft.Shared.DiagnosticIds;
9+
using Microsoft.Shared.Diagnostics;
10+
11+
namespace Microsoft.Extensions.Diagnostics.HealthChecks;
12+
13+
/// <summary>
14+
/// Represents a health check for in-container resources <see cref="IHealthCheck"/>.
15+
/// </summary>
16+
internal sealed partial class ResourceUtilizationHealthCheck : IHealthCheck
17+
{
18+
#pragma warning disable CS0436 // Type conflicts with imported type
19+
[Obsolete(DiagnosticIds.Obsoletions.NonObservableResourceMonitoringApiMessage,
20+
DiagnosticId = DiagnosticIds.Obsoletions.NonObservableResourceMonitoringApiDiagId,
21+
UrlFormat = DiagnosticIds.UrlFormat)]
22+
public void ObsoleteConstructor(IResourceMonitor dataTracker) => _dataTracker = Throw.IfNull(dataTracker);
23+
24+
/// <summary>
25+
/// Runs the health check.
26+
/// </summary>
27+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the health check.</param>
28+
/// <returns>A <see cref="Task{HealthCheckResult}"/> that completes when the health check has finished, yielding the status of the component being checked.</returns>
29+
#pragma warning disable IDE0060 // Remove unused parameter
30+
[Obsolete(DiagnosticIds.Obsoletions.NonObservableResourceMonitoringApiMessage,
31+
DiagnosticId = DiagnosticIds.Obsoletions.NonObservableResourceMonitoringApiDiagId,
32+
UrlFormat = DiagnosticIds.UrlFormat)]
33+
public Task<HealthCheckResult> ObsoleteCheckHealthAsync(CancellationToken cancellationToken = default)
34+
{
35+
var utilization = _dataTracker!.GetUtilization(_options.SamplingWindow);
36+
return ResourceUtilizationHealthCheck.EvaluateHealthStatusAsync(utilization.CpuUsedPercentage, utilization.MemoryUsedPercentage, _options);
37+
}
38+
}
Lines changed: 118 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Collections.Generic;
6+
using System.Diagnostics.Metrics;
57
using System.Threading;
68
using System.Threading.Tasks;
79
using Microsoft.Extensions.Diagnostics.ResourceMonitoring;
@@ -13,44 +15,30 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks;
1315
/// <summary>
1416
/// Represents a health check for in-container resources <see cref="IHealthCheck"/>.
1517
/// </summary>
16-
internal sealed class ResourceUtilizationHealthCheck : IHealthCheck
18+
internal sealed partial class ResourceUtilizationHealthCheck : IHealthCheck, IDisposable
1719
{
20+
private readonly double _multiplier;
21+
private readonly MeterListener? _meterListener;
1822
private readonly ResourceUtilizationHealthCheckOptions _options;
19-
private readonly IResourceMonitor _dataTracker;
23+
private IResourceMonitor? _dataTracker;
24+
private double _cpuUsedPercentage;
25+
private double _memoryUsedPercentage;
2026

21-
/// <summary>
22-
/// Initializes a new instance of the <see cref="ResourceUtilizationHealthCheck"/> class.
23-
/// </summary>
24-
/// <param name="options">The options.</param>
25-
/// <param name="dataTracker">The datatracker.</param>
26-
public ResourceUtilizationHealthCheck(IOptions<ResourceUtilizationHealthCheckOptions> options,
27-
IResourceMonitor dataTracker)
28-
{
29-
_options = Throw.IfMemberNull(options, options.Value);
30-
_dataTracker = Throw.IfNull(dataTracker);
31-
}
32-
33-
/// <summary>
34-
/// Runs the health check.
35-
/// </summary>
36-
/// <param name="context">A context object associated with the current execution.</param>
37-
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the health check.</param>
38-
/// <returns>A <see cref="Task{HealthCheckResult}"/> that completes when the health check has finished, yielding the status of the component being checked.</returns>
39-
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
27+
#pragma warning disable EA0014 // The async method doesn't support cancellation
28+
public static Task<HealthCheckResult> EvaluateHealthStatusAsync(double cpuUsedPercentage, double memoryUsedPercentage, ResourceUtilizationHealthCheckOptions options)
4029
{
41-
var utilization = _dataTracker.GetUtilization(_options.SamplingWindow);
4230
IReadOnlyDictionary<string, object> data = new Dictionary<string, object>
4331
{
44-
{ nameof(utilization.CpuUsedPercentage), utilization.CpuUsedPercentage },
45-
{ nameof(utilization.MemoryUsedPercentage), utilization.MemoryUsedPercentage },
32+
{ "CpuUsedPercentage", cpuUsedPercentage },
33+
{ "MemoryUsedPercentage", memoryUsedPercentage },
4634
};
4735

48-
bool cpuUnhealthy = utilization.CpuUsedPercentage > _options.CpuThresholds.UnhealthyUtilizationPercentage;
49-
bool memoryUnhealthy = utilization.MemoryUsedPercentage > _options.MemoryThresholds.UnhealthyUtilizationPercentage;
36+
bool cpuUnhealthy = cpuUsedPercentage > options.CpuThresholds.UnhealthyUtilizationPercentage;
37+
bool memoryUnhealthy = memoryUsedPercentage > options.MemoryThresholds.UnhealthyUtilizationPercentage;
5038

5139
if (cpuUnhealthy || memoryUnhealthy)
5240
{
53-
string message = string.Empty;
41+
string message;
5442
if (cpuUnhealthy && memoryUnhealthy)
5543
{
5644
message = "CPU and memory usage is above the limit";
@@ -67,12 +55,12 @@ public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, Canc
6755
return Task.FromResult(HealthCheckResult.Unhealthy(message, default, data));
6856
}
6957

70-
bool cpuDegraded = utilization.CpuUsedPercentage > _options.CpuThresholds.DegradedUtilizationPercentage;
71-
bool memoryDegraded = utilization.MemoryUsedPercentage > _options.MemoryThresholds.DegradedUtilizationPercentage;
58+
bool cpuDegraded = cpuUsedPercentage > options.CpuThresholds.DegradedUtilizationPercentage;
59+
bool memoryDegraded = memoryUsedPercentage > options.MemoryThresholds.DegradedUtilizationPercentage;
7260

7361
if (cpuDegraded || memoryDegraded)
7462
{
75-
string message = string.Empty;
63+
string message;
7664
if (cpuDegraded && memoryDegraded)
7765
{
7866
message = "CPU and memory usage is close to the limit";
@@ -91,4 +79,104 @@ public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, Canc
9179

9280
return Task.FromResult(HealthCheckResult.Healthy(default, data));
9381
}
82+
#pragma warning restore EA0014 // The async method doesn't support cancellation
83+
84+
/// <summary>
85+
/// Initializes a new instance of the <see cref="ResourceUtilizationHealthCheck"/> class.
86+
/// </summary>
87+
/// <param name="options">The options.</param>
88+
/// <param name="dataTracker">The datatracker.</param>
89+
public ResourceUtilizationHealthCheck(IOptions<ResourceUtilizationHealthCheckOptions> options, IResourceMonitor dataTracker)
90+
{
91+
_options = Throw.IfMemberNull(options, options.Value);
92+
if (!_options.UseObservableResourceMonitoringInstruments)
93+
{
94+
ObsoleteConstructor(dataTracker);
95+
return;
96+
}
97+
98+
#if NETFRAMEWORK
99+
_multiplier = 1;
100+
#else
101+
// Due to a bug on Windows https://github.com/dotnet/extensions/issues/5472,
102+
// the CPU utilization comes in the range [0, 100].
103+
if (OperatingSystem.IsWindows())
104+
{
105+
_multiplier = 1;
106+
}
107+
108+
// On Linux, the CPU utilization comes in the correct range [0, 1], which we will be converting to percentage.
109+
else
110+
{
111+
#pragma warning disable S109 // Magic numbers should not be used
112+
_multiplier = 100;
113+
#pragma warning restore S109 // Magic numbers should not be used
114+
}
115+
#endif
116+
117+
_meterListener = new()
118+
{
119+
InstrumentPublished = OnInstrumentPublished
120+
};
121+
122+
_meterListener.SetMeasurementEventCallback<double>(OnMeasurementRecorded);
123+
_meterListener.Start();
124+
}
125+
126+
/// <summary>
127+
/// Runs the health check.
128+
/// </summary>
129+
/// <param name="context">A context object associated with the current execution.</param>
130+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the health check.</param>
131+
/// <returns>A <see cref="Task{HealthCheckResult}"/> that completes when the health check has finished, yielding the status of the component being checked.</returns>
132+
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
133+
{
134+
if (!_options.UseObservableResourceMonitoringInstruments)
135+
{
136+
return ObsoleteCheckHealthAsync(cancellationToken);
137+
}
138+
139+
_meterListener!.RecordObservableInstruments();
140+
141+
return EvaluateHealthStatusAsync(_cpuUsedPercentage, _memoryUsedPercentage, _options);
142+
}
143+
144+
/// <inheritdoc />
145+
public void Dispose()
146+
{
147+
Dispose(true);
148+
}
149+
150+
private void Dispose(bool disposing)
151+
{
152+
if (disposing)
153+
{
154+
_meterListener?.Dispose();
155+
}
156+
}
157+
158+
private void OnInstrumentPublished(Instrument instrument, MeterListener listener)
159+
{
160+
if (instrument.Meter.Name is "Microsoft.Extensions.Diagnostics.ResourceMonitoring")
161+
{
162+
listener.EnableMeasurementEvents(instrument);
163+
}
164+
}
165+
166+
private void OnMeasurementRecorded(
167+
Instrument instrument, double measurement,
168+
ReadOnlySpan<KeyValuePair<string, object?>> tags, object? state)
169+
{
170+
switch (instrument.Name)
171+
{
172+
case "process.cpu.utilization":
173+
case "container.cpu.limit.utilization":
174+
_cpuUsedPercentage = measurement * _multiplier;
175+
break;
176+
case "dotnet.process.memory.virtual.utilization":
177+
case "container.memory.limit.utilization":
178+
_memoryUsedPercentage = measurement * _multiplier;
179+
break;
180+
}
181+
}
94182
}

src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckOptions.cs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5-
using Microsoft.Extensions.Diagnostics.ResourceMonitoring;
5+
using System.Diagnostics.CodeAnalysis;
66
using Microsoft.Extensions.Options;
77
using Microsoft.Shared.Data.Validation;
8+
using Microsoft.Shared.DiagnosticIds;
89

910
namespace Microsoft.Extensions.Diagnostics.HealthChecks;
1011

@@ -20,8 +21,7 @@ public class ResourceUtilizationHealthCheckOptions
2021
/// Gets or sets thresholds for CPU utilization.
2122
/// </summary>
2223
/// <remarks>
23-
/// The thresholds are periodically compared against the utilization samples provided by
24-
/// the registered <see cref="IResourceMonitor"/>.
24+
/// The thresholds are periodically compared against the utilization samples provided by the Resource Monitoring library.
2525
/// </remarks>
2626
[ValidateObjectMembers]
2727
public ResourceUsageThresholds CpuThresholds { get; set; } = new ResourceUsageThresholds();
@@ -30,18 +30,33 @@ public class ResourceUtilizationHealthCheckOptions
3030
/// Gets or sets thresholds for memory utilization.
3131
/// </summary>
3232
/// <remarks>
33-
/// The thresholds are periodically compared against the utilization samples provided by
34-
/// the registered <see cref="IResourceMonitor"/>.
33+
/// The thresholds are periodically compared against the utilization samples provided by the Resource Monitoring library.
3534
/// </remarks>
3635
[ValidateObjectMembers]
3736
public ResourceUsageThresholds MemoryThresholds { get; set; } = new ResourceUsageThresholds();
3837

3938
/// <summary>
40-
/// Gets or sets the time window for used for calculating CPU and memory utilization averages.
39+
/// Gets or sets the time window used for calculating CPU and memory utilization averages.
4140
/// </summary>
4241
/// <value>
4342
/// The default value is 5 seconds.
4443
/// </value>
44+
#pragma warning disable CS0436 // Type conflicts with imported type
45+
[Obsolete(DiagnosticIds.Obsoletions.NonObservableResourceMonitoringApiMessage,
46+
DiagnosticId = DiagnosticIds.Obsoletions.NonObservableResourceMonitoringApiDiagId,
47+
UrlFormat = DiagnosticIds.UrlFormat)]
48+
#pragma warning restore CS0436 // Type conflicts with imported type
4549
[TimeSpan(MinimumSamplingWindow, int.MaxValue)]
4650
public TimeSpan SamplingWindow { get; set; } = DefaultSamplingWindow;
51+
52+
/// <summary>
53+
/// Gets or sets a value indicating whether the observable instruments will be used for getting CPU and Memory usage
54+
/// as opposed to the default <see cref="Microsoft.Extensions.Diagnostics.ResourceMonitoring.IResourceMonitor"/> API which is obsolete.
55+
/// </summary>
56+
/// <value>
57+
/// <see langword="true" /> if the observable instruments are used. The default is <see langword="false" />.
58+
/// In the future the default will be <see langword="true" />.
59+
/// </value>
60+
[Experimental(diagnosticId: DiagnosticIds.Experiments.HealthChecks, UrlFormat = DiagnosticIds.UrlFormat)]
61+
public bool UseObservableResourceMonitoringInstruments { get; set; }
4762
}

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,6 @@
4747
<ItemGroup>
4848
<InternalsVisibleToDynamicProxyGenAssembly2 Include="*" />
4949
<InternalsVisibleToTest Include="$(AssemblyName).Tests" />
50+
<InternalsVisibleToTest Include="Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests" />
5051
</ItemGroup>
5152
</Project>

0 commit comments

Comments
 (0)