From 952166e2b9b0b489c09c674e60f656c83f981a23 Mon Sep 17 00:00:00 2001 From: Russ Hammett Date: Fri, 4 Dec 2020 14:07:58 -0500 Subject: [PATCH] Updates health check json response with additional descriptions and "data" to describe what makes the system degraded or unhealthy. --- .../HealthChecks/BasicHealthCheckGrain.cs | 4 +- .../HealthChecks/CpuHealthCheckGrain.cs | 16 ++++-- .../HealthChecks/MemoryHealthCheckGrain.cs | 23 ++++++--- .../Helpers/HealthCheckResponseWriter.cs | 35 +++++++++++++ .../Program.cs | 9 +++- .../Startup.cs | 49 ++----------------- 6 files changed, 77 insertions(+), 59 deletions(-) create mode 100644 src/Kritner.OrleansGettingStarted.SiloHost/Helpers/HealthCheckResponseWriter.cs diff --git a/src/Kritner.Orleans.GettingStarted.Grains/HealthChecks/BasicHealthCheckGrain.cs b/src/Kritner.Orleans.GettingStarted.Grains/HealthChecks/BasicHealthCheckGrain.cs index 89ab730..293fb5e 100644 --- a/src/Kritner.Orleans.GettingStarted.Grains/HealthChecks/BasicHealthCheckGrain.cs +++ b/src/Kritner.Orleans.GettingStarted.Grains/HealthChecks/BasicHealthCheckGrain.cs @@ -10,9 +10,11 @@ namespace Kritner.Orleans.GettingStarted.Grains.HealthChecks [StatelessWorker(1)] public class BasicHealthCheckGrain : Grain, IBasicHealthCheckGrain { + private const string HealthCheckDescription = "A basic health check. Should only ever return healthy assuming the Orleans cluster is up."; + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken()) { - return Task.FromResult(new HealthCheckResult(HealthStatus.Healthy)); + return Task.FromResult(new HealthCheckResult(HealthStatus.Healthy, HealthCheckDescription)); } } } \ No newline at end of file diff --git a/src/Kritner.Orleans.GettingStarted.Grains/HealthChecks/CpuHealthCheckGrain.cs b/src/Kritner.Orleans.GettingStarted.Grains/HealthChecks/CpuHealthCheckGrain.cs index 9c33d93..f2492e7 100644 --- a/src/Kritner.Orleans.GettingStarted.Grains/HealthChecks/CpuHealthCheckGrain.cs +++ b/src/Kritner.Orleans.GettingStarted.Grains/HealthChecks/CpuHealthCheckGrain.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; using Kritner.Orleans.GettingStarted.GrainInterfaces.HealthChecks; @@ -13,6 +15,12 @@ public class CpuHealthCheckGrain : Grain, ICpuHealthCheckGrain { private const float UnhealthyThreshold = 90; private const float DegradedThreshold = 70; + private readonly ReadOnlyDictionary HealthCheckData = new ReadOnlyDictionary( + new Dictionary() + { + { "Unhealthy Threshold", UnhealthyThreshold}, + { "Degraded Threshold", DegradedThreshold} + }); private readonly IHostEnvironmentStatistics _hostEnvironmentStatistics; @@ -25,23 +33,23 @@ public Task CheckHealthAsync(HealthCheckContext context, Canc { if (_hostEnvironmentStatistics.CpuUsage == null) { - return Task.FromResult(HealthCheckResult.Unhealthy("Could not determine CPU usage.")); + 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}%.")); + $"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}%.")); + $"CPU utilization is degraded at {_hostEnvironmentStatistics.CpuUsage:0.00}%.", data: HealthCheckData)); } return Task.FromResult(HealthCheckResult.Healthy( - $"CPU utilization is healthy at {_hostEnvironmentStatistics.CpuUsage}%.")); + $"CPU utilization is healthy at {_hostEnvironmentStatistics.CpuUsage:0.00}%.", data: HealthCheckData)); } } } \ No newline at end of file diff --git a/src/Kritner.Orleans.GettingStarted.Grains/HealthChecks/MemoryHealthCheckGrain.cs b/src/Kritner.Orleans.GettingStarted.Grains/HealthChecks/MemoryHealthCheckGrain.cs index 7d67d22..3ce6f69 100644 --- a/src/Kritner.Orleans.GettingStarted.Grains/HealthChecks/MemoryHealthCheckGrain.cs +++ b/src/Kritner.Orleans.GettingStarted.Grains/HealthChecks/MemoryHealthCheckGrain.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; using Kritner.Orleans.GettingStarted.GrainInterfaces.HealthChecks; @@ -12,8 +14,15 @@ namespace Kritner.Orleans.GettingStarted.Grains.HealthChecks public class MemoryHealthCheckGrain : Grain, IMemoryHealthCheckGrain { private const float UnhealthyThreshold = 95; - private const float DegradedThreshold = 90; - + private const float DegradedThreshold = 90; + + private readonly ReadOnlyDictionary HealthCheckData = new ReadOnlyDictionary( + new Dictionary() + { + { "Unhealthy Threshold", UnhealthyThreshold}, + { "Degraded Threshold", DegradedThreshold} + }); + private readonly IHostEnvironmentStatistics _hostEnvironmentStatistics; public MemoryHealthCheckGrain(IHostEnvironmentStatistics hostEnvironmentStatistics) @@ -25,12 +34,12 @@ public Task CheckHealthAsync(HealthCheckContext context, Canc { if (_hostEnvironmentStatistics?.AvailableMemory == null || _hostEnvironmentStatistics?.TotalPhysicalMemory == null) { - return Task.FromResult(HealthCheckResult.Unhealthy("Could not determine memory calculation.")); + return Task.FromResult(HealthCheckResult.Unhealthy("Could not determine memory calculation.", data: HealthCheckData)); } if (_hostEnvironmentStatistics?.AvailableMemory == 0 && _hostEnvironmentStatistics?.AvailableMemory == 0) { - return Task.FromResult(HealthCheckResult.Unhealthy("Could not determine memory calculation.")); + return Task.FromResult(HealthCheckResult.Unhealthy("Could not determine memory calculation.", data: HealthCheckData)); } var memoryUsed = 100 - ((float)_hostEnvironmentStatistics.AvailableMemory / (float)_hostEnvironmentStatistics.TotalPhysicalMemory * 100); @@ -38,18 +47,18 @@ public Task CheckHealthAsync(HealthCheckContext context, Canc if (memoryUsed > UnhealthyThreshold) { return Task.FromResult(HealthCheckResult.Unhealthy( - $"Memory utilization is unhealthy at {memoryUsed:0.00}%.")); + $"Memory utilization is unhealthy at {memoryUsed:0.00}%.", data: HealthCheckData)); } if (memoryUsed > DegradedThreshold) { return Task.FromResult(HealthCheckResult.Degraded( - $"Memory utilization is degraded at {memoryUsed:0.00}%.")); + $"Memory utilization is degraded at {memoryUsed:0.00}%.", data: HealthCheckData)); } return Task.FromResult(HealthCheckResult.Healthy( - $"Memory utilization is healthy at {memoryUsed:0.00}%.")); + $"Memory utilization is healthy at {memoryUsed:0.00}%.", data: HealthCheckData)); } } } \ No newline at end of file diff --git a/src/Kritner.OrleansGettingStarted.SiloHost/Helpers/HealthCheckResponseWriter.cs b/src/Kritner.OrleansGettingStarted.SiloHost/Helpers/HealthCheckResponseWriter.cs new file mode 100644 index 0000000..4c08357 --- /dev/null +++ b/src/Kritner.OrleansGettingStarted.SiloHost/Helpers/HealthCheckResponseWriter.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Kritner.OrleansGettingStarted.SiloHost.Helpers +{ + 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); + } + } +} diff --git a/src/Kritner.OrleansGettingStarted.SiloHost/Program.cs b/src/Kritner.OrleansGettingStarted.SiloHost/Program.cs index 47b7041..3189c76 100644 --- a/src/Kritner.OrleansGettingStarted.SiloHost/Program.cs +++ b/src/Kritner.OrleansGettingStarted.SiloHost/Program.cs @@ -34,7 +34,14 @@ private static IHostBuilder CreateHostBuilder(string[] args, string env, IConfig builder.AddConfiguration(configurationRoot); }) .ConfigureServices(DependencyInjectionHelper.IocContainerRegistration) - .ConfigureWebHostDefaults(builder => { builder.UseStartup(); }) + .ConfigureLogging(logging => { + logging.AddConsole(); + logging.SetMinimumLevel(LogLevel.Information); + }) + .ConfigureWebHostDefaults(builder => + { + builder.UseStartup(); + }) .UseOrleans(siloBuilder => { siloBuilder diff --git a/src/Kritner.OrleansGettingStarted.SiloHost/Startup.cs b/src/Kritner.OrleansGettingStarted.SiloHost/Startup.cs index 0b79fe9..0ac6152 100644 --- a/src/Kritner.OrleansGettingStarted.SiloHost/Startup.cs +++ b/src/Kritner.OrleansGettingStarted.SiloHost/Startup.cs @@ -1,16 +1,11 @@ -using System.IO; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; using Kritner.OrleansGettingStarted.SiloHost.HealthChecks; +using Kritner.OrleansGettingStarted.SiloHost.Helpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; namespace Kritner.OrleansGettingStarted.SiloHost @@ -43,6 +38,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) } app.UseHttpsRedirection(); + app.UseHsts(); app.UseRouting(); @@ -55,51 +51,12 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) new HealthCheckOptions { AllowCachingResponses = false, - ResponseWriter = WriteResponse + ResponseWriter = HealthCheckResponseWriter.WriteResponse }) .WithMetadata(new AllowAnonymousAttribute()); endpoints.MapControllers(); }); } - 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); - } } } \ No newline at end of file