Measure Application Performance in .NET Using IMeterFactory
Authored by Muhammed Saleem, this article explores practical techniques for integrating and utilizing IMeterFactory to measure and optimize application performance in .NET, focusing on metrics collection and monitoring within ASP.NET Core applications.
Measure Application Performance in .NET Using IMeterFactory
Author: Muhammed Saleem
Originally appeared on Code Maze
Introduction
Performance monitoring is essential for ensuring applications run efficiently and reliably. .NET provides robust tools for this purpose, accessible via the IMeterFactory
API. This article explains the use of these tools to check application health, measure performance, collect optimization data, and adhere to best practices for metric instrumentation in .NET, particularly within ASP.NET Core Web APIs.
.NET Metric Instruments Overview
.NET provides a variety of metric instruments:
- **Counter
**: Tracks increasing counts (e.g., total requests or clicks). - **Gauge
**: Measures fluctuating non-cumulative values (e.g., current memory consumption). - **UpDownCounter
**: Captures values that can both increase and decrease (e.g., queue sizes). - **Histogram
**: Visualizes data distribution across value ranges. - **ObservableCounter
**, **ObservableGauge **, **ObservableUpDownCounter **: Report their values as observed in real-time.
These instruments support accurate, meaningful, and diverse monitoring needs.
Configuring IMeterFactory in ASP.NET Core Web API
IMeterFactory is part of the System.Diagnostics.Metrics
namespace, included by default in .NET 8+. Here’s how to set it up:
public class MetricsService {
public MetricsService(IMeterFactory meterFactory) {
var meter = meterFactory.Create("Metrics.Service");
// Metric instrument initialization follows
}
}
Using Dependency Injection (DI), IMeterFactory
is provided to the MetricsService
, allowing direct creation of metrics collectors.
Defining Metric Instruments
Example code to declare and initialize common metric instruments:
public class MetricsService {
private readonly Counter<int> _userClicks;
private readonly Histogram<double> _responseTime;
private int _requests;
private double _memoryConsumption;
public MetricsService(IMeterFactory meterFactory) {
var meter = meterFactory.Create("Metrics.Service");
_userClicks = meter.CreateCounter<int>("metrics.service.user_clicks");
_responseTime = meter.CreateHistogram<double>("metrics.service.response_time");
meter.CreateObservableCounter("metrics.service.requests", () => _requests);
meter.CreateObservableGauge("metrics.service.memory_consumption", () => _memoryConsumption);
}
}
- Counter and Histogram are for event counts and duration/distribution, respectively.
- ObservableCounter and ObservableGauge are initialized with lambda callbacks that return current values.
Capturing Metrics
Define an interface for the service:
public interface IMetricsService {
void RecordUserClick();
void RecordResponseTime(double value);
void RecordRequest();
void RecordMemoryConsumption(double value);
}
Implement the interface:
public class MetricsService : IMetricsService {
// Private fields and constructor omitted for brevity
public void RecordUserClick() {
_userClicks.Add(1);
}
public void RecordResponseTime(double value) {
_responseTime.Record(value);
}
public void RecordRequest() {
Interlocked.Increment(ref _requests);
}
public void RecordMemoryConsumption(double value) {
_memoryConsumption = value;
}
}
Using Metrics in Controllers
Inject IMetricsService
into your controller and record metrics:
[Route("api/[controller]")]
[ApiController]
public class MetricsController(IMetricsService metricsService) : ControllerBase {
[HttpGet]
public IActionResult Get() {
var random = Random.Shared;
metricsService.RecordUserClick();
for (int i = 0; i < 100; i++) {
metricsService.RecordResponseTime(random.NextDouble());
}
metricsService.RecordRequest();
metricsService.RecordMemoryConsumption(GC.GetAllocatedBytesForCurrentThread() / (1024 * 1024));
return Ok();
}
}
Registering the Service
Register MetricsService
as a singleton for IMetricsService
in your application setup:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IMetricsService, MetricsService>();
var app = builder.Build();
app.Run();
Optionally, add Swagger for API exploration:
builder.Services.AddSwaggerGen();
if (app.Environment.IsDevelopment()) {
app.UseSwagger();
app.UseSwaggerUI();
}
Visualizing the Metrics
Install the dotnet-counters tool for runtime metrics monitoring:
dotnet tool update -g dotnet-counters
To monitor the metrics for your API:
dotnet-counters monitor -n MetricsAPI --counters Metrics.Service
Run your API and invoke endpoints to generate real-time metric data. Data such as user clicks, request counts, response time percentiles, and memory consumption can be observed live in the console.
Enhancing Clarity: Units and Descriptions
Add units and descriptions when defining instruments to improve data presentation:
_responseTime = meter.CreateHistogram<double>(
name: "metrics.service.response_time",
unit: "Seconds",
description: "This metric measures the time taken for the application to respond to user requests."
);
meter.CreateObservableGauge(
name: "metrics.service.memory_consumption",
() => _memoryConsumption,
unit: "Megabytes",
description: "This metric measures the amount of memory used by the application."
);
Visible units aid interpretation in tools like dotnet-counters, even if the description is not displayed.
Multi-Dimensional Metrics (Tags)
Metrics can be tagged to produce additional dimensions (e.g., region, feature):
public void RecordUserClickDetailed(string region, string feature) {
_userClicks.Add(1, new KeyValuePair<string, object?>("user.region", region), new KeyValuePair<string, object?>("user.feature", feature));
}
For resource consumption:
private double _cpu, _memory, _threadCount;
private IEnumerable<Measurement<double>> GetResourceConsumption() {
return [
new Measurement<double>(_cpu, new KeyValuePair<string,object?>("resource_usage", "cpu")),
new Measurement<double>(_memory, new KeyValuePair<string,object?>("resource_usage", "memory")),
new Measurement<double>(_threadCount, new KeyValuePair<string,object?>("resource_usage", "thread_count")),
];
}
meter.CreateObservableGauge(name: "metrics.service.resource_consumption", () => GetResourceConsumption());
Calls to the above metric tracking enhance observability with rich context.
Utility Class Example (CPU Usage Calculation)
public static class Utilities {
public static double GetCpuUsagePercentage() {
var process = Process.GetCurrentProcess();
var startTime = DateTime.UtcNow;
var initialCpuTime = process.TotalProcessorTime;
Thread.Sleep(1000);
var endTime = DateTime.UtcNow;
var finalCpuTime = process.TotalProcessorTime;
var totalCpuTimeUsed = (finalCpuTime - initialCpuTime).TotalMilliseconds;
var totalTimeElapsed = (endTime - startTime).TotalMilliseconds;
var cpuUsage = (totalCpuTimeUsed / (Environment.ProcessorCount * totalTimeElapsed)) * 100;
return cpuUsage;
}
}
Testing Metrics with MetricCollector
Use MetricCollector<T>
from the Microsoft.Extensions.Diagnostics.Testing
package to verify custom metrics:
private static ServiceProvider CreateServiceProvider() {
var serviceCollection = new ServiceCollection();
serviceCollection.AddMetrics();
serviceCollection.AddSingleton<MetricsService>();
return serviceCollection.BuildServiceProvider();
}
public void GivenMetricsConfigured_WhenUserClickRecorded_ThenCounterCaptured() {
using var services = CreateServiceProvider();
var metrics = services.GetRequiredService<MetricsService>();
var meterFactory = services.GetRequiredService<IMeterFactory>();
var collector = new MetricCollector<int>(meterFactory, "Metrics.Service", "metrics.service.user_clicks");
metrics.RecordUserClick();
var measurements = collector.GetMeasurementSnapshot();
Assert.Single(measurements);
Assert.Equal(1, measurements[0].Value);
}
Call RecordObservableInstruments()
before reading from observable counters/gauges.
IMeterFactory Best Practices
- Prefer dependency injection for DI-aware libraries over static variable usage.
- Use unique, lower-case, dot-separated names for meters and instruments, often including assembly or namespace context (see OpenTelemetry guidelines).
- Match instrument type to scenario: use Observable instruments for high-frequency events.
- Use histograms for event timings requiring percentile distribution.
- Use UpDownCounter for quantities that both increment and decrement (e.g., current queue size).
- Adopt standard strings (such as UCUM) for units, and prefer numeric/string types for metric tags.
Conclusion
This article presented a practical approach to implementing performance monitoring in .NET applications using IMeterFactory. Key takeaways include:
- Instrumenting code with appropriate metrics.
- Using dotnet-counters for visualization.
- Employing best practices for naming, tagging, and unit descriptions.
- Utilizing MetricCollector for test-driven development.
These steps contribute to building more maintainable, observable, high-performing applications in the .NET ecosystem.
This post appeared first on “Code Maze Blog”. Read the entire article here