Retire IResourceMonitor set of API in favor of observable instruments
Currently, ResourceMonitoring can be consumed in two ways:
- via IResourceMonitor API which internally uses ResourceMonitorService implementing
BackgroundService - via Observable instruments (metrics).
Both ways provide the same resource monitoring data, so essentially we have duplicated functionality. We can simplify it all by deprecating the IResourceMonitor API and recommend the observable instruments as the only way forward. Namely, we can deprecate following classes (or structs):
src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs
src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Snapshot.cs
src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceUtilization.cs
src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ISnapshotProvider.cs
src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceMonitorBuilder.cs
src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceMonitor.cs
Pros:
- Simplified API
- Improved performance, avoiding wasting resources for the
BackgroundService
Cons:
- Users of the
IResourceMonitorwill be blocked in case they don't use .NET Metrics
@RussKie @joperezr @geeknoid @andrey-noskov - please take a look when you have time
Regarding your "con", given that we wouldn't be deleting the API and just marking it obsolete, then nobody would be 'blocked', right?
yes, please!
this probably should include ResourceMonitorService, CircularBuffer, Calculator and many other internal utilities that power our IResourceMonitor implementation.
Regarding your "con", given that we wouldn't be deleting the API and just marking it obsolete, then nobody would be 'blocked', right?
Yes.
Another question - I assume we will be able to delete any Obsolete API when .NET 8 support ends, e.g. in 2026?
@joperezr Are obsoleted APIs ever removed from the code base?
Yes, they can be. Typically that is done in the subsequent LTS version after being marked as obsoleted (in an LTS version too). This policy is documented here: https://learn.microsoft.com/en-us/dotnet/core/compatibility/api-removal
@evgenyfedorov2: sounds good to me. We can mark these API (and anything else related) as obsolete in the .NET 9 release (i.e., the dev branch). We'll also need to create migration guides.
There isn't much time before we switch to .NET 9, so we should do this before then.
Using IResourceMonitor makes it very easy for a consumer to obtain and hook into cgroup-aware CPU and memory metrics without:
- needing to obtain the values themselves
- having to know how to compute the values
If IResourceMonitor is deprecated, what is the replacement? I would not want to have to understand or implement the code that the underlying implementation (ResourceMonitorService) uses for obtaining metrics. That process seems like a nightmare to maintain and IResourceMonitor makes it dead simple for consumers:
var services = new ServiceCollection()
.AddLogging(static builder => builder.AddConsole())
.AddResourceMonitoring();
var provider = services.BuildServiceProvider();
var monitor = provider.GetRequiredService<IResourceMonitor>();
var utilization = monitor.GetUtilization(window);
var resources = utilization.SystemResources;
I brought this up in this StackOverflow post. As a dotnet developer who does not have a great understanding of the details of cgroups or the dotnet metrics ecosystem, the other solutions seem overly complicated compared to IResourceMonitor. In addition to this, it was my understanding that the CPU and memory returned by the Process class isn't valid when the code is running in a CPU and/or memory limited container - another thing that IResourceMonitor handles for consumers.
The replacement is Resource Monitoring metrics. You don't have to understand how to compute the values, it is as simple as using any other .NET metrics. For example, based on this snippet https://learn.microsoft.com/en-us/dotnet/core/diagnostics/metrics-collection#configure-the-example-app-to-use-opentelemetrys-prometheus-exporter we should be able to export metrics to Prometheus this way:
static void Main(string[] args)
{
using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter("Microsoft.Extensions.Diagnostics.ResourceMonitoring")
.ConfigureServices(services => services.AddResourceMonitoring())
.AddPrometheusHttpListener(options => options.UriPrefixes = new string[] { "http://localhost:9184/" })
.Build();
}
You don't need IResourceMonitor for that, right values are exported directly to backend
Thanks for the info. I want to make sure of the following:
- Is the code above cgroup aware? I assume it is since you are adding
services.AddResourceMonitoring()which has the code that queries the cgroup file system. - I need the values within my C# application. The code provided above sends them to an external system (Prometheus). If I want to capture them within my application, would the recommended approach be to use the ConsoleExporter/a custom exporter?
I tried putting together a simple example, but I am not able to see any metrics in the console. What am I missing here?
Sdk.CreateMeterProviderBuilder()
.AddMeter("Microsoft.Extensions.Diagnostics.ResourceMonitoring")
.ConfigureServices(services => services.AddResourceMonitoring())
.AddConsoleExporter()
.Build();
// Keep app running to observe metrics
Console.WriteLine("Collecting metrics. Press Ctrl+C to exit.");
List<byte[]> memory = [];
while (true)
{
var array = new byte[1024];
var index = Random.Shared.Next(array.Length);
array[index] = (byte)Random.Shared.Next(0, 255); // Touch memory to make sure it's being used
Console.WriteLine(array[index]);
memory.Add(array); // Keep adding memory
await Task.Delay(TimeSpan.FromSeconds(3));
}
Thanks again
- Yes
- If you need values within your C# app, please consider this guide https://learn.microsoft.com/en-us/dotnet/core/diagnostics/metrics-collection#create-a-custom-collection-tool-using-the-net-meterlistener-api - the only difference is that metrics and values are already produced by Resource Monitoring, you don't need to create
MeterorCountermanually. You just useMeterListenerand subscribe to the callback likemeterListener.SetMeasurementEventCallback<int>(OnMeasurementRecorded);and receive the values within the callback
Please also check out a sample app https://github.com/dotnet/extensions-samples/tree/main/src/Diagnostics/ResourceMonitoring which runs in Docker and Linux containers and sends metric values to the console.
Thanks again for following up.
I used this example from the official documentation and it does not work correctly based on my findings. The values it produces are incorrect - it just outputs the same incorrect values over and over. You can run it and see for yourself. I believe the difference between that example and the one you linked here is the latter one builds and uses a dotnet generic host which I think is what ultimately starts the ResourceMonitorService BackgroundService which will ensure values are actually computed - the former example does not actually start the BackgroundService. If you follow the former example, it just repeats the incorrect initial values over and over.
As far as using MeterListener, the only way I was able to get my callback to actually fire was to call listener.RecordObservableInstruments() repeatedly as shown below:
using MeterListener listener = new();
listener.InstrumentPublished = (instrument, l) =>
{
l.EnableMeasurementEvents(instrument);
};
listener.SetMeasurementEventCallback<long>((instrument, value, tags, state) =>
{
Console.WriteLine("{0} = {1} {2}", instrument.Name, value, instrument.Unit);
});
listener.SetMeasurementEventCallback<double>((instrument, value, tags, state) =>
{
Console.WriteLine("{0} = {1} {2}", instrument.Name, value, instrument.Unit);
});
listener.SetMeasurementEventCallback<decimal>((instrument, value, tags, state) =>
{
Console.WriteLine("{0} = {1} {2}", instrument.Name, value, instrument.Unit);
});
listener.Start();
Console.WriteLine("Listening for metrics...");
List<byte[]> memory = [];
while (true)
{
var array = new byte[1024];
var index = Random.Shared.Next(array.Length);
array[index] = (byte)Random.Shared.Next(0, 255); // Touch memory to make sure it's being used
Console.WriteLine(array[index]);
memory.Add(array); // Keep adding memory
await Task.Delay(TimeSpan.FromSeconds(3));
listener.RecordObservableInstruments(); // IF YOU DON'T CALL THIS, THEN THE CALLBACKS WILL NEVER FIRE
}
This is not mentioned in any example or document and I only knew about this from a StackOverflow user.
As someone new to these libraries, it was overly complicated and unnecessarily difficult to come up with/find a simple working example. As far as I can tell, the one working example is not mentioned in any of the documentation.
Thanks again for following up.
I used this example from the official documentation and it does not work correctly based on my findings. The values it produces are incorrect - it just outputs the same incorrect values over and over. You can run it and see for yourself. I believe the difference between that example and the one you linked here is the latter one builds and uses a dotnet generic host which I think is what ultimately starts the
ResourceMonitorServiceBackgroundServicewhich will ensure values are actually computed - the former example does not actually start theBackgroundService. If you follow the former example, it just repeats the incorrect initial values over and over.
Thanks for pointing at this issue, I was not aware of it. Here is a fix https://github.com/dotnet/docs/pull/47058.
As far as using MeterListener, the only way I was able to get my callback to actually fire was to call listener.RecordObservableInstruments() repeatedly as shown below:
That's right, and we use this method a lot in unit tests in this repo. I am not aware of why it is not properly documented at learn.microsoft.com. Would you be interested to submit a new issue for them at https://github.com/dotnet/docs/issues, please?
@evgenyfedorov2 Thank you very much for following up and confirming my understanding. While I understand you want to limit the surface area and duplication, as a consumer I feel like IResourceMonitor is a very useful thing and my team and I find value in IResourceMonitor. However, with that said, if there is an example using MeterListener to capture CPU and memory and it's cgroup aware, I think that would be sufficient. Thanks for all your help and I will submit an issue for the documentation.
@evgenyfedorov2 I created a new issue requesting a new article here.