From f1b6ccfc2cb65a6edf89fbfc3a24217c2ed8b977 Mon Sep 17 00:00:00 2001 From: Jonathan Bout Date: Fri, 24 Jan 2025 15:36:28 +0100 Subject: [PATCH] More detailed health checks in standalone & leave --dump-config available in Release builds as well --- extensions/Redis/CustomRedisCacheService.cs | 11 +++-- extensions/Redis/RedisCacheConfiguration.cs | 2 +- .../Redis/SimpleCDNBuilderExtensions.cs | 6 ++- .../Configuration/ConfigurationExtensions.cs | 1 + src/standalone/AdditionalEndpoints.cs | 31 ++++++++++++-- .../ApplicationBuilderExtensions.cs | 18 +++++++- src/standalone/Dockerfile | 2 +- src/standalone/GlobalConstants.cs | 4 +- src/standalone/Program.cs | 41 +++++++++++-------- src/standalone/SimpleCDN.Standalone.csproj | 1 + 10 files changed, 85 insertions(+), 32 deletions(-) diff --git a/extensions/Redis/CustomRedisCacheService.cs b/extensions/Redis/CustomRedisCacheService.cs index 23e0d65..8b0a923 100644 --- a/extensions/Redis/CustomRedisCacheService.cs +++ b/extensions/Redis/CustomRedisCacheService.cs @@ -6,7 +6,7 @@ namespace SimpleCDN.Extensions.Redis { - internal sealed class CustomRedisCacheService(IOptionsMonitor options, IOptionsMonitor cacheOptions) + public sealed class CustomRedisCacheService(IOptionsMonitor options, IOptionsMonitor cacheOptions) : IDistributedCache, IAsyncDisposable, IDisposable { private readonly IOptionsMonitor options = options; @@ -21,7 +21,7 @@ internal sealed class CustomRedisCacheService(IOptionsMonitor /// Checks if the Redis connection is still valid and creates a new one if necessary. /// - private ConnectionMultiplexer GetRedisConnection() + public ConnectionMultiplexer GetRedisConnection() { _redisConnectionLock.Wait(); @@ -42,12 +42,15 @@ private ConnectionMultiplexer GetRedisConnection() _redisConnectionLock.Release(); } } - private async Task GetRedisConnectionAsync() + + /// + /// Checks if the Redis connection is still valid and creates a new one if necessary. + /// + public async Task GetRedisConnectionAsync() { await _redisConnectionLock.WaitAsync(); try { - if (_redisConnection is not { IsConnected: true } or { IsConnecting: true }) { _redisConnection?.Dispose(); diff --git a/extensions/Redis/RedisCacheConfiguration.cs b/extensions/Redis/RedisCacheConfiguration.cs index cf5f045..a2ce4bd 100644 --- a/extensions/Redis/RedisCacheConfiguration.cs +++ b/extensions/Redis/RedisCacheConfiguration.cs @@ -10,7 +10,7 @@ public class RedisCacheConfiguration /// /// The connection string to the Redis server. Default is localhost:6379. /// - public string ConnectionString { get; set; } = "localhost:6379"; + public string ConnectionString { get; set; } = null!; /// /// How this client should be identified to Redis. Default is SimpleCDN. diff --git a/extensions/Redis/SimpleCDNBuilderExtensions.cs b/extensions/Redis/SimpleCDNBuilderExtensions.cs index e0306fa..02518b3 100644 --- a/extensions/Redis/SimpleCDNBuilderExtensions.cs +++ b/extensions/Redis/SimpleCDNBuilderExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SimpleCDN.Configuration; +using StackExchange.Redis; namespace SimpleCDN.Extensions.Redis { @@ -17,7 +18,10 @@ public static class SimpleCDNBuilderExtensions /// public static ISimpleCDNBuilder AddRedisCache(this ISimpleCDNBuilder builder, Action configure) { - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(sp => sp.GetRequiredService()); + builder.Services.AddOptionsWithValidateOnStart() .Configure(configure) .Validate>((config, logger) => config.Validate(logger), InvalidConfigurationMessage); diff --git a/src/core/Configuration/ConfigurationExtensions.cs b/src/core/Configuration/ConfigurationExtensions.cs index fe11a09..21754f7 100644 --- a/src/core/Configuration/ConfigurationExtensions.cs +++ b/src/core/Configuration/ConfigurationExtensions.cs @@ -62,6 +62,7 @@ public SimpleCDNBuilder(IServiceCollection services) Services.AddSingleton(); Services.AddSingleton(); Services.AddSingleton(sp => new CacheImplementationResolver(sp, _cacheImplementationType)); + Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(SourceGenerationContext.Default)); } public IServiceCollection Services { get; } diff --git a/src/standalone/AdditionalEndpoints.cs b/src/standalone/AdditionalEndpoints.cs index cccbf4b..5d79cb8 100644 --- a/src/standalone/AdditionalEndpoints.cs +++ b/src/standalone/AdditionalEndpoints.cs @@ -1,6 +1,11 @@ -using SimpleCDN.Endpoints; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.Options; +using SimpleCDN.Endpoints; using SimpleCDN.Services.Caching; using SimpleCDN.Services.Caching.Implementations; +using System.Net.Mime; +using System.Text.Json.Serialization; namespace SimpleCDN.Standalone { @@ -9,7 +14,6 @@ public static class AdditionalEndpoints public static WebApplication MapEndpoints(this WebApplication app) { app.MapSimpleCDN(); - #if DEBUG if (app.Configuration.GetSection("Cache:Type").Get() == CacheType.InMemory) { @@ -28,18 +32,37 @@ public static WebApplication MapEndpoints(this WebApplication app) // force garbage collection to make sure all memory // used by the cached files is properly released GC.Collect(); + return Results.Ok(); } + return Results.NotFound(); }); } #endif - // health check endpoint - app.MapGet("/" + GlobalConstants.SystemFilesRelativePath + "/server/health", () => "healthy"); + app.MapHealthChecks(); app.MapGet("/favicon.ico", () => Results.Redirect("/" + GlobalConstants.SystemFilesRelativePath + "/logo.ico", true)); return app; } + + private static WebApplication MapHealthChecks(this WebApplication app) + { + app.MapHealthChecks("/" + GlobalConstants.SystemFilesRelativePath + "/server/health", new HealthCheckOptions + { + ResponseWriter = async (ctx, health) => + { + JsonOptions jsonOptions = ctx.RequestServices.GetRequiredService>().Value; + ctx.Response.ContentType = MediaTypeNames.Application.Json; +#pragma warning disable IL2026, IL3050 // it thinks it requires unreferenced code, + // but the TypeInfoResolverChain actually provides the necessary context + await ctx.Response.WriteAsJsonAsync(health); +#pragma warning restore IL2026, IL3050 + } + }); + + return app; + } } } diff --git a/src/standalone/ApplicationBuilderExtensions.cs b/src/standalone/ApplicationBuilderExtensions.cs index dc5ddab..9aea0cb 100644 --- a/src/standalone/ApplicationBuilderExtensions.cs +++ b/src/standalone/ApplicationBuilderExtensions.cs @@ -25,10 +25,24 @@ internal static ISimpleCDNBuilder MapConfiguration(this ISimpleCDNBuilder builde switch (configuration.GetSection("Cache:Type").Get()) { case CacheType.Redis: - builder.AddRedisCache(config => redisSection.Bind(config)); + builder.AddRedisCache(config => + { + if (redisSection.Exists()) + { + redisSection.Bind(config); + } + }); + builder.Services.AddHealthChecks() + .AddRedis(sp => sp.GetRequiredService().GetRedisConnection(), "Redis"); break; case CacheType.InMemory: - builder.AddInMemoryCache(config => inMemorySection.Bind(config)); + builder.AddInMemoryCache(config => + { + if (inMemorySection.Exists()) + { + inMemorySection.Bind(config); + } + }); break; case CacheType.Unspecified: // if no provider is explicitly specified, we look at what is configured, diff --git a/src/standalone/Dockerfile b/src/standalone/Dockerfile index fdfa35a..93949f3 100644 --- a/src/standalone/Dockerfile +++ b/src/standalone/Dockerfile @@ -72,7 +72,7 @@ USER app COPY --from=publish /app/publish . -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD curl --silent --fail http://localhost:8080/_cdn/server/health || exit 1 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD curl http://localhost:8080/_cdn/server/health -s | grep -v "Unhealthy" EXPOSE 8080 diff --git a/src/standalone/GlobalConstants.cs b/src/standalone/GlobalConstants.cs index a40da96..c4cee75 100644 --- a/src/standalone/GlobalConstants.cs +++ b/src/standalone/GlobalConstants.cs @@ -2,6 +2,7 @@ * This file is basically AssemblyInfo.cs, but with the option to add global suppressions, * or global constants. */ +using Microsoft.Extensions.Diagnostics.HealthChecks; using SimpleCDN.Configuration; using SimpleCDN.Extensions.Redis; using System.Runtime.CompilerServices; @@ -9,10 +10,9 @@ [assembly: InternalsVisibleTo("SimpleCDN.Tests.Integration")] -#if DEBUG // only generate serializers for debug views in debug mode [JsonSerializable(typeof(CacheConfiguration))] [JsonSerializable(typeof(CDNConfiguration))] [JsonSerializable(typeof(RedisCacheConfiguration))] [JsonSerializable(typeof(InMemoryCacheConfiguration))] +[JsonSerializable(typeof(HealthReport))] internal partial class ExtraSourceGenerationContext : JsonSerializerContext; -#endif diff --git a/src/standalone/Program.cs b/src/standalone/Program.cs index b0ef71e..4cb131a 100644 --- a/src/standalone/Program.cs +++ b/src/standalone/Program.cs @@ -1,9 +1,12 @@ +using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; using SimpleCDN.Configuration; using SimpleCDN.Extensions.Redis; using SimpleCDN.Services.Caching; using System.Runtime.CompilerServices; using System.Text.Json; +using System.Text.Json.Serialization; using TomLonghurst.ReadableTimeSpan; namespace SimpleCDN.Standalone @@ -28,47 +31,53 @@ private static void Main(string[] args) builder.Services.AddSimpleCDN() .MapConfiguration(builder.Configuration); -#if DEBUG - builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(ExtraSourceGenerationContext.Default)); -#endif + builder.Services.ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.TypeInfoResolverChain.Add(ExtraSourceGenerationContext.Default); + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + + builder.Services.AddHealthChecks() + .AddCheck("Self", () => HealthCheckResult.Healthy()); WebApplication app = builder .Build(); -#if DEBUG + // useful for debugging configuration issues if (args.Contains("--dump-config")) { - if (RuntimeFeature.IsDynamicCodeSupported) - { - DumpConfiguration(app); - } + DumpConfiguration(app); return; } -#endif app .MapEndpoints() .Run(); } -#if DEBUG private static void DumpConfiguration(WebApplication app) { IOptions cdnConfig = app.Services.GetRequiredService>(); IOptions cacheConfig = app.Services.GetRequiredService>(); IOptions inMemoryConfig = app.Services.GetRequiredService>(); IOptions redisConfig = app.Services.GetRequiredService>(); + IOptions jsonOptions = app.Services.GetRequiredService>(); - var jsonConfig = new JsonSerializerOptions { WriteIndented = true }; + var jsonConfig = new JsonSerializerOptions(jsonOptions.Value.SerializerOptions) + { + WriteIndented = true + }; +#pragma warning disable IL2026, IL3050 // requires unreferenced code, but the TypeInfoResolverChain actually provides the necessary context Console.WriteLine("CDN Configuration:"); - Console.WriteLine(JsonSerializer.Serialize(cdnConfig.Value, ExtraSourceGenerationContext.Default.CDNConfiguration)); + Console.WriteLine(JsonSerializer.Serialize(cdnConfig.Value, jsonConfig)); Console.WriteLine("Cache Configuration:"); - Console.WriteLine(JsonSerializer.Serialize(cacheConfig.Value, ExtraSourceGenerationContext.Default.CacheConfiguration)); + Console.WriteLine(JsonSerializer.Serialize(cacheConfig.Value, jsonConfig)); Console.WriteLine("InMemory Cache Configuration:"); - Console.WriteLine(JsonSerializer.Serialize(inMemoryConfig.Value, ExtraSourceGenerationContext.Default.InMemoryCacheConfiguration)); + Console.WriteLine(JsonSerializer.Serialize(inMemoryConfig.Value, jsonConfig)); Console.WriteLine("Redis Cache Configuration:"); - Console.WriteLine(JsonSerializer.Serialize(redisConfig.Value, ExtraSourceGenerationContext.Default.RedisCacheConfiguration)); + Console.WriteLine(JsonSerializer.Serialize(redisConfig.Value, jsonConfig)); +#pragma warning restore IL2026, IL3050 Console.WriteLine(); Console.Write("Selected cache implementation: "); @@ -76,8 +85,6 @@ private static void DumpConfiguration(WebApplication app) var cache = app.Services.GetRequiredService().Implementation.GetType().Name; Console.WriteLine(cache); } -#endif - } } #pragma warning restore RCS1102 // Make class static diff --git a/src/standalone/SimpleCDN.Standalone.csproj b/src/standalone/SimpleCDN.Standalone.csproj index bc28d07..50ab306 100644 --- a/src/standalone/SimpleCDN.Standalone.csproj +++ b/src/standalone/SimpleCDN.Standalone.csproj @@ -35,6 +35,7 @@ +