.NET Monitoring Techniques
Introduction
Monitoring is a critical aspect of maintaining healthy .NET applications in production environments. Effective monitoring allows developers and operations teams to identify issues before they impact users, troubleshoot problems quickly, and optimize application performance.
In this guide, we'll explore various techniques and tools for monitoring .NET applications, from basic logging to advanced Application Performance Monitoring (APM) solutions. Whether you're deploying a simple web application or a complex microservices architecture, these monitoring approaches will help ensure your .NET applications run smoothly.
Why Monitoring Matters
Before diving into specific techniques, let's understand why monitoring is essential:
- Early Issue Detection: Identify problems before they affect users
- Performance Optimization: Pinpoint bottlenecks and resource-intensive operations
- Capacity Planning: Make informed decisions about scaling resources
- Security Monitoring: Detect unusual access patterns or potential breaches
- User Experience Insights: Understand how real users interact with your application
Basic Logging Techniques
Using the Built-in Logging Framework
.NET provides a robust logging infrastructure through the Microsoft.Extensions.Logging
namespace. This is the foundation of most monitoring strategies.
First, add the required package to your project:
dotnet add package Microsoft.Extensions.Logging
Here's a basic example of configuring and using the logger:
using Microsoft.Extensions.Logging;
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public void ProcessOrder(Order order)
{
_logger.LogInformation("Starting to process order {OrderId}", order.Id);
try
{
// Order processing logic
_logger.LogInformation("Order {OrderId} processed successfully", order.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", order.Id);
throw;
}
}
}
Configuring Logging in ASP.NET Core
In ASP.NET Core applications, you can configure logging in the Program.cs
file:
var builder = WebApplication.CreateBuilder(args);
// Configure logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
builder.Logging.AddEventLog();
// Set minimum log level
builder.Logging.SetMinimumLevel(LogLevel.Information);
var app = builder.Build();
// Rest of your application setup
Structured Logging with Serilog
For more advanced logging capabilities, Serilog is a popular choice among .NET developers:
dotnet add package Serilog.AspNetCore
Configure Serilog in your application:
var builder = WebApplication.CreateBuilder(args);
// Setup Serilog
builder.Host.UseSerilog((ctx, lc) => lc
.ReadFrom.Configuration(ctx.Configuration)
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File("logs/app.log", rollingInterval: RollingInterval.Day));
// Services and middleware setup
var app = builder.Build();
Example appsettings.json
configuration:
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
}
}
}
Health Checks
Health checks provide a simple way to report the health status of your application and its dependencies.
Implementing Health Checks in ASP.NET Core
Add the health checks package:
dotnet add package Microsoft.AspNetCore.Diagnostics.HealthChecks
Configure health checks in your application:
var builder = WebApplication.CreateBuilder(args);
// Add health checks
builder.Services.AddHealthChecks()
.AddSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
.AddRedis(builder.Configuration.GetConnectionString("Redis"))
.AddCheck<CustomHealthCheck>("custom_check");
var app = builder.Build();
// Configure middleware
app.UseHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.Run();
Example of a custom health check:
public class CustomHealthCheck : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
CancellationToken cancellationToken = default)
{
// Check some business-critical dependency or functionality
var isHealthy = await CheckCriticalServiceAsync();
if (isHealthy)
{
return HealthCheckResult.Healthy("The service is functioning normally.");
}
return HealthCheckResult.Unhealthy("The service is experiencing issues.");
}
private async Task<bool> CheckCriticalServiceAsync()
{
// Implementation of your critical service check
return true;
}
}
Performance Monitoring
Performance Counters
.NET provides access to system performance counters which can be used to monitor various aspects of your application:
using System.Diagnostics;
public class PerformanceMonitor
{
public void MonitorCpuUsage()
{
try
{
using var cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
var firstValue = cpuCounter.NextValue();
// Need to wait a short period to get accurate reading
Thread.Sleep(1000);
var secondValue = cpuCounter.NextValue();
Console.WriteLine($"CPU Usage: {secondValue}%");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to monitor CPU: {ex.Message}");
}
}
public void MonitorMemoryUsage()
{
try
{
using var memCounter = new PerformanceCounter("Memory", "Available MBytes");
var availableMb = memCounter.NextValue();
Console.WriteLine($"Available Memory: {availableMb} MB");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to monitor memory: {ex.Message}");
}
}
}
.NET EventCounters
EventCounters provide a modern, lightweight alternative to performance counters:
using System.Diagnostics.Tracing;
[EventSource(Name = "MyCompany-MyApp-Monitoring")]
public sealed class MyAppEventSource : EventSource
{
public static readonly MyAppEventSource Log = new MyAppEventSource();
private IncrementingEventCounter _requestCounter;
private EventCounter _requestDuration;
private MyAppEventSource()
{
_requestCounter = new IncrementingEventCounter("request-count", this)
{
DisplayName = "Request Count",
DisplayRateTimeScale = TimeSpan.FromSeconds(1)
};
_requestDuration = new EventCounter("request-duration", this)
{
DisplayName = "Request Duration",
DisplayUnits = "ms"
};
}
public void RequestStart()
{
_requestCounter.Increment();
}
public void RequestEnd(float duration)
{
_requestDuration.WriteMetric(duration);
}
protected override void Dispose(bool disposing)
{
_requestCounter?.Dispose();
_requestCounter = null;
_requestDuration?.Dispose();
_requestDuration = null;
base.Dispose(disposing);
}
}
To use this event source in your application:
public async Task ProcessRequestAsync()
{
MyAppEventSource.Log.RequestStart();
var stopwatch = Stopwatch.StartNew();
try
{
// Process request
await Task.Delay(100); // Simulating work
}
finally
{
stopwatch.Stop();
MyAppEventSource.Log.RequestEnd(stopwatch.ElapsedMilliseconds);
}
}
Diagnostic Tools
Using dotnet-trace
dotnet-trace
is a command-line tool for collecting traces from .NET applications:
# Install the tool
dotnet tool install --global dotnet-trace
# Collect a trace
dotnet trace collect --process-id <PID> --output trace.nettrace
# Convert the trace to a format readable by perfview
dotnet trace convert trace.nettrace --format speedscope
Using dotnet-counters
dotnet-counters
allows you to monitor .NET Core applications using EventCounters:
# Install the tool
dotnet tool install --global dotnet-counters
# Monitor system.runtime counters
dotnet counters monitor --process-id <PID> System.Runtime
# Monitor custom counters
dotnet counters monitor --process-id <PID> MyCompany-MyApp-Monitoring
Example output:
Press p to pause, r to resume, q to quit.
Status: Running
[System.Runtime]
CPU Usage (%) 24
Working Set (MB) 112
GC Heap Size (MB) 54
Gen 0 GC Count 115
Gen 1 GC Count 49
Gen 2 GC Count 5
Exception Count 0
Using dotnet-dump
For analyzing memory issues and crashes:
# Install the tool
dotnet tool install --global dotnet-dump
# Collect a memory dump
dotnet dump collect --process-id <PID>
# Analyze the dump
dotnet dump analyze core_20201016_143440