Browse Source

Use AspNetCore health check.

pull/336/head
Sebastian Stehle 7 years ago
parent
commit
189cd14986
  1. 12
      src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs
  2. 11
      src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs
  3. 13
      src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs
  4. 16
      src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs
  5. 27
      src/Squidex.Infrastructure/Diagnostics/HealthCheckResult.cs
  6. 16
      src/Squidex.Infrastructure/Diagnostics/HealthCheckScopes.cs
  7. 20
      src/Squidex.Infrastructure/Diagnostics/IHealthCheck.cs
  8. 13
      src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs
  9. 1
      src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  10. 1
      src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs
  11. 5
      src/Squidex/Config/Domain/EntitiesServices.cs
  12. 7
      src/Squidex/Config/Domain/EventStoreServices.cs
  13. 12
      src/Squidex/Config/Domain/InfrastructureServices.cs
  14. 4
      src/Squidex/Config/Domain/StoreServices.cs
  15. 55
      src/Squidex/Config/Web/WebExtensions.cs
  16. 4
      src/Squidex/Config/Web/WebServices.cs
  17. 113
      src/Squidex/Pipeline/Diagnostics/HealthCheckMiddleware.cs
  18. 1
      src/Squidex/Squidex.csproj

12
src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs

@ -5,13 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
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
@ -20,11 +19,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics
{
private readonly IAppsByNameIndex index;
public IEnumerable<string> Scopes
{
get { yield return HealthCheckScopes.Cluster; }
}
public OrleansAppsHealthCheck(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
@ -32,11 +26,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics
index = grainFactory.GetGrain<IAppsByNameIndex>(SingleGrain.Id);
}
public async Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
await index.CountAsync();
return new HealthCheckResult(true);
return HealthCheckResult.Healthy("Orleans must establish communication.");
}
}
}

11
src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs

@ -5,10 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using EventStore.ClientAPI;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Squidex.Infrastructure.Diagnostics
{
@ -16,11 +16,6 @@ namespace Squidex.Infrastructure.Diagnostics
{
private readonly IEventStoreConnection connection;
public IEnumerable<string> Scopes
{
get { yield return HealthCheckScopes.Node; }
}
public GetEventStoreHealthCheck(IEventStoreConnection connection)
{
Guard.NotNull(connection, nameof(connection));
@ -28,11 +23,11 @@ namespace Squidex.Infrastructure.Diagnostics
this.connection = connection;
}
public async Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
await connection.ReadEventAsync("test", 1, false);
return new HealthCheckResult(true, "Querying test event from event store.");
return HealthCheckResult.Healthy("Application must query data from EventStore.");
}
}
}

13
src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs

@ -5,9 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using MongoDB.Driver;
namespace Squidex.Infrastructure.Diagnostics
@ -16,11 +16,6 @@ namespace Squidex.Infrastructure.Diagnostics
{
private readonly IMongoDatabase mongoDatabase;
public IEnumerable<string> Scopes
{
get { yield return HealthCheckScopes.Node; }
}
public MongoDBHealthCheck(IMongoDatabase mongoDatabase)
{
Guard.NotNull(mongoDatabase, nameof(mongoDatabase));
@ -28,13 +23,15 @@ namespace Squidex.Infrastructure.Diagnostics
this.mongoDatabase = mongoDatabase;
}
public async Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
var collectionNames = await mongoDatabase.ListCollectionNamesAsync(cancellationToken: cancellationToken);
var result = await collectionNames.AnyAsync(cancellationToken);
return new HealthCheckResult(result);
var status = result ? HealthStatus.Healthy : HealthStatus.Unhealthy;
return new HealthCheckResult(status, "Application must query data from MongoDB");
}
}
}

16
src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
namespace Squidex.Infrastructure.Diagnostics
@ -17,11 +18,6 @@ namespace Squidex.Infrastructure.Diagnostics
{
private readonly long threshold;
public IEnumerable<string> Scopes
{
get { yield return HealthCheckScopes.Node; }
}
public GCHealthCheck(IOptions<GCHealthCheckOptions> options)
{
Guard.NotNull(options, nameof(options));
@ -29,19 +25,21 @@ namespace Squidex.Infrastructure.Diagnostics
threshold = 1024 * 1024 * options.Value.Threshold;
}
public Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
var allocated = GC.GetTotalMemory(false);
var data = new Dictionary<string, object>()
var data = new Dictionary<string, object>
{
{ "Allocated", allocated },
{ "Allocated", allocated.ToReadableSize() },
{ "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));
var status = allocated < threshold ? HealthStatus.Healthy : HealthStatus.Unhealthy;
return Task.FromResult(new HealthCheckResult(status, $"Application must consum less than {threshold.ToReadableSize()} memory.", data: data));
}
}
}

27
src/Squidex.Infrastructure/Diagnostics/HealthCheckResult.cs

@ -1,27 +0,0 @@
// ==========================================================================
// 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 IsHealthy { get; }
public string Description { get; }
public Dictionary<string, object> Data { get; }
public HealthCheckResult(bool isHealthy, string description = null, Dictionary<string, object> data = null)
{
IsHealthy = isHealthy;
Data = data;
Description = description;
}
}
}

16
src/Squidex.Infrastructure/Diagnostics/HealthCheckScopes.cs

@ -1,16 +0,0 @@
// ==========================================================================
// 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";
}
}

20
src/Squidex.Infrastructure/Diagnostics/IHealthCheck.cs

@ -1,20 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Diagnostics
{
public interface IHealthCheck
{
IEnumerable<string> Scopes { get; }
Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken));
}
}

13
src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs

@ -5,9 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Orleans;
using Orleans.Runtime;
@ -17,11 +17,6 @@ namespace Squidex.Infrastructure.Diagnostics
{
private readonly IManagementGrain managementGrain;
public IEnumerable<string> Scopes
{
get { yield return HealthCheckScopes.Cluster; }
}
public OrleansHealthCheck(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
@ -29,11 +24,13 @@ namespace Squidex.Infrastructure.Diagnostics
managementGrain = grainFactory.GetGrain<IManagementGrain>(0);
}
public async Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
var activationCount = await managementGrain.GetTotalActivationCount();
return new HealthCheckResult(activationCount > 0, "Orleans must have at least one activation.");
var status = activationCount > 0 ? HealthStatus.Healthy : HealthStatus.Unhealthy;
return new HealthCheckResult(status, "Orleans must have at least one activation.");
}
}
}

1
src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -9,6 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.2" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.1.2">

1
src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs

@ -10,7 +10,6 @@ using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NJsonSchema.Infrastructure;
using NSwag.Annotations;
using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
using Squidex.Infrastructure.Tasks;

5
src/Squidex/Config/Domain/EntitiesServices.cs

@ -20,7 +20,6 @@ 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;
@ -42,7 +41,6 @@ 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.EventSourcing;
using Squidex.Infrastructure.Migrations;
using Squidex.Pipeline;
@ -97,9 +95,6 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<SchemaHistoryEventsCreator>()
.As<IHistoryEventsCreator>();
services.AddSingletonAs<OrleansAppsHealthCheck>()
.As<IHealthCheck>();
services.AddSingletonAs<RolePermissionsProvider>()
.AsSelf();

7
src/Squidex/Config/Domain/EventStoreServices.cs

@ -48,11 +48,14 @@ namespace Squidex.Config.Domain
var connection = EventStoreConnection.Create(eventStoreConfiguration);
services.AddSingletonAs(c => new GetEventStoreHealthCheck(connection))
.As<IHealthCheck>();
services.AddSingletonAs(connection)
.As<IEventStoreConnection>();
services.AddSingletonAs(c => new GetEventStore(connection, c.GetRequiredService<IJsonSerializer>(), eventStorePrefix, eventStoreProjectionHost))
.As<IEventStore>();
services.AddHealthChecks()
.AddCheck<GetEventStoreHealthCheck>("EventStore", tags: new[] { "node" });
}
});

12
src/Squidex/Config/Domain/InfrastructureServices.cs

@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using NodaTime;
using Squidex.Domain.Apps.Entities.Apps.Diagnostics;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
@ -27,6 +28,11 @@ namespace Squidex.Config.Domain
{
public static void AddMyInfrastructureServices(this IServiceCollection services)
{
services.AddHealthChecks()
.AddCheck<GCHealthCheck>("GC", tags: new[] { "node" })
.AddCheck<OrleansHealthCheck>("Orleans", tags: new[] { "cluster" })
.AddCheck<OrleansAppsHealthCheck>("Orleans App", tags: new[] { "cluster" });
services.AddSingletonAs(SystemClock.Instance)
.As<IClock>();
@ -39,12 +45,6 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AsyncLocalCache>()
.As<ILocalCache>();
services.AddSingletonAs<GCHealthCheck>()
.As<IHealthCheck>();
services.AddSingletonAs<OrleansHealthCheck>()
.As<IHealthCheck>();
services.AddSingletonAs<HttpContextAccessor>()
.As<IHttpContextAccessor>();

4
src/Squidex/Config/Domain/StoreServices.cs

@ -60,8 +60,8 @@ namespace Squidex.Config.Domain
services.AddSingletonAs(mongoDatabase)
.As<IMongoDatabase>();
services.AddSingletonAs<MongoDBHealthCheck>()
.As<IHealthCheck>();
services.AddHealthChecks()
.AddCheck<MongoDBHealthCheck>("MongoDB", tags: new[] { "node" });
services.AddSingletonAs<MongoMigrationStatus>()
.As<IMigrationStatus>();

55
src/Squidex/Config/Web/WebExtensions.cs

@ -5,11 +5,18 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Squidex.Infrastructure.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Squidex.Infrastructure.Json;
using Squidex.Pipeline;
using Squidex.Pipeline.Diagnostics;
using Squidex.Pipeline.Robots;
namespace Squidex.Config.Web
@ -32,19 +39,51 @@ namespace Squidex.Config.Web
public static IApplicationBuilder UseMyHealthCheck(this IApplicationBuilder app)
{
app.Map("/readiness", builder =>
var serializer = app.ApplicationServices.GetRequiredService<IJsonSerializer>();
var writer = new Func<HttpContext, HealthReport, Task>((httpContext, report) =>
{
var response = new
{
Entries = report.Entries.ToDictionary(x => x.Key, x =>
{
var value = x.Value;
return new
{
Data = value.Data.Count > 0 ? new Dictionary<string, object>(value.Data) : null,
value.Description,
value.Duration,
value.Status
};
}),
report.Status,
report.TotalDuration
};
var json = serializer.Serialize(response);
httpContext.Response.Headers["Content-Types"] = "text/json";
return httpContext.Response.WriteAsync(json);
});
app.UseHealthChecks("/readiness", new HealthCheckOptions
{
builder.UseMiddleware<HealthCheckMiddleware>(HealthCheckScopes.Any);
Predicate = check => true,
ResponseWriter = writer
});
app.Map("/healthz", builder =>
app.UseHealthChecks("/healthz", new HealthCheckOptions
{
builder.UseMiddleware<HealthCheckMiddleware>(HealthCheckScopes.Node);
Predicate = check => check.Tags.Contains("node"),
ResponseWriter = writer
});
app.Map("/cluster-healthz", builder =>
app.UseHealthChecks("/cluster-healthz", new HealthCheckOptions
{
builder.UseMiddleware<HealthCheckMiddleware>(HealthCheckScopes.Cluster);
Predicate = check => check.Tags.Contains("cluster"),
ResponseWriter = writer
});
return app;

4
src/Squidex/Config/Web/WebServices.cs

@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Config.Domain;
using Squidex.Pipeline;
using Squidex.Pipeline.Diagnostics;
using Squidex.Pipeline.Robots;
namespace Squidex.Config.Web
@ -27,9 +26,6 @@ namespace Squidex.Config.Web
services.AddSingletonAs<AppResolver>()
.AsSelf();
services.AddSingletonAs<HealthCheckMiddleware>()
.AsSelf();
services.AddSingletonAs<RobotsTxtMiddleware>()
.AsSelf();

113
src/Squidex/Pipeline/Diagnostics/HealthCheckMiddleware.cs

@ -1,113 +0,0 @@
// ==========================================================================
// 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 Squidex.Infrastructure;
using Squidex.Infrastructure.Diagnostics;
using Squidex.Infrastructure.Json;
namespace Squidex.Pipeline.Diagnostics
{
public sealed class HealthCheckMiddleware
{
private const string Suffix = "HealthCheck";
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2);
private readonly Dictionary<string, IHealthCheck> healthChecks;
private readonly IJsonSerializer serializer;
private readonly RequestDelegate next;
private readonly List<string> scopes;
public HealthCheckMiddleware(IEnumerable<IHealthCheck> healthChecks, IJsonSerializer serializer, RequestDelegate next, string scopes)
{
Guard.NotNull(healthChecks, nameof(healthChecks));
Guard.NotNull(serializer, nameof(serializer));
this.healthChecks = healthChecks.ToDictionary(GetName);
this.next = next;
this.serializer = serializer;
this.scopes = SplitScopes(scopes);
}
public async Task Invoke(HttpContext context)
{
if (CanServeRequest(context.Request))
{
using (var cts = new CancellationTokenSource(Timeout))
{
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 (results.Any(x => !x.Result.IsHealthy))
{
context.Response.StatusCode = 503;
}
var response = results.ToDictionary(x => x.Name, x => x.Result);
var json = serializer.Serialize(new { status = response });
await context.Response.WriteAsync(json);
}
}
else
{
await next(context);
}
}
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<string> SplitScopes(string scopes)
{
return scopes.Split(",").Where(x => x != "*").ToList();
}
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));
}
}
}
}

1
src/Squidex/Squidex.csproj

@ -65,6 +65,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />

Loading…
Cancel
Save