{"slug": "creating-and-consuming-metrics-with-system-diagnostics-metrics-apis-system-apis", "title": "Creating and consuming metrics with System.Diagnostics.Metrics APIs: System.Diagnostics.Metrics APIs - Part 1", "summary": "The article introduces the `System.Diagnostics.Metrics` API, which was built into .NET 6 and is also available for earlier .NET versions via a NuGet package. It explains core concepts like `Instrument` and `Meter`, describes various metric types (such as `Counter`, `UpDownCounter`, `Gauge`, and `Histogram`), and demonstrates how to use the `dotnet-counters` tool for local monitoring of these metrics.", "body_md": "In this post I provide an introduction to the *System.Diagnostics.Metrics* API, show how to use `dotnet-counters`\n\nfor local monitoring of metrics, and show how to add a custom metric to your application.\n\n[The ](#the-system-diagnostics-metrics-apis)*System.Diagnostics.Metrics* APIs\n\n*System.Diagnostics.Metrics*APIs\n\nThe *System.Diagnostics.Metrics* APIs were originally introduced as a built-in in feature in .NET 6, but are also supported in earlier versions of .NET Core and .NET Framework using the [ System.Diagnostics.DiagnosticSource](https://www.nuget.org/packages/System.Diagnostics.DiagnosticSource/) NuGet package. The metrics APIs provide a way to both create and report on metrics generated by an application, such as simple counters, gauges, or histograms of values. I'll describe each of the available metric types later.\n\nThe *System.Diagnostics.Metrics* APIs are designed to easily interoperate with OpenTelemetry, and so can be consumed by a large range of applications. You can also read the metrics using .NET SDK tools like `dotnet-counters`\n\n.\n\nThe word \"metric\" is often used in multiple different ways. Is it a single \"point\" with associated \"tags\"? Is it the full set of the recorded values for a single concept? Is it the \"aggregated\" statistics for all of these points? It's common to see both meanings. In this post I mainly use \"metric\" to mean a stream of recordings for a single concept.\n\nThere are two core concepts exposed by the *System.Diagnostics.Metrics* APIs. These are `Instrument`\n\ns and `Meter`\n\ns:\n\n`Instrument`\n\n: An instrument records the values for a single metric of interest. You might have separate`Instrument`\n\ns for \"products sold\", \"invoices created\", \"invoice total\", and \"GC heap size\".`Meter`\n\n: A`Meter`\n\nis a logical grouping of multiple instruments. For example, thecontains multiple`System.Runtime`\n\n`Meter`\n\n`Instrument`\n\ns about the workings of the runtime, while[the](https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/built-in?view=aspnetcore-10.0#microsoftaspnetcorehosting)contains`Microsoft.AspNetCore.Hosting`\n\n`Meter`\n\n`Instrument`\n\ns about the HTTP requests received by ASP.NET Core.\n\nThere are also several different *types* of `Instrument`\n\n:\n\n`Counter<T>`\n\n/`ObservableCounter<T>`\n\n: These represent a count of occurrences, so return a non-negative values. For example, the number of requests received might be a`Counter<T>`\n\n.`UpDownCounter<T>`\n\n/`ObservableUpDownCounter<T>`\n\n: These are similar to a counter, but can be used to record both positive and negative values. This may be used to report the change in queue size or the number of*active*requests.`Gauge<T>`\n\n/`ObservableGauge<T>`\n\n: These return a value that represents the \"current value\". The values it emits effectively \"replace\" the previous value. For example, the amount of memory used might be a`Gauge<T>`\n\n.`Histogram<T>`\n\n: Reports arbitrary values, which could be subsequently processed to calculate further statistics, or plot as a graph. For example, the duration of each request might be recorded as a`Histogram`\n\n.\n\nYou'll note that the `Counter<T>`\n\n, `UpDownCounter<T>`\n\n, and `Gauge<T>`\n\nall have *observable* versions. This difference relates to how the `Instrument`\n\nrecords and emits values; observable instruments only retrieve their values when explicitly requested, whereas the non-observable versions emit a value as soon as that value is recorded.\n\nThe choice of whether an\n\n`Instrument`\n\nshould be implemented as`Observable*`\n\nis driven partly by performance considerations, and partly by how the value is obtained. I'll cover more about the implementation differences with observable`Instrument`\n\ns in a future post.\n\n[Collecting metrics with ](#collecting-metrics-with-dotnet-counters)`dotnet-counters`\n\n`dotnet-counters`\n\nWhen running in production, you'll likely want to collect your metrics using [an OpenTelemetry exporter integration](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/getting-started-prometheus-grafana#collect-metrics-using-prometheus) or another solution (e.g. Datadog can collect these metrics without requiring application changes), but for local testing\n\n`dotnet-counters`\n\nis a very convenient tool.`dotnet-counters`\n\nis a .NET tool shipped by Microsoft that you can install by running:\n\n```\ndotnet tool install -g dotnet-counters\n```\n\nYou can then run the tool by specifying a process ID or process name to monitor, using:\n\n```\ndotnet-counters monitor -n MyApp\n# or \ndotnet-counters monitor -p 123\n```\n\nAlternatively, you can specify a command to run when starting the tool, and it will monitor the target process:\n\n```\ndotnet-counters monitor -- dotnet MyApp.dll\n```\n\nWhen you run `dotnet-counters`\n\nin this \"monitor\" mode, the counter values are written to the console and periodically refresh:\n\n```\nPress p to pause, r to resume, q to quit.\n    Status: Running\nName                                                                          Current Value\n[System.Runtime]\n    dotnet.assembly.count ({assembly})                                              100\n    dotnet.gc.collections ({collection})\n        gc.heap.generation\n        ------------------\n        gen0                                                                         67\n        gen1                                                                          6\n        gen2                                                                          1\n    dotnet.gc.heap.total_allocated (By)                                       4,134,656\n    dotnet.gc.last_collection.heap.fragmentation.size (By)\n        gc.heap.generation\n        ------------------\n        gen0                                                                    911,896\n        gen1                                                                      5,544\n        gen2                                                                      1,656\n        loh                                                                           0\n        poh                                                                           0\n    dotnet.gc.last_collection.heap.size (By)\n        gc.heap.generation\n        ------------------\n        gen0                                                                    943,560\n        gen1                                                                    271,288\n        gen2                                                                    840,136\n        loh                                                                           0\n        poh                                                                      24,528\n    dotnet.gc.last_collection.memory.committed_size (By)                      3,981,312\n    dotnet.gc.pause.time (s)                                                          0.106\n    dotnet.jit.compilation.time (s)                                                   1.096\n    dotnet.jit.compiled_il.size (By)                                            199,280\n    dotnet.jit.compiled_methods ({method})                                        2,126\n    dotnet.monitor.lock_contentions ({contention})                                    1\n    dotnet.process.cpu.count ({cpu})                                                  4\n    dotnet.process.cpu.time (s)\n        cpu.mode\n        --------\n        system                                                                        5.453\n        user                                                                          9.313\n    dotnet.process.memory.working_set (By)                                   51,384,320\n    dotnet.thread_pool.queue.length ({work_item})                                     0\n    dotnet.thread_pool.thread.count ({thread})                                        4\n    dotnet.thread_pool.work_item.count ({work_item})                             61,911\n    dotnet.timer.count ({timer})                                                      0\n```\n\nYou can choose which `Meter`\n\ns to display by passing a comma-separated list of counters using `--counters`\n\n, for example to show the `Microsoft.AspNetCore.Hosting`\n\n`Meter`\n\n, you would use:\n\n```\n> dotnet-counters monitor --counters 'Microsoft.AspNetCore.Hosting' -- dotnet MyApp.dll\n\nPress p to pause, r to resume, q to quit.\n    Status: Running\n\nName                                                                                                             Current Value\n[Microsoft.AspNetCore.Hosting]\n    http.server.active_requests ({request})\n        http.request.method url.scheme\n        ------------------- ----------\n        GET                 http                                                                                          0\n    http.server.request.duration (s)\n        http.request.method http.response.status_code http.route network.protocol.version url.scheme Percentile\n        ------------------- ------------------------- ---------- ------------------------ ---------- ----------\n        GET                 200                       /          1.1                      http       50                   0\n        GET                 200                       /          1.1                      http       95                   0\n        GET                 200                       /          1.1                      http       99                   0\n```\n\nThe metrics in the above image were created by hitting the same endpoint in a sample app several times, but they show some of the different features of the `Instrument`\n\ns available. Each metric has an associated unit (`{request}`\n\nand `s`\n\n), and also an associated set of *tags*. Tags are an important aspect when recording metrics, as they allow you to more easily group and segregate data.\n\nFor example, the `http.server.active_requests`\n\nup/down counter has tags for `http.request.method`\n\nand `url.scheme`\n\n. Seeing as I only made `GET`\n\nrequests to `http://localhost:5000`\n\n, you only see one set of tags. But if I had made `POST`\n\nrequests, or requests using `https`\n\nthen you would have seen other values there. Similarly, the values in the `http.server.request.duration`\n\nhistogram include tags for each value.\n\nManaging tag\n\ncardinality(the number of possible values) is an important aspect of dealing with tags in all observability data. Depending on how your data is stored, large tag cardinality could cause large data storage costs and an impact on performance. Those limits will generally be controlled by whatever system you're exporting your metrics to.\n\nAs well as \"immediate\" monitoring approaches like the example above, which just outputs to the console, `dotnet-counters`\n\nalso has options for just collecting the metrics and [exporting them in a variety of formats](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters#dotnet-counters-collYouect). You *could* drive a production monitoring system this way, but I suspect most usages of `dotnet-counters`\n\nare for the local testing scenario.\n\n[Creating your own metrics](#creating-your-own-metrics)\n\nThe `dotnet-counters`\n\nexample above demonstrates some of the built-in metrics available in .NET 10. The `System.Runtime`\n\nmeter is available since .NET 9, and the `Microsoft.AspNetCore.Routing`\n\nmeter is available since .NET 8, but there are many other additional built-in metrics available in different versions of .NET. You can find what's available here:\n\nThese metrics can provide a reasonable overview of how your system is operating in general, but there might also be application-specific or business related metrics that would be useful to record from the application itself.\n\nAs an example, we'll create a very simple counter metric that just records the number of requests sent to a particular API. To make it slightly less abstract, we'll imagine this to be a product pricing endpoint, and we want to track how often the details are checked for a given product.\n\n[Creating the initial app](#creating-the-initial-app)\n\nWe'll start by creating the basic app using\n\n```\ndotnet new web\n```\n\nand updating the application to the following:\n\n``` js\nvar builder = WebApplication.CreateBuilder(args);\nvar app = builder.Build();\n\napp.MapGet(\"/product/{id}\", (int id) =>\n{\n    // This would return the real details\n    // TODO: add metrics\n    return $\"Pricing for product {id}\";\n});\n\napp.Run();\n```\n\nWe would obviously return real pricing details from this API, but this is just a demo after all.\n\n[Creating our ](#creating-our-instrument-and-meter)`Instrument`\n\nand `Meter`\n\n`Instrument`\n\nand `Meter`\n\nNow let's add our metrics. We need to create two things:\n\n- An\n`Instrument`\n\nto track the number of requests. - A\n`Meter`\n\nto hold our instrument (and any future related instruments).\n\nWe need to be careful about the naming of both of these, as they essentially serve as the public API for subsequent consumers of our metrics.\n\nSeeing as this is an ASP.NET Core application and we generally avoid global `static`\n\nvariables, the example below shows how we would create a class to encapsulate our `Instrument`\n\nand `Meter`\n\n, so that we can register it with the dependency injection container later. If you were creating an app that doesn't use DI, you could just as easily use `new Meter()`\n\n, and save the variable in a global variable.\n\n```\npublic class ProductMetrics\n{\n    private readonly Counter<long> _pricingDetailsViewed;\n\n    public ProductMetrics(IMeterFactory meterFactory)\n    {\n        var meter = meterFactory.Create(\"MyApp.Products\");\n        _pricingDetailsViewed = meter.CreateCounter<long>(\"myapp.products.pricing_page_requests\");\n    }\n\n    public void PricingPageViewed(int id)\n    {\n        _pricingDetailsViewed.Add(delta: 1, new KeyValuePair<string, object?>(\"product_id\", id));\n    }\n}\n```\n\nIn the code above we:\n\n- Create a new\n`Meter`\n\ncalled`MyApp.Products`\n\n. This is named following similar guidelines to the built-in meters; we have \"namespaced\" using our app's name, and the broad category of the instruments it will include. - We create a\n`Counter<long>`\n\ncalled`myapp.products.pricing_page_requests`\n\n. This is named using the[OpenTelemetry naming guidelines](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/metrics.md#general-guidelines). I opted for`long`\n\nbecause I anticipate that some pages will get a*lot*of reviews in the lifetime of the app (more than`int.MaxValue`\n\n). - We added a convenience method for recording a view of a product's pricing page, tagging the view with the ID of the product we're viewing. We could add other tags&mash;maybe the product name would be more useful for example&smash;but this tag will do for our purposes.\n\nIf we want to add additional `Instrument`\n\ns to the same `Meter`\n\nlater, we would create them here, and likely add similar convenience methods.\n\n[Hooking up the ](#hooking-up-the-instrument-in-the-app)`Instrument`\n\nin the app\n\n`Instrument`\n\nin the appNow that we have our metrics helper, we need to make use of it in our app. This involves both registering the helper in DI, and using it in our API:\n\n```\nusing System.Diagnostics.Metrics;\nusing Microsoft.Extensions.Diagnostics.Metrics;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n// 👇 Register in DI\nbuilder.Services.AddSingleton<ProductMetrics>();\n\nvar app = builder.Build();\n\n// Inject in API handler    👇 \napp.MapGet(\"/product/{id}\", (int id, ProductMetrics metrics) =>\n{\n    metrics.PricingPageViewed(id); // 👈 Record\n    return $\"Details for product {id}\";\n});\n\napp.Run();\n```\n\nWe can now try it out using `dotnet-counters`\n\nto view the metrics.\n\n[Testing our new metric](#testing-our-new-metric)\n\nWe'll start by running our app:\n\n```\ndotnet run\n```\n\nand then in a separate terminal window, we'll set `dotnet-counters`\n\nrunning using\n\n```\ndotnet-counters monitor -n MyApp --counters MyApp.Products\n```\n\nI've used the `-n`\n\noption to find the app by name, `MyApp`\n\n, and made sure to only show the `MyApp.Products`\n\ninstrument.\n\nIf we hit the product endpoint a few times with various IDs, we can see that the metrics are reported to `dotnet-counters`\n\nas expected!\n\nWith that, we have confirmed that we have a custom metric being successfully recorded 🎉\n\n[Adding extra information for consumers](#adding-extra-information-for-consumers)\n\nIn the `dotnet-counters`\n\noutput above, we can see that the Instrument is reported with the unit `Count`\n\n, inferred from the instrument type. That's fine, but the `Instrument`\n\nAPI lets us provide additional details that can be optionally used by consumers to customise the display or metrics.\n\nFor example, we could add some additional details to our instrument, as follows:\n\n```\n_pricingDetailsViewed = meter.CreateCounter<int>(\n    \"myapp.products.pricing_page_requests\",\n    unit: \"requests\",\n    description: \"The number of requests to the pricing details page for the product with the given product_id\");\n```\n\nIf we run and monitor the app again, the `dotnet-counters`\n\noutput has changed slightly. The unit for `myapp.products.pricing_page_requests`\n\nhas changed to `requests`\n\ninstead of `Count`\n\n:\n\n```\nName                                                        Current Value\n[MyApp.Products]\n    myapp.products.pricing_page_requests (requests)\n        product_id\n        ----------\n        1                                                           1\n        234                                                         1\n        5                                                           4\n```\n\nThat's a small nicity, and the description isn't used anywhere by `dotnet-counters`\n\n, but other exporters might choose to use it. Depending on how you're exporting your metrics out of process, your metric should now be available everywhere!\n\n[Summary](#summary)\n\nIn this post, I provided an introduction to the *System.Diagnostics.Metrics* APIs. I described some of the terminology used, such as `Meter`\n\nand `Instrument`\n\n, and the various different types of `Instrument`\n\navailable. I then showed how you can use `dotnet-counters`\n\nto monitor the metrics produced by your app, primarily for local investigation. Finally, I showed how you could create a custom metric, customize it, hook it up to dependency injection, and report it in `dotnet-counters`\n\n.", "url": "https://wpnews.pro/news/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis-system-apis", "canonical_source": "https://andrewlock.net/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis/", "published_at": "2026-01-27 10:00:00+00:00", "updated_at": "2026-05-23 21:39:07.106660+00:00", "lang": "en", "topics": ["developer-tools", "data", "open-source"], "entities": ["System.Diagnostics.Metrics", "dotnet-counters", "OpenTelemetry", ".NET", "NuGet"], "alternates": {"html": "https://wpnews.pro/news/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis-system-apis", "markdown": "https://wpnews.pro/news/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis-system-apis.md", "text": "https://wpnews.pro/news/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis-system-apis.txt", "jsonld": "https://wpnews.pro/news/creating-and-consuming-metrics-with-system-diagnostics-metrics-apis-system-apis.jsonld"}}