Start Debugging

Hangfire を使わずに .NET 9 と .NET 10 のバックグラウンドジョブを監視する: ヘルス + メトリクス + アラート

.NET 9 と .NET 10 で BackgroundService のジョブを Hangfire なしで監視する方法。ハートビートのヘルスチェック、所要時間メトリクス、失敗アラートを実用的なコード例とともに紹介します。

今日 r/dotnet で出てきた質問です。「(Hangfire なしで) .NET のバックグラウンドジョブをどう監視・アラートしていますか?」。よくある誤りは、“サービスが起動している” を “ジョブが動いている” と同じ意味にとらえてしまうことです。バックグラウンド処理には、ジョブの進行に紐づいた生存シグナルが必要です。

元の議論: https://www.reddit.com/r/dotnet/comments/1q86tv7/how_do_you_monitor_alert_on_background_jobs_in/

重要な 3 つのシグナル

ログしかない場合、“遅いけれど死んではいない” 種類のインシデントで盲目になります。ヘルスチェックを 1 つ、そして少なくとも 1 つのメトリクスを追加してください。

シンプルなパターン: ハートビート + 直近のエラー + 所要時間メトリクス

これは普通の BackgroundService で .NET 9 / .NET 10 で動きます。

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));
    }
}

配線のしかた:

何にアラートを出すか (みんな飛ばしがちな部分)

ひとつだけやるなら、ハートビートのヘルスチェックです。これは “プロセスは生きているか?” を “ジョブは生きているか?” に変換します。後者こそ本当に問うべき質問です。

参考: 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.

< 戻る