しばやん雑記

ASP.NET Core とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET Core 2.2 で追加された Health Checks API の基本的な使い方とカスタマイズ

ASP.NET Core 2.2 から Health Checks API が実装されました。最近の Container や Microservices の流れでは必須機能と言えるので、シンプルな API として実装されたのは良い感じです。

ドキュメントがありますが重要な機能だからかとても長いです。過去最長ぐらいのレベルで。

API はシンプルな割に行えることがとても多いので、実際に自分で触ってみておくことにします。

ちなみに Health Check に関係するデザインパターンやアーキテクチャのドキュメントも、Microsoft Docs にて公開されています。使ってる技術は古いですが、こういうのは考え方が重要なので問題ありません。

目を通しておくと、何故 Health Check が今必要なのかが理解できると思います。

以下に実際に自分が触って動作を確認したり、試してみて気になったことをまとめました。

Health Checks の基本

ASP.NET Core 2.2 で追加された Health Checks API はデザインパターンや Microservices Architecture で紹介されていたものとは API 周りが異なっていますが、考え方は同じなので安心してください。

状態としては Health / Degraded / Unhealthy の 3 つがあり、チェック用のコンポーネントが 3 つの内のどれかを返す形です。例によって DI ベースなので自由度は高いです。

public void ConfigureServices(IServiceCollection services)
{
    services.AddHealthChecks()
            .AddCheck("kazuakix", () => HealthCheckResult.Unhealthy());
}

組み込みの Health Check は用意されていないので、今回は適当に固定値を返すだけのものを追加しておきます。本来なら IHealthCheck を実装したクラスを用意しますが、動作を見るためのテストなので。

追加した Health Check の結果はエンドポイントを用意することで確認出来ます。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseHealthChecks("/health");
}

今回はよくある名前のエンドポイントを用意しました。これが最低限のコードです。

実際に /health にアクセスすると、以下のように結果が返ってきます。

f:id:shiba-yan:20181218165928p:plain

ブラウザだとぱっと見確認出来ないですが、Unhealthy の時には 503 が返ってきます。

複数の Health Check を追加して、それぞれが別々の状態を返した場合は一番悪いものが返ります。なので以下のような場合は Unhealthy と 503 が返るようになっています。

public void ConfigureServices(IServiceCollection services)
{
    services.AddHealthChecks()
            .AddCheck("kazuakix", () => HealthCheckResult.Unhealthy())
            .AddCheck("buchizo", () => HealthCheckResult.Healthy());
}

基本はこれだけなのですぐに理解できると思います。後はカスタマイズです。

API レスポンスのカスタマイズ

デフォルトの API はテキストとステータスコードを返すだけの非常にシンプルなものでした。

大体はステータスコードで区別すると思うので問題ないと思いますが、もうちょっと情報が欲しい場合には ResponseWriter をカスタマイズする必要があります。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseHealthChecks("/health", new HealthCheckOptions
    {
        ResponseWriter = async (context, report) =>
        {
            var json = JsonConvert.SerializeObject(report);

            context.Response.ContentType = "application/json";

            await context.Response.WriteAsync(json);
        }
    });
}

上のようにカスタマイズすると、HealthReport の内容を JSON にして返してくれます。

f:id:shiba-yan:20181218170005p:plain

ステータスコードの変更に関しては ResultStatusCodes にマッピング用テーブルが用意されているので、そこのマッピングを弄った方が楽です。

Tags

それぞれの Health Check には Tags が追加出来ます。追加しておくと API 側で特定の Tag が付いたものだけを含めたり、柔軟に定義できるようになります。要するにグルーピングに使えるということです。

以下のような定義を用意すると、Tag 毎に Health Check のエンドポイントを分けることが出来ます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddHealthChecks()
            .AddCheck("kazuakix", () => HealthCheckResult.Unhealthy(), new[] { "wakayama" })
            .AddCheck("buchizo", () => HealthCheckResult.Healthy(), new[] { "osaka" });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseHealthChecks("/wakayama-health", new HealthCheckOptions
    {
        Predicate = registration => registration.Tags.Contains("wakayama")
    });

    app.UseHealthChecks("/osaka-health", new HealthCheckOptions
    {
        Predicate = registration => registration.Tags.Contains("osaka")
    });
}

エンドポイントを複数、それも簡単に用意できるのも Health Checks API の特徴ですね。ちなみに Predicate で常に false を返すと、Kestrel が生きてるかだけの確認が出来ます。*1

Publisher

Health Check API の中でもかなり面白いと思ったのがこの Publisher です。

以下のような IHealthCheckPublisher を実装したクラスを DI に追加しておくと、定期的に Health Check の結果とともに呼び出されるので、他のサービスに状態を送信できます。

public class SamplePublisher : IHealthCheckPublisher
{
    public Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
    {
        Debug.WriteLine(JsonConvert.SerializeObject(report));

        return Task.CompletedTask;
    }
}

DI に追加する時には IHealthCheckPublisher を指定しないと動かないので注意。自分は少しはまりました。

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IHealthCheckPublisher, SamplePublisher>();
}

実行するとデフォルトでは 30 秒ごとに呼び出されていることが確認出来ます。

f:id:shiba-yan:20181218172124p:plain

この辺りの設定は HealthCheckPublisherOptions で変更できるので、間隔を長くしたりチェック対象を絞ったりは簡単です。Pull 型ではなく Push 型になるので、Statuspage.io などの外部サービスに送信して、ステータスページの実装を簡単に出来るでしょう。

特に面白いと思ったのがこの Publisher は IHostedService として実装されている点です。アプリケーションと同じプロセス内で、別の Long-running な処理を同時に走らせることが出来るので、よくある Sidecar 的なものを簡単に組み込めると思います。

公式提供されている Health Check

ASP.NET Core 2.2 のリリース時点では Entity Framework Core 向けの Health Check が提供されています。使うためには以下の NuGet パッケージをインストールする必要があります。

使い方は簡単で DbContext を DI に追加しつつ、AddDbContextCheck を呼び出せば良いです。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddHealthChecks()
            .AddDbContextCheck<AppDbContext>();
}

分かりやすいようにタグを付けておくのも良さそうです。個人的にはテストにかかった時間を計測して、遅くなったら Degraded も返すようにしたい気持ちがあります。

デフォルトのテストクエリはデータベースへの接続になっているので、実際にテーブルへのクエリを投げて確認する処理に変えても良さそうな感じです。この辺りは AddDbContextCheck の引数としてカスタムのテストクエリを指定出来るので、適宜オーバーライドすれば良いです。

カスタム Health Check を作成する

ここまではモックレベルだったり、Entity Framework Core のみ使えるなど微妙な感じでしたが、実際に IHealthCheck を実装してカスタム Health Check を用意してみることにします。

IHealthCheck はシンプルなインターフェースで CheckHealthAsync を実装するだけです。

public class BlogHealthCheck : IHealthCheck
{
    public BlogHealthCheck(string feedUrl)
    {
        _feedUrl = feedUrl;
    }

    private readonly string _feedUrl;

    private static readonly HttpClient _httpClient = new HttpClient();

    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
    {
        var feed = await _httpClient.GetStringAsync(_feedUrl);

        var pubDateElement = XDocument.Parse(feed)
                                      .Descendants("pubDate")
                                      .First();

        var pubDate = DateTime.Parse(pubDateElement.Value);

        var dateDiff = DateTime.Now - pubDate;

        if (dateDiff.TotalDays <= 3)
        {
            return HealthCheckResult.Healthy($"Last updated: {pubDate}");
        }

        if (dateDiff.TotalDays <= 7)
        {
            return HealthCheckResult.Degraded($"Last updated: {pubDate}");
        }

        return HealthCheckResult.Unhealthy($"Last updated: {pubDate}");
    }
}

テスト用なので適当に Feed を読み取って、何日前に更新されたかで HealthStatus を変えるように実装しました。外部の API や SQL Server などを叩く場合は、応答時間を条件に組み込むと良い感じがします。

Health Checks API への追加には通常は拡張メソッドを用意しますが、今回はインスタンスを直接指定しました。拡張メソッドにするとこの辺りを綺麗に隠せます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddHealthChecks()
            .AddCheck("kazuakix", new BlogHealthCheck("https://blog.kazuakix.jp/rss"))
            .AddCheck("buchizo", new BlogHealthCheck("https://buchizo.wordpress.com/feed/"))
            .AddCheck("shibayan", new BlogHealthCheck("https://blog.shibayan.jp/rss"));
}

これで Health Checks が行われるようになるので、エンドポイントを叩くと状態が返ってきます。

f:id:shiba-yan:20181218185035p:plain

今回は ResponseWriter をオーバーライドして分かりやすくしています。HTTP ステータスコードも Unhealthy が混ざっているので 503 が返ってきています。

アプリケーションに必要な Health Checks はそれぞれに特化した形で実装した方が適切なケースが多いと思うので、カスタム Health Check の作成は知っておいて損がないと思います。

BeatPulse との統合

既に ASP.NET Core 向けにはコミュニティベースで BeatPulse というライブラリが公開されていましたが、ASP.NET Core 2.2 の Health Checks API のリリースに合わせてポーティングが行われました。

かなりの数のサービスに対しての Health Check が提供されているので、最低限のチェックはこれを使うことで恩恵を受けることが出来るでしょう。

Publisher についても既に Application Insights と Prometheus 向けがポーティングされています。

その中でも便利なのが BeatPulse で提供されていた UI です。それも Health Checks API 向けにポーティングされたので、非常に簡単に利用できるようになっています。

使うためには以下の NuGet パッケージをインストールして、少し設定を追加するぐらいです。

インストールしたら、例によって Services の設定を追加します。Health Checks のエンドポイントは専用の ResponseWrirter を指定する必要があるので注意。

オプションも指定出来るようになってますが、いったんはデフォルトのまま進めます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddHealthChecksUI();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseHealthChecks("/healthz", new HealthCheckOptions
    {
        Predicate = _ => true,
        ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
    });

    app.UseHealthChecksUI();
}

Startup にコードを追加すれば、後は appsettings.json に HealthChecks-UI のセクションを追加します。ここでチェックするエンドポイントを指定する必要がありますが、絶対 URL が必要なのでちょっと手間です。

とりあえず今回はローカル向けのエンドポイントを指定しました。ポートは変わるので注意。

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "HealthChecks-UI": {
    "HealthChecks": [
      {
        "Name": "Blogs",
        "Uri": "https://localhost:44380/healthz"
      }
    ],
    "EvaluationTimeOnSeconds": 10,
    "MinimumSecondsBetweenFailureNotifications": 60
  }
}

Health Checks API のエンドポイント以外にも Webhooks も設定できますが、今回は省略しました。

設定はこれで終わりなので Visual Studio からアプリケーションを実行し、ブラウザから /healthchecks-ui にアクセスすると以下のようなページが表示されるはずです。

f:id:shiba-yan:20181218180509p:plain

一目で Health Check の結果が理解できると思います。こういうステータスをまとめたページは外部公開向けっぽいイメージがありますが、Health Check 時にそうなった理由や例外情報などを任意で含めることが出来るので、内部での監視用としても便利に使えるでしょう。

Application Insights への Publisher はありますが、見た感じカスタムイベントを送信するだけでなので、各コンポーネントの状態を一目で確認というような用途では使いにくそうです。Web Test を使ってエンドポイントを叩くようにした方が良さそうな気がします。

*1:Liveness Probe と Readiness Probe を定義できるということ