Уявіть, що ваш API розгорнуто у production:
# Користувачі скаржаться на помилки
curl https://api.example.com/products
→ 500 Internal Server Error
# DevOps перевіряє сервер
ssh production-server
ps aux | grep dotnet
→ dotnet процес працює ✓
# Але API не відповідає!
Що сталося?
Проблема: Процес "живий", але API не функціональний.
Типовий підхід — ручна перевірка:
# DevOps вручну перевіряє кожен компонент
curl https://api.example.com/health
→ 200 OK (але це нічого не каже про залежності!)
# Перевірка БД
psql -h db-server -U user -c "SELECT 1"
→ Connection refused ❌
# Перевірка Redis
redis-cli -h redis-server ping
→ PONG ✓
# Перевірка диску
df -h
→ /dev/sda1 100% ❌
Проблеми:
Реальний сценарій:
09:00 - База даних перезапускається (планове обслуговування)
09:01 - API не може підключитися до БД
09:01 - Kubernetes думає, що API "живий" (процес працює)
09:01 - Load balancer відправляє 50% трафіку на цей instance
09:01 - Користувачі отримують 500 помилки
09:15 - DevOps помічає проблему через алерти
09:20 - Вручну перезапускають API
09:25 - API знову працює
❌ 24 хвилини downtime через відсутність health checks!
Рішення — Health Checks — автоматична перевірка стану API та його залежностей з можливістю інтеграції з Kubernetes, load balancers та моніторинговими системами.
Ми побудуємо E-commerce API з комплексною системою health checks:
1. Базові health checks:
GET /health
→ 200 OK { "status": "Healthy" }
GET /health/ready
→ 503 Service Unavailable { "status": "Unhealthy", "database": "Disconnected" }
2. Вбудовані чеки:
builder.Services.AddHealthChecks()
.AddSqlServer(connectionString)
.AddRedis(redisConnection)
.AddRabbitMQ(rabbitConnection);
3. Кастомні чеки:
public class DiskSpaceHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context)
{
var freeSpace = GetFreeSpace();
return freeSpace > 1_000_000_000 // 1 GB
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy("Low disk space");
}
}
4. Health Check UI:
https://api.example.com/health-ui
→ Інтерактивний dashboard з історією перевірок
5. Kubernetes probes:
livenessProbe:
httpGet:
path: /health/live
port: 80
readinessProbe:
httpGet:
path: /health/ready
port: 80
До кінця статті ви зможете:
Три типи health checks:
| Тип | Призначення | Коли використовувати | Приклад |
|---|---|---|---|
| Startup | Чи завершився запуск? | Повільний старт (завантаження даних) | Міграції БД, warming up cache |
| Liveness | Чи живий процес? | Deadlock, infinite loop | Процес працює, але не відповідає |
| Readiness | Чи готовий приймати трафік? | Залежності недоступні | БД offline, Redis недоступний |
Приклад:
Startup: ✓ Міграції БД завершені
Liveness: ✓ Процес відповідає на запити
Readiness: ✗ Redis недоступний → Не приймати трафік
public enum HealthStatus
{
Unhealthy = 0, // Критична помилка
Degraded = 1, // Працює, але з проблемами
Healthy = 2 // Все OK
}
Приклад використання:
// Healthy - все працює
return HealthCheckResult.Healthy("Database connected");
// Degraded - працює повільно
return HealthCheckResult.Degraded("Database response time > 1s");
// Unhealthy - не працює
return HealthCheckResult.Unhealthy("Database connection failed");
Групування health checks за категоріями:
builder.Services.AddHealthChecks()
.AddSqlServer(connectionString, tags: new[] { "database", "ready" })
.AddRedis(redisConnection, tags: new[] { "cache", "ready" })
.AddCheck<MemoryHealthCheck>("memory", tags: new[] { "resources", "live" });
Використання:
// Тільки "ready" чеки для readiness probe
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
// Тільки "live" чеки для liveness probe
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live")
});
Створіть файл Program.cs:
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using HealthChecks.UI.Client;
using Microsoft.Extensions.Diagnostics.HealthChecks;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// Базові health checks
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy("API is running"), tags: new[] { "live" });
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
// Health check endpoints
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.Run();
Декомпозиція:
AddHealthChecks() — реєструє health checks системуAddCheck("self") — простий чек "API працює"MapHealthChecks("/health") — endpoint для всіх чеківUIResponseWriter — форматує відповідь у JSON (з бібліотеки UI.Client)Predicate — фільтрує чеки за tagsТестування:
curl https://localhost:5001/health
{
"status": "Healthy",
"totalDuration": "00:00:00.0012345",
"entries": {
"self": {
"status": "Healthy",
"description": "API is running",
"duration": "00:00:00.0001234"
}
}
}
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" })
.AddSqlServer(
connectionString: connectionString!,
name: "database",
failureStatus: HealthStatus.Unhealthy,
tags: new[] { "database", "ready" },
timeout: TimeSpan.FromSeconds(5));
Що перевіряє:
SELECT 1)var redisConnection = builder.Configuration.GetConnectionString("Redis");
builder.Services.AddHealthChecks()
// ... попередні чеки
.AddRedis(
redisConnectionString: redisConnection!,
name: "redis",
failureStatus: HealthStatus.Degraded, // Не критично
tags: new[] { "cache", "ready" },
timeout: TimeSpan.FromSeconds(3));
Що перевіряє:
PING командиfailureStatus: HealthStatus.Degraded — якщо Redis недоступний, API все ще може працювати (без кешу), тому статус Degraded замість Unhealthy.builder.Services.AddHealthChecks()
// ... попередні чеки
.AddUrlGroup(
uri: new Uri("https://api.stripe.com/healthcheck"),
name: "stripe-api",
failureStatus: HealthStatus.Degraded,
tags: new[] { "external", "ready" },
timeout: TimeSpan.FromSeconds(10));
Що перевіряє:
Створіть файл HealthChecks/DiskSpaceHealthCheck.cs:
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace EcommerceHealthChecksApi.HealthChecks;
public class DiskSpaceHealthCheck : IHealthCheck
{
private readonly long _minimumFreeBytesThreshold;
public DiskSpaceHealthCheck(long minimumFreeBytesThreshold = 1_000_000_000) // 1 GB
{
_minimumFreeBytesThreshold = minimumFreeBytesThreshold;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var drive = DriveInfo.GetDrives()
.FirstOrDefault(d => d.IsReady && d.DriveType == DriveType.Fixed);
if (drive == null)
{
return Task.FromResult(
HealthCheckResult.Unhealthy("No fixed drive found"));
}
var freeSpaceBytes = drive.AvailableFreeSpace;
var freeSpaceGB = freeSpaceBytes / 1_000_000_000.0;
var data = new Dictionary<string, object>
{
["Drive"] = drive.Name,
["FreeSpaceGB"] = Math.Round(freeSpaceGB, 2),
["TotalSizeGB"] = Math.Round(drive.TotalSize / 1_000_000_000.0, 2),
["UsedPercentage"] = Math.Round((1 - (double)freeSpaceBytes / drive.TotalSize) * 100, 2)
};
if (freeSpaceBytes < _minimumFreeBytesThreshold)
{
return Task.FromResult(
HealthCheckResult.Unhealthy(
$"Low disk space: {freeSpaceGB:F2} GB free",
data: data));
}
if (freeSpaceBytes < _minimumFreeBytesThreshold * 2)
{
return Task.FromResult(
HealthCheckResult.Degraded(
$"Disk space getting low: {freeSpaceGB:F2} GB free",
data: data));
}
return Task.FromResult(
HealthCheckResult.Healthy(
$"Sufficient disk space: {freeSpaceGB:F2} GB free",
data: data));
}
catch (Exception ex)
{
return Task.FromResult(
HealthCheckResult.Unhealthy(
"Error checking disk space",
exception: ex));
}
}
}
Декомпозиція:
IHealthCheck — інтерфейс для кастомних чеківCheckHealthAsync — метод перевіркиHealthCheckResult — результат (Healthy/Degraded/Unhealthy)data — додаткові дані для моніторингуСтворіть файл HealthChecks/MemoryHealthCheck.cs:
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace EcommerceHealthChecksApi.HealthChecks;
public class MemoryHealthCheck : IHealthCheck
{
private readonly long _thresholdBytes;
public MemoryHealthCheck(long thresholdBytes = 1_000_000_000) // 1 GB
{
_thresholdBytes = thresholdBytes;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var allocated = GC.GetTotalMemory(forceFullCollection: false);
var allocatedMB = allocated / 1_000_000.0;
var data = new Dictionary<string, object>
{
["AllocatedMB"] = Math.Round(allocatedMB, 2),
["Gen0Collections"] = GC.CollectionCount(0),
["Gen1Collections"] = GC.CollectionCount(1),
["Gen2Collections"] = GC.CollectionCount(2)
};
if (allocated > _thresholdBytes)
{
return Task.FromResult(
HealthCheckResult.Degraded(
$"High memory usage: {allocatedMB:F2} MB",
data: data));
}
return Task.FromResult(
HealthCheckResult.Healthy(
$"Memory usage normal: {allocatedMB:F2} MB",
data: data));
}
}
Створіть файл HealthChecks/ExternalApiHealthCheck.cs:
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace EcommerceHealthChecksApi.HealthChecks;
public class ExternalApiHealthCheck : IHealthCheck
{
private readonly HttpClient _httpClient;
private readonly string _url;
public ExternalApiHealthCheck(HttpClient httpClient, string url)
{
_httpClient = httpClient;
_url = url;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await _httpClient.GetAsync(_url, cancellationToken);
stopwatch.Stop();
var data = new Dictionary<string, object>
{
["Url"] = _url,
["StatusCode"] = (int)response.StatusCode,
["ResponseTimeMs"] = stopwatch.ElapsedMilliseconds
};
if (!response.IsSuccessStatusCode)
{
return HealthCheckResult.Unhealthy(
$"External API returned {response.StatusCode}",
data: data);
}
if (stopwatch.ElapsedMilliseconds > 5000)
{
return HealthCheckResult.Degraded(
$"External API slow: {stopwatch.ElapsedMilliseconds}ms",
data: data);
}
return HealthCheckResult.Healthy(
$"External API responsive: {stopwatch.ElapsedMilliseconds}ms",
data: data);
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(
$"External API unreachable: {ex.Message}",
exception: ex);
}
}
}
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" })
.AddSqlServer(connectionString!, tags: new[] { "database", "ready" })
.AddRedis(redisConnection!, tags: new[] { "cache", "ready" })
.AddCheck<DiskSpaceHealthCheck>(
"disk-space",
failureStatus: HealthStatus.Unhealthy,
tags: new[] { "resources", "live" })
.AddCheck<MemoryHealthCheck>(
"memory",
failureStatus: HealthStatus.Degraded,
tags: new[] { "resources", "live" })
.AddTypeActivatedCheck<ExternalApiHealthCheck>(
"stripe-api",
failureStatus: HealthStatus.Degraded,
tags: new[] { "external", "ready" },
args: new object[] { "https://api.stripe.com/healthcheck" });
Декомпозиція:
AddCheck<T> — для чеків без параметрів конструктораAddTypeActivatedCheck<T> — для чеків з параметрами (DI + args)failureStatus — який статус при помилціtags — для фільтрації (live/ready)Health Check UI надає інтерактивний dashboard для моніторингу:
// Program.cs
builder.Services
.AddHealthChecksUI(options =>
{
options.SetEvaluationTimeInSeconds(10); // Перевірка кожні 10 секунд
options.MaximumHistoryEntriesPerEndpoint(50); // Зберігати 50 записів історії
options.AddHealthCheckEndpoint("API Health", "/health");
})
.AddInMemoryStorage(); // Зберігання історії у пам'яті
// ... після app.Build()
app.MapHealthChecksUI(options =>
{
options.UIPath = "/health-ui"; // UI доступний на /health-ui
options.ApiPath = "/health-ui-api"; // API для UI
});
Результат: Відкрийте https://localhost:5001/health-ui для інтерактивного dashboard.
apiVersion: apps/v1
kind: Deployment
metadata:
name: ecommerce-api
spec:
replicas: 3
selector:
matchLabels:
app: ecommerce-api
template:
metadata:
labels:
app: ecommerce-api
spec:
containers:
- name: api
image: ecommerce-api:latest
ports:
- containerPort: 80
# Startup Probe - чи завершився запуск?
startupProbe:
httpGet:
path: /health/startup
port: 80
initialDelaySeconds: 0
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30 # 30 * 5s = 150s максимум на старт
# Liveness Probe - чи живий процес?
livenessProbe:
httpGet:
path: /health/live
port: 80
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3 # 3 невдалі спроби → restart
# Readiness Probe - чи готовий приймати трафік?
readinessProbe:
httpGet:
path: /health/ready
port: 80
initialDelaySeconds: 0
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3 # 3 невдалі спроби → remove from service
// Startup probe - перевіряє тільки базові речі
app.MapHealthChecks("/health/startup", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("startup"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
// Liveness probe - перевіряє, чи процес не "завис"
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
// Readiness probe - перевіряє всі залежності
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
Реєстрація чеків з правильними tags:
builder.Services.AddHealthChecks()
// Startup - тільки критичні речі для старту
.AddCheck("self", () => HealthCheckResult.Healthy(),
tags: new[] { "startup", "live" })
// Live - перевірка deadlock, infinite loops
.AddCheck<MemoryHealthCheck>("memory",
tags: new[] { "live" })
.AddCheck<DiskSpaceHealthCheck>("disk-space",
tags: new[] { "live" })
// Ready - всі залежності
.AddSqlServer(connectionString!,
tags: new[] { "database", "ready" })
.AddRedis(redisConnection!,
tags: new[] { "cache", "ready" })
.AddUrlGroup(new Uri("https://api.stripe.com/healthcheck"),
tags: new[] { "external", "ready" });
Автоматична публікація результатів у зовнішні системи:
public class CloudWatchHealthCheckPublisher : IHealthCheckPublisher
{
private readonly ILogger<CloudWatchHealthCheckPublisher> _logger;
public CloudWatchHealthCheckPublisher(ILogger<CloudWatchHealthCheckPublisher> logger)
{
_logger = logger;
}
public Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
{
_logger.LogInformation(
"Health Check Status: {Status}, Duration: {Duration}ms",
report.Status,
report.TotalDuration.TotalMilliseconds);
// Відправка метрик у CloudWatch, Prometheus, тощо
foreach (var entry in report.Entries)
{
_logger.LogInformation(
" {Name}: {Status} ({Duration}ms)",
entry.Key,
entry.Value.Status,
entry.Value.Duration.TotalMilliseconds);
}
return Task.CompletedTask;
}
}
Реєстрація:
builder.Services.AddSingleton<IHealthCheckPublisher, CloudWatchHealthCheckPublisher>();
builder.Services.Configure<HealthCheckPublisherOptions>(options =>
{
options.Delay = TimeSpan.FromSeconds(10); // Публікувати кожні 10 секунд
options.Period = TimeSpan.FromSeconds(30); // Період між публікаціями
});
Вимкнення чеків у Development:
var healthChecksBuilder = builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" });
if (!builder.Environment.IsDevelopment())
{
// Production-only checks
healthChecksBuilder
.AddSqlServer(connectionString!, tags: new[] { "database", "ready" })
.AddRedis(redisConnection!, tags: new[] { "cache", "ready" });
}
else
{
// Development - mock checks
healthChecksBuilder
.AddCheck("database-mock", () => HealthCheckResult.Healthy("Mock DB"),
tags: new[] { "database", "ready" })
.AddCheck("redis-mock", () => HealthCheckResult.Healthy("Mock Redis"),
tags: new[] { "cache", "ready" });
}
API продовжує працювати навіть якщо деякі залежності недоступні:
public class GracefulDegradationHealthCheck : IHealthCheck
{
private readonly IProductService _productService;
public GracefulDegradationHealthCheck(IProductService productService)
{
_productService = productService;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var canUseCache = await _productService.CanUseCacheAsync();
var canUseDatabase = await _productService.CanUseDatabaseAsync();
if (canUseDatabase && canUseCache)
{
return HealthCheckResult.Healthy("All systems operational");
}
if (canUseDatabase && !canUseCache)
{
return HealthCheckResult.Degraded(
"Cache unavailable, using database only",
data: new Dictionary<string, object>
{
["Cache"] = "Unavailable",
["Database"] = "Available",
["Mode"] = "Degraded"
});
}
if (!canUseDatabase)
{
return HealthCheckResult.Unhealthy(
"Database unavailable",
data: new Dictionary<string, object>
{
["Cache"] = canUseCache ? "Available" : "Unavailable",
["Database"] = "Unavailable"
});
}
return HealthCheckResult.Healthy();
}
}
Інтеграція з Polly Circuit Breaker:
public class CircuitBreakerHealthCheck : IHealthCheck
{
private readonly ICircuitBreakerPolicy _circuitBreaker;
public CircuitBreakerHealthCheck(ICircuitBreakerPolicy circuitBreaker)
{
_circuitBreaker = circuitBreaker;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var state = _circuitBreaker.CircuitState;
var data = new Dictionary<string, object>
{
["State"] = state.ToString(),
["FailureCount"] = _circuitBreaker.FailureCount
};
return state switch
{
CircuitState.Closed => Task.FromResult(
HealthCheckResult.Healthy("Circuit breaker closed", data)),
CircuitState.HalfOpen => Task.FromResult(
HealthCheckResult.Degraded("Circuit breaker half-open", data)),
CircuitState.Open => Task.FromResult(
HealthCheckResult.Unhealthy("Circuit breaker open", data)),
_ => Task.FromResult(
HealthCheckResult.Unhealthy("Unknown circuit breaker state", data))
};
}
}
Кастомний формат відповіді:
app.MapHealthChecks("/health/custom", new HealthCheckOptions
{
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
var response = new
{
status = report.Status.ToString(),
timestamp = DateTime.UtcNow,
duration = report.TotalDuration.TotalMilliseconds,
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
duration = e.Value.Duration.TotalMilliseconds,
description = e.Value.Description,
data = e.Value.Data,
exception = e.Value.Exception?.Message
})
};
await context.Response.WriteAsJsonAsync(response);
}
});
Які health checks мають бути у liveness probe, а які у readiness probe?
Liveness (чи процес живий?):
Readiness (чи готовий приймати трафік?):
Правило: Liveness перевіряє сам процес, Readiness перевіряє залежності.
Який HealthStatus повернути у кожному сценарії?
Створіть health check для перевірки connection pool:
public class DatabaseConnectionPoolHealthCheck : IHealthCheck
{
private readonly string _connectionString;
public DatabaseConnectionPoolHealthCheck(string connectionString)
{
_connectionString = connectionString;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken);
// Перевіряємо connection pool statistics
var poolSize = connection.Database; // Simplified - у production використовуйте performance counters
var data = new Dictionary<string, object>
{
["ConnectionState"] = connection.State.ToString(),
["Database"] = connection.Database,
["ServerVersion"] = connection.ServerVersion
};
// Виконуємо тестовий запит
using var command = connection.CreateCommand();
command.CommandText = "SELECT @@VERSION";
command.CommandTimeout = 5;
var version = await command.ExecuteScalarAsync(cancellationToken);
return HealthCheckResult.Healthy(
"Database connection pool healthy",
data: data);
}
catch (SqlException ex) when (ex.Number == -2) // Timeout
{
return HealthCheckResult.Degraded(
"Database connection timeout",
exception: ex);
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(
"Database connection failed",
exception: ex);
}
}
}
Перевірте, чи не досягнуто ліміту rate limiting:
public class RateLimitHealthCheck : IHealthCheck
{
private readonly IRateLimitService _rateLimitService;
public RateLimitHealthCheck(IRateLimitService rateLimitService)
{
_rateLimitService = rateLimitService;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var stats = _rateLimitService.GetStatistics();
var data = new Dictionary<string, object>
{
["TotalRequests"] = stats.TotalRequests,
["RejectedRequests"] = stats.RejectedRequests,
["RejectionRate"] = stats.RejectionRate,
["CurrentLimit"] = stats.CurrentLimit
};
if (stats.RejectionRate > 0.5) // > 50% rejected
{
return Task.FromResult(
HealthCheckResult.Unhealthy(
$"High rejection rate: {stats.RejectionRate:P}",
data: data));
}
if (stats.RejectionRate > 0.2) // > 20% rejected
{
return Task.FromResult(
HealthCheckResult.Degraded(
$"Elevated rejection rate: {stats.RejectionRate:P}",
data: data));
}
return Task.FromResult(
HealthCheckResult.Healthy(
$"Rate limiting normal: {stats.RejectionRate:P} rejected",
data: data));
}
}
Створіть health check, що агрегує результати кількох інших:
public class CompositeHealthCheck : IHealthCheck
{
private readonly IHealthCheckService _healthCheckService;
public CompositeHealthCheck(IHealthCheckService healthCheckService)
{
_healthCheckService = healthCheckService;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
// Виконуємо всі "ready" чеки
var report = await _healthCheckService.CheckHealthAsync(
check => check.Tags.Contains("ready"),
cancellationToken);
var data = new Dictionary<string, object>();
var unhealthyChecks = new List<string>();
var degradedChecks = new List<string>();
foreach (var entry in report.Entries)
{
data[entry.Key] = entry.Value.Status.ToString();
if (entry.Value.Status == HealthStatus.Unhealthy)
{
unhealthyChecks.Add(entry.Key);
}
else if (entry.Value.Status == HealthStatus.Degraded)
{
degradedChecks.Add(entry.Key);
}
}
if (unhealthyChecks.Any())
{
return HealthCheckResult.Unhealthy(
$"Unhealthy checks: {string.Join(", ", unhealthyChecks)}",
data: data);
}
if (degradedChecks.Any())
{
return HealthCheckResult.Degraded(
$"Degraded checks: {string.Join(", ", degradedChecks)}",
data: data);
}
return HealthCheckResult.Healthy(
"All checks passed",
data: data);
}
}
Створіть middleware для кешування результатів health checks:
public class CachedHealthCheckMiddleware
{
private readonly RequestDelegate _next;
private readonly IHealthCheckService _healthCheckService;
private readonly IMemoryCache _cache;
private readonly TimeSpan _cacheDuration;
public CachedHealthCheckMiddleware(
RequestDelegate next,
IHealthCheckService healthCheckService,
IMemoryCache cache,
TimeSpan cacheDuration)
{
_next = next;
_healthCheckService = healthCheckService;
_cache = cache;
_cacheDuration = cacheDuration;
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.Request.Path.StartsWithSegments("/health"))
{
await _next(context);
return;
}
var cacheKey = $"health-check:{context.Request.Path}";
if (_cache.TryGetValue<HealthReport>(cacheKey, out var cachedReport))
{
await WriteResponse(context, cachedReport!);
return;
}
var report = await _healthCheckService.CheckHealthAsync();
_cache.Set(cacheKey, report, _cacheDuration);
await WriteResponse(context, report);
}
private static async Task WriteResponse(HttpContext context, HealthReport report)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = report.Status == HealthStatus.Healthy ? 200 : 503;
await context.Response.WriteAsJsonAsync(new
{
status = report.Status.ToString(),
duration = report.TotalDuration.TotalMilliseconds,
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString()
})
});
}
}
Реєстрація:
app.UseMiddleware<CachedHealthCheckMiddleware>(TimeSpan.FromSeconds(10));
Створіть систему алертів на основі health checks:
public class HealthCheckAlertingPublisher : IHealthCheckPublisher
{
private readonly ILogger<HealthCheckAlertingPublisher> _logger;
private readonly IEmailService _emailService;
private readonly Dictionary<string, HealthStatus> _previousStatuses = new();
public HealthCheckAlertingPublisher(
ILogger<HealthCheckAlertingPublisher> logger,
IEmailService emailService)
{
_logger = logger;
_emailService = emailService;
}
public async Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
{
foreach (var entry in report.Entries)
{
var currentStatus = entry.Value.Status;
var checkName = entry.Key;
// Перевіряємо, чи змінився статус
if (_previousStatuses.TryGetValue(checkName, out var previousStatus))
{
if (previousStatus != currentStatus)
{
await SendAlert(checkName, previousStatus, currentStatus, entry.Value);
}
}
_previousStatuses[checkName] = currentStatus;
}
}
private async Task SendAlert(
string checkName,
HealthStatus previousStatus,
HealthStatus currentStatus,
HealthReportEntry entry)
{
var severity = currentStatus switch
{
HealthStatus.Unhealthy => "CRITICAL",
HealthStatus.Degraded => "WARNING",
HealthStatus.Healthy => "INFO",
_ => "UNKNOWN"
};
var message = $@"
Health Check Alert
Check: {checkName}
Previous Status: {previousStatus}
Current Status: {currentStatus}
Severity: {severity}
Description: {entry.Description}
Duration: {entry.Duration.TotalMilliseconds}ms
Exception: {entry.Exception?.Message ?? "None"}
";
_logger.LogWarning(
"Health check status changed: {CheckName} {PreviousStatus} → {CurrentStatus}",
checkName,
previousStatus,
currentStatus);
if (currentStatus == HealthStatus.Unhealthy)
{
await _emailService.SendAlertAsync(
"ops-team@example.com",
$"[{severity}] Health Check Alert: {checkName}",
message);
}
}
}
Експортуйте health check метрики у Prometheus:
public class PrometheusHealthCheckPublisher : IHealthCheckPublisher
{
private readonly ILogger<PrometheusHealthCheckPublisher> _logger;
// Prometheus metrics (використовуйте prometheus-net бібліотеку)
private static readonly Gauge HealthCheckStatus = Metrics.CreateGauge(
"health_check_status",
"Health check status (0=Unhealthy, 1=Degraded, 2=Healthy)",
new GaugeConfiguration
{
LabelNames = new[] { "check_name" }
});
private static readonly Gauge HealthCheckDuration = Metrics.CreateGauge(
"health_check_duration_milliseconds",
"Health check duration in milliseconds",
new GaugeConfiguration
{
LabelNames = new[] { "check_name" }
});
public PrometheusHealthCheckPublisher(ILogger<PrometheusHealthCheckPublisher> logger)
{
_logger = logger;
}
public Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
{
foreach (var entry in report.Entries)
{
var statusValue = entry.Value.Status switch
{
HealthStatus.Healthy => 2,
HealthStatus.Degraded => 1,
HealthStatus.Unhealthy => 0,
_ => -1
};
HealthCheckStatus.WithLabels(entry.Key).Set(statusValue);
HealthCheckDuration.WithLabels(entry.Key).Set(entry.Value.Duration.TotalMilliseconds);
}
return Task.CompletedTask;
}
}
Prometheus scrape config:
scrape_configs:
- job_name: 'ecommerce-api'
static_configs:
- targets: ['api.example.com:80']
metrics_path: '/metrics'
scrape_interval: 15s
Grafana dashboard query:
# Health check status
health_check_status{check_name="database"}
# Health check duration
health_check_duration_milliseconds{check_name="database"}
# Alert rule
health_check_status{check_name="database"} < 2
У цій статті ви навчилися створювати комплексну систему health checks для Web API:
1. Три типи health checks:
2. HealthStatus:
3. Вбудовані чеки:
4. Кастомні чеки:
5. Health Check UI:
6. Kubernetes Integration:
✅ Використовуйте tags для розділення live/ready чеків
✅ Встановлюйте timeouts для всіх чеків (3-5 секунд)
✅ Логуйте зміни статусу для debugging
✅ Кешуйте результати для зменшення навантаження
✅ Моніторьте health checks через Prometheus/CloudWatch
✅ Налаштуйте алерти на Unhealthy статус
✅ Тестуйте health checks у staging перед production
ASP.NET Core Health Checks
AspNetCore.Diagnostics.HealthChecks
Kubernetes Probes
Health Check UI
Документація API - Swashbuckle, NSwag та генерація клієнтів
Production-level документація для Web API Controllers, XML-коментарі → OpenAPI, Swashbuckle фільтри (IOperationFilter, IDocumentFilter), аутентифікація у Swagger UI, NSwag для генерації C# та TypeScript клієнтів, Refit автоматичний HTTP-клієнт.
Підсумковий проєкт - Production-Ready REST API
Наскрізний проєкт Book Store REST API, що поєднує всі попередні статті - ControllerBase, ActionResult, Content Negotiation, API Versioning, ProblemDetails, Filters, Pagination, HATEOAS, Hybrid Architecture, Documentation, Health Checks. Повний production-ready проєкт з best practices.