{"slug": "recording-metrics-in-process-using-meterlistener-system-diagnostics-metrics-apis", "title": "Recording metrics in-process using MeterListener: System.Diagnostics.Metrics APIs - Part 4", "summary": "This article explains how to use the `MeterListener` type from the `System.Diagnostics.Metrics` APIs to consume and record metric values in-process within a .NET application. The author demonstrates this by creating a simple ASP.NET Core app that generates HTTP load and uses `MeterListener` to capture specific metrics, displaying them in a live-updating table with Spectre.Console. However, the article notes that for production environments, developers should use dedicated solutions like OpenTelemetry or Datadog instead of directly implementing `MeterListener`.", "body_md": "So far in this series I've described how to [create and consume metrics using dotnet-counters](/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/), how to\n\n[create each of the different](/creating-standard-and-observable-instruments/#system-diagnostics-metrics-apis)exposed by the\n\n`Instrument`\n\ntypes*System.Diagnostics.Metrics*APIs, and how to\n\n[use a source generator to produce values](/creating-strongly-typed-metics-with-a-source-generator/). In this post, I look at how to\n\n*consume*the stream of values produced by\n\n`Instrument`\n\nimplementations in-process, using the `MeterListener`\n\ntype.I start by describing the scenario of an app that wants to record and process a specific subset of metrics exposed via the *System.Diagnostics.Metrics* APIs. We'll create a simple app that generates some load, use `MeterListener`\n\nto listen for `Instrument`\n\nmeasurements, and display the results in a table using [Spectre.Console](https://spectreconsole.net/) (because everyone loves [Spectre.Console](https://spectreconsole.net/))!\n\nNote that I'm\n\nnotsuggesting you use`MeterListener`\n\ndirectly in your production apps. In production, you'll likely want to use a solution like OpenTelemetry or Datadog that does all this work for you!\n\n[Creating the test ASP.NET Core app](#creating-the-test-asp-net-core-app)\n\nAs described above, for the purposes of this post, I created a simple \"hello world\" ASP.NET Core app using `dotnet new web`\n\n, and tweaked it so that it will send requests to itself, as long as the app is running:\n\n```\nusing Microsoft.AspNetCore.Hosting.Server;\nusing Microsoft.AspNetCore.Hosting.Server.Features;\n\n// Very basic hello-world app\nvar builder = WebApplication.CreateBuilder(args);\nvar app = builder.Build();\n\napp.MapGet(\"/\", () => \"Hello World!\");\n\nvar task = app.RunAsync();\n\n// Grab the address Kestrel's listening on\nvar address = app.Services.GetRequiredService<IServer>()!\n        .Features.Get<IServerAddressesFeature>()!\n        .Addresses.First();\n\ntry\n{\n    // Run 4 loops in parallel, sending HTTP requests continuously\n    // until the app gets the shut down notification\n    await Parallel.ForAsync(0, 4, app.Lifetime.ApplicationStopping, async (i, ct) =>\n    {\n        var httpClient = new HttpClient()\n        {\n            BaseAddress = new Uri(address),\n        };\n\n        // Just keep hammering requests!\n        while (!ct.IsCancellationRequested)\n        {\n            string _ = await httpClient.GetStringAsync(\"/\");\n        }\n    });\n}\ncatch (OperationCanceledException)\n{\n    // expected on shutdown\n}\n\n// Wait for the final cleanup\nawait task;\n```\n\nThe code above isn't particularly pretty, but it does the following:\n\n- Creates a \"hello world\" minimal API ASP.NET Core app.\n- After the app starts up, it starts 4 parallel jobs\n- Each job has its own\n`HttpClient`\n\nand continuously makes HTTP requests to the app `ctrl`+` c`in the console stops the requests and shut's down the app.\n\nNow that we have this app, we can start grabbing some metrics out of it. We're aiming for something like the following, which shows the majority of metrics from [my previous post](/creating-standard-and-observable-instruments/#system-diagnostics-metrics-apis/) in a [live-updating Spectre.Console](https://spectreconsole.net/console/how-to/live-rendering-and-dynamic-updates) [table](https://spectreconsole.net/console/how-to/displaying-tabular-data):\n\n```\n                                  ASP.NET Core Metrics                                  \n┌────────────────────────────────────────────┬─────────────────────────┬─────────────┐\n│ Metric                                     │ Type                    │       Value │\n├────────────────────────────────────────────┼─────────────────────────┼─────────────┤\n│ aspnetcore.routing.match_attempts          │ Counter                 │     250,428 │\n│ dotnet.gc.heap.total_allocated             │ ObservableCounter       │ 849,743,376 │\n│ http.server.active_requests                │ UpDownCounter           │           4 │\n│ dotnet.gc.last_collection.heap.size (gen0) │ ObservableUpDownCounter │   2,497,080 │\n│ dotnet.gc.last_collection.heap.size (gen1) │ ObservableUpDownCounter │     774,872 │\n│ dotnet.gc.last_collection.heap.size (gen2) │ ObservableUpDownCounter │   1,219,120 │\n│ dotnet.gc.last_collection.heap.size (loh)  │ ObservableUpDownCounter │      98,384 │\n│ dotnet.gc.last_collection.heap.size (poh)  │ ObservableUpDownCounter │      65,728 │\n│ process.cpu.utilization                    │ ObservableGauge         │         36% │\n│ http.server.request.duration               │ Histogram               │     0.011ms │\n│ http.server.request.duration (count)       │ Histogram               │     250,425 │\n└────────────────────────────────────────────┴─────────────────────────┴─────────────┘\n```\n\nTo record the values from these metrics, we're going to use the `MeterListener`\n\ntype.\n\n[Recording metrics with ](#recording-metrics-with-meterlistener)`MeterListener`\n\n`MeterListener`\n\nIn my previous post I discussed how `Instrument`\n\ns have both a consumer and a producer side. To consume the output of `Instrument`\n\ns inside your app you must subscribe to them using a `MeterListener`\n\n. To manage all this configuration, we'll create a helper type called `MetricManager`\n\n.\n\n[Creating a wrapper ](#creating-a-wrapper-metricmanager-for-working-with-metrics)`MetricManager`\n\nfor working with metrics\n\n`MetricManager`\n\nfor working with metricsTo encapsulate the collection and aggregation of metrics emitted by the *System.Diagnostics.Metrics* APIs, I'm going to create a type called `MetricManager`\n\n. This is entirely optional, it's just helpful for my scenario. The public API for this type is shown below, which we'll be fleshing out shortly.\n\n```\npublic class MetricManager : IDisposable\n{\n    public void Dispose();\n    public MetricValues GetMetrics();\n}\n```\n\nThe `MetricManager`\n\nis responsible for interacting with the *System.Diagnostics.Metrics* APIs. And when you call `GetMetrics()`\n\n, the manager returns the values for each of the `Instruments`\n\nwe listed above:\n\n```\npublic readonly record struct MetricValues(\n    long TotalMatchAttempts,\n    long TotalHeapAllocated,\n    long ActiveRequests,\n    long HeapSizeGen0,\n    long HeapSizeGen1,\n    long HeapSizeGen2,\n    long HeapSizeLoh,\n    long HeapSizePoh,\n    double CpuUtilization,\n    double AverageDuration,\n    long TotalRequests);\n```\n\nJust to reiterate, this is not *required*. It's just how I've chosen in this post to expose the interactions with the *System.Diagnostics.Metrics* APIs.\n\nNote also that I'm creating a very well-defined API here. If you want to have more of a \"generalised\" listener, that can listen to\n\nallmetrics, and records all the tags for those metrics, I strongly recommend looking at OpenTelemetry instead!\n\nSo we have our basic public API, now let's create a `MeterListener`\n\nand hook it up.\n\n[Creating a ](#creating-a-meterlistener-and-configuring-callbacks)`MeterListener`\n\nand configuring callbacks\n\n`MeterListener`\n\nand configuring callbacksOne of the design tenants of the *System.Diagnostics.Metrics* APIs is that they should be high performance. Commonly for .NET, that mostly means \"you don't need additional allocations\". That shows up in some of the design of the `MeterListener`\n\nas you'll see shortly.\n\nThe code below shows how we would extend `MetricManager`\n\nto create a `MeterListener`\n\n, initialize it, and configure callbacks:\n\n```\npublic class MetricManager : IDisposable\n{\n    private readonly MeterListener _listener;\n\n    public MetricManager()\n    {\n        // Create a MeterListener, and configure the method to call\n        // when a new instrument is published in the application\n        _listener = new()\n        {\n            InstrumentPublished = OnInstrumentPublished\n        };\n\n        // Configure the callbacks to invoke when an Instrument emits a value.\n        // In this case, we know that the .NET runtime instruments we listen to only\n        // produce long or double values, so that's all we listen for here\n        _listener.SetMeasurementEventCallback<long>(OnMeasurementRecordedLong);\n        _listener.SetMeasurementEventCallback<double>(OnMeasurementRecordedDouble);\n\n        // Start the listener, which invokes OnInstrumentPublished for already-published Instruments\n        _listener.Start();\n    }\n\n    // Call Dispose on the listener to prevent further callbacks being invoked\n    public void Dispose() => _listener.Dispose();\n\n    // Callback invoked whenever an instrument is published\n    private void OnInstrumentPublished(Instrument instrument, MeterListener listener)\n    {\n        // ...\n    }\n\n    // Callback invoked whenever a `long` measurement is recorded\n    private static void OnMeasurementRecordedLong(Instrument instrument, long measurement,\n        ReadOnlySpan<KeyValuePair<string, object?>> tags, object? state)\n    {\n        // ...\n    }\n\n    // Callback invoked whenever a `double` measurement is recorded\n    private static void OnMeasurementRecordedDouble(Instrument instrument, double measurement,\n        ReadOnlySpan<KeyValuePair<string, object?>> tags, object? state)\n    {\n        // ...\n    }\n}\n```\n\nI've heavily commented the code above, but I'll highlight some interesting points.\n\nFirstly, the `OnInstrument`\n\ncallback allows the listener to choose which `Meter`\n\ns and `Instrument`\n\ns it wants to subscribe to. This callback is invoked once for each existing `Instrument`\n\nwhen you call `MeterListener.Start()`\n\n, and is then subsequently invoked whenever a new `Meter`\n\nor `Instrument`\n\nis subsequently registered.\n\nIn addition, we have the `SetMeasurementEventCallback<T>()`\n\nmethod. This is a generic method, because it allows you to register a different callback for each *type* of `Instrument`\n\nmeasurement you might receive. Instruments can be created with `byte`\n\n, `short`\n\n, `int`\n\n, `long`\n\n, `float`\n\n, `double`\n\n, and `decimal`\n\ntypes, so [it's recommended](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/metrics-collection#create-a-custom-collection-tool-using-the-net-meterlistener-api) that you register a callback for each of these types.\n\nNote that if you use a generic argument that\n\nisn'tone of these types, you'll get an exception at runtime.\n\nThis kind of API might seem a little unusual; having to register virtually identical callbacks for each different type feels a bit clumsy. But it's written this way for performance reasons. By having a dedicated callback for each supported `T`\n\n, you can avoid any allocation or overhead that would come from having a \"generic\" callback that would only work with `object`\n\n.\n\nAlso note that the callback you register doesn't *have* to be different methods like I have used above. You *could* also have a single generic method with a signature like this:\n\n```\nstatic void OnMeasurementRecorded<T>(\n    Instrument instrument,\n    T measurement,\n    ReadOnlySpan<KeyValuePair<string, object?>> tags,\n    object? state);\n```\n\nHowever, you would still need to call `SetMeasurementEventCallback<T>`\n\nonce for each measurement type you want to handle, for example:\n\n```\n_listener.SetMeasurementEventCallback<long>(OnMeasurementRecorded);\n_listener.SetMeasurementEventCallback<double>(OnMeasurementRecorded);\n```\n\nWe are yet to implement these measurement callbacks, but before we get to that, we'll take a look at the `OnInstrumentPublished()`\n\ncallback.\n\n[Selecting which ](#selecting-which-instruments-to-listen-to)`Instrument`\n\ns to listen to\n\n`Instrument`\n\ns to listen toThe `MeterListener`\n\nis \"connected\" to all of the `Meter`\n\ns and `Instrument`\n\ns in the application, but it won't automatically receive measurements from all of them unless you enable each one. For this demo, we only care about a subset of `Meter`\n\ns and `Instrument`\n\ns, so our `OnInstrumentPublished()`\n\ncallback uses a switch expression to check for specific values of `Instrument.Name`\n\nand `Meter.Name`\n\n:\n\n```\nprivate void OnInstrumentPublished(Instrument instrument, MeterListener listener)\n{\n    string meterName = instrument.Meter.Name;\n    string instrumentName = instrument.Name;\n\n    // Is this a Meter and Instrument we care about?\n    var enable = meterName switch\n    {\n        \"Microsoft.AspNetCore.Routing\" => instrumentName == \"aspnetcore.routing.match_attempts\",\n        \"System.Runtime\"               => instrumentName is \"dotnet.gc.heap.total_allocated\"\n                                                            or \"dotnet.gc.last_collection.heap.size\",\n        \"Microsoft.AspNetCore.Hosting\" => instrumentName is \"http.server.active_requests\"\n                                                            or \"http.server.request.duration\",\n        \"Microsoft.Extensions.Diagnostics.ResourceMonitoring\" => instrumentName == \"process.cpu.utilization\",\n        _ => false\n    };\n\n    if (enable)\n    {\n        // If yes, enable measurements, and pass the `MetricManager` as \"state\"\n        listener.EnableMeasurementEvents(instrument, state: this);\n    }\n}\n```\n\nTo enable measurements, you call `MeterListener.EnableMeasurementEvents()`\n\n, passing in the `Instrument`\n\nto listen to. One interesting point here is that we're also passing the `MetricManager`\n\nas the `state`\n\nvariable. This variable is passed in to our `OnMeasurementRecorded`\n\ncallbacks and is a way of avoiding closures or expensive lookups in the callback events. You'll see how it's used shortly.\n\nNote that if we were creating a generic implementation that listened to *all* `Insturment`\n\ns emitted by the app, we could implement this method very simply:\n\n```\nprivate void OnInstrumentPublished(Instrument instrument, MeterListener listener)\n    => listener.EnableMeasurementEvents(instrument, state: this);\n```\n\nSo at this point we've enabled the instruments, we've called `MeterListener.Start()`\n\n, and it's time to start receiving some measurements!\n\n[Triggering ](#triggering-observableinstruments-to-emit-measurements)`ObservableInstrument`\n\ns to emit measurements\n\n`ObservableInstrument`\n\ns to emit measurementsNow that we've subscribed to the instruments, the `OnMeasurementRecorded`\n\ncallbacks are invoked whenever an `Instrument`\n\nemits a value. For \"standard\" `Instrument`\n\ns, that happens immediately, whenever a value is recorded: add a value to a `Counter<long>`\n\n, and our `OnMeasurementRecorded`\n\ncallback is immediately called. But that's not how it works for observable instruments.\n\nIn my [previous post](/creating-standard-and-observable-instruments/#what-is-an-observable-instrument-), I described how observable instruments don't emit any values until the consumer *asks* them to. Well, the consumer here is `MeterListener`\n\n, and it needs to ask all the `Instrument`\n\ns it is interested in to emit values when `GetMetrics()`\n\nis called:\n\n```\npublic MetricValues GetMetrics()\n{\n    // This triggers the observable metrics to go and read the values and\n    // then invoke the OnMeasurementRecorded callback to send the values to us\n    _listener.RecordObservableInstruments();\n\n    // ...\n}\n```\n\nCalling `RecordObservableInstruments()`\n\ntriggers all the observable instruments that we enabled to emit a measurement (by invoking their associated callbacks, such as [those described in my previous post](/creating-standard-and-observable-instruments/#observablecountert)). These measurements are then reported via the callbacks registered with the `MeterListener`\n\n.\n\nOur `MeterListener`\n\nis now completely configured, so it's time to flesh out the `OnMeasurementRecorded`\n\ncallbacks.\n\n[Recording ](#recording-instrument-measurements)`Instrument`\n\nmeasurements\n\n`Instrument`\n\nmeasurementsWhenever a measurement is recorded by an `Instrument`\n\n, the registered callback of the appropriate type is invoked (if you haven't registered an appropriate callback, none will be invoked). Exactly what you should *do* with that metric depends on how you want to aggregate your data.\n\nThe following implementation of the `OnMeasurementRecordedLong`\n\nmethod shows one way to aggregate the data, focusing on displaying long running totals for the duration of the app:\n\n```\nprivate static void OnMeasurementRecordedLong(Instrument instrument, long measurement,\n    ReadOnlySpan<KeyValuePair<string, object?>> tags, object? state)\n{\n    var handler = (MetricManager)state!;\n    switch (instrument.Name)\n    {\n        // Counter\n        case \"aspnetcore.routing.match_attempts\":\n            Interlocked.Add(ref handler._matchAttempts, measurement);\n            break;\n\n        // ObservableCounter\n        case \"dotnet.gc.heap.total_allocated\":\n            Interlocked.Exchange(ref handler._totalHeapAllocated, measurement);\n            break;\n\n        // UpDownCounter\n        case \"http.server.active_requests\":\n            Interlocked.Add(ref handler._activeRequests, measurement);\n            break;\n\n        // ObservableUpDownCounter\n        case \"dotnet.gc.last_collection.heap.size\":\n            foreach (var tag in tags)\n            {\n                if (tag is { Key: \"gc.heap.generation\", Value: string gen })\n                {\n                    switch (gen)\n                    {\n                        case \"gen0\": Interlocked.Exchange(ref handler._heapSizeGen0, measurement); break;\n                        case \"gen1\": Interlocked.Exchange(ref handler._heapSizeGen1, measurement); break;\n                        case \"gen2\": Interlocked.Exchange(ref handler._heapSizeGen2, measurement); break;\n                        case \"loh\": Interlocked.Exchange(ref handler._heapSizeLoh, measurement); break;\n                        case \"poh\": Interlocked.Exchange(ref handler._heapSizePoh, measurement); break;\n                    }\n                }\n            }\n\n            break;\n    }\n}\n```\n\nThe first step is to cast the `state`\n\nobject back to the `MetricManager`\n\ninstance that we passed in when calling `EnableMeasurementEvents()`\n\n. We then switch based on the instrument name, and handle the measurement value differently depending on the instrument type:\n\n- For\n`Counter`\n\nand`UpDownCounter`\n\n, the callback is invoked once for every time a new value is recorded, with the`measurement`\n\nvalue as the increment. To create a running total of values, you must*add*the new measurement to the current running total. - For\n`ObservableCounter`\n\nand`ObservableUpDownCounter`\n\n, the callback is only invoked when you call`RecordObservableInstruments()`\n\n. The`measurement`\n\nvalue in these cases*aren't*incremental values, but rather they're the \"final\" current value, so you can use the value \"as is\" for the current running total.\n\nYou can see these rules applied in the above method, where the `Counter`\n\nand `UpDownCounter`\n\nmetrics are aggregated using `Interlocked.Add()`\n\n, whereas the `ObservableCounter`\n\nand `ObservableUpDownCounter`\n\nmetrics are \"aggregated\" by using `Interlocked.Exchange`\n\n.\n\nAnother interesting aspect is the handling of tags. The `\"dotnet.gc.last_collection.heap.size\"`\n\nis an `ObservableUpDownCounter`\n\n, so the values are emitted only when you call `RecordObservableInstruments()`\n\n. In this case, we receive one invocation of the callback *per generation*, with the `gc.heap.generation`\n\ntag indicating to which generation the current measurement applies.\n\nIn addition to the `OnMeasurementRecordedLong`\n\ncallback, we also have the `OnMeasurementRecordedDouble`\n\ncallback, which we use to record the `ObservableGauge`\n\nand `Histogram`\n\nmetrics:\n\n```\nprivate static void OnMeasurementRecordedDouble(Instrument instrument, double measurement,\n    ReadOnlySpan<KeyValuePair<string, object?>> tags, object? state)\n{\n    var handler = (MetricManager)state!;\n    switch (instrument.Name)\n    {\n        // ObservableGauge\n        case \"process.cpu.utilization\":\n            Interlocked.Exchange(ref handler._cpuUtilization, measurement);\n            break;\n\n        // Histogram\n        case \"http.server.request.duration\":\n            Interlocked.Increment(ref handler._totalRequestCount);\n            lock (handler._durationLock)\n            {\n                handler._intervalRequests++;\n                handler._totalDuration += measurement;\n            }\n\n            break;\n    }\n}\n```\n\nThe structure for this callback is very similar to the previous one:\n\n- We cast the\n`state`\n\nvariable to our`MetricManager`\n\ninstance that we passed in when registering the callback. - For the\n`ObservableGauge`\n\n(as for all of the observable instruments), we*replace*our recorded value, using`Interlocked.Exchange()`\n\n- For the\n`Histogram`\n\n, there are many different ways we could aggregate the data, especially considering that these measurements contain a lot of high cardinality tags. I chose to calculate just two values from this data:- The total number of requests since app start, stored in\n`_totalRequestCount`\n\n. - The average request duration in the current time interval. This requires recording the number of requests (\n`_intervalRequests`\n\n) during the interval, and the sum of the durations of requests during the interval (`_totalDuration`\n\n). We'll use these values to calculate the average shortly.\n\n- The total number of requests since app start, stored in\n\nSome of these measurements may be recorded concurrently with when while we are reading the values, which is why I've used `Interlocked`\n\nwhere possible, to make updates atomic. Where I couldn't use `Interlocked`\n\n, I used a `lock`\n\nfor simplicity, though you should be careful about this in practice; in high performance applications it might be possible to run into lock contention, if many requests are trying to increment these values.\n\nNow that all of our `Instrument`\n\ns are recording values, both standard and observable, it's time to report the results.\n\n[Reporting the results from ](#reporting-the-results-from-getmetrics)`GetMetrics`\n\n`GetMetrics`\n\nI have already partially shown the `GetMetrics()`\n\nimplementation, in so far as it's where we called `RecordObservableInstruments()`\n\n. Other than triggering the observable measurements to be taken, all `GetMetrics()`\n\ndoes is read the values recorded in the fields, calculate the average duration, and return a `MetricValues`\n\ninstance:\n\n```\npublic MetricValues GetMetrics()\n{\n    // This triggers the observable metrics to go and read the values\n    // and then call the OnMeasurement callbacks to send the values to us\n    _listener.RecordObservableInstruments();\n\n    // Read all of the values from the fields and return a MetricValues object\n    return new MetricValues(\n        TotalMatchAttempts: Interlocked.Read(ref _matchAttempts),\n        TotalHeapAllocated: Interlocked.Read(ref _totalHeapAllocated),\n        ActiveRequests: Interlocked.Read(ref _activeRequests),\n        HeapSizeGen0: Interlocked.Read(ref _heapSizeGen0),\n        HeapSizeGen1: Interlocked.Read(ref _heapSizeGen1),\n        HeapSizeGen2: Interlocked.Read(ref _heapSizeGen2),\n        HeapSizeLoh: Interlocked.Read(ref _heapSizeLoh),\n        HeapSizePoh: Interlocked.Read(ref _heapSizePoh),\n        CpuUtilization: Volatile.Read(ref _cpuUtilization),\n        AverageDuration: ComputeAndResetAverageDuration(),\n        TotalRequests: Interlocked.Read(ref _totalRequestCount)\n    );\n\n    double ComputeAndResetAverageDuration()\n    {\n        long count;\n        double sum;\n        lock (_durationLock)\n        {\n            // Grab the current values\n            count = _intervalRequests;\n            sum = _totalDuration;\n            // Reset the values\n            _intervalRequests = 0;\n            _totalDuration = 0;\n        }\n\n        // Do the calculation\n        return count > 0 ? sum / count : 0;\n    }\n}\n```\n\nAnd with that, the implementation of `MetricManager`\n\nand its usage of `MeterListener`\n\nis complete. All that remains is to plug the listener into our app.\n\n[Creating a service to display the results](#creating-a-service-to-display-the-results)\n\nTo view the metrics being collected by `MetricManager`\n\nand its `MeterListener`\n\n, I created a `BackgroundService`\n\nthat would render a [Spectre.Console](https://spectreconsole.net/) live table to the console, and update it periodically:\n\n```\nusing MyMetrics;\nusing Spectre.Console;\n\ninternal class MetricDisplayService : BackgroundService\n{\n    protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n    {\n        using var manager = new MetricManager();\n        \n        var table = new Table()\n            .Title(\"[bold]ASP.NET Core Metrics[/]\")\n            .Border(TableBorder.Rounded)\n            .AddColumn(\"Metric\")\n            .AddColumn(\"Type\")\n            .AddColumn(new TableColumn(\"Value\").RightAligned());\n\n        table.AddRow(\"aspnetcore.routing.match_attempts\", \"Counter\", \"0\");\n        table.AddRow(\"dotnet.gc.heap.total_allocated\", \"ObservableCounter\", \"0\");\n        table.AddRow(\"http.server.active_requests\", \"UpDownCounter\", \"0\");\n        table.AddRow(\"dotnet.gc.last_collection.heap.size (gen0)\", \"ObservableUpDownCounter\", \"0\");\n        table.AddRow(\"dotnet.gc.last_collection.heap.size (gen1)\", \"ObservableUpDownCounter\", \"0\");\n        table.AddRow(\"dotnet.gc.last_collection.heap.size (gen2)\", \"ObservableUpDownCounter\", \"0\");\n        table.AddRow(\"dotnet.gc.last_collection.heap.size (loh)\", \"ObservableUpDownCounter\", \"0\");\n        table.AddRow(\"dotnet.gc.last_collection.heap.size (poh)\", \"ObservableUpDownCounter\", \"0\");\n        table.AddRow(\"process.cpu.utilization\", \"ObservableGauge\", \"0%\");\n        table.AddRow(\"http.server.request.duration\", \"Histogram\", \"0.000ms\");\n        table.AddRow(\"http.server.request.duration (count)\", \"Histogram\", \"0\");\n\n        await AnsiConsole.Live(table).StartAsync(async ctx =>\n        {\n            // This is the update loop, where we poll the `MetricManager`\n            while (!stoppingToken.IsCancellationRequested)\n            {\n                await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);\n                RenderMetricValues(table, ctx, manager.GetMetrics());\n            }\n        });\n    }\n\n    private void RenderMetricValues(Table table, LiveDisplayContext ctx, in MetricManager.MetricValues values)\n    {\n        table.UpdateCell(0, 2, values.TotalMatchAttempts.ToString(\"N0\"));\n        table.UpdateCell(1, 2, values.TotalHeapAllocated.ToString(\"N0\"));\n        table.UpdateCell(2, 2, values.ActiveRequests.ToString(\"N0\"));\n        table.UpdateCell(3, 2, values.HeapSizeGen0.ToString(\"N0\"));\n        table.UpdateCell(4, 2, values.HeapSizeGen1.ToString(\"N0\"));\n        table.UpdateCell(5, 2, values.HeapSizeGen2.ToString(\"N0\"));\n        table.UpdateCell(6, 2, values.HeapSizeLoh.ToString(\"N0\"));\n        table.UpdateCell(7, 2, values.HeapSizePoh.ToString(\"N0\"));\n        table.UpdateCell(8, 2, $\"{values.CpuUtilization:F0}%\");\n        table.UpdateCell(9, 2, $\"{values.AverageDuration * 1000:F3}ms\");\n        table.UpdateCell(10, 2, values.TotalRequests.ToString(\"N0\"));\n        ctx.Refresh();\n    }\n}\n```\n\nMost of this code is simply setting up the table, the \"important\" part in terms of the interaction with the `MetricManager`\n\nall takes place in the `AnsiConsole.Live`\n\nblock:\n\n```\n// As long as the app keeps running...\nwhile (!stoppingToken.IsCancellationRequested)\n{\n    // ...wait 1 second...\n    await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);\n    // ...and then grab the metrics, and render them\n    RenderMetricValues(table, ctx, manager.GetMetrics());\n}\n```\n\nAll that remains is to plug our background service into our app:\n\n```\nusing Microsoft.AspNetCore.Hosting.Server;\nusing Microsoft.AspNetCore.Hosting.Server.Features;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n// Register the MetricDisplayService as an `IHostedService`\nbuilder.Services.AddHostedService<MetricDisplayService>();\n\n// Add the ResourceMonitoring package so that we can retrieve \"process.cpu.utilization\"\nbuilder.Services.AddResourceMonitoring();\nvar app = builder.Build();\n\napp.MapGet(\"/\", () => \"Hello World!\");\n\napp.Run();\n```\n\nand that's it! If we run the app, and generate some load, we'll see our metrics being reported to the console 🎉\n\n```\n┌────────────────────────────────────────────┬─────────────────────────┬─────────────┐\n│ Metric                                     │ Type                    │       Value │\n├────────────────────────────────────────────┼─────────────────────────┼─────────────┤\n│ aspnetcore.routing.match_attempts          │ Counter                 │     250,428 │\n│ dotnet.gc.heap.total_allocated             │ ObservableCounter       │ 849,743,376 │\n│ http.server.active_requests                │ UpDownCounter           │           4 │\n│ dotnet.gc.last_collection.heap.size (gen0) │ ObservableUpDownCounter │   2,497,080 │\n│ dotnet.gc.last_collection.heap.size (gen1) │ ObservableUpDownCounter │     774,872 │\n│ dotnet.gc.last_collection.heap.size (gen2) │ ObservableUpDownCounter │   1,219,120 │\n│ dotnet.gc.last_collection.heap.size (loh)  │ ObservableUpDownCounter │      98,384 │\n│ dotnet.gc.last_collection.heap.size (poh)  │ ObservableUpDownCounter │      65,728 │\n│ process.cpu.utilization                    │ ObservableGauge         │         36% │\n│ http.server.request.duration               │ Histogram               │     0.011ms │\n│ http.server.request.duration (count)       │ Histogram               │     250,425 │\n└────────────────────────────────────────────┴─────────────────────────┴─────────────┘\n```\n\nAnd with that we reach the end. Our app is able to report metrics about itself, and report those in any way it sees fit. In this example we just blindly report them to the console, but you could do anything with them. That said, if you're thinking of doing anything *serious* with these metrics, you should likely consider using [the OpenTelemetry libraries](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-with-otel) instead!\n\n[Summary](#summary)\n\nIn this post I describe the scenario of an app that wants to record and process a specific subset of metrics exposed via the *System.Diagnostics.Metrics* APIs. I then show a simple app that generates some load, use `MeterListener`\n\nto listen for `Instrument`\n\nmeasurements, and display the results in a table using [Spectre.Console](https://spectreconsole.net/). Along the way I show the difference between the standard `Instrument`\n\nand `ObservableInstrument`\n\nmeasurements, show how to trigger observable measurements to be reported, and discuss performance aspects, such as passing state to the callback functions.", "url": "https://wpnews.pro/news/recording-metrics-in-process-using-meterlistener-system-diagnostics-metrics-apis", "canonical_source": "https://andrewlock.net/recording-metrics-in-process-using-meterlistener/", "published_at": "2026-02-24 10:00:00+00:00", "updated_at": "2026-05-23 21:38:09.609389+00:00", "lang": "en", "topics": ["developer-tools", "data", "open-source"], "entities": ["System.Diagnostics.Metrics", "MeterListener", "Spectre.Console", "OpenTelemetry", "Datadog", "ASP.NET Core", "Kestrel"], "alternates": {"html": "https://wpnews.pro/news/recording-metrics-in-process-using-meterlistener-system-diagnostics-metrics-apis", "markdown": "https://wpnews.pro/news/recording-metrics-in-process-using-meterlistener-system-diagnostics-metrics-apis.md", "text": "https://wpnews.pro/news/recording-metrics-in-process-using-meterlistener-system-diagnostics-metrics-apis.txt", "jsonld": "https://wpnews.pro/news/recording-metrics-in-process-using-meterlistener-system-diagnostics-metrics-apis.jsonld"}}