Start Debugging

Мониторинг фоновых задач в .NET 9 и .NET 10 без Hangfire: здоровье + метрики + оповещения

Мониторинг задач BackgroundService в .NET 9 и .NET 10 без Hangfire с помощью heartbeat-проверок здоровья, метрик длительности и оповещений о сбоях, с практическим примером кода.

Сегодня в r/dotnet всплыл вопрос: “Как вы мониторите и оповещаете о фоновых задачах в .NET (без Hangfire)?”. Главная ошибка — считать, что “сервис поднят” равно “задача работает”. Для фоновой работы нужен сигнал жизни, привязанный к её прогрессу.

Источник обсуждения: https://www.reddit.com/r/dotnet/comments/1q86tv7/how_do_you_monitor_alert_on_background_jobs_in/

Три важных сигнала

Если у вас только журналы, вы будете слепы во время инцидентов “медленно, но не мертво”. Добавьте проверку здоровья и хотя бы одну метрику.

Простой шаблон: heartbeat + последняя ошибка + метрика длительности

Это работает в .NET 9 / .NET 10 с обычным BackgroundService:

using System.Diagnostics;
using System.Diagnostics.Metrics;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;

public sealed class JobState
{
    public DateTimeOffset LastSuccessUtc { get; private set; } = DateTimeOffset.MinValue;
    public Exception? LastError { get; private set; }

    public void MarkSuccess() { LastSuccessUtc = DateTimeOffset.UtcNow; LastError = null; }
    public void MarkFailure(Exception ex) { LastError = ex; }
}

public sealed class MyJob : BackgroundService
{
    private static readonly Meter Meter = new("MyApp.Jobs", "1.0");
    private static readonly Histogram<double> DurationMs = Meter.CreateHistogram<double>("myjob.duration_ms");
    private readonly JobState _state;
    private readonly ILogger<MyJob> _logger;

    public MyJob(JobState state, ILogger<MyJob> logger) { _state = state; _logger = logger; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var start = Stopwatch.GetTimestamp();
            try
            {
                await DoWorkOnce(stoppingToken);
                _state.MarkSuccess();
            }
            catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
            {
                _state.MarkFailure(ex);
                _logger.LogError(ex, "Background job failed.");
            }
            finally
            {
                var elapsedMs = Stopwatch.GetElapsedTime(start).TotalMilliseconds;
                DurationMs.Record(elapsedMs);
            }

            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }
    }

    private static Task DoWorkOnce(CancellationToken ct) => Task.CompletedTask;
}

public sealed class JobHealthCheck(JobState state) : IHealthCheck
{
    private readonly JobState _state = state;

    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken)
    {
        var age = DateTimeOffset.UtcNow - _state.LastSuccessUtc;
        if (age <= TimeSpan.FromMinutes(2))
            return Task.FromResult(HealthCheckResult.Healthy("Job heartbeat OK."));

        var msg = _state.LastError is null
            ? $"No successful run in {age.TotalSeconds:n0}s."
            : $"Last error: {_state.LastError.GetType().Name}. No success in {age.TotalSeconds:n0}s.";

        return Task.FromResult(HealthCheckResult.Unhealthy(msg));
    }
}

Подключите всё:

На что оповещать (часть, которую обычно пропускают)

Если делать только что-то одно — делайте heartbeat-проверку здоровья. Она превращает “жив ли процесс?” в “жива ли задача?”, и это и есть настоящий вопрос.

Дополнительно: https://learn.microsoft.com/aspnet/core/host-and-deploy/health-checks

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад