Skip to content

Healthcheck

IOF edited this page Nov 28, 2025 · 2 revisions

Services

builder.Services.AddHealthCheckServices(config);

HealthCheckServices

public static class HealthCheckServices
{
    public static IServiceCollection AddHealthCheckServices(
        this IServiceCollection services,
        IConfiguration config
    )
    {
        services
            .AddHealthChecks()
            // Проверка подключения к БД
            .AddNpgSql(config.GetConnectionString("PostgreSQL")!, name: "Postgres")
            // Проверка RabbitMQ
            .AddCheck<RabbitMqHealthCheck>("RabbitMQ")
            // Проверка Outbox
            .AddCheck<OutboxHealthCheck>("OutboxPending");

        return services;
    }
}

public class RabbitMqHealthCheck : IHealthCheck
{
    private readonly IOptions<EventBusOptions> _options;
    private readonly ILogger<RabbitMqHealthCheck> _logger;

    public RabbitMqHealthCheck(
        IOptions<EventBusOptions> options,
        ILogger<RabbitMqHealthCheck> logger
    )
    {
        _options = options;
        _logger = logger;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default
    )
    {
        try
        {
            var rmqConnection = new EventBusConnection(_options);
            var rmqChannel = await rmqConnection.CreateChannelAsync();
            await rmqChannel.CloseAsync();

            _logger.LogInformation("HEALTH CHECK: RabbitMQ connection OK");
            return HealthCheckResult.Healthy();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "HEALTH CHECK: RabbitMQ connection failed");
            return HealthCheckResult.Unhealthy(ex.Message);
        }
    }
}

public class OutboxHealthCheck : IHealthCheck
{
    private readonly ModuleBankAppContext _db;
    private readonly ILogger<OutboxHealthCheck> _logger;

    public OutboxHealthCheck(ModuleBankAppContext db, ILogger<OutboxHealthCheck> logger)
    {
        _db = db;
        _logger = logger;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default
    )
    {
        var pendingCount = await _db.Outbox.CountAsync(
            e => e.Status == OutboxStatus.Pending,
            cancellationToken
        );
        if (pendingCount > 100)
        {
            _logger.LogWarning(
                "HEALTH CHECK: Outbox backlog {PendingCount} pending events",
                pendingCount
            );
            return HealthCheckResult.Degraded($"Outbox backlog: {pendingCount}");
        }

        _logger.LogInformation("HEALTH CHECK: Outbox backlog OK ({PendingCount})", pendingCount);
        return HealthCheckResult.Healthy($"Outbox backlog: {pendingCount}");
    }
}

HealthCheckMiddleware

public static class HealthMiddleware
{
    public static WebApplication UseHealthMiddleware(this WebApplication app)
    {
        // Live и Ready endpoints
        app.MapHealthChecks(
                "/health/live",
                new HealthCheckOptions
                {
                    Predicate = _ => false, // только сервис жив
                    ResponseWriter = async (context, report) =>
                    {
                        var correlationId =
                            context.Request.Headers["X-Correlation-Id"].FirstOrDefault()
                            ?? Guid.NewGuid().ToString();

                        var log = new
                        {
                            Path = context.Request.Path,
                            Method = context.Request.Method,
                            Status = report.Status.ToString(),
                            CorrelationId = correlationId,
                            Timestamp = DateTime.UtcNow,
                        };

                        var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
                        logger.LogInformation("Health check called {@Log}", log);

                        // Отправляем стандартный ответ
                        context.Response.ContentType = "application/json";
                        await context.Response.WriteAsJsonAsync(
                            new { status = report.Status.ToString() }
                        );
                    },
                }
            )
            .RequireAuthorization("AllowAll");

        app.MapHealthChecks(
                "/health/ready",
                new HealthCheckOptions
                {
                    Predicate = _ => true,
                    ResponseWriter = async (context, report) =>
                    {
                        context.Response.ContentType = "application/json";

                        var result = new
                        {
                            status = report.Status.ToString(),
                            checks = report.Entries.Select(e => new
                            {
                                name = e.Key,
                                status = e.Value.Status.ToString(),
                                description = e.Value.Description,
                            }),
                        };

                        await context.Response.WriteAsJsonAsync(result);
                    },
                }
            )
            .RequireAuthorization(["AllowAll"]);

        return app;
    }
}

Clone this wiki locally