From bfb03cd42506dbc7ff0741cb5531a87ed44767f2 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 7 Nov 2018 21:20:41 +0100 Subject: [PATCH] Health checks. --- .../Diagnostics/OrleansAppsHealthCheck.cs | 36 ++++++++ .../Apps/Indexes/AppsByNameIndexGrain.cs | 5 ++ .../Apps/Indexes/IAppsByNameIndex.cs | 2 + .../Assets/AssetQueryService.cs | 5 +- .../Contents/ContentQueryService.cs | 5 +- .../Diagnostics/GetEventStoreHealthCheck.cs | 32 +++++++ .../Diagnostics/MongoDBHealthCheck.cs | 34 ++++++++ .../Diagnostics/GCHealthCheck.cs | 42 ++++++++++ .../Diagnostics/GCHealthCheckOptions.cs | 14 ++++ .../Diagnostics/HealthCheckResult.cs | 27 ++++++ .../Diagnostics/IHealthCheck.cs | 17 ++++ .../Diagnostics/OrleansHealthCheck.cs | 33 ++++++++ src/Squidex/AppServices.cs | 3 + .../Api/Controllers/Rules/RulesController.cs | 1 - src/Squidex/Config/Domain/EntitiesServices.cs | 11 ++- .../Config/Domain/EventStoreServices.cs | 4 + .../Config/Domain/InfrastructureServices.cs | 7 ++ src/Squidex/Config/Domain/StoreServices.cs | 4 + src/Squidex/Config/Web/WebExtensions.cs | 8 ++ src/Squidex/Config/Web/WebServices.cs | 4 + .../Diagnostics/HealthCheckMiddleware.cs | 83 +++++++++++++++++++ src/Squidex/Squidex.csproj | 14 ++-- src/Squidex/WebStartup.cs | 1 + tests/RunCoverage.ps1 | 2 +- .../Apps/Indexes/AppsByNameIndexGrainTests.cs | 2 + .../Assets/AssetQueryServiceTests.cs | 3 +- .../Contents/ContentQueryServiceTests.cs | 3 +- 27 files changed, 381 insertions(+), 21 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs create mode 100644 src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs create mode 100644 src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs create mode 100644 src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs create mode 100644 src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs create mode 100644 src/Squidex.Infrastructure/Diagnostics/HealthCheckResult.cs create mode 100644 src/Squidex.Infrastructure/Diagnostics/IHealthCheck.cs create mode 100644 src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs create mode 100644 src/Squidex/Pipeline/Diagnostics/HealthCheckMiddleware.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs b/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs new file mode 100644 index 000000000..279f14790 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.Indexes; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Diagnostics; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics +{ + public sealed class OrleansAppsHealthCheck : IHealthCheck + { + private readonly IAppsByNameIndex index; + + public OrleansAppsHealthCheck(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + index = grainFactory.GetGrain(SingleGrain.Id); + } + + public async Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + await index.CountAsync(); + + return new HealthCheckResult(true); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs index 09c3e646f..a743c7e9e 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs @@ -130,5 +130,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { return Task.FromResult(state.Apps.Values.ToList()); } + + public Task CountAsync() + { + return Task.FromResult((long)state.Apps.Count); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs index 90fc1a8bd..2e98ade47 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs @@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { public interface IAppsByNameIndex : IGrainWithStringKey { + Task CountAsync(); + Task ReserveAppAsync(Guid appId, string name); Task AddAppAsync(Guid appId, string name); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs index 763603162..0bd7a6975 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using Microsoft.OData; using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Assets.Edm; @@ -26,14 +27,14 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly IAssetRepository assetRepository; private readonly AssetOptions options; - public AssetQueryService(ITagService tagService, IAssetRepository assetRepository, AssetOptions options) + public AssetQueryService(ITagService tagService, IAssetRepository assetRepository, IOptions options) { Guard.NotNull(tagService, nameof(tagService)); Guard.NotNull(options, nameof(options)); Guard.NotNull(assetRepository, nameof(assetRepository)); this.assetRepository = assetRepository; - this.options = options; + this.options = options.Value; this.tagService = tagService; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index bfa407ada..6160796fa 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using Microsoft.OData; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; @@ -48,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Contents IContentRepository contentRepository, IContentVersionLoader contentVersionLoader, IScriptEngine scriptEngine, - ContentOptions options, + IOptions options, EdmModelBuilder modelBuilder) { Guard.NotNull(appProvider, nameof(appProvider)); @@ -62,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Contents this.contentRepository = contentRepository; this.contentVersionLoader = contentVersionLoader; this.modelBuilder = modelBuilder; - this.options = options; + this.options = options.Value; this.scriptEngine = scriptEngine; } diff --git a/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs b/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs new file mode 100644 index 000000000..42ef4e206 --- /dev/null +++ b/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using EventStore.ClientAPI; + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class GetEventStoreHealthCheck : IHealthCheck + { + private readonly IEventStoreConnection connection; + + public GetEventStoreHealthCheck(IEventStoreConnection connection) + { + Guard.NotNull(connection, nameof(connection)); + + this.connection = connection; + } + + public async Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + await connection.ReadEventAsync("test", 1, false); + + return new HealthCheckResult(true, "Querying test event from event store."); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs b/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs new file mode 100644 index 000000000..33ed7b077 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class MongoDBHealthCheck : IHealthCheck + { + private readonly IMongoDatabase mongoDatabase; + + public MongoDBHealthCheck(IMongoDatabase mongoDatabase) + { + Guard.NotNull(mongoDatabase, nameof(mongoDatabase)); + + this.mongoDatabase = mongoDatabase; + } + + public async Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + var collectionNames = await mongoDatabase.ListCollectionNamesAsync(cancellationToken: cancellationToken); + + var result = await collectionNames.AnyAsync(cancellationToken); + + return new HealthCheckResult(result); + } + } +} diff --git a/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs b/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs new file mode 100644 index 000000000..62d175e74 --- /dev/null +++ b/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class GCHealthCheck : IHealthCheck + { + private readonly long threshold; + + public GCHealthCheck(IOptions options) + { + Guard.NotNull(options, nameof(options)); + + threshold = 1024 * 1024 * options.Value.Threshold; + } + + public Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + var allocated = GC.GetTotalMemory(false); + + var data = new Dictionary() + { + { "Allocated", allocated }, + { "Gen0Collections", GC.CollectionCount(0) }, + { "Gen1Collections", GC.CollectionCount(1) }, + { "Gen2Collections", GC.CollectionCount(2) }, + }; + + return Task.FromResult(new HealthCheckResult(allocated < threshold, $"Reports degraded status if allocated bytes >= {threshold.ToReadableSize()}", data)); + } + } +} diff --git a/src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs b/src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs new file mode 100644 index 000000000..3532eb589 --- /dev/null +++ b/src/Squidex.Infrastructure/Diagnostics/GCHealthCheckOptions.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class GCHealthCheckOptions + { + public long Threshold { get; set; } = 4 * 1024L; + } +} diff --git a/src/Squidex.Infrastructure/Diagnostics/HealthCheckResult.cs b/src/Squidex.Infrastructure/Diagnostics/HealthCheckResult.cs new file mode 100644 index 000000000..f396a5a8e --- /dev/null +++ b/src/Squidex.Infrastructure/Diagnostics/HealthCheckResult.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class HealthCheckResult + { + public bool IsValid { get; } + + public string Description { get; } + + public Dictionary Data { get; } + + public HealthCheckResult(bool isValid, string description = null, Dictionary data = null) + { + IsValid = isValid; + Data = data; + Description = description; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Diagnostics/IHealthCheck.cs b/src/Squidex.Infrastructure/Diagnostics/IHealthCheck.cs new file mode 100644 index 000000000..f139deaed --- /dev/null +++ b/src/Squidex.Infrastructure/Diagnostics/IHealthCheck.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Diagnostics +{ + public interface IHealthCheck + { + Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs b/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs new file mode 100644 index 000000000..8c6a26f1a --- /dev/null +++ b/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Orleans; +using Orleans.Runtime; + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class OrleansHealthCheck : IHealthCheck + { + private readonly IManagementGrain managementGrain; + + public OrleansHealthCheck(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + managementGrain = grainFactory.GetGrain(0); + } + + public async Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + var activationCount = await managementGrain.GetTotalActivationCount(); + + return new HealthCheckResult(activationCount > 0, "Orleans must have at least one activation."); + } + } +} diff --git a/src/Squidex/AppServices.cs b/src/Squidex/AppServices.cs index 3e5cc0f53..5dcfcf664 100644 --- a/src/Squidex/AppServices.cs +++ b/src/Squidex/AppServices.cs @@ -18,6 +18,7 @@ using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Extensions.Actions.Twitter; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Diagnostics; namespace Squidex { @@ -54,6 +55,8 @@ namespace Squidex config.GetSection("mode")); services.Configure( config.GetSection("twitter")); + services.Configure( + config.GetSection("healthz:gc")); services.Configure( config.GetSection("contentsController")); diff --git a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index bacfabd87..a567605c0 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using IdentityServer4.Models; using Microsoft.AspNetCore.Mvc; using NodaTime; using Squidex.Areas.Api.Controllers.Rules.Models; diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index 51459d275..d662d6560 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -19,6 +19,7 @@ using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Diagnostics; using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Domain.Apps.Entities.Assets; @@ -40,6 +41,7 @@ using Squidex.Domain.Apps.Entities.Schemas.Indexes; using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.Migrations; using Squidex.Pipeline; using Squidex.Pipeline.CommandMiddlewares; @@ -67,15 +69,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs(c => c.GetRequiredService>().Value) - .AsSelf(); - services.AddSingletonAs() .As(); - services.AddSingletonAs(c => c.GetRequiredService>().Value) - .AsSelf(); - services.AddSingletonAs() .As(); @@ -91,6 +87,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .AsSelf(); diff --git a/src/Squidex/Config/Domain/EventStoreServices.cs b/src/Squidex/Config/Domain/EventStoreServices.cs index 8f3c76ff3..0338487d0 100644 --- a/src/Squidex/Config/Domain/EventStoreServices.cs +++ b/src/Squidex/Config/Domain/EventStoreServices.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using Squidex.Infrastructure; +using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.States; @@ -46,6 +47,9 @@ namespace Squidex.Config.Domain var connection = EventStoreConnection.Create(eventStoreConfiguration); + services.AddSingletonAs(c => new GetEventStoreHealthCheck(connection)) + .As(); + services.AddSingletonAs(c => new GetEventStore(connection, eventStorePrefix, eventStoreProjectionHost)) .As() .As(); diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs index e4a223a5c..61d963821 100644 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection; using NodaTime; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; +using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.UsageTracking; #pragma warning disable RECS0092 // Convert field to readonly @@ -35,6 +36,12 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index 972d60141..0d4a01be2 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -28,6 +28,7 @@ using Squidex.Domain.Users; using Squidex.Domain.Users.MongoDb; using Squidex.Domain.Users.MongoDb.Infrastructure; using Squidex.Infrastructure; +using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.MongoDb; @@ -60,6 +61,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs(mongoDatabase) .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As() .As(); diff --git a/src/Squidex/Config/Web/WebExtensions.cs b/src/Squidex/Config/Web/WebExtensions.cs index 50e4ce74f..dea5651d6 100644 --- a/src/Squidex/Config/Web/WebExtensions.cs +++ b/src/Squidex/Config/Web/WebExtensions.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.HttpOverrides; using Squidex.Pipeline; +using Squidex.Pipeline.Diagnostics; namespace Squidex.Config.Web { @@ -27,6 +28,13 @@ namespace Squidex.Config.Web return app; } + public static IApplicationBuilder UseMyHealthCheck(this IApplicationBuilder app) + { + app.Map("/healthz", builder => builder.UseMiddleware()); + + return app; + } + public static void UseMyCors(this IApplicationBuilder app) { app.UseCors(builder => builder diff --git a/src/Squidex/Config/Web/WebServices.cs b/src/Squidex/Config/Web/WebServices.cs index bbb0ad4bd..fdb965701 100644 --- a/src/Squidex/Config/Web/WebServices.cs +++ b/src/Squidex/Config/Web/WebServices.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; using Squidex.Config.Domain; using Squidex.Pipeline; +using Squidex.Pipeline.Diagnostics; namespace Squidex.Config.Web { @@ -25,6 +26,9 @@ namespace Squidex.Config.Web services.AddSingletonAs() .AsSelf(); + services.AddSingletonAs() + .AsSelf(); + services.AddSingletonAs() .AsSelf(); diff --git a/src/Squidex/Pipeline/Diagnostics/HealthCheckMiddleware.cs b/src/Squidex/Pipeline/Diagnostics/HealthCheckMiddleware.cs new file mode 100644 index 000000000..a7e229164 --- /dev/null +++ b/src/Squidex/Pipeline/Diagnostics/HealthCheckMiddleware.cs @@ -0,0 +1,83 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Diagnostics; + +namespace Squidex.Pipeline.Diagnostics +{ + public sealed class HealthCheckMiddleware : IMiddleware + { + private const string Suffix = "HealthCheck"; + private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); + + private readonly Dictionary healthChecks; + private readonly JsonSerializerSettings serializerSettings; + + public HealthCheckMiddleware(IEnumerable healthChecks, JsonSerializerSettings serializerSettings) + { + Guard.NotNull(healthChecks, nameof(healthChecks)); + Guard.NotNull(serializerSettings, nameof(serializerSettings)); + + this.healthChecks = healthChecks.ToDictionary(GetName); + this.serializerSettings = serializerSettings; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + using (var cts = new CancellationTokenSource(Timeout)) + { + var checks = await Task.WhenAll(healthChecks.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.IsValid)) + { + context.Response.StatusCode = 503; + } + + var response = checks.ToDictionary(x => x.Name, x => x.Result); + + await context.Response.WriteAsync(JsonConvert.SerializeObject(new { status = response }, Formatting.Indented, serializerSettings)); + } + } + + private static string GetName(IHealthCheck check) + { + var name = check.GetType().Name.ToCamelCase(); + + if (name.EndsWith(Suffix, StringComparison.OrdinalIgnoreCase)) + { + name = name.Substring(0, name.Length - Suffix.Length); + } + + return name; + } + + private async Task<(string Name, HealthCheckResult Result)> MakeHealthCheckAsync(string name, IHealthCheck check, CancellationToken ct) + { + try + { + var result = await check.CheckHealthAsync(ct); + + return (name, result); + } + catch + { + return (name, new HealthCheckResult(false)); + } + } + } +} diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 536d85ab3..6ef23a2c1 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -73,19 +73,19 @@ - - - - + + + + - + - + - + diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs index 3bc5f10d9..f080dac12 100644 --- a/src/Squidex/WebStartup.cs +++ b/src/Squidex/WebStartup.cs @@ -41,6 +41,7 @@ namespace Squidex { app.ApplicationServices.LogConfiguration(); + app.UseMyHealthCheck(); app.UseMyTracking(); app.UseMyLocalCache(); app.UseMyCors(); diff --git a/tests/RunCoverage.ps1 b/tests/RunCoverage.ps1 index aa392b454..77042505d 100644 --- a/tests/RunCoverage.ps1 +++ b/tests/RunCoverage.ps1 @@ -76,6 +76,6 @@ if ($all -Or $web) { -oldStyle } -&"$folderHome\.nuget\packages\ReportGenerator\3.1.2\tools\ReportGenerator.exe" ` +&"$folderHome\.nuget\packages\ReportGenerator\4.0.2\tools\ReportGenerator.exe" ` -reports:"$folderWorking\$folderReports\*.xml" ` -targetdir:"$folderWorking\$folderReports\Output" \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs index c943b9f2c..24c364fcd 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs @@ -146,6 +146,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes Assert.Equal(new List { appId1, appId2 }, await sut.GetAppIdsAsync()); + Assert.Equal(2, await sut.CountAsync()); + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .MustHaveHappened(); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs index 516f2ca7b..ba52baa49 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; using FakeItEasy; +using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Apps; @@ -49,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Assets ["id3"] = "name3" }); - sut = new AssetQueryService(tagService, assetRepository, new AssetOptions()); + sut = new AssetQueryService(tagService, assetRepository, Options.Create(new AssetOptions())); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs index 2cbeba0a2..6318c1272 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using FakeItEasy; +using Microsoft.Extensions.Options; using Microsoft.OData; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; @@ -69,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents context = new ContentQueryContext(QueryContext.Create(app, user)); - sut = new ContentQueryService(appProvider, contentRepository, contentVersionLoader, scriptEngine, new ContentOptions(), modelBuilder); + sut = new ContentQueryService(appProvider, contentRepository, contentVersionLoader, scriptEngine, Options.Create(new ContentOptions()), modelBuilder); } [Fact]