Ранее я писал о создании Проверки работоспособности для Microsoft Orleans, но ответ в формате JSON был слишком минимальным. В этом посте мы увидим, как украсить этот вывод!

В предыдущем посте мы немного узнали о проверках работоспособности, о том, как их создавать и просматривать их «состояние» с точки зрения Microsoft Orleans. Конечным результатом был ответ из одного слова «Здоровый», «Деградированный» или «Нездоровый»; не очень увлекательная вещь.

В этом посте я хотел бы быстро рассказать, как вы будете не только сообщать об «общем статусе», но и предоставлять подробную информацию об отдельных проверках работоспособности, которые составляют этот всеобъемлющий статус.

(Примечание: у меня действительно была проблема с лейблом hacktoberfest, за которую я получил PR, но в конце концов я хотел пойти немного другим путем, хотя я и сделал интегрируйте его в мастер на некоторое время.)

В Документации по проверке работоспособности есть некоторые подробности о том, как выполнить предварительную проверку работоспособности, но я не большой поклонник ручного написания JSON; вместо этого я выбрал анонимный объект.

Несколько новых вещей, о которых я хочу сообщить из ответа на конечную точку проверки работоспособности GET:

  • Имя проверки работоспособности
  • Описание проверки работоспособности
  • Индивидуальный статус проверки здоровья
  • Некоторая дополнительная информация, относящаяся к проверке работоспособности, которая описывает, почему проверка работоспособности возвращает «Ухудшение состояния» или «Неработоспособность».

К счастью, вся эта информация может быть доступна нам через HealthReport, сгенерированный как часть проверки работоспособности.

Мы собираемся представить новый метод, который записывает настраиваемый ответ для нашей конечной точки проверки работоспособности. При запуске мы хотим предоставить пользовательский ResponseWriter в MapHealthChecks:

app.UseEndpoints(endpoints =>
{
 endpoints.MapHealthChecks(
   "/health",
   new HealthCheckOptions
   {
    AllowCachingResponses = false,
    ResponseWriter = HealthCheckResponseWriter.WriteResponse
   })
  .WithMetadata(new AllowAnonymousAttribute());
 endpoints.MapControllers();
});

Где указанный HealthCheckResponseWriter является новым статическим классом, который мы представим далее.

ResponseWriter ожидает метод со следующей сигнатурой:

Вы заметили выше, что метод получает HttpContext, а также HealthReport. Это HealthReport предоставит нам несколько фрагментов данных, о которых мы можем сообщить, специфичных для каждой отдельной проверки работоспособности.

Что касается нашей фактической реализации модуля записи ответов, то вот оригинал, который был объединен с мастером из PR L-Dogg:

private static Task WriteResponse(HttpContext context, HealthReport result)
{
    context.Response.ContentType = "application/json; charset=utf-8";

    var options = new JsonWriterOptions
    {
        Indented = true
    };

    using var stream = new MemoryStream();
    using (var writer = new Utf8JsonWriter(stream, options))
    {
        writer.WriteStartObject();
        writer.WriteString("status", result.Status.ToString());
        writer.WriteStartObject("results");
        foreach (var entry in result.Entries)
        {
            writer.WriteStartObject(entry.Key);
            writer.WriteString("status", entry.Value.Status.ToString());
            writer.WriteString("description", entry.Value.Description);
            writer.WriteStartObject("data");
            foreach (var item in entry.Value.Data)
            {
                writer.WritePropertyName(item.Key);
                JsonSerializer.Serialize(
                    writer, item.Value, item.Value?.GetType() ??
                                        typeof(object));
            }
            writer.WriteEndObject();
            writer.WriteEndObject();
        }
        writer.WriteEndObject();
        writer.WriteEndObject();
    }

    var json = Encoding.UTF8.GetString(stream.ToArray());

    return context.Response.WriteAsync(json);
}

Вышеупомянутое определенно работает, но я не очень люблю писать json «вручную» (если это имеет смысл). Я все равно хотел написать об этом еще один пост в блоге, так как у меня уже была ветка (и на самом деле я не ожидал пиара :O), так что вот мое решение:

internal static class HealthCheckResponseWriter
{
    public static Task WriteResponse(HttpContext context, HealthReport healthReport)
    {
        context.Response.ContentType = "application/json; charset=utf-8";

        var result = JsonConvert.SerializeObject(new
        {
            status = healthReport.Status.ToString(),
            details = healthReport.Entries.Select(e => new
            {
                key = e.Key,
                description = e.Value.Description,
                status = e.Value.Status.ToString(),
                data = e.Value.Data
            })
        }, Formatting.Indented);
        
        return context.Response.WriteAsync(result);
    }
}

Я считаю, что работа с анонимным объектом будет немного более лаконичной.

В настоящее время мы не генерируем «данные» из проверок работоспособности, которыми мог бы воспользоваться HealthCheckResponseWriter, так что давайте посмотрим, что мы можем там сделать.

Мое намерение в отношении свойства «данные» анонимного объекта состоит в том, чтобы описать, что заставит конкретную проверку работоспособности вернуть «Ухудшенный» или «Неработоспособный», все, кроме этих двух статусов, можно считать «Здоровым».

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

Взглянем на класс HealthCheckResult:

вы увидите, что этот метод принимает необязательный IReadOnlyDictionary<string, object> data = null, который является элементом данных, который мы обязательно вернули из нашего метода WriteResponse в предыдущем разделе поста.

Мы будем использовать этот IReadonlyDictionary для предоставления нашей «пороговой» информации для каждого зерна. Я буду помещать эту пороговую информацию как в процессор, так и в память, но в качестве примера вот как будет выглядеть один из них:

[StatelessWorker(1)]
public class CpuHealthCheckGrain : Grain, ICpuHealthCheckGrain
{
    private const float UnhealthyThreshold = 90;
    private const float DegradedThreshold = 70;
    private readonly ReadOnlyDictionary<string, object> HealthCheckData = new ReadOnlyDictionary<string, object>(
        new Dictionary<string, object>()
        {
            { "Unhealthy Threshold",  UnhealthyThreshold},
            { "Degraded Threshold",  DegradedThreshold}
        });

    private readonly IHostEnvironmentStatistics _hostEnvironmentStatistics;

    public CpuHealthCheckGrain(IHostEnvironmentStatistics hostEnvironmentStatistics)
    {
        _hostEnvironmentStatistics = hostEnvironmentStatistics;
    }

    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
    {
        if (_hostEnvironmentStatistics.CpuUsage == null)
        {
            return Task.FromResult(HealthCheckResult.Unhealthy("Could not determine CPU usage.", data: HealthCheckData));
        }

        if (_hostEnvironmentStatistics.CpuUsage > UnhealthyThreshold)
        {
            return Task.FromResult(HealthCheckResult.Unhealthy(
                $"CPU utilization is unhealthy at {_hostEnvironmentStatistics.CpuUsage:0.00}%.", data: HealthCheckData));
        }

        if (_hostEnvironmentStatistics.CpuUsage > DegradedThreshold)
        {
            return Task.FromResult(HealthCheckResult.Degraded(
                $"CPU utilization is degraded at {_hostEnvironmentStatistics.CpuUsage:0.00}%.", data: HealthCheckData));
        }

        return Task.FromResult(HealthCheckResult.Healthy(
            $"CPU utilization is healthy at {_hostEnvironmentStatistics.CpuUsage:0.00}%.", data: HealthCheckData));
    }
}

Вы должны заметить, что в приведенном выше примере мы ввели ReadOnlyDictionary с пороговыми значениями для деградировавших и неработоспособных, а затем передали это ReadOnlyDictionary параметру data статического метода в HealthCheckResult.

Осталось только протестировать! Возможно, вы видели изображение обложки, которое содержало спойлеры, но просто чтобы подвести итог, вот как это выглядит при нажатии на конечную точку «/health» после наших изменений:

использованная литература

Первоначально опубликовано на https://blog.kritner.com 4 декабря 2020 г.