Learn ASP.NET Core observability: structured logging with Serilog, application metrics, health checks, and logging best practices.
Observability in ASP.NET Core starts with structured logging. Serilog enriches log events with contextual data; application metrics track business KPIs and system health; and health check endpoints enable Kubernetes liveness/readiness probes and uptime monitoring.
Use ILogger<T> for structured logging and Serilog for enriched output:
1// Configure logging in Program.cs
2builder.Logging.ClearProviders();
3builder.Logging.AddConsole();
4builder.Logging.AddDebug();
5builder.Logging.SetMinimumLevel(LogLevel.Information);
6
7// Log levels configuration
8builder.Logging.AddFilter("Microsoft", LogLevel.Warning);
9builder.Logging.AddFilter("System", LogLevel.Warning);
10builder.Logging.AddFilter("MyApp", LogLevel.Debug);
11
12// Using ILogger in services
13public class OrderService
14{
15 private readonly ILogger<OrderService> _logger;
16
17 public OrderService(ILogger<OrderService> logger)
18 {
19 _logger = logger;
20 }
21
22 public async Task<Order> CreateOrderAsync(CreateOrderDto dto)
23 {
24 // Structured logging with message templates
25 _logger.LogInformation(
26 "Creating order for customer {CustomerId} with {ItemCount} items",
27 dto.CustomerId,
28 dto.Items.Count);
29
30 try
31 {
32 var order = await ProcessOrder(dto);
33
34 _logger.LogInformation(
35 "Order {OrderId} created successfully. Total: {Total:C}",
36 order.Id,
37 order.Total);
38
39 return order;
40 }
41 catch (Exception ex)
42 {
43 _logger.LogError(ex,
44 "Failed to create order for customer {CustomerId}. Error: {ErrorMessage}",
45 dto.CustomerId,
46 ex.Message);
47 throw;
48 }
49 }
50}
51
52// Using scopes for correlated logs
53public async Task ProcessPaymentAsync(Order order)
54{
55 using (_logger.BeginScope(new Dictionary<string, object>
56 {
57 ["OrderId"] = order.Id,
58 ["CustomerId"] = order.CustomerId,
59 ["CorrelationId"] = Guid.NewGuid()
60 }))
61 {
62 _logger.LogInformation("Starting payment processing");
63
64 // All logs within this scope will include OrderId, CustomerId, CorrelationId
65 await ValidatePayment(order);
66 await ChargeCustomer(order);
67 await UpdateOrderStatus(order);
68
69 _logger.LogInformation("Payment processing completed");
70 }
71}
72
73// High-performance logging with LoggerMessage
74public static partial class LoggerExtensions
75{
76 [LoggerMessage(
77 EventId = 1001,
78 Level = LogLevel.Information,
79 Message = "Order {OrderId} created for customer {CustomerId}")]
80 public static partial void LogOrderCreated(
81 this ILogger logger, int orderId, string customerId);
82
83 [LoggerMessage(
84 EventId = 1002,
85 Level = LogLevel.Warning,
86 Message = "Low stock alert: Product {ProductId} has only {Stock} items")]
87 public static partial void LogLowStock(
88 this ILogger logger, int productId, int stock);
89}Track business and system metrics using EventCounters or OpenTelemetry:
1// Install packages:
2// Serilog.AspNetCore
3// Serilog.Sinks.Console
4// Serilog.Sinks.File
5// Serilog.Sinks.Seq
6// Serilog.Enrichers.Environment
7// Serilog.Enrichers.Thread
8
9// Program.cs - Configure Serilog
10Log.Logger = new LoggerConfiguration()
11 .MinimumLevel.Information()
12 .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
13 .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
14 .Enrich.FromLogContext()
15 .Enrich.WithEnvironmentName()
16 .Enrich.WithMachineName()
17 .Enrich.WithThreadId()
18 .WriteTo.Console(outputTemplate:
19 "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
20 .WriteTo.File(
21 path: "logs/app-.log",
22 rollingInterval: RollingInterval.Day,
23 retainedFileCountLimit: 30,
24 outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
25 .WriteTo.Seq("http://localhost:5341")
26 .CreateLogger();
27
28builder.Host.UseSerilog();
29
30// appsettings.json configuration
31{
32 "Serilog": {
33 "MinimumLevel": {
34 "Default": "Information",
35 "Override": {
36 "Microsoft": "Warning",
37 "System": "Warning"
38 }
39 },
40 "WriteTo": [
41 { "Name": "Console" },
42 {
43 "Name": "File",
44 "Args": {
45 "path": "logs/app-.log",
46 "rollingInterval": "Day"
47 }
48 }
49 ],
50 "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
51 }
52}
53
54// Using Serilog with enrichment
55public class PaymentService
56{
57 private readonly ILogger<PaymentService> _logger;
58
59 public async Task ProcessPaymentAsync(Payment payment)
60 {
61 using (LogContext.PushProperty("PaymentId", payment.Id))
62 using (LogContext.PushProperty("Amount", payment.Amount))
63 using (LogContext.PushProperty("Currency", payment.Currency))
64 {
65 _logger.LogInformation("Processing payment");
66
67 // Simulate processing
68 await Task.Delay(100);
69
70 if (payment.Amount > 10000)
71 {
72 _logger.LogWarning("High-value payment detected. Requires review.");
73 }
74
75 _logger.LogInformation("Payment processed successfully");
76 }
77 }
78}
79
80// Request logging middleware
81app.UseSerilogRequestLogging(options =>
82{
83 options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";
84 options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
85 {
86 diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
87 diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString());
88 diagnosticContext.Set("ClientIP", httpContext.Connection.RemoteIpAddress?.ToString());
89 };
90});Add health check endpoints for database, cache, and external service connectivity:
1// Install: System.Diagnostics.DiagnosticSource
2// For Prometheus: prometheus-net.AspNetCore
3
4using System.Diagnostics;
5using System.Diagnostics.Metrics;
6
7// Define metrics
8public class AppMetrics
9{
10 private readonly Counter<long> _requestCounter;
11 private readonly Histogram<double> _requestDuration;
12 private readonly Counter<long> _orderCounter;
13 private readonly UpDownCounter<int> _activeUsers;
14 private readonly ObservableGauge<int> _queueLength;
15 private int _currentQueueLength = 0;
16
17 public AppMetrics(IMeterFactory meterFactory)
18 {
19 var meter = meterFactory.Create("MyApp");
20
21 _requestCounter = meter.CreateCounter<long>(
22 "myapp_requests_total",
23 description: "Total number of HTTP requests");
24
25 _requestDuration = meter.CreateHistogram<double>(
26 "myapp_request_duration_seconds",
27 unit: "s",
28 description: "HTTP request duration in seconds");
29
30 _orderCounter = meter.CreateCounter<long>(
31 "myapp_orders_total",
32 description: "Total orders processed");
33
34 _activeUsers = meter.CreateUpDownCounter<int>(
35 "myapp_active_users",
36 description: "Current number of active users");
37
38 _queueLength = meter.CreateObservableGauge<int>(
39 "myapp_queue_length",
40 () => _currentQueueLength,
41 description: "Current queue length");
42 }
43
44 public void RecordRequest(string method, string path, int statusCode, double duration)
45 {
46 _requestCounter.Add(1,
47 new KeyValuePair<string, object?>("method", method),
48 new KeyValuePair<string, object?>("path", path),
49 new KeyValuePair<string, object?>("status", statusCode));
50
51 _requestDuration.Record(duration,
52 new KeyValuePair<string, object?>("method", method),
53 new KeyValuePair<string, object?>("path", path));
54 }
55
56 public void RecordOrder(string status, string paymentMethod)
57 {
58 _orderCounter.Add(1,
59 new KeyValuePair<string, object?>("status", status),
60 new KeyValuePair<string, object?>("payment_method", paymentMethod));
61 }
62
63 public void UserLoggedIn() => _activeUsers.Add(1);
64 public void UserLoggedOut() => _activeUsers.Add(-1);
65 public void SetQueueLength(int length) => _currentQueueLength = length;
66}
67
68// Register in DI
69builder.Services.AddSingleton<AppMetrics>();
70
71// OpenTelemetry configuration
72builder.Services.AddOpenTelemetry()
73 .WithMetrics(metrics =>
74 {
75 metrics.AddAspNetCoreInstrumentation();
76 metrics.AddHttpClientInstrumentation();
77 metrics.AddMeter("MyApp");
78 metrics.AddPrometheusExporter();
79 })
80 .WithTracing(tracing =>
81 {
82 tracing.AddAspNetCoreInstrumentation();
83 tracing.AddHttpClientInstrumentation();
84 tracing.AddSource("MyApp");
85 tracing.AddJaegerExporter();
86 });
87
88// Expose Prometheus metrics endpoint
89app.MapPrometheusScrapingEndpoint();
90
91// Using metrics in controllers
92[ApiController]
93public class OrdersController : ControllerBase
94{
95 private readonly AppMetrics _metrics;
96
97 [HttpPost]
98 public async Task<IActionResult> CreateOrder(OrderDto dto)
99 {
100 var sw = Stopwatch.StartNew();
101 try
102 {
103 var order = await _orderService.CreateAsync(dto);
104 _metrics.RecordOrder("success", dto.PaymentMethod);
105 return Ok(order);
106 }
107 finally
108 {
109 sw.Stop();
110 _metrics.RecordRequest("POST", "/api/orders", 200, sw.Elapsed.TotalSeconds);
111 }
112 }
113}Apply these logging best practices for production-grade observability:
1// Install: AspNetCore.HealthChecks.*
2
3// Configure health checks
4builder.Services.AddHealthChecks()
5 .AddNpgSql(
6 builder.Configuration.GetConnectionString("Default")!,
7 name: "database",
8 tags: new[] { "db", "critical" })
9 .AddRedis(
10 builder.Configuration.GetConnectionString("Redis")!,
11 name: "redis",
12 tags: new[] { "cache" })
13 .AddUrlGroup(
14 new Uri("https://api.external.com/health"),
15 name: "external-api",
16 tags: new[] { "external" })
17 .AddCheck<CustomHealthCheck>("custom");
18
19// Custom health check
20public class CustomHealthCheck : IHealthCheck
21{
22 private readonly IOrderService _orderService;
23
24 public CustomHealthCheck(IOrderService orderService)
25 {
26 _orderService = orderService;
27 }
28
29 public async Task<HealthCheckResult> CheckHealthAsync(
30 HealthCheckContext context,
31 CancellationToken cancellationToken = default)
32 {
33 try
34 {
35 // Check if order processing is working
36 var pendingOrders = await _orderService.GetPendingCountAsync(cancellationToken);
37
38 if (pendingOrders > 1000)
39 {
40 return HealthCheckResult.Degraded(
41 $"High number of pending orders: {pendingOrders}",
42 data: new Dictionary<string, object>
43 {
44 ["pending_orders"] = pendingOrders
45 });
46 }
47
48 return HealthCheckResult.Healthy(
49 "Order processing is healthy",
50 data: new Dictionary<string, object>
51 {
52 ["pending_orders"] = pendingOrders
53 });
54 }
55 catch (Exception ex)
56 {
57 return HealthCheckResult.Unhealthy(
58 "Order service is unavailable",
59 exception: ex);
60 }
61 }
62}
63
64// Map health check endpoints
65app.MapHealthChecks("/health", new HealthCheckOptions
66{
67 ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
68});
69
70app.MapHealthChecks("/health/ready", new HealthCheckOptions
71{
72 Predicate = check => check.Tags.Contains("critical"),
73 ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
74});
75
76app.MapHealthChecks("/health/live", new HealthCheckOptions
77{
78 Predicate = _ => false // No checks, just confirms app is running
79});
80
81// Health check UI (optional)
82builder.Services.AddHealthChecksUI(options =>
83{
84 options.SetEvaluationTimeInSeconds(30);
85 options.MaximumHistoryEntriesPerEndpoint(50);
86 options.AddHealthCheckEndpoint("API", "/health");
87})
88.AddInMemoryStorage();
89
90app.MapHealthChecksUI(options =>
91{
92 options.UIPath = "/health-ui";
93});
94
95/* Health check response:
96{
97 "status": "Healthy",
98 "totalDuration": "00:00:00.1234567",
99 "entries": {
100 "database": {
101 "status": "Healthy",
102 "duration": "00:00:00.0500000"
103 },
104 "redis": {
105 "status": "Healthy",
106 "duration": "00:00:00.0200000"
107 },
108 "custom": {
109 "status": "Healthy",
110 "data": { "pending_orders": 42 }
111 }
112 }
113}
114*/Your progress is saved in this browser, on this device only.