diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs b/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs index 279f14790..0e5f68fd9 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Orleans; @@ -19,6 +20,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics { private readonly IAppsByNameIndex index; + public IEnumerable Scopes + { + get { yield return HealthCheckScopes.Cluster; } + } + public OrleansAppsHealthCheck(IGrainFactory grainFactory) { Guard.NotNull(grainFactory, nameof(grainFactory)); diff --git a/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs b/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs index 42ef4e206..a4f2d3aea 100644 --- a/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs +++ b/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using EventStore.ClientAPI; @@ -15,6 +16,11 @@ namespace Squidex.Infrastructure.Diagnostics { private readonly IEventStoreConnection connection; + public IEnumerable Scopes + { + get { yield return HealthCheckScopes.Node; } + } + public GetEventStoreHealthCheck(IEventStoreConnection connection) { Guard.NotNull(connection, nameof(connection)); diff --git a/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs b/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs index 33ed7b077..492c0eae6 100644 --- a/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs +++ b/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; @@ -15,6 +16,11 @@ namespace Squidex.Infrastructure.Diagnostics { private readonly IMongoDatabase mongoDatabase; + public IEnumerable Scopes + { + get { yield return HealthCheckScopes.Node; } + } + public MongoDBHealthCheck(IMongoDatabase mongoDatabase) { Guard.NotNull(mongoDatabase, nameof(mongoDatabase)); diff --git a/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs b/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs index 62d175e74..0a9b4aac7 100644 --- a/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs +++ b/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs @@ -17,6 +17,11 @@ namespace Squidex.Infrastructure.Diagnostics { private readonly long threshold; + public IEnumerable Scopes + { + get { yield return HealthCheckScopes.Node; } + } + public GCHealthCheck(IOptions options) { Guard.NotNull(options, nameof(options)); diff --git a/src/Squidex.Infrastructure/Diagnostics/HealthCheckScopes.cs b/src/Squidex.Infrastructure/Diagnostics/HealthCheckScopes.cs new file mode 100644 index 000000000..327c46c22 --- /dev/null +++ b/src/Squidex.Infrastructure/Diagnostics/HealthCheckScopes.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Diagnostics +{ + public static class HealthCheckScopes + { + public const string Any = "*"; + public const string Node = "node"; + public const string Cluster = "cluster"; + } +} diff --git a/src/Squidex.Infrastructure/Diagnostics/IHealthCheck.cs b/src/Squidex.Infrastructure/Diagnostics/IHealthCheck.cs index f139deaed..eaf8e11b0 100644 --- a/src/Squidex.Infrastructure/Diagnostics/IHealthCheck.cs +++ b/src/Squidex.Infrastructure/Diagnostics/IHealthCheck.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -12,6 +13,8 @@ namespace Squidex.Infrastructure.Diagnostics { public interface IHealthCheck { + IEnumerable Scopes { get; } + Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs b/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs index 8c6a26f1a..8351626d9 100644 --- a/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs +++ b/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Orleans; @@ -16,6 +17,11 @@ namespace Squidex.Infrastructure.Diagnostics { private readonly IManagementGrain managementGrain; + public IEnumerable Scopes + { + get { yield return HealthCheckScopes.Cluster; } + } + public OrleansHealthCheck(IGrainFactory grainFactory) { Guard.NotNull(grainFactory, nameof(grainFactory)); diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs index 18671e30b..bf8d71575 100644 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -42,6 +42,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); @@ -51,12 +54,12 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() - .As(); - services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddTransient(typeof(Lazy<>), typeof(Lazier<>)); } } diff --git a/src/Squidex/Config/Web/WebExtensions.cs b/src/Squidex/Config/Web/WebExtensions.cs index fa649cab3..9886fbc73 100644 --- a/src/Squidex/Config/Web/WebExtensions.cs +++ b/src/Squidex/Config/Web/WebExtensions.cs @@ -5,8 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.HttpOverrides; +using Squidex.Infrastructure.Diagnostics; using Squidex.Pipeline; using Squidex.Pipeline.Diagnostics; using Squidex.Pipeline.Robots; @@ -31,7 +33,20 @@ namespace Squidex.Config.Web public static IApplicationBuilder UseMyHealthCheck(this IApplicationBuilder app) { - app.Map("/healthz", builder => builder.UseMiddleware()); + app.Map("/readiness", builder => + { + builder.UseMiddleware(HealthCheckScopes.Any); + }); + + app.Map("/healthz", builder => + { + builder.UseMiddleware(HealthCheckScopes.Node); + }); + + app.Map("/cluster-healthz", builder => + { + builder.UseMiddleware(HealthCheckScopes.Cluster); + }); return app; } diff --git a/src/Squidex/Pipeline/Diagnostics/HealthCheckMiddleware.cs b/src/Squidex/Pipeline/Diagnostics/HealthCheckMiddleware.cs index 11eec2ded..2025dbbfb 100644 --- a/src/Squidex/Pipeline/Diagnostics/HealthCheckMiddleware.cs +++ b/src/Squidex/Pipeline/Diagnostics/HealthCheckMiddleware.cs @@ -17,40 +17,46 @@ using Squidex.Infrastructure.Diagnostics; namespace Squidex.Pipeline.Diagnostics { - public sealed class HealthCheckMiddleware : IMiddleware + public sealed class HealthCheckMiddleware { private const string Suffix = "HealthCheck"; private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); private readonly Dictionary healthChecks; private readonly JsonSerializerSettings serializerSettings; + private readonly RequestDelegate next; + private readonly List scopes; - public HealthCheckMiddleware(IEnumerable healthChecks, JsonSerializerSettings serializerSettings) + public HealthCheckMiddleware(IEnumerable healthChecks, JsonSerializerSettings serializerSettings, RequestDelegate next, string scopes) { Guard.NotNull(healthChecks, nameof(healthChecks)); Guard.NotNull(serializerSettings, nameof(serializerSettings)); this.healthChecks = healthChecks.ToDictionary(GetName); + this.next = next; + this.scopes = SplitScopes(scopes); this.serializerSettings = serializerSettings; } - public async Task InvokeAsync(HttpContext context, RequestDelegate next) + public async Task Invoke(HttpContext context) { if (CanServeRequest(context.Request)) { using (var cts = new CancellationTokenSource(Timeout)) { - var checks = await Task.WhenAll(healthChecks.Select(x => MakeHealthCheckAsync(x.Key, x.Value, cts.Token))); + var matchingChecks = healthChecks.Where(x => CanUseCheck(x.Value)); + + var results = await Task.WhenAll(matchingChecks.Select(x => MakeHealthCheckAsync(x.Key, x.Value, cts.Token))); context.Response.StatusCode = 200; context.Response.Headers.Add("Content-Type", "application/json"); - if (checks.Any(x => !x.Result.IsHealthy)) + if (results.Any(x => !x.Result.IsHealthy)) { context.Response.StatusCode = 503; } - var response = checks.ToDictionary(x => x.Name, x => x.Result); + var response = results.ToDictionary(x => x.Name, x => x.Result); await context.Response.WriteAsync(JsonConvert.SerializeObject(new { status = response }, Formatting.Indented, serializerSettings)); } @@ -61,11 +67,21 @@ namespace Squidex.Pipeline.Diagnostics } } - private static bool CanServeRequest(HttpRequest request) + private bool CanUseCheck(IHealthCheck check) + { + return scopes.Count == 0 || check.Scopes.Intersect(scopes).Any(); + } + + private bool CanServeRequest(HttpRequest request) { return HttpMethods.IsGet(request.Method) && (request.Path == "/" || string.IsNullOrEmpty(request.Path)); } + private static List SplitScopes(string scopes) + { + return scopes.Split(",").Where(x => x != "*").ToList(); + } + private static string GetName(IHealthCheck check) { var name = check.GetType().Name.ToCamelCase();