extensions icon indicating copy to clipboard operation
extensions copied to clipboard

Retire IResourceMonitor set of API in favor of observable instruments

Open evgenyfedorov2 opened this issue 1 year ago • 7 comments

Currently, ResourceMonitoring can be consumed in two ways:

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 IResourceMonitor will be blocked in case they don't use .NET Metrics

evgenyfedorov2 avatar Sep 10 '24 14:09 evgenyfedorov2

@RussKie @joperezr @geeknoid @andrey-noskov - please take a look when you have time

evgenyfedorov2 avatar Sep 10 '24 14:09 evgenyfedorov2

Regarding your "con", given that we wouldn't be deleting the API and just marking it obsolete, then nobody would be 'blocked', right?

geeknoid avatar Sep 10 '24 15:09 geeknoid

yes, please! this probably should include ResourceMonitorService, CircularBuffer, Calculator and many other internal utilities that power our IResourceMonitor implementation.

andrey-noskov avatar Sep 10 '24 16:09 andrey-noskov

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?

evgenyfedorov2 avatar Sep 10 '24 18:09 evgenyfedorov2

@joperezr Are obsoleted APIs ever removed from the code base?

geeknoid avatar Sep 10 '24 19:09 geeknoid

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

joperezr avatar Sep 10 '24 20:09 joperezr

@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.

RussKie avatar Sep 11 '24 05:09 RussKie

Using IResourceMonitor makes it very easy for a consumer to obtain and hook into cgroup-aware CPU and memory metrics without:

  1. needing to obtain the values themselves
  2. 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.

roblappcoupa avatar Jun 25 '25 14:06 roblappcoupa

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

evgenyfedorov2 avatar Jun 25 '25 15:06 evgenyfedorov2

Thanks for the info. I want to make sure of the following:

  1. 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.
  2. 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

roblappcoupa avatar Jun 25 '25 15:06 roblappcoupa

  1. Yes
  2. 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 Meter or Counter manually. You just use MeterListener and subscribe to the callback like meterListener.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.

evgenyfedorov2 avatar Jun 30 '25 11:06 evgenyfedorov2

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.

roblappcoupa avatar Jun 30 '25 13:06 roblappcoupa

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.

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 avatar Jul 02 '25 09:07 evgenyfedorov2

@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.

roblappcoupa avatar Jul 02 '25 13:07 roblappcoupa

@evgenyfedorov2 I created a new issue requesting a new article here.

roblappcoupa avatar Jul 02 '25 14:07 roblappcoupa